#include "player.h" #include "storage.h" #include "download.h" #include "ui.h" #include #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include #define MINIMP3_IMPLEMENTATION #define MINIMP3_NO_SIMD #include "minimp3.h" bool find_audio_device(){ struct Device* dev=device_find_by_name("audio-stream"); if(dev){ G.stream_dev=dev; return true; } dev=device_find_first_by_type(&AUDIO_STREAM_TYPE); if(dev){ G.stream_dev=dev; return true; } return false; } void close_stream(){ if(G.stream_handle){ audio_stream_close(G.stream_handle); G.stream_handle=NULL; } } bool open_stream(uint32_t sr, uint8_t ch){ close_stream(); if(!G.stream_dev && !find_audio_device()) return false; struct AudioStreamConfig cfg={ .sample_rate=sr, .bits_per_sample=16, .channels=ch }; error_t err=audio_stream_open_output(G.stream_dev,&cfg,&G.stream_handle); return err==ERROR_NONE; } void wait_play_exit(){ if(G.play_handle){ G.need_stop=true; while(G.play_handle!=NULL){ tt_lvgl_unlock(); vTaskDelay(pdMS_TO_TICKS(10)); tt_lvgl_lock(portMAX_DELAY); } } close_stream(); } void stop_playback_sync(){ if(G.play_handle){ G.need_stop=true; int tries=200; while(G.play_handle!=NULL && tries-- >0){ vTaskDelay(pdMS_TO_TICKS(10)); } } close_stream(); G.is_playing=false; G.is_paused=false; } void play_task(void* arg){ (void)arg; int idx=G.cur_ep_idx; if(idx<0||idx>=G.ep_cnt){ G.play_handle=NULL; vTaskDelete(NULL); return; } EpInfo* ep=&G.eps[idx]; char path[300]; make_sd_path(path,sizeof(path),ep->slug); FILE* file=fopen(path,"rb"); if(!file){ ESP_LOGE(TAG,"open fail %s",path); G.is_playing=false; G.play_handle=NULL; vTaskDelete(NULL); return; } fseek(file,0,SEEK_END); long fsize=ftell(file); fseek(file,0,SEEK_SET); if(fsize>0) G.total_sec=(int)(fsize/16000); if(!find_audio_device()){ ESP_LOGE(TAG,"no audio-stream dev"); fclose(file); G.is_playing=false; G.play_handle=NULL; vTaskDelete(NULL); return; } uint8_t id3hdr[10]; long id3_skip=0; if(fread(id3hdr,1,10,file)==10 && memcmp(id3hdr,"ID3",3)==0){ int id3size = (id3hdr[6]&0x7F)<<21 | (id3hdr[7]&0x7F)<<14 | (id3hdr[8]&0x7F)<<7 | (id3hdr[9]&0x7F); ESP_LOGI(TAG,"ID3 found %d, skip",id3size); id3_skip=id3size+10; fseek(file, id3_skip, SEEK_SET); fsize -= id3_skip; if(fsize>0) G.total_sec=(int)(fsize/16000); } else { fseek(file,0,SEEK_SET); } int resume_sec = G.pos_sec; if(resume_sec>5){ long seek_bytes = (long)resume_sec * 16000; if(seek_bytes < fsize){ fseek(file, id3_skip + seek_bytes, SEEK_SET); ESP_LOGI(TAG,"Resuming %s from %d sec (~%ld bytes)", ep->slug, resume_sec, seek_bytes); } } mp3dec_t* dec = heap_caps_malloc(sizeof(mp3dec_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if(!dec){ ESP_LOGE(TAG,"dec malloc fail"); fclose(file); G.play_handle=NULL; vTaskDelete(NULL); return; } mp3dec_init(dec); uint8_t* inbuf=malloc(MP3_BUF); int16_t* pcm=malloc(1152*2*2); if(!inbuf||!pcm){ if(inbuf) free(inbuf); if(pcm) free(pcm); fclose(file); heap_caps_free(dec); G.play_handle=NULL; vTaskDelete(NULL); return; } size_t buffered=0; mp3dec_frame_info_t info; memset(&info,0,sizeof(info)); bool configured=false; int sr=0, ch=0; size_t r=fread(inbuf,1,MP3_BUF,file); buffered=r; ESP_LOGI(TAG,"play task start %s size %ld resume=%d",path,fsize,resume_sec); long total_decoded_samples = 0; if(resume_sec>5){ total_decoded_samples = (long)resume_sec * 16000; } while(!G.need_stop){ if(G.is_paused){ if(configured){ close_stream(); configured=false; sr=0; ch=0; } vTaskDelay(pdMS_TO_TICKS(100)); continue; } int samples=mp3dec_decode_frame(dec,inbuf,(int)buffered,pcm,&info); if(samples>0){ total_decoded_samples += samples; if(info.hz>0) G.pos_sec = total_decoded_samples / info.hz; else G.pos_sec = total_decoded_samples / 16000; if(!configured || info.hz!=sr || info.channels!=ch){ ESP_LOGI(TAG,"open_stream sr=%d ch=%d",info.hz,info.channels); if(!open_stream((uint32_t)info.hz,(uint8_t)info.channels)){ ESP_LOGE(TAG,"open_stream fail %d %d",info.hz,info.channels); break; } ESP_LOGI(TAG,"open_stream ok"); sr=info.hz; ch=info.channels; configured=true; if(resume_sec>5 && total_decoded_samples < (long)resume_sec * info.hz + info.hz){ total_decoded_samples = (long)resume_sec * info.hz; G.pos_sec = resume_sec; } } float vol=G.volume/100.0f; if(vol<0) vol=0; if(vol>1) vol=1; int total_samples = samples * (info.channels>0?info.channels:1); for(int i=0;i32767) s=32767; if(s<-32768) s=-32768; pcm[i]=(int16_t)s; } size_t to_write = total_samples * sizeof(int16_t); size_t off=0; while(off0 && info.frame_bytes <= (int)buffered){ memmove(inbuf,inbuf+info.frame_bytes,buffered-info.frame_bytes); buffered-=info.frame_bytes; } else { buffered=0; } if(buffered < MP3_BUF/2){ size_t nr=fread(inbuf+buffered,1,MP3_BUF-buffered,file); if(nr>0) buffered+=nr; else { if(samples==0) break; } } if(buffered==0) break; } fclose(file); free(inbuf); free(pcm); heap_caps_free(dec); close_stream(); bool finished_naturally = !G.need_stop; if(finished_naturally){ ESP_LOGI(TAG,"playback finished naturally for %s, clearing saved pos", ep->slug); set_saved_pos_for_slug(ep->slug, 0); G.pos_sec=0; save_state(); } else { set_saved_pos_for_slug(ep->slug, G.pos_sec); save_state(); } ESP_LOGI(TAG,"play task end finished_naturally=%d", finished_naturally); G.is_playing=false; G.play_handle=NULL; vTaskDelete(NULL); } void start_idx_internal(int idx){ if(idx<0||idx>=G.ep_cnt) return; EpInfo* ep=&G.eps[idx]; if(G.cur_ep_idx!=idx){ int saved = get_saved_pos_for_slug(ep->slug); if(saved>5){ G.pos_sec=saved; ESP_LOGI(TAG,"Restoring saved pos %d for %s", saved, ep->slug); } G.last_pct=-1; } tt_lvgl_lock(portMAX_DELAY); wait_play_exit(); G.cur_ep_idx=idx; G.cur_season=ep->season; strncpy(G.cur_title,ep->title,sizeof(G.cur_title)-1); strncpy(G.last_slug,ep->slug,sizeof(G.last_slug)-1); G.need_stop=false; G.is_paused=false; G.is_playing=true; save_state(); tt_lvgl_unlock(); xTaskCreate(play_task,"dm_play",16384,NULL,5,&G.play_handle); } void start_idx(int idx){ if(idx<0||idx>=G.ep_cnt) return; EpInfo* ep=&G.eps[idx]; if(!has_slug(ep->slug)){ stop_playback_sync(); tt_lvgl_lock(portMAX_DELAY); show_dl_overlay_locked(ep->title, ep->slug, ep->season, ep->ep_num); if(G.lbl_status) lv_label_set_text(G.lbl_status,"Downloading…"); tt_lvgl_unlock(); if(G.dl_handle){ return; } xTaskCreate(dl_task,"dm_dl",16384,(void*)(intptr_t)idx,5,&G.dl_handle); return; } start_idx_internal(idx); } void select_ep(int idx){ if(idx<0||idx>=G.ep_cnt) return; if(G.cur_ep_idx>=0 && G.cur_ep_idx < G.ep_cnt && G.cur_ep_idx!=idx){ save_current_ep_pos(); } G.cur_ep_idx=idx; EpInfo* ep=&G.eps[idx]; G.cur_season=ep->season; strncpy(G.last_slug, ep->slug, sizeof(G.last_slug)-1); int saved = get_saved_pos_for_slug(ep->slug); if(saved>5){ G.pos_sec=saved; ESP_LOGI(TAG,"select_ep restore %s pos=%d", ep->slug, saved); } else { G.pos_sec=saved; } G.last_pct=-1; G.total_sec=0; tt_lvgl_lock(portMAX_DELAY); if(G.lbl_title) lv_label_set_text(G.lbl_title,ep->title); if(G.lbl_meta){ char b[64]; if(saved>5){ char tbuf[16]; fmt_time(tbuf,saved); snprintf(b,sizeof(b),"S%02d E%02d %s • %s",ep->season,ep->ep_num,ep->seen?"*":"",tbuf); } else { snprintf(b,sizeof(b),"S%02d E%02d %s",ep->season,ep->ep_num,ep->seen?"*":""); } lv_label_set_text(G.lbl_meta,b); } if(G.lbl_status){ if(saved>5) lv_label_set_text(G.lbl_status,"Resume – tap Play"); else lv_label_set_text(G.lbl_status,ep->seen?"On SD – tap Play":"Tap Play to download"); } if(G.bar){ lv_bar_set_value(G.bar,0,LV_ANIM_OFF); } if(G.lbl_time){ if(saved>5){ char a[16]; fmt_time(a,saved); char buf[40]; snprintf(buf,sizeof(buf),"%s / --:--",a); lv_label_set_text(G.lbl_time,buf); } else { lv_label_set_text(G.lbl_time,"0:00 / --:--"); } } tt_lvgl_unlock(); save_state(); } void go_next(){ if(G.ep_cnt==0) return; save_current_ep_pos(); save_state(); stop_playback_sync(); int n=G.cur_ep_idx+1; if(n>=G.ep_cnt) n=0; select_ep(n); start_idx(n); } void go_prev(){ if(G.ep_cnt==0) return; if(G.pos_sec>5){ ESP_LOGI(TAG,"go_prev restart current %d sec",G.pos_sec); if(G.cur_ep_idx>=0){ set_saved_pos_for_slug(G.eps[G.cur_ep_idx].slug, 0); save_state(); } stop_playback_sync(); G.pos_sec=0; int cur=G.cur_ep_idx; if(cur<0) cur=0; select_ep(cur); start_idx(cur); return; } save_current_ep_pos(); save_state(); stop_playback_sync(); int p=G.cur_ep_idx-1; if(p<0) p=G.ep_cnt-1; select_ep(p); start_idx(p); }