Files
tactility_apps/Apps/DiscoveryMountain/main/Source/player.c
T
Adolfo be9229b80d 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
2026-07-22 17:11:59 -04:00

345 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
}