Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/play/player_manager.c
T

1149 lines
59 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.
/* 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"
#include "mem_watch.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;
/* 任务3(§re-entry):pending_replay=1 表示「切音质/缓存后点了正在播的同曲」——此时重播须换档
* (P12 语义,seq++),不得被「同曲不重播」护栏吞掉。set_quality 置位;player_manager_play 的
* 同曲护栏据它分流:pending=1 走重播(换档),pending=0 只聚焦不重播(点正在播的曲)。 */
static int g_pending_replay;
/* ---- 播放模式三态(REPORT §NEW:随机 / 列表循环 / 单曲循环)----
* 运行时态在 g_play.play_mode(mu 保护,播放线程读);持久化走 g_st.play_mode(appstate)。
* 随机下一首用 xorshift32(种懒初始化自 ncm_now_ms ^ 本变量地址,避免恒同序列);
* 随机「上一首」用有界历史栈回溯(next/自动顺延 压栈当前曲目,prev 弹栈回放)。 */
static uint32_t g_shuffle_seed;
#define SHUFFLE_HIST_DEPTH 32
static int g_shuffle_hist[SHUFFLE_HIST_DEPTH];
static int g_shuffle_hist_len;
/* 随机取一个 ≠cur 的队列下标(队列>1 时;队列≤1 时返回 cur——单曲等价重播)。持 g_play.mu。 */
static int shuffle_pick(int cur, int cnt)
{
if (cnt <= 1) return (cur >= 0 && cur < cnt) ? cur : 0;
if (g_shuffle_seed == 0)
g_shuffle_seed = ((uint32_t)ncm_now_ms() ^ (uint32_t)(uintptr_t)&g_shuffle_seed) | 1u;
uint32_t x = g_shuffle_seed;
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
g_shuffle_seed = x;
int r = (int)(x % (uint32_t)cnt);
if (r < 0) r = 0;
if (r == cur) r = (r + 1) % cnt;
return r;
}
/* 随机历史压栈/弹栈(持 g_play.mu;换模式时清空)。 */
static void shuffle_hist_push(int cur)
{
if (cur < 0) return;
if (g_shuffle_hist_len >= SHUFFLE_HIST_DEPTH) {
memmove(g_shuffle_hist, g_shuffle_hist + 1,
(size_t)(SHUFFLE_HIST_DEPTH - 1) * sizeof(int));
g_shuffle_hist_len = SHUFFLE_HIST_DEPTH - 1;
}
g_shuffle_hist[g_shuffle_hist_len++] = cur;
}
static int shuffle_hist_pop(void)
{
return (g_shuffle_hist_len > 0) ? g_shuffle_hist[--g_shuffle_hist_len] : -1;
}
int player_manager_play_mode(void)
{
int m; pthread_mutex_lock(&g_play.mu); m = g_play.play_mode; pthread_mutex_unlock(&g_play.mu);
return m;
}
void player_manager_set_play_mode(int m)
{
if (m < NCM_MODE_SHUFFLE || m >= NCM_MODE_COUNT) return;
pthread_mutex_lock(&g_play.mu);
if (m != g_play.play_mode) {
g_play.play_mode = m;
g_shuffle_hist_len = 0; /* 换模式清随机历史(上一首不再回溯旧队列) */
}
pthread_mutex_unlock(&g_play.mu);
}
/* 被抢占回调(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);
g_pending_replay = 1; /* 任务3:改档后点正在播的同曲 → 强制换档重播(§25 P12) */
pthread_mutex_unlock(&g_play.mu);
}
/* 同曲重入护栏的「同曲」判据(任务:跨歌单切换后,同一队列下标指向的是**另一首歌**)。
* 裸下标(song_idx==cur_song)在换歌单(播放队列内容变化)后不再成立——实测真机日志
* 「play: re-enter current idx=0 state=2 -> no restart」:A 停在 idx=0,切到 B 后点 idx=0
* 被当「点正在播的曲」吞掉 → 新歌永不起播、播放页停在上首+暂停;恢复播放(旧曲播完自动顺延
* 到不同下标)后才正常切换。故改以「候选曲 id == 正在播快照 id」判同曲:确证同曲才免重播
* 聚焦;候选未物化/队列已换(id 不符)→ 非确证,按新播放意图起播(正确方向)。
* 候选经 appstate_song_copy/appstate_search_song 做**廉价窗/数组拷贝**,绝不触网络
* (占位行未物化即返回 0,交下方正常起播)。不持 g_play.mu 调用(内部自锁读快照 id)。 */
static int play_is_same_song(int song_idx)
{
ncm_song_t cand;
int have = (appstate_get_queue() == NCM_QUEUE_SEARCH)
? (appstate_search_song(song_idx, &cand) == NCM_OK)
: (appstate_song_copy(song_idx, &cand) == NCM_OK);
if (!have || !cand.id[0]) return 0; /* 未物化:不能确证同曲 → 交下方正常起播 */
pthread_mutex_lock(&g_play.mu);
int same = g_play.play_song_valid && g_play.play_song.id[0] &&
strcmp(cand.id, g_play.play_song.id) == 0;
pthread_mutex_unlock(&g_play.mu);
return same;
}
void player_manager_play(int song_idx)
{
pthread_mutex_lock(&g_play.mu);
int is_cur = (song_idx == g_play.cur_song);
int pending = g_pending_replay;
int st = g_play.state;
pthread_mutex_unlock(&g_play.mu);
/* 任务3:点「正在播的曲」→ 只聚焦不重播;**但以曲目身份(id)而非裸下标判定**——跨歌单
* 切换后同一队列下标已指向另一首歌(真实日志 `play: re-enter current idx=0` 误吞跨歌单
* 点歌),故在 PLAYING/PAUSED 且同下标时再校验候选曲 == 正在播快照:同曲才免重播,
* 不同曲(队列已换)/未物化 → 照常起播。注意:set_quality 已置 g_pending_replay=1
* (切音质后重播同曲须换档,P12),此护栏放行。非 PLAYING/PAUSED 不受影响,照旧起播。 */
if (is_cur && !pending && (st == NCM_PC_PLAYING || st == NCM_PC_PAUSED) &&
play_is_same_song(song_idx)) {
meow_log(MEOW_LOG_INFO, "play: re-enter current idx=%d -> no restart", song_idx);
return;
}
pthread_mutex_lock(&g_play.mu);
g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */
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;
g_play.refaults = 0; /* §2.3:refault 取证随新播放意图清零 */
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);
}
/* 落地一个新播放意图(cur=idx):置 BUFFER + 清进度/错误/下载统计 + seq++(新代际,§25)。
* 持 g_play.mu。idx 可与当前 cur 相同(单曲循环重播本曲:seq++ 触 run_song 重跑换档)。 */
static void start_intent(int idx)
{
g_pending_replay = 0; /* 新播放意图落地:清「切档重播」待办 */
g_play.cur_song = idx;
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;
g_play.refaults = 0;
}
/* 手动 下一首(dir=+1) / 上一首(dir=-1):按播放模式定语义。
* - 列表循环/单曲循环:顺序 ±1(队列内回绕)。
* - 随机:下一首=随机另一首(并把当前曲压入历史);上一首=历史回溯(栈空则随机另一首)。
* 队列空/未物化(cur 越界)时不改状态。持 g_play.mu 外调用(内部自锁)。 */
void player_manager_next(int dir)
{
pthread_mutex_lock(&g_play.mu);
int mode = g_play.play_mode;
int c = g_play.cur_song;
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
int newc = -1;
if (cnt > 0) {
if (dir > 0) {
if (mode == NCM_MODE_SHUFFLE) {
shuffle_hist_push(c);
newc = shuffle_pick(c, cnt);
} else {
newc = (c < 0) ? 0 : ((c + 1) % cnt); /* 列表/单曲:顺序下一首(回绕) */
}
} else {
if (mode == NCM_MODE_SHUFFLE) {
int h = shuffle_hist_pop();
newc = (h >= 0 && h < cnt) ? h : shuffle_pick(c, cnt);
} else {
newc = (c < 0) ? 0 : ((c - 1 + cnt) % cnt); /* 列表/单曲:顺序上一首(回绕) */
}
}
}
if (newc >= 0 && newc < cnt && newc != c)
start_intent(newc);
pthread_mutex_unlock(&g_play.mu);
}
/* 曲目自然播完(decode 循环退出、state 仍 PLAYING)→ 按播放模式决定下一首:
* - 单曲循环:重播本曲(start_intent(c),seq++ → run_song 重跑)。
* - 列表循环:顺序下一首(队尾回绕队首)。
* - 随机:随机另一首(并压栈当前曲供上一首回溯)。
* 队列空/cur 非法 → 停止。持 g_play.mu 外调用(内部自锁)。 */
void player_manager_next_intent(void)
{
pthread_mutex_lock(&g_play.mu);
int mode = g_play.play_mode;
int c = g_play.cur_song;
int cnt = appstate_queue_count();
if (cnt <= 0 || c < 0 || c >= cnt) {
/* 无可续播目标:停止(清态) */
g_pending_replay = 0;
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);
return;
}
if (mode == NCM_MODE_SINGLE) {
start_intent(c); /* 单曲循环:重播本曲 */
} else if (mode == NCM_MODE_LISTLOOP) {
start_intent((c + 1) % cnt); /* 列表循环:顺序下一首(回绕) */
} else {
shuffle_hist_push(c); /* 随机:压栈当前曲 + 随机下一首 */
start_intent(shuffle_pick(c, cnt));
}
pthread_mutex_unlock(&g_play.mu);
}
void player_manager_stop(void)
{
pthread_mutex_lock(&g_play.mu);
g_pending_replay = 0; /* 停止:清「切档重播」待办 */
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 前缀重复,丢弃 */
/* page-cache 治理(A:meow_cachegov)登记路径:播放实际读的文件(流式=.tmp/.part,命中=final)。 */
char gov_path[512];
/* 曲终 L0 用:commit 后的正式路径(流式 commit rename 后 = final;命中=gov_path 同)。 */
char gov_final_path[512];
} song_src_t;
/* page-cache 治理(A):SDK meow_cachegov 的 read_pos 回调——返回当前解码逻辑读指针
* (stream_src 内部维护,经 stream_src_read_pos 读取;回调上下文=stream_src*)。 */
static uint64_t gov_read_pos(void *ctx)
{
return stream_src_read_pos((stream_src_t *)ctx);
}
/* 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 */
/* page-cache 治理(§2.3/§3):下载 cb 查 meow_cachegov_download_paused()(A 回退硬暂停
* OR svc_perf C Cached 降载)→ 暂停下载(限速),优先解码播放;不打断播放线程
* (读端追上下载走既有 underrun/停滞联动)。每 50ms 查一次中止,切歌可立即脱身。 */
while (meow_cachegov_download_paused() && !stream_src_aborted(ss->src))
usleep(50 * 1000);
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,此处已近曲终) */
int64_t fin = (int64_t)stream_src_final(ss->src);
meow_log(MEOW_LOG_INFO, "play: dl done id? final=%lld commit", (long long)fin);
if (ncm_cache_commit(ss->w) == 0)
ncm_cache_evict(&g_st.ctx.cache); /* 写后检查:新落盘可能推过上限 */
} else if (ss->need_fresh) {
/* 服务器拒绝续传(416/其它):残体不可再续,删(下首整首重下) */
meow_log(MEOW_LOG_WARN, "play: dl 416/refuse resume -> drop .part (dl_rc=%d)", ss->dl_rc);
ncm_cache_abort(ss->w);
} else {
/* 中断(切歌/断流):保留 .part 残体,供下次播放 Range 续传(§5.2) */
meow_log(MEOW_LOG_INFO, "play: dl interrupt dl_rc=%d kept bytes=%lld -> .part",
ss->dl_rc, (long long)stream_src_written(ss->src));
ncm_cache_abort_part(ss->w);
}
ss->w = NULL;
}
if (ss->src) { stream_src_close(ss->src); ss->src = NULL; }
}
/* page-cache 治理(A)曲终清理:注销 L1 登记条目 + 下载完整(commit 成功)则对正式文件做
* L0 整文件 DONTNEED(仅曲终安全窗口;未 commit/中断保留 .part 不丢)。 */
static void song_gov_cleanup(song_src_t *ss)
{
if (ss->gov_path[0]) meow_cachegov_unregister(ss->gov_path);
if (ss->dl_rc == NCM_OK && ss->gov_final_path[0])
meow_cachegov_drop_finished(ss->gov_final_path);
}
/* 缓存键(用户定案 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));
}
/* §档位保真(REPORT §NEW 任务2②):显示档以响应实际格式/码率为准——
* - type=flac:采信降级链 eff(无损档链已保证 flac,br/采样由解码后定)。
* - type=mp3:绝不显示 Hi-Res 及以上(Hi-Res 需 >16bit/高采样,mp3 达不到)。按响应 br
* 参与判定:≤128k→标准;否则→极高 HQ(≤320k,仅降级链中间档「较高 192k」不进 UI,不上屏)。
* - br 未知:钳 chain_eff 到 ≤EXHIGH(保守不夸大)。
* 返回 UI 显示用档位(g_play.eff_level)。 */
static int eff_level_for_resp(const ncm_play_url_t *u, int chain_eff, int want)
{
if (u && ncm_streq(u->type, "flac")) return (chain_eff >= 0) ? chain_eff : want;
int br = u ? u->br : 0;
if (br > 0) return (br <= 128000) ? NCM_LEVEL_STANDARD : NCM_LEVEL_EXHIGH; /* 按实际码率 */
int e = (chain_eff >= 0) ? chain_eff : want;
return (e < NCM_LEVEL_EXHIGH) ? e : NCM_LEVEL_EXHIGH; /* 未知:钳到 ≤EXHIGH */
}
/* 同曲重播护栏(§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; /* 每次尝试干净 */
/* 播放地址:账户/fee 感知降级(auto_av 链内钳顶+门槛跳档+fee 门禁,300 修复) */
ncm_play_url_t u;
int eff = -1;
int rc = ncm_api_song_url_auto_av(g_st.ctx.device_id, g_st.ctx.cookie, id,
account_vip(), s->fee, 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_level_for_resp(&u, eff, want); /* §2②:显示档按实际格式/码率 */
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;
/* §NEW 任务1(SIGSEGV 定位):下载链路诊断日志——起播即留档,断流/崩溃后从 log 回溯。 */
meow_log(MEOW_LOG_INFO,
"play: dl start id=%s lv=%s eff=%s size=%lld resume=%s url=%.56s",
id, ncm_api_level_key(want), ncm_api_level_key(eff >= 0 ? eff : want),
(long long)u.size, resuming ? "yes" : "no", u.url);
/* 上限: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 (ncm_thread_create(&ss->dl_thr, 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(含历史双点名 song_<id>..mp3)。
* §NEW 任务5:命中即**迁移重命名**到当前档位键 song_<id>_<level>.mp3(下方 hit==2
* 分支做 rename,不重下),之后不再回退旧键。 */
hit = 2;
}
if (hit) {
struct stat st;
if (stat(path, &st) != 0) return NCM_ERR_NOENT;
if (hit == 2) {
/* §NEW 任务5:存量旧键(无档位后缀 song_<id>.mp3 / 历史双点 song_<id>..mp3)
* 迁移重命名到当前档位键 song_<id>_<g_level>.<ext>(**不重下**,仅改名)。
* 目标已存在(该档已有正式条目)则删旧;迁移失败回退旧路径照常播。 */
char nk[64], np[512];
audio_cache_key(nk, sizeof nk, id, g_level);
const char *dot = strrchr(path, '.');
const char *ext = dot ? dot + 1 : "mp3";
if (ncm_cache_path(&g_st.ctx.cache, nk, ext, np, sizeof np) == NCM_OK) {
struct stat nst;
if (stat(np, &nst) == 0) unlink(path); /* 目标已在(或并发建成):删旧双点/旧名 */
else rename(path, np);
ncm_strlcpy(path, np, psz); /* 迁移后即用新路径(无论 rename 成败,
* 覆盖到同一目标;失败则下次会再试) */
if (stat(np, &st) != 0) return NCM_ERR_NOENT;
}
}
/* §NEW 任务2:静默降级时代产物修复(用户实锤:hires 档键下存着 mp3,播放命中旧档)。
* 用户档 > EXHIGH(无损/更高)且缓存实体是 .mp3 = flac 门禁前把 exhigh 的 mp3 写进了
* 无损域键(当年降级产物归用户档键)。迁移重命名到 exhigh 键 song_<id>_exhigh.mp3
* (选「极高」档时仍可用;目标已存在则删旧),然后本档视为未命中走下载——
* 311 的 flac 门禁会拿到真无损或正确降级。放在 hit==2 迁移块之后、静态流打开之前。 */
const char *mdot = strrchr(path, '.');
if (g_level > NCM_LEVEL_EXHIGH && mdot && ncm_streq(mdot, ".mp3")) {
char kh[64], ph[512];
audio_cache_key(kh, sizeof kh, id, NCM_LEVEL_EXHIGH);
if (ncm_cache_path(&g_st.ctx.cache, kh, "mp3", ph, sizeof ph) == NCM_OK) {
struct stat hst;
if (stat(ph, &hst) == 0) unlink(path); /* exhigh 目标已在:删旧(降级产物) */
else rename(path, ph);
meow_log(MEOW_LOG_INFO,
"play: stale-degraded id=%s lv=%s entity=mp3 -> exhigh key; redownload",
id, ncm_api_level_key(g_level));
hit = 0; /* 本档不符 → 走下载(flac 门禁取真档) */
}
}
if (hit) {
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 档内降级会高估显示,已知取舍。)
* 注:mp3 + 用户档 > 极高 的组合已被上方迁移块拦截走下载,此处仅存防御。 */
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);
mem_watch_song_begin(); /* 边播边监控:本曲峰值从当前起算(下载/解码/播放全程记录) */
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);
song_gov_cleanup(&ss); /* 未登记(register 在其后)时为 no-op,统一清理 */
mem_watch_song_end(); /* 释放钩子:join 下载线程+关源+缓冲 free+.part 收尾 */
return;
}
ncm_strlcpy(g_path, path, sizeof g_path);
g_file_bytes = file_bytes;
/* page-cache 治理(A):登记本曲缓存文件(播放实际读的路径)+ read_pos 回调,供 L1 丢已读
* 前缀 / refault 回退。cache 命中(ss.w==NULL)path=正式文件;流式 path=.tmp/.part
* (cur file 即正被 pread 的文件,DONTNEED 只作用于它;正式路径待 commit 后 L0 用)。 */
ncm_strlcpy(ss.gov_path, path, sizeof ss.gov_path);
if (ss.w) ncm_strlcpy(ss.gov_final_path, ncm_cache_writer_final_path(ss.w), sizeof ss.gov_final_path);
else ncm_strlcpy(ss.gov_final_path, path, sizeof ss.gov_final_path);
meow_cachegov_register(ss.gov_path, (uint64_t)(file_bytes > 0 ? file_bytes : 0),
gov_read_pos, ss.src);
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);
song_gov_cleanup(&ss);
mem_watch_song_end(); /* 释放钩子 */
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); song_gov_cleanup(&ss);
mem_watch_song_end(); /* 释放钩子 */
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;
int refaults_seen = 0; /* §2.3:本曲 DONTNEED 丢页重读 TF 计数(取证) */
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) */
/* §NEW 任务1(SIGSEGV 定位):播完时区分自然完成 vs 截断(下载未满/失败)。
* expected=标称 size;final=实际落盘;failed<0=中断。供真机崩溃回溯。 */
int64_t fin = (int64_t)stream_src_final(ss.src);
int frc = stream_src_failed(ss.src);
int64_t exp = ss.total > 0 ? ss.total : 0;
meow_log(MEOW_LOG_INFO,
"play: done idx=%d frames=%lld fin=%lld exp=%lld fail=%d underrun=%d",
idx, (long long)frames_done, (long long)fin, (long long)exp, frc,
(int)stream_src_written(ss.src));
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);
}
}
/* page-cache 治理(A):refault 取证落 g_play + 节拍执行 L1 丢弃/refault 回退 */
{
int rf = stream_src_refaults(ss.src);
if (rf != refaults_seen) {
refaults_seen = rf;
pthread_mutex_lock(&g_play.mu);
g_play.refaults = rf;
pthread_mutex_unlock(&g_play.mu);
meow_log(MEOW_LOG_INFO, "play: refault idx=%d count=%d", idx, rf);
}
meow_cachegov_tick();
}
/* 更新进度(~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);
song_gov_cleanup(&ss); /* page-cache 治理(A)曲终:注销 L1 条目 + commit 成功则 L0 整文件 DONTNEED */
mem_watch_song_end(); /* 调度器释放钩子:上一首缓冲完全清空(decoder 关 + 下载线程
* join + 源关 + 缓冲 free + .part 收尾)——给下一首让内存 */
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 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) {
player_manager_next_intent();
} 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; /* 同一曲已拉/在拉 */
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
* (A 回退硬暂停 OR svc_perf C Cached 降载),
* 不置 last_id → 降载恢复后下轮再拉。 */
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;
}
lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */
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);
ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */
} else {
ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */
}
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 (ncm_thread_create(&g_lyr_thr, 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;
g_play.play_mode = g_st.play_mode; /* §NEW:恢复持久化的播放模式(默认列表循环) */
g_shuffle_hist_len = 0;
g_shuffle_seed = 0;
pthread_mutex_init(&g_play.mu, NULL);
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
mem_watch_start(); /* 边播边监控(防 OOM):起监控线程读 RSS,驱下载降载 */
if (ncm_thread_create(&g_thr, 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(); /* 打断阻塞写 */
mem_watch_stop(); /* 停监控线程 */
if (g_thr_on) {
pthread_join(g_thr, NULL);
g_thr_on = 0;
}
}