真机反馈:切音质后再播同一首歌,声音已出但播放页恒显示「缓冲中」。
根因(app/play/player_manager.c):同 idx 的新播放意图不被解码主循环识别。
切音质后重播同曲 = player_manager_play(idx),而 idx 与在播曲相同——cur_song 不变
(仍是 idx)、state 由 PLAYING 置 BUFFER、seq++。旧仍_current(cur==idx && state==BUFFER
恒真)与解码主循环(只认 cur!=idx / STOPPED)均不放过,于是:
①旧曲(旧档位)继续出声、state 停在 BUFFER → UI 恒「缓冲中…」(dl_done=0 无百分比);
②新档位下载/缓存条目从未发起;
③旧曲收尾后外层把 BUFFER 当「自然播完」顺延下一首——新档位永不生效。
三条取数路径(缓存命中/边下边播/.part续传)统一受影响(根因在 intent 层)。
修法:play intent 以 seq 标识,层层判别「我的 intent 是否仍有效」:
- 新增 still_intent(idx, my_seq)(cur==idx && state==BUFFER && seq==my_seq),取代
仍_current(同 idx 重播时 seq 变即判过期),贯穿 预缓冲/重试退避/物化/解码 open 各护栏;
- run_song 入口捕获 my_seq,沿 song_local_file/song_download_attempt 下传;
- 解码主循环 seq != my_seq → break 回外层重跑 run_song(换档重播);
- 外层「自然播完」收紧为 state==PLAYING,不把 BUFFER(挂起新 intent)当播完顺延下一首。
验证:player_flow_sim 新增 P12(同 idx 重播换档重启)——修前恒 PLAYING 不了/键仍旧档/
downloads=0/eff=-1;修后 state 回 PLAYING、下载新档位、缓存键 1000_lossless、eff_level
按非会员钳顶回填 EXHIGH。P1-P11 零回归(宿主+qemu-mipsel ALL PASS)。build_ncm.sh
自有代码 -Wall -Wextra 0 warning。0 真实 NCM 请求。未部署真机(留主会话)。
REPORT.md 另记(docs commit)。
842 lines
41 KiB
C
842 lines
41 KiB
C
/* play/player_manager.c — 播放线程:URL 获取(全档表逐级降级)→缓存(命中直接播/否则边下边播)→
|
||
* minimp3/dr_flac 解码(stream_src 回调)→audio_out 直写。UI 不卡(网络+解码全在此线程)。
|
||
*
|
||
* 模型:一个持久的播放线程。外层循环取「目标曲目」,run_song() 负责一首歌的
|
||
* 「下载(缓冲) → 解码 → 输出 → 结束/被指令打断」。暂停=冻结(停读停写停推 pos,状态置 PAUSED);
|
||
* seek=seek 解码器+重开输出到目标 ms(flac 精确帧/mp3 字节 offset 近似);下一/上一首=打断当前 run_song 返回外层。
|
||
* 所有跨线程共享状态在 g_play (mu)。g_st.ctx 的 device_id/cookie/cache 在播放期间稳定(登出会先停播)。
|
||
* 返回码/错误经 g_play.err/errmsg 供 UI 提示。
|
||
* §19:解码经 decoder.c 按 magic 分发(fLaC→dr_flac / 其它→minimp3);rate/ch/bits 按实际流
|
||
* 给 audio_out(flac ≤16bit→S16,>16bit→S32;audiod 格式归一/DAC 原生率跟随在服务端);
|
||
* 音频缓存键 = 歌曲ID+音质档位(用户定案 NCM_CACHE_DESIGN §2.5),旧无档位键温和兼容。
|
||
* §20:音质体系全量——用户档超会员权限不硬拒(钳顶后逐级降级,eff_level 回填供播放页
|
||
* 「已降级至 xx」标注;下一首回到用户档);fee∈{1,4} 且非会员 → NCM_ERR_VIP 不发请求。
|
||
* §21:边下边播(NCM_STREAM_PLAY 方案 A)——下载拆到独立线程(write(2) 直写 .tmp +
|
||
* stream_src 推进可读边界),解码经 ncm_dec_open_src 的 read_cb(严格 fread 语义,F7)
|
||
* pread 同一文件;预缓冲 ≥128KB 才 open(minimp3 MINIMP3_IO_SIZE 强制门槛,F3);
|
||
* 下载完成由播放线程 commit(rename);缓存命中用 final=size 的静态 stream_src(P1:
|
||
* 不再 mmap 整文件);http 30s 绝对 deadline 已改空闲超时(P2)。.part 续传/LRU 防淘汰
|
||
* 留阶段 3。
|
||
* §25:切音质后重播同一曲——同 idx 再 play 时 cur/state 都不变(cur 同、state 由 PLAYING
|
||
* 置 BUFFER),解码主循环此前只认 cur!=idx / STOPPED,导致:
|
||
* ①旧曲(旧档)继续出声而 state 停在 BUFFER(UI 恒「缓冲中」);②旧曲收尾后外层把
|
||
* BUFFER 当「自然播完」顺延下一首,新档位永不生效。修法:以 seq 标识 play intent
|
||
* (still_intent 取代仍_current),decode 主循环 seq 变化即 break,外层「自然播完」仅指
|
||
* state==PLAYING——同 idx 新 intent 回到外层重跑 run_song(换档重播)。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <sys/stat.h>
|
||
#include "decoder.h"
|
||
#include "stream_src.h"
|
||
|
||
static pthread_t g_thr;
|
||
static int g_thr_on;
|
||
static int g_want_run = 1; /* 线程运行标志(quit=0) */
|
||
#define PCM_CHUNK_FRAMES 4096 /* 每块帧数(≈旧 8192 样本/双声道) */
|
||
|
||
/* ---- 线程内部一次性歌的状态 ---- */
|
||
static char g_path[512]; /* 本地音频路径(缓存,mp3/flac) */
|
||
static int64_t g_file_bytes;
|
||
static int g_level; /* 音质档(来自设置) */
|
||
static int g_cache_gb;
|
||
|
||
/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁),
|
||
* 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */
|
||
static void on_audio_preempt(const char *by_app, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
meow_log(MEOW_LOG_WARN, "play: preempted by [%s] -> pause", (by_app && by_app[0]) ? by_app : "?");
|
||
player_manager_pause(1);
|
||
}
|
||
|
||
int player_manager_state(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.state; pthread_mutex_unlock(&g_play.mu); return s; }
|
||
int player_manager_playing_song(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.cur_song; pthread_mutex_unlock(&g_play.mu); return s; }
|
||
int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu); p = g_play.pos_ms; pthread_mutex_unlock(&g_play.mu); return p; }
|
||
int64_t player_manager_dur_ms(void) { int64_t d; pthread_mutex_lock(&g_play.mu); d = g_play.dur_ms; pthread_mutex_unlock(&g_play.mu); return d; }
|
||
int player_manager_error(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.err; pthread_mutex_unlock(&g_play.mu); return e; }
|
||
|
||
/* 实际生效音质档(§20):下载时由 song_url_auto 回填;缓存命中按 ext 推断;-1=未定。 */
|
||
int player_manager_eff_level(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.eff_level; pthread_mutex_unlock(&g_play.mu); return e; }
|
||
|
||
/* 本曲下载进度(UI 缓冲反馈,§18)。total=0 表示未知(song_url 未给 size / 缓存命中)。 */
|
||
void player_manager_dl(int64_t *done, int64_t *total)
|
||
{
|
||
pthread_mutex_lock(&g_play.mu);
|
||
if (done) *done = g_play.dl_done;
|
||
if (total) *total = g_play.dl_total;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
/* 播放曲快照(REPORT §17):run_song 起播时物化并拷入 g_play.play_song(mu 保护),
|
||
* 之后播放/歌词/IPC/UI 一律读快照——窗口滑出当前播放曲不影响播放,不许崩。 */
|
||
int player_manager_current_song(ncm_song_t *out)
|
||
{
|
||
if (!out) return -1;
|
||
pthread_mutex_lock(&g_play.mu);
|
||
int ok = g_play.play_song_valid;
|
||
if (ok) *out = g_play.play_song;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
return ok ? 0 : -1;
|
||
}
|
||
|
||
void player_manager_set_quality(int level, int cache_gb)
|
||
{
|
||
g_level = level;
|
||
g_cache_gb = cache_gb;
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.err = 0;
|
||
ncm_cache_set_limit_gb(&g_st.ctx.cache, cache_gb);
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
void player_manager_play(int song_idx)
|
||
{
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.cur_song = song_idx;
|
||
g_play.cmd = 0;
|
||
g_play.state = NCM_PC_BUFFER;
|
||
g_play.pos_ms = 0;
|
||
g_play.err = 0; /* 新播放意图清旧错误(UI 错误条随新点歌消失) */
|
||
g_play.dl_done = 0;
|
||
g_play.dl_total = 0;
|
||
g_play.seq++;
|
||
g_play.eff_level = -1; /* 新播放意图:生效档未定(song_local_file 回填) */
|
||
g_play.underruns = 0; /* §21:边下边播统计随新播放意图清零 */
|
||
g_play.underrun_ms = 0;
|
||
g_play.stalled = 0;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
void player_manager_pause(int on)
|
||
{
|
||
pthread_mutex_lock(&g_play.mu);
|
||
if (on) { if (g_play.state == NCM_PC_PLAYING) g_play.state = NCM_PC_PAUSED; }
|
||
else { if (g_play.state == NCM_PC_PAUSED) g_play.state = NCM_PC_PLAYING; }
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
void player_manager_seek(int64_t ms)
|
||
{
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.cmd = NCM_CTL_SEEK;
|
||
g_play.cmd_seek_ms = ms;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
void player_manager_next(int dir)
|
||
{
|
||
pthread_mutex_lock(&g_play.mu);
|
||
int c = g_play.cur_song + dir;
|
||
if (c < 0) c = 0;
|
||
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
|
||
if (c >= cnt) c = cnt - 1;
|
||
if (c != g_play.cur_song) {
|
||
g_play.cur_song = c;
|
||
g_play.cmd = 0;
|
||
g_play.state = NCM_PC_BUFFER;
|
||
g_play.pos_ms = 0;
|
||
g_play.err = 0;
|
||
g_play.dl_done = 0;
|
||
g_play.dl_total = 0;
|
||
g_play.seq++;
|
||
g_play.eff_level = -1; /* 切歌:生效档随新曲重估(下首恢复用户档,§20) */
|
||
g_play.underruns = 0;
|
||
g_play.underrun_ms = 0;
|
||
g_play.stalled = 0;
|
||
}
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
void player_manager_stop(void)
|
||
{
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.cur_song = -1;
|
||
g_play.state = NCM_PC_STOPPED;
|
||
g_play.pos_ms = 0;
|
||
g_play.play_song_valid = 0; /* 快照失效 */
|
||
g_play.seq++;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
/* ---- 每首歌的播放体(在外层循环内调用)---- */
|
||
|
||
/* 边下边播每首曲的源对象(§21/阶段3):读端 stream_src + 下载路径写端/线程。
|
||
* run_song 栈上分配,下载线程 join 后才返回,生命周期安全。 */
|
||
#define PREBUFFER_BYTES (128 * 1024) /* 预缓冲门槛 = minimp3 MINIMP3_IO_SIZE(F3 实测强制) */
|
||
#define STREAM_RETRY_MAX 1 /* 阶段3(§21.8/§4.1):首开声前失败重试上限(1 次,防风控) */
|
||
#define STREAM_RETRY_BACKOFF_MS 800 /* 重试退避:避免连环请求轰炸(真实 NCM 节制,§7 风控) */
|
||
|
||
/* song_download_attempt 返回哨兵(与 NCM_ERR_* 不冲突) */
|
||
#define DL_RETRYABLE (-1001) /* 预缓冲前网络失败:可整首重试 */
|
||
#define DL_ABORTED (-1002) /* 用户切歌/退出:静默放弃,不重试 */
|
||
|
||
typedef struct {
|
||
stream_src_t *src; /* 读端(下载路径 written 由下载线程推进;缓存命中=静态源) */
|
||
ncm_cache_writer_t *w; /* 下载路径写端(commit/abort 归播放线程);缓存命中=NULL */
|
||
pthread_t dl_thr;
|
||
int dl_thr_on;
|
||
int dl_rc; /* 下载线程结果(NCM_OK / 负错误码) */
|
||
char url[1024];/* 下载线程用(song_local_file 栈上 u.url 生命周期不够,拷贝) */
|
||
size_t maxb; /* 下载限幅 */
|
||
int64_t total; /* 标称大小(song_url size;0=未知;mp3 seek 分母) */
|
||
/* 阶段3 .part 续传(§5.2) */
|
||
uint64_t resume_base; /* >0=断点续传起点(.part 大小);Http 带 Range: bytes=<n>- */
|
||
int first_body; /* 首个 body 回调未处理(按响应状态切换写姿态) */
|
||
int *st_ptr; /* 指向 dl_thread 的 status 局部变量(同线程,无竞态) */
|
||
int need_fresh; /* 416/其它:服务器拒绝续传 → 放 .part、交调用方整首重下 */
|
||
uint64_t skip; /* 200 忽略 Range:body 前 skip 字节与 .part 前缀重复,丢弃 */
|
||
} song_src_t;
|
||
|
||
/* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 +
|
||
* 锁内推进 written + broadcast 唤醒读端 + 进度反馈。
|
||
* 阶段3 续传(§5.2):首 body 字节按 response status 切换写姿态——
|
||
* 206(服务器支持 Range):正常追加到 .part 尾;
|
||
* 200(服务器忽略 Range):body 头 resume_base 字节与 .part 前缀重复 → skip 丢弃;
|
||
* 416/其它:放弃续传(need_fresh=1,abort),交调用方删残体整首重下。 */
|
||
static int stream_dl_cb(void *ctx, const unsigned char *data, size_t len)
|
||
{
|
||
song_src_t *ss = (song_src_t *)ctx;
|
||
if (stream_src_aborted(ss->src)) return 1; /* 切歌/停止:中止 http */
|
||
if (ss->first_body) {
|
||
ss->first_body = 0;
|
||
int st = ss->st_ptr ? *ss->st_ptr : 0;
|
||
if (ss->resume_base > 0 && st != 206 && st != 200) {
|
||
ss->need_fresh = 1; /* 416/其它:放弃续传 */
|
||
return 1; /* abort http(error body 不入盘) */
|
||
}
|
||
if (ss->resume_base > 0 && st == 200) ss->skip = ss->resume_base; /* 全量头段与 .part 重复 */
|
||
}
|
||
if (ss->skip) { /* 丢弃重复前缀字节 */
|
||
if (ss->skip >= len) { ss->skip -= len; return 0; }
|
||
data += (size_t)ss->skip; len -= (size_t)ss->skip; ss->skip = 0;
|
||
}
|
||
if (ncm_cache_write(ss->w, data, len) != NCM_OK) return 1;
|
||
stream_src_note_written(ss->src, (uint64_t)len);
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.dl_done += (int64_t)len;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
return 0;
|
||
}
|
||
|
||
static void *dl_thread(void *arg)
|
||
{
|
||
song_src_t *ss = (song_src_t *)arg;
|
||
ncm_http_req_t req;
|
||
memset(&req, 0, sizeof req);
|
||
req.url = ss->url;
|
||
req.method = "GET";
|
||
/* 续传:Range 头(§5.2)。URL 为本次新拉签名(不复用旧 URL,风控不增请求)。
|
||
* UA 现状保留;Connection: close 由 http 层补。 */
|
||
char hdrs[1200];
|
||
if (ss->resume_base > 0)
|
||
snprintf(hdrs, sizeof hdrs,
|
||
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\n"
|
||
"Range: bytes=%llu-", (unsigned long long)ss->resume_base);
|
||
else
|
||
snprintf(hdrs, sizeof hdrs,
|
||
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36");
|
||
req.extra_hdrs = hdrs;
|
||
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
|
||
req.timeout_ms = 15000; /* 绝对段(连接/握手/头)上限;body 空闲超时 15s(P2) */
|
||
|
||
/* 状态感知下载:首 body 前写入 *status(续传分支)。同线程,st_ptr 无竞态。 */
|
||
int status = 0;
|
||
ss->st_ptr = &status;
|
||
ss->first_body = 1;
|
||
ncm_http_resp_t resp;
|
||
int rc = ncm_http_download_status(&req, stream_dl_cb, ss, ss->maxb, &resp, &status);
|
||
ss->st_ptr = NULL;
|
||
if (rc == NCM_OK) {
|
||
/* 放行 200 与(续传时的)206;其余按 HTTP 错。续传遇 200=服务器忽略 Range(skip 已丢头段)。 */
|
||
int ok_st = (ss->resume_base > 0) ? (resp.status == 206 || resp.status == 200)
|
||
: (resp.status == 200);
|
||
if (!ok_st) rc = NCM_ERR_HTTP;
|
||
}
|
||
ss->dl_rc = rc;
|
||
/* 结束通知:成功 final=全长;失败/中止 final=截断点+failed(读端播到截断点自然结束,F5) */
|
||
stream_src_finish(ss->src, rc == NCM_OK ? 0 : rc);
|
||
return NULL;
|
||
}
|
||
|
||
/* 收尾(播放线程,解码循环退出后调用):中止在途下载并 join → commit(rename 转正) 或
|
||
* abort(阶段3:中断保留 .part 供下次续传;拒绝续传的 416 删残体)。 */
|
||
static void song_src_dispose(song_src_t *ss)
|
||
{
|
||
if (ss->dl_thr_on) {
|
||
stream_src_abort(ss->src); /* 已完成则无副作用;在途则 cb 返回 NZ 中止 */
|
||
pthread_join(ss->dl_thr, NULL);
|
||
ss->dl_thr_on = 0;
|
||
}
|
||
if (ss->w) {
|
||
if (ss->dl_rc == NCM_OK) {
|
||
/* 下载完整:commit 转正进 LRU 池(rename 不影响读端已开 fd,此处已近曲终) */
|
||
if (ncm_cache_commit(ss->w) == 0)
|
||
ncm_cache_evict(&g_st.ctx.cache); /* 写后检查:新落盘可能推过上限 */
|
||
} else if (ss->need_fresh) {
|
||
/* 服务器拒绝续传(416/其它):残体不可再续,删(下首整首重下) */
|
||
ncm_cache_abort(ss->w);
|
||
} else {
|
||
/* 中断(切歌/断流):保留 .part 残体,供下次播放 Range 续传(§5.2) */
|
||
ncm_cache_abort_part(ss->w);
|
||
}
|
||
ss->w = NULL;
|
||
}
|
||
if (ss->src) { stream_src_close(ss->src); ss->src = NULL; }
|
||
}
|
||
|
||
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
|
||
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。
|
||
* §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */
|
||
static void audio_cache_key(char *out, size_t sz, const char *id, int level)
|
||
{
|
||
snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level));
|
||
}
|
||
|
||
/* 同曲重播护栏(§25 切音质后重播同一曲,替代 §18 的 still_current):仅 cur/state 不够——
|
||
* 切音质时 play_manager_play
|
||
* 用的 idx 与在播曲相同(cur 不变)且置 state=BUFFER,仍会被 still_current 当「我的 intent
|
||
* 有效」而漏判。play/next 都会 seq++(新播放意图),故以 seq 判别「本次 intent 是否仍有效」。
|
||
* 调用方需持有 run_song 开始时捕获的 my_seq(本曲 intent 的 seq)。 */
|
||
static int still_intent(int idx, int my_seq)
|
||
{
|
||
pthread_mutex_lock(&g_play.mu);
|
||
int ok = (g_play.cur_song == idx) && (g_play.state == NCM_PC_BUFFER) &&
|
||
(g_play.seq == my_seq);
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
return ok;
|
||
}
|
||
|
||
/* 一次「取 URL → 开写端(命中 .part 则续传) → 起下载线程 → 等预缓冲 ≥128KB」尝试(阶段3)。
|
||
* 返回:NCM_OK=src 就绪(下载在后台继续);DL_RETRYABLE=预缓冲前网络失败(调用方整首重试);
|
||
* DL_ABORTED=切歌/退出(静默放弃,不重试);其它负数=硬错误(不重试)。
|
||
* *real_err 回填预缓冲失败的真实错误码。失败时 ss->w/src/dl_thr 已清理干净。 */
|
||
static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const char *key, int want,
|
||
song_src_t *ss, char *path, size_t psz, int64_t *bytes,
|
||
int *real_err)
|
||
{
|
||
const char *id = s->id;
|
||
ss->resume_base = 0; ss->skip = 0; ss->need_fresh = 0; ss->first_body = 1; /* 每次尝试干净 */
|
||
/* 播放地址:用户档已在调用方按会员钳顶,此处逐档降级,接受 mp3 与 flac */
|
||
ncm_play_url_t u;
|
||
int eff = -1;
|
||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, want, &u, &eff);
|
||
if (rc != NCM_OK) return rc;
|
||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.eff_level = (eff >= 0) ? eff : want;
|
||
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
/* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */
|
||
const char *ext = "mp3";
|
||
if (ncm_streq(u.type, "flac")) ext = "flac";
|
||
else if (!u.type[0] && strstr(u.url, ".flac")) ext = "flac";
|
||
|
||
/* 写前检查:池大小超限先按 LRU 淘汰(半成品 .tmp/.part 已被 evict 跳过,无在途误删风险) */
|
||
ncm_cache_evict(&g_st.ctx.cache);
|
||
|
||
/* 断点续传起点(§5.2):.part 残体存在且本次 URL 的 size > 残体大小 → 续传:
|
||
* 追加打开 .part(O_APPEND,保留已写字节),written 从 part_size 起算(读端秒开播)。 */
|
||
uint64_t part_size = 0;
|
||
int have_part = (ncm_cache_part_size(&g_st.ctx.cache, key, ext, &part_size) == NCM_OK) &&
|
||
part_size > 0;
|
||
int resuming = 0;
|
||
if (have_part && (long)part_size < u.size) {
|
||
rc = ncm_cache_open_resume(&g_st.ctx.cache, key, ext, &ss->w);
|
||
if (rc == NCM_OK) { ss->resume_base = part_size; resuming = 1; }
|
||
else have_part = 0; /* 残体打开失败:转整首 */
|
||
}
|
||
if (!resuming) {
|
||
if (have_part) ncm_cache_part_remove(&g_st.ctx.cache, key, ext); /* 残体过期(size 不符):删 */
|
||
rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &ss->w);
|
||
if (rc != NCM_OK) return rc;
|
||
}
|
||
if (stream_src_open_stream_at(&ss->src, ncm_cache_writer_tmp_path(ss->w),
|
||
resuming ? part_size : 0) != 0) {
|
||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||
return NCM_ERR_IO;
|
||
}
|
||
ncm_strlcpy(path, ncm_cache_writer_tmp_path(ss->w), psz); /* 日志用 */
|
||
ss->total = (u.size > 0) ? u.size : 0;
|
||
/* 上限:Hi-Res flac 单曲可达数百 MB,flac 上限 1GB;mp3 维持 64MB。
|
||
* size 可信时按 size 限幅(防超额),未知按格式默认上限(续传 body ≤ size 亦安全)。 */
|
||
size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20);
|
||
ss->maxb = (u.size > 0 && (uint64_t)u.size < cap) ? (size_t)u.size : cap;
|
||
ncm_strlcpy(ss->url, u.url, sizeof ss->url);
|
||
if (pthread_create(&ss->dl_thr, NULL, dl_thread, ss) != 0) {
|
||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||
stream_src_close(ss->src); ss->src = NULL;
|
||
return NCM_ERR_IO;
|
||
}
|
||
ss->dl_thr_on = 1;
|
||
|
||
/* 预缓冲:≥128KB(minimp3 强制门槛,F3)才放行 open_cb;final/failed 提前出。
|
||
* still_intent 护栏覆盖本阻塞点:等待期间切歌/停止/同曲重播 → 保留 .part 静默放弃。 */
|
||
for (;;) {
|
||
int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100);
|
||
if (wr != 0) break;
|
||
if (g_app_quit || !still_intent(idx, seq)) {
|
||
stream_src_abort(ss->src);
|
||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||
ncm_cache_abort_part(ss->w); ss->w = NULL; /* 切歌/退出:保留残体供下次续传 */
|
||
stream_src_close(ss->src); ss->src = NULL;
|
||
return DL_ABORTED; /* 调用方 still_intent 复核后静默返回 */
|
||
}
|
||
}
|
||
if (stream_src_aborted(ss->src)) {
|
||
int frc = stream_src_failed(ss->src);
|
||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||
ncm_cache_abort_part(ss->w); ss->w = NULL; /* 中止:保留残体 */
|
||
stream_src_close(ss->src); ss->src = NULL;
|
||
return frc ? frc : NCM_ERR_NET;
|
||
}
|
||
if (stream_src_failed(ss->src) && stream_src_written(ss->src) < PREBUFFER_BYTES) {
|
||
int frc = stream_src_failed(ss->src);
|
||
if (real_err) *real_err = frc;
|
||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||
/* 预缓冲前网络失败:删残体(不保留 .part),交调用方整首重试(含续传失败重下)。
|
||
* 已下过门槛的断流照常放行——读端播到截断点自然结束(F5)。 */
|
||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||
stream_src_close(ss->src); ss->src = NULL;
|
||
return DL_RETRYABLE;
|
||
}
|
||
/* final < 门槛(小文件/截断):照样放行,open_cb 读到的就是全部 */
|
||
*bytes = ss->total > 0 ? ss->total : (int64_t)stream_src_written(ss->src);
|
||
return NCM_OK;
|
||
}
|
||
|
||
/* 得到本地音频源(§21 边下边播)。命中缓存 → 静态 stream_src(final=size,read 不阻塞,
|
||
* 顺带修 P1:不再 mmap 整文件);否则 URL 降级 → 起下载线程写 .tmp → 预缓冲 ≥128KB 即返回
|
||
* (下载在后台继续,解码经 stream_src read_cb 跟播)。
|
||
* 返回 NCM_OK 或负错误码;成功时 ss->src 就绪、ss->total 与 *bytes 回填(下载未完成时
|
||
* total=song_url 标称 size,0=未知)。s 为播放曲快照(run_song 物化,与窗口解耦 §17)。
|
||
* §20 权限/会员:
|
||
* - fee 门禁(BIZ_NOTES §3 社区口径):fee∈{1,4}(VIP 才能听/付费专)且当前非会员
|
||
* → 直接 NCM_ERR_VIP,**不发 song_url 请求**(省一次请求,防风控);fee=0/8 放行
|
||
* (8=免费听低音质,播放允许)。本地缓存命中优先于门禁(缓存可播不拦)。
|
||
* - VIP 档上限:用户档超会员权限不硬拒,播放时先钳到会员允许的最高档再逐级降级
|
||
* (免去注定空 URL 的高档请求);实际生效档回填 g_play.eff_level。 */
|
||
static int song_local_file(const ncm_song_t *s, int idx, int seq, song_src_t *ss,
|
||
char *path, size_t psz, int64_t *bytes)
|
||
{
|
||
memset(ss, 0, sizeof *ss);
|
||
const char *id = s->id;
|
||
char key[64];
|
||
audio_cache_key(key, sizeof key, id, g_level);
|
||
|
||
/* 缓存命中 → 静态 stream_src(新键两扩展名都试;档位=用户设定档,降级产物也归该档键) */
|
||
int hit = 0;
|
||
if (ncm_cache_lookup(&g_st.ctx.cache, key, "flac", path, psz) == NCM_OK ||
|
||
ncm_cache_lookup(&g_st.ctx.cache, key, "mp3", path, psz) == NCM_OK) {
|
||
hit = 1;
|
||
} else if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
|
||
/* 旧键温和兼容(§19 前存量:无档位后缀的 song_<id>[.]mp3,含历史双点名):
|
||
* 命中即播,不主动改名/删除,由 LRU 自然淘汰。 */
|
||
hit = 2;
|
||
}
|
||
if (hit) {
|
||
struct stat st;
|
||
if (stat(path, &st) != 0) return NCM_ERR_NOENT;
|
||
if (stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) != 0)
|
||
return NCM_ERR_IO;
|
||
ss->total = (int64_t)st.st_size;
|
||
*bytes = ss->total;
|
||
/* 生效档推断(缓存键=用户档,降级产物归用户档键,无法精确还原):
|
||
* mp3 实体 + 用户档 > 极高 → 必为降级产物,标 EXHIGH;其余按用户档显示。
|
||
* (flac 实体视为与用户档一致;hires→lossless 档内降级会高估显示,已知取舍。) */
|
||
const char *dot = strrchr(path, '.');
|
||
int eff = g_level;
|
||
if (dot && ncm_streq(dot, "mp3") && g_level > NCM_LEVEL_EXHIGH)
|
||
eff = NCM_LEVEL_EXHIGH;
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.eff_level = eff;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
return NCM_OK;
|
||
}
|
||
|
||
/* fee 门禁:VIP 才能听(1=VIP 歌曲 / 4=付费数字专辑)且非会员 → 不请求 url。
|
||
* (必须在缓存查找之后:已缓存的过期/下架曲本地可播,NCM_CACHE_DESIGN §2.4。) */
|
||
if ((s->fee == 1 || s->fee == 4) && account_vip() < NCM_VIP_CVIP)
|
||
return NCM_ERR_VIP;
|
||
|
||
/* 播放下载(阶段3 重试策略 §4.1,风控 §7:真实 NCM 请求节制):用户档先按会员权限
|
||
* 钳顶,再逐档降级(§20)。预缓冲前网络失败重试 STREAM_RETRY_MAX 次(退避
|
||
* STREAM_RETRY_BACKOFF_MS 防连环轰炸),重试每次重新 song_url(新签名 URL,eapi 便宜);
|
||
* 硬错误/切歌/退出不重试。返回值:NCM_OK=src 就绪;DL_ABORTED=切歌/退出;其它负=错误。 */
|
||
int want = g_level;
|
||
int vip_cap = account_level_cap();
|
||
if (want > vip_cap) want = vip_cap;
|
||
int last_rc = NCM_ERR_NET;
|
||
for (int attempt = 0; attempt <= STREAM_RETRY_MAX; attempt++) {
|
||
if (attempt > 0) {
|
||
meow_log(MEOW_LOG_WARN, "play: download retry %d/%d id=%s backoff=%dms",
|
||
attempt, STREAM_RETRY_MAX, id, STREAM_RETRY_BACKOFF_MS);
|
||
usleep(STREAM_RETRY_BACKOFF_MS * 1000); /* 退避:不轰炸 */
|
||
if (g_app_quit || !still_intent(idx, seq)) return NCM_ERR_IO;
|
||
}
|
||
int real_err = NCM_ERR_NET;
|
||
int rc = song_download_attempt(s, idx, seq, key, want, ss, path, psz, bytes, &real_err);
|
||
if (rc == NCM_OK) return NCM_OK;
|
||
if (rc == DL_RETRYABLE) { last_rc = real_err; continue; } /* 重试整首 */
|
||
return rc; /* DL_ABORTED / 硬错误:不重试 */
|
||
}
|
||
return last_rc; /* 重试耗尽:最后一次网络错误 */
|
||
}
|
||
|
||
static void set_err(int err, const char *msg)
|
||
{
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.err = err;
|
||
if (msg) { ncm_strlcpy(g_play.errmsg, msg, sizeof g_play.errmsg); }
|
||
g_play.state = NCM_PC_STOPPED;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
/* 一首歌:源就绪(缓存/下载线程+预缓冲)→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
|
||
static void run_song(int idx)
|
||
{
|
||
/* §17 滑窗兼容:物化目标曲(窗内命中 / 盘缓存 / song_detail 单曲)并整体快照进
|
||
* g_play.play_song——之后本曲所有字段读快照,窗口滑出当前播放曲不影响播放。 */
|
||
/* §25:本条 run_song 服务的 play intent 用 seq 标识(play/next 递增)。切音质后重播
|
||
* 同一曲(同 idx 再 play)也会 seq++,故「我的 intent 是否仍有效」必须以 seq 判别
|
||
* (仅 cur/state 不够:同 idx 重播时 cur 不变、state 仍 BUFFER,详见 still_intent)。 */
|
||
pthread_mutex_lock(&g_play.mu);
|
||
int my_seq = g_play.seq;
|
||
int my_cur = g_play.cur_song;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
if (my_cur != idx) return; /* 外层已同步过,防御 */
|
||
|
||
ncm_song_t s;
|
||
if (appstate_queue_song(idx, &s) != 0) { /* §23:按 queue_src 取(窗口 或 搜索队列) */
|
||
set_err(NCM_ERR_ARG, "load failed");
|
||
return;
|
||
}
|
||
if (!still_intent(idx, my_seq)) return; /* 物化期间已切歌/重播:不写过期快照 */
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.play_song = s;
|
||
g_play.play_song_valid = 1;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
|
||
song_src_t ss;
|
||
int64_t file_bytes = 0;
|
||
char path[512];
|
||
int rc = song_local_file(&s, idx, my_seq, &ss, path, sizeof path, &file_bytes);
|
||
if (rc != NCM_OK) {
|
||
if (!still_intent(idx, my_seq)) return; /* 期间已切歌/同曲重播:静默放弃(已落盘部分不亏) */
|
||
const char *m = (rc == NCM_ERR_VIP) ? "VIP only (fee gate)"
|
||
: (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
|
||
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
|
||
set_err(rc, m);
|
||
return;
|
||
}
|
||
if (!still_intent(idx, my_seq)) { /* 预缓冲完成但已切歌/同曲重播:勿开解码/勿动状态 */
|
||
song_src_dispose(&ss);
|
||
return;
|
||
}
|
||
ncm_strlcpy(g_path, path, sizeof g_path);
|
||
g_file_bytes = file_bytes;
|
||
|
||
int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0;
|
||
|
||
/* 解码分发(§19/§21):stream_src 上 magic 探测 fLaC→dr_flac / 其它→minimp3(open_cb
|
||
* SEEK_TO_BYTE|DO_NOT_SCAN);rate/ch/bits 按实际流。 */
|
||
ncm_decoder_t *dec = NULL;
|
||
int rate = 0, ch = 0, bits = 0;
|
||
int64_t total_frames = 0;
|
||
if (ncm_dec_open_src(&dec, ss.src, &rate, &ch, &bits, &total_frames) != 0) {
|
||
song_src_dispose(&ss);
|
||
if (!still_intent(idx, my_seq)) return; /* open 期间(flac 元数据等待)已切歌/重播 */
|
||
set_err(NCM_ERR_IO, "decode open failed"); return;
|
||
}
|
||
if (dur <= 0 && total_frames > 0 && rate > 0) dur = total_frames * 1000 / rate;
|
||
|
||
/* 音频输出:按实际流参数开(flac ≤16bit→S16,>16bit→S32;audiod 侧格式归一+原生率跟随) */
|
||
if (audio_out_open(rate, ch, bits) != 0) {
|
||
ncm_dec_close(dec); song_src_dispose(&ss);
|
||
set_err(NCM_ERR_IO, "audio open failed"); return;
|
||
}
|
||
|
||
/* 状态→播放 */
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.state = NCM_PC_PLAYING;
|
||
g_play.pos_ms = 0;
|
||
g_play.dur_ms = dur;
|
||
g_play.err = 0;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
meow_log(MEOW_LOG_INFO, "play: start idx=%d id=%s fmt=%s %dHz/%dch/%dbit dur=%lldms level=%s eff=%s",
|
||
idx, s.id, ncm_dec_format(dec), rate, ch, bits, (long long)dur,
|
||
ncm_api_level_key(g_level), ncm_api_level_key(g_play.eff_level >= 0 ? g_play.eff_level : g_level));
|
||
|
||
/* 单块缓冲:4096 帧 × ≤2ch × 4B(S32 上限)= 32KB,静态(播放线程单实例,省 32KB 栈)。 */
|
||
static int32_t pcm[PCM_CHUNK_FRAMES * 2];
|
||
int64_t frames_done = 0; /* 已输出帧(每通道) */
|
||
int need_seek_ms = -1;
|
||
int paused_edge = 0; /* 暂停沿:进暂停已向 audiod 发 pause */
|
||
int stalled_edge = 0; /* §21 停滞沿:已对 audiod pause(1),待数据恢复 pause(0) */
|
||
int underruns_seen = 0;
|
||
|
||
for (;;) {
|
||
if (g_app_quit) break;
|
||
|
||
/* 取命令 */
|
||
pthread_mutex_lock(&g_play.mu);
|
||
int cmd = g_play.cmd; g_play.cmd = 0;
|
||
int64_t seek_ms = g_play.cmd_seek_ms;
|
||
int cur = g_play.cur_song;
|
||
int st = g_play.state;
|
||
int seq = g_play.seq;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
|
||
if (cur != idx) { meow_log(MEOW_LOG_INFO, "play: switch idx=%d -> %d", idx, cur); break; } /* 切歌 → 回外层 */
|
||
if (st == NCM_PC_STOPPED) { meow_log(MEOW_LOG_INFO, "play: stopped idx=%d", idx); break; }
|
||
if (seq != my_seq) { /* §25:同 idx 新播放意图(切音质后重播同一曲)——seq 变
|
||
* 即新 intent,必须打断当前曲回外层重跑(换档重播)。 */
|
||
meow_log(MEOW_LOG_INFO, "play: new intent idx=%d seq=%d->%d -> restart", idx, my_seq, seq);
|
||
break; /* 当前曲(旧档)立即停,外层按新 cur/state 重跑 run_song */
|
||
}
|
||
|
||
if (cmd == NCM_CTL_SEEK && seek_ms >= 0) { need_seek_ms = (int)seek_ms; }
|
||
|
||
if (st == NCM_PC_PAUSED) {
|
||
if (!paused_edge) { /* 进暂停:停 PCM 时钟但不丢流(audiod MSG_AUDIO_PAUSE) */
|
||
paused_edge = 1;
|
||
meow_log(MEOW_LOG_INFO, "play: paused idx=%d pos=%lldms", idx,
|
||
rate > 0 ? (long long)(frames_done * 1000 / rate) : 0);
|
||
audio_out_pause(1);
|
||
}
|
||
usleep(40 * 1000);
|
||
continue; /* 冻结 */
|
||
}
|
||
if (paused_edge) { /* 出暂停:恢复 audiod 流(流死则自动重开) */
|
||
paused_edge = 0;
|
||
meow_log(MEOW_LOG_INFO, "play: resumed idx=%d", idx);
|
||
if (audio_out_pause(0) != 0) { set_err(NCM_ERR_IO, "audio resume failed"); break; }
|
||
}
|
||
|
||
if (need_seek_ms >= 0) {
|
||
/* seek:flac 精确帧 / mp3 字节近似(MP3D_SEEK_TO_BYTE 直接 seek,无需关重开);
|
||
* 目标超已下载区时 read_cb 自然阻塞等待(F4),输出重开 */
|
||
meow_log(MEOW_LOG_INFO, "play: seek idx=%d -> %dms", idx, need_seek_ms);
|
||
audio_out_close();
|
||
if (ncm_dec_seek_ms(dec, need_seek_ms, g_file_bytes, dur) != 0) {
|
||
set_err(NCM_ERR_IO, "seek failed"); break;
|
||
}
|
||
if (audio_out_open(rate, ch, bits) != 0) { set_err(NCM_ERR_IO, "audio reopen failed"); break; }
|
||
frames_done = (int64_t)need_seek_ms * rate / 1000;
|
||
need_seek_ms = -1;
|
||
continue;
|
||
}
|
||
|
||
/* §21 停滞联动:read_cb 单次等待 >1s(audiod 流饥饿风险)→ 先对 audiod pause(1)
|
||
* 保住流(暂停路径现状已有),数据恢复后 pause(0)(流死自动重开)。 */
|
||
if (stream_src_take_stalled(ss.src) && !stalled_edge) {
|
||
stalled_edge = 1;
|
||
meow_log(MEOW_LOG_INFO, "play: stalled idx=%d -> audio pause(1)", idx);
|
||
audio_out_pause(1);
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.stalled = 1;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
int64_t frames = ncm_dec_read(dec, pcm, PCM_CHUNK_FRAMES);
|
||
if (frames == 0) { /* 播完(下载中断时=截断点,F5) */
|
||
meow_log(MEOW_LOG_INFO, "play: done idx=%d frames=%lld", idx, (long long)frames_done);
|
||
break;
|
||
}
|
||
/* 写失败(流死/audiod 断链):置错停播——旧版静默 break 会被外层当「自然播完」
|
||
* 自动顺延下一首,流死时连环跳歌并狂发 song_url 请求(§18 修)。 */
|
||
if (audio_out_write(pcm, (int)frames) != (int)frames) {
|
||
if (!g_app_quit) set_err(NCM_ERR_IO, "audio write failed");
|
||
break;
|
||
}
|
||
frames_done += frames;
|
||
if (stalled_edge) { /* 数据恢复:解除停滞 pause */
|
||
stalled_edge = 0;
|
||
meow_log(MEOW_LOG_INFO, "play: stall recovered idx=%d -> audio pause(0)", idx);
|
||
if (audio_out_pause(0) != 0) { set_err(NCM_ERR_IO, "audio resume failed"); break; }
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.stalled = 0;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
/* underrun 统计落 g_play(§21,设置页/日志可见;变化才持锁写) */
|
||
{
|
||
int u; uint64_t wm;
|
||
stream_src_stats(ss.src, &u, &wm);
|
||
if (u != underruns_seen) {
|
||
underruns_seen = u;
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.underruns = u;
|
||
g_play.underrun_ms = wm;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
meow_log(MEOW_LOG_INFO, "play: underrun idx=%d count=%d wait=%llums",
|
||
idx, u, (unsigned long long)wm);
|
||
}
|
||
}
|
||
/* 更新进度(~200ms 节流)*/
|
||
int64_t pos = rate > 0 ? frames_done * 1000 / rate : 0;
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.pos_ms = pos;
|
||
if (dur > 0 && pos >= dur) g_play.pos_ms = dur;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
}
|
||
|
||
audio_out_close();
|
||
ncm_dec_close(dec);
|
||
/* 下载完成由播放线程 commit(rename) / 中断 abort(阶段 2 语义,§21) */
|
||
song_src_dispose(&ss);
|
||
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
|
||
}
|
||
|
||
/* 外层播放线程循环 */
|
||
static void *player_thread(void *arg)
|
||
{
|
||
(void)arg;
|
||
while (g_want_run && !g_app_quit) {
|
||
pthread_mutex_lock(&g_play.mu);
|
||
int cur = g_play.cur_song;
|
||
int st = g_play.state;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
|
||
if (cur < 0 || cur >= appstate_queue_count() || st == NCM_PC_STOPPED) {
|
||
usleep(60 * 1000);
|
||
continue;
|
||
}
|
||
/* 以本地快照播放(idx 被外层线程独占,无需再锁) */
|
||
run_song(cur);
|
||
|
||
pthread_mutex_lock(&g_play.mu);
|
||
int cur2 = g_play.cur_song;
|
||
int st2 = g_play.state;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
/* 自然播完(decode 循环正常退出,state 仍 PLAYING)且无新命令 → 顺延下一首;
|
||
* 若是因同 idx 重播(seq 变、state=BUFFER)返回,则循环顶会按当前 cur/state 重跑
|
||
* run_song(§25:切音质后重播同一曲换档生效),**不得**当播完顺延下一首。 */
|
||
int qc = appstate_queue_count();
|
||
if (cur2 >= 0 && cur2 < qc && st2 == NCM_PC_PLAYING && cur2 == cur) {
|
||
int nx = cur + 1;
|
||
if (nx < qc) { player_manager_next(1); }
|
||
else { player_manager_stop(); }
|
||
} else {
|
||
usleep(50 * 1000);
|
||
}
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* ---- 歌词后台加载(二期):UI 不阻塞 —— 持久 worker 监视当前播放歌曲,首播/切歌/自动顺延
|
||
* 时在独立线程拉取并解析,UI 经 lyric_state() 判「加载中 / 无词」。 ----
|
||
* §17:曲目 id 改读播放快照(g_play.play_song,mu 保护),不再依赖窗口内
|
||
* g_st.songs[]——窗口滑出当前播放曲歌词照常。 */
|
||
static pthread_mutex_t g_lyrmu = PTHREAD_MUTEX_INITIALIZER;
|
||
static pthread_t g_lyr_thr;
|
||
static int g_lyr_thr_on;
|
||
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
|
||
static char g_lyr_song_id[24]; /* 歌词归属歌曲 id */
|
||
|
||
static void *lyric_worker(void *arg)
|
||
{
|
||
(void)arg;
|
||
char last_id[24] = "";
|
||
while (!g_app_quit) {
|
||
usleep(300 * 1000);
|
||
char id[24];
|
||
pthread_mutex_lock(&g_play.mu);
|
||
int idx = g_play.cur_song;
|
||
int have = g_play.play_song_valid;
|
||
ncm_strlcpy(id, have ? g_play.play_song.id : "", sizeof id);
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
if (idx < 0 || idx >= appstate_queue_count() || !have) {
|
||
lyric_clear();
|
||
pthread_mutex_lock(&g_lyrmu);
|
||
g_lyr_song_id[0] = 0;
|
||
g_lyr_state = 0;
|
||
pthread_mutex_unlock(&g_lyrmu);
|
||
last_id[0] = 0;
|
||
continue;
|
||
}
|
||
if (!id[0]) continue;
|
||
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
|
||
|
||
ncm_strlcpy(last_id, id, sizeof last_id);
|
||
lyric_clear();
|
||
pthread_mutex_lock(&g_lyrmu);
|
||
g_lyr_state = 1; /* 加载中 */
|
||
pthread_mutex_unlock(&g_lyrmu);
|
||
|
||
char *lrc = (char *)malloc(64 * 1024);
|
||
char *tl = (char *)malloc(32 * 1024);
|
||
if (!lrc || !tl) {
|
||
free(lrc); free(tl);
|
||
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
|
||
continue;
|
||
}
|
||
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
|
||
if (rc == NCM_OK) lyric_parse(lrc, tl);
|
||
free(lrc); free(tl);
|
||
|
||
pthread_mutex_lock(&g_lyrmu);
|
||
ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id);
|
||
g_lyr_state = 2; /* 拉完(可能 0 行=无词) */
|
||
pthread_mutex_unlock(&g_lyrmu);
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
static void lyric_ensure_worker(void)
|
||
{
|
||
pthread_mutex_lock(&g_lyrmu);
|
||
if (!g_lyr_thr_on) {
|
||
g_lyr_thr_on = 1;
|
||
if (pthread_create(&g_lyr_thr, NULL, lyric_worker, NULL) == 0) pthread_detach(g_lyr_thr);
|
||
else g_lyr_thr_on = 0;
|
||
}
|
||
pthread_mutex_unlock(&g_lyrmu);
|
||
}
|
||
|
||
/* 为当前曲目「异步」拉歌词:确保后台 worker 在跑即返回(UI 不阻塞网络/解析)。
|
||
* 实际拉取+解析在 worker 线程(约 0.3s 内开始),结束置 lyric_state()=2。 */
|
||
int player_manager_load_lyric(void)
|
||
{
|
||
lyric_ensure_worker();
|
||
return NCM_OK;
|
||
}
|
||
|
||
/* 歌词加载状态(UI 判「loading / no lyric」)。 */
|
||
int lyric_state(void)
|
||
{
|
||
int s;
|
||
pthread_mutex_lock(&g_lyrmu);
|
||
s = g_lyr_state;
|
||
pthread_mutex_unlock(&g_lyrmu);
|
||
return s;
|
||
}
|
||
|
||
int player_manager_start(void)
|
||
{
|
||
if (g_thr_on) return 0;
|
||
g_want_run = 1;
|
||
g_level = g_st.level;
|
||
g_cache_gb = g_st.cache_limit_gb;
|
||
pthread_mutex_init(&g_play.mu, NULL);
|
||
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
|
||
if (pthread_create(&g_thr, NULL, player_thread, NULL) != 0) return -1;
|
||
g_thr_on = 1;
|
||
return 0;
|
||
}
|
||
|
||
void player_manager_stop_thread(void)
|
||
{
|
||
g_want_run = 0;
|
||
g_app_quit = 1;
|
||
audio_out_close(); /* 打断阻塞写 */
|
||
if (g_thr_on) {
|
||
pthread_join(g_thr, NULL);
|
||
g_thr_on = 0;
|
||
}
|
||
}
|