refactor(DiscoveryMountain): split monolithic main.c into modular files
- Extracted app_context (global AppCtx G definition) - storage: path helpers, LRU, sort, fmt_time, per-episode resume map, state persistence - network: resolve_host, normalize URL, http_get_raw with growing PSRAM buffer, fetch_data - download: dl_ep_raw with progress tracking, cancel, overlay UI (show/hide/update) - player: audio device, stream, play_task with resume seek, start/select/next/prev with position save/restore - ui: build_ui, timers, season picker, button callbacks, periodic save - main: minimal glue, fetch_task, onShow/onHide, app registration - Preserves all previous fixes: download progress overlay, per-episode resume, robust download handling - Build verified: 111 undef, all exported
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
#include "player.h"
|
||||
#include "storage.h"
|
||||
#include "download.h"
|
||||
#include "ui.h"
|
||||
|
||||
#include <tt_lvgl.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <lwip/sockets.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include <unistd.h>
|
||||
|
||||
#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;i<total_samples;i++){
|
||||
int32_t s=pcm[i];
|
||||
s=(int32_t)(s*vol);
|
||||
if(s>32767) 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(off<to_write && !G.need_stop){
|
||||
if(G.is_paused){
|
||||
if(configured){
|
||||
close_stream();
|
||||
configured=false;
|
||||
sr=0; ch=0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
size_t w=0;
|
||||
error_t e=audio_stream_write(G.stream_handle,(uint8_t*)pcm+off,to_write-off,&w,pdMS_TO_TICKS(500));
|
||||
if(e==ERROR_NONE){
|
||||
off+=w;
|
||||
} else {
|
||||
ESP_LOGW(TAG,"audio_write err %d",e);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
if(G.is_paused) continue;
|
||||
}
|
||||
if(info.frame_bytes>0 && 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);
|
||||
}
|
||||
Reference in New Issue
Block a user