Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/play/player_manager.c
T
AskaEth 68833cf642 feat(ncm): 下载落盘名用响应实际档 + 命中按文件名标实际档(M6 播放链路)
- song_download_attempt:URL/类型确定后算实际档,.part/open/commit 统一用实际档键
  (hires 请求被降级为 320k mp3 → song_<id>_exhigh.mp3),eff_level=实际档
- song_local_file:命中路径由 ncm_cache_level_from_path 取文件名档位并按容器钳顶
  (mp3 永不高于极高),旧 hires.mp3 正确显示「极高」
- 移除旧「stale-degraded 改名+强制重下」与「无档位键迁移」:老卡文件读可用、不迁移
- player_flow_sim 适配:补实际档/路径档位桩与 M5 链接桩;P7/P12 期望键改实际档,
  P13 改为旧命名命中即播、不重下不迁移
2026-11-04 21:11:00 +08:00

1193 lines
61 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 src = appstate_get_queue();
int have = (src == NCM_QUEUE_SEARCH) ? (appstate_search_song(song_idx, &cand) == NCM_OK)
: (src == NCM_QUEUE_CACHE) ? (appstate_cache_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 分母) */
/* 缓存文件元数据嵌入(NCM_CACHE_DESIGN §2.5 + M5):下载完整后由下载线程把
* id3/vorbis tag 写进 .tmp(按实际容器分支),再交播放线程 commit。失败只记日志,
* commit 裸流——播放主链路不受影响。 */
ncm_tag_meta_t tag;
char ext[8]; /* 实际容器(song_url type / url 扩展名) */
/* 阶段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;
/* 缓存文件元数据嵌入(M5):下载完整 → 把 id3/vorbis tag 流式写进 .tmp(64KB 分块,
* 同 inode 替换:解码端持有旧 fd 继续播,不受影响)。先 finish 让读端自然收尾,再重写;
* dispose 的 join 保证「重写完成 → commit」顺序。任何失败都只降级为裸流。 */
stream_src_finish(ss->src, rc == NCM_OK ? 0 : rc);
if (rc == NCM_OK && ss->w && !g_app_quit) {
int trc = ncm_tag_embed_file(ncm_cache_writer_tmp_path(ss->w), ss->ext, &ss->tag);
if (trc != NCM_OK)
meow_log(MEOW_LOG_WARN, "play: tag embed skipped rc=%d ext=%s (raw stream kept)",
trc, ss->ext);
}
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>"。
* 只用于**查找**(命中下探由 ncm_cache_lookup 处理);**写入**的正式名用响应实际档
* (M6:hires 请求被降级链落到 exhigh mp3 → 落盘为 song_<id>_exhigh.mp3,名实相符)。
* 同歌同档命中不重复下载;换档=新条目,旧档留给 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));
}
/* 响应 → 实际档(写盘文件名 / 播放页显示 / 已缓存列表三处同源,core ncm_cache_actual_level):
* 容器 type 权威;mp3 按响应 br(≤128k→标准,否则极高,永不高于极高);flac 取降级链
* 生效档 eff(头参数下载后才能测,落盘名不夸大)。只降不升(min(请求档, 实际档))。
* 返回实际档(g_play.eff_level 用)。 */
static int actual_level_for_resp(const ncm_play_url_t *u, const char *ext, int chain_eff, int want)
{
ncm_audio_info_t ai;
memset(&ai, 0, sizeof ai);
ncm_strlcpy(ai.container, ext ? ext : "", sizeof ai.container);
if (u && u->br > 0 && ncm_streq(ai.container, "mp3")) ai.bitrate_kbps = u->br / 1000;
return ncm_cache_actual_level(want, &ai, chain_eff);
}
/* 同曲重播护栏(§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, 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;
/* 落盘扩展名: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";
ncm_strlcpy(ss->ext, ext, sizeof ss->ext);
/* M6:写盘名用**响应实际档**(不是请求档)。hires 请求被降级链落到 exhigh mp3
* → song_<id>_exhigh.mp3,文件名与内容相符;同档再播按实际档键直接命中。
* .part 续传也用同一实际档键(同名才可续传/commit)。 */
int actual = actual_level_for_resp(&u, ext, eff, want);
char akey[64];
audio_cache_key(akey, sizeof akey, id, actual);
pthread_mutex_lock(&g_play.mu);
g_play.eff_level = actual; /* 显示档 = 实际档(与文件名/列表同源) */
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
pthread_mutex_unlock(&g_play.mu);
/* 文件内 tag 字段(下载完整后由 dl_thread 写入;空字段不写)。 */
memset(&ss->tag, 0, sizeof ss->tag);
ncm_strlcpy(ss->tag.title, s->name, sizeof ss->tag.title);
ncm_strlcpy(ss->tag.artist, s->art_name, sizeof ss->tag.artist);
ncm_strlcpy(ss->tag.album, s->album_name, sizeof ss->tag.album);
/* 写前检查:池大小超限先按 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, akey, 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, akey, 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, akey, ext); /* 残体过期(size 不符):删 */
rc = ncm_cache_open(&g_st.ctx.cache, akey, 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 req=%s actual=%s size=%lld resume=%s url=%.56s",
id, ncm_api_level_key(want), ncm_api_level_key(actual),
(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);
/* 已缓存列表点播(NCM_QUEUE_CACHE):用条目**真实键**(id+文件名档位)与实际扩展名直接
* 命中,不走用户当前档键——否则别的档位/降级产物条目会 miss 而被重下(与「点缓存就该放缓存」
* 相悖,也会白耗流量)。条目已被删/被 LRU 淘汰 → NOENT(不重下,UI 报错)。 */
if (appstate_get_queue() == NCM_QUEUE_CACHE) {
ncm_cache_item_t it;
if (appstate_cache_item(idx, &it) == NCM_OK) {
char ck[48];
if (it.level[0]) snprintf(ck, sizeof ck, "%s_%s", it.id, it.level);
else ncm_strlcpy(ck, it.id, sizeof ck);
if (ncm_cache_lookup(&g_st.ctx.cache, ck, it.ext, path, psz) == NCM_OK) {
struct stat st;
if (stat(path, &st) == 0 &&
stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) == 0) {
ss->total = (int64_t)st.st_size;
*bytes = ss->total;
pthread_mutex_lock(&g_play.mu);
g_play.eff_level = it.actual_level; /* 实际档(非请求档),UI 标注用 */
pthread_mutex_unlock(&g_play.mu);
return NCM_OK;
}
}
}
return NCM_ERR_NOENT;
}
/* 缓存命中 → 静态 stream_src。查找顺序(档位下探在 ncm_cache_lookup 内):
* ① 请求档精确(flac 优先于 mp3)
* ② 请求档以下逐档下探——缓存名已按**实际档**命名,hires 请求命中已存的
* song_<id>_exhigh.mp3(降级产物不重下)
* ③ 历史无档位键 song_<id>.mp3 / 双点名(老卡存量:读可用,**不强制迁移**) */
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 ||
ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
hit = 1;
}
if (hit) {
struct stat st;
if (stat(path, &st) != 0) return NCM_ERR_NOENT;
/* 生效/显示档:新命名文件名即实际档;旧命名(请求档)向下钳——mp3 实体不可能高于
* 极高,故旧 song_<id>_hires.mp3 标注 EXHIGH;旧无档位键条目按用户档同规则钳。
* 只改显示不改文件名(老卡文件继续可播/可删/可淘汰,免迁移)。 */
const char *dot = strrchr(path, '.');
int eff = ncm_cache_level_from_path(path);
if (eff < 0) eff = g_level;
if (dot && ncm_streq(dot, ".mp3") && eff > NCM_LEVEL_EXHIGH) eff = NCM_LEVEL_EXHIGH;
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;
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, 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 */
/* 歌词拉取失败后的重试退避(ms,指数翻倍封顶 OS):掉线瞬间失败的请求**不**写死「无歌词」,
* 交回退避重试;网络恢复后下一轮到点即重拉成功(真机症状:掉线一会儿再重连后恒「暂无歌词」)。 */
#define LYRIC_RETRY_BASE_MS 1000
#define LYRIC_RETRY_MAX_MS (30 * 1000)
static void *lyric_worker(void *arg)
{
(void)arg;
char last_id[24] = "";
int last_ok = 1; /* last_id 对应结果是否成功(成功不重拉;失败记退避待重试) */
int retry_delay = 0; /* 失败后逐次翻倍的退避基值(ms) */
int64_t retry_at = 0; /* 单调时刻:下次重试;0=无待重试 */
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;
last_ok = 1;
retry_delay = 0;
retry_at = 0;
continue;
}
if (!id[0]) continue;
int is_new = (strcmp(id, last_id) != 0);
if (is_new) {
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);
last_ok = 0;
retry_delay = 0;
retry_at = 0;
} else {
if (last_ok) continue; /* 同一曲已成功拉完(含 NCM_OK 空词=真无词),不重拉 */
if (meow_cachegov_prefetch_paused()) continue;
if (retry_at > ncm_monotonic_ms()) continue; /* 失败退避未到 */
/* 到点重试:保留 retry_delay 以便失败时继续翻倍 */
}
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);
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
retry_at = ncm_monotonic_ms() + retry_delay;
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) {
last_ok = 1; /* 真正结果:空词(NCM_OK 无词也算「已拉完」) */
retry_delay = 0;
retry_at = 0;
lyric_parse(lrc, tl);
ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */
} else {
/* 拉取失败(网络/HTTP/会话):不写死「无词」,交回退避重试;UI 呈「加载中」而非「暂无歌词」 */
retry_delay = retry_delay ? (retry_delay * 2) : LYRIC_RETRY_BASE_MS;
if (retry_delay > LYRIC_RETRY_MAX_MS) retry_delay = LYRIC_RETRY_MAX_MS;
retry_at = ncm_monotonic_ms() + retry_delay;
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 = last_ok ? 2 : 0; /* 2=已拉完(可 0 行=无词);0=失败待重试(UI 呈加载中) */
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;
}
}