diff --git a/patch_v2/ncm_app/app/play/player_manager.c b/patch_v2/ncm_app/app/play/player_manager.c index 6956355..830bbb9 100644 --- a/patch_v2/ncm_app/app/play/player_manager.c +++ b/patch_v2/ncm_app/app/play/player_manager.c @@ -17,6 +17,12 @@ * 下载完成由播放线程 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 @@ -292,12 +298,16 @@ 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)); } -/* 切歌竞态护栏(§18):物化/下载/预缓冲期间用户可能已切歌——只有仍是当前目标且处于 - * BUFFER(本次播放意图未过期)才继续,否则直接回外层(外层会以新 cur 重跑 run_song)。 */ -static int still_current(int idx) +/* 同曲重播护栏(§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); + 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; } @@ -306,7 +316,7 @@ static int still_current(int idx) * 返回: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, const char *key, int want, +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) { @@ -366,16 +376,16 @@ static int song_download_attempt(const ncm_song_t *s, int idx, const char *key, ss->dl_thr_on = 1; /* 预缓冲:≥128KB(minimp3 强制门槛,F3)才放行 open_cb;final/failed 提前出。 - * still_current 护栏覆盖本阻塞点:等待期间切歌/停止 → 保留 .part 静默放弃。 */ + * still_intent 护栏覆盖本阻塞点:等待期间切歌/停止/同曲重播 → 保留 .part 静默放弃。 */ for (;;) { int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100); if (wr != 0) break; - if (g_app_quit || !still_current(idx)) { + 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_current 复核后静默返回 */ + return DL_ABORTED; /* 调用方 still_intent 复核后静默返回 */ } } if (stream_src_aborted(ss->src)) { @@ -411,7 +421,7 @@ static int song_download_attempt(const ncm_song_t *s, int idx, const char *key, * (8=免费听低音质,播放允许)。本地缓存命中优先于门禁(缓存可播不拦)。 * - VIP 档上限:用户档超会员权限不硬拒,播放时先钳到会员允许的最高档再逐级降级 * (免去注定空 URL 的高档请求);实际生效档回填 g_play.eff_level。 */ -static int song_local_file(const ncm_song_t *s, int idx, song_src_t *ss, +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); @@ -467,10 +477,10 @@ static int song_local_file(const ncm_song_t *s, int idx, song_src_t *ss, 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_current(idx)) return NCM_ERR_IO; + 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, key, want, ss, path, psz, bytes, &real_err); + 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 / 硬错误:不重试 */ @@ -492,12 +502,21 @@ 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_current(idx)) 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; @@ -506,16 +525,16 @@ static void run_song(int idx) song_src_t ss; int64_t file_bytes = 0; char path[512]; - int rc = song_local_file(&s, idx, &ss, path, sizeof path, &file_bytes); + int rc = song_local_file(&s, idx, my_seq, &ss, path, sizeof path, &file_bytes); if (rc != NCM_OK) { - if (!still_current(idx)) return; /* 下载期间已切歌:静默放弃(已落盘部分不亏) */ + 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_current(idx)) { /* 预缓冲完成但已切歌:勿开解码/勿动状态 */ + if (!still_intent(idx, my_seq)) { /* 预缓冲完成但已切歌/同曲重播:勿开解码/勿动状态 */ song_src_dispose(&ss); return; } @@ -531,7 +550,7 @@ static void run_song(int idx) 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_current(idx)) return; /* open 期间(flac 元数据等待)已切歌 */ + 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; @@ -570,10 +589,16 @@ static void run_song(int idx) 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; } @@ -688,9 +713,11 @@ static void *player_thread(void *arg) 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_STOPPED && cur2 == cur) { + 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(); } diff --git a/patch_v2/ncm_app/bench/player_flow_sim.c b/patch_v2/ncm_app/bench/player_flow_sim.c index e2d0f49..3901ae6 100644 --- a/patch_v2/ncm_app/bench/player_flow_sim.c +++ b/patch_v2/ncm_app/bench/player_flow_sim.c @@ -11,6 +11,9 @@ * P4 下载途中切歌 → 旧曲下载完成后**不得**把状态改回 PLAYING/写过期快照 * (still_current 护栏;旧代码在切歌后 ~下载完成点 会把 BUFFER 覆盖成 PLAYING); * P5 错误后 play(cur) 重试 → 状态重新走 BUFFER→PLAYING,err 清零。 + * P12 切音质后重播同一曲(§25):同 idx 在播时再 play —— 修前被解码主循环忽略 + * (旧曲继续出声、state 恒 BUFFER/「缓冲中」,收尾被外层当播完顺延下一首); + * 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。 * * 编译(宿主,ncm_app 目录下): * gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ @@ -508,6 +511,37 @@ int main(void) CHECK(player_manager_error() != 0, "err set after retry exhausted (rc=%d)", player_manager_error()); g_dl_fail = 0; + printf("== P12: 切音质后重播同一曲(同 idx 在播时再 play)-> 换档重启,非卡「缓冲中」(§25)==\n"); + usleep(300 * 1000); + g_eof = 0; g_reads_left = -1; g_cache_ok = 1; g_url_rc = NCM_OK; g_fee = 0; + g_part_size = 0; g_http_status = 200; g_416_first = 0; g_dl_delay_ms = 0; g_dl_fail = 0; + g_vip = NCM_VIP_NONE; g_writes = 0; g_downloads = 0; g_url_calls = 0; g_last_key[0] = 0; + /* 以 standard 档播放 song0(缓存命中,PLAYING) */ + player_manager_set_quality(NCM_LEVEL_STANDARD, 10); + player_manager_play(0); + CHECK(wait_state(NCM_PC_PLAYING, 3000), "song0 playing at standard (cache hit)"); + CHECK(g_writes > 0, "standard song actually emitting audio (writes=%d)", g_writes); + /* 切音质到 lossless,然后重播同一曲(同 idx=0 再次 play)。 + * 新档位 = 新缓存条目(lookup 必 miss)→ 如实置 g_cache_ok=0 强制走下载(边下边播)。 */ + player_manager_set_quality(NCM_LEVEL_LOSSLESS, 10); + g_cache_ok = 0; + player_manager_play(0); + /* §25 修前:同 idx 重播被解码主循环忽略(cur 不变、state 仍 BUFFER)→ 旧曲继续出声、 + * 恒「缓冲中」,且旧曲收尾被外层当「自然播完」顺延下一首(新档位永不生效)。 + * 修后:seq 变化即 break → 外层重跑 run_song(0) 换档下载 → 状态回到 PLAYING + * (UI 显示采样率/时间而非「缓冲中」)。 */ + CHECK(wait_state(NCM_PC_PLAYING, 4000), + "re-tap same idx -> back to PLAYING (expect 换档开播,非卡 缓冲中)"); + CHECK(g_downloads >= 1, "re-tap started new-quality download (downloads=%d)", g_downloads); + CHECK(strcmp(g_last_key, "1000_lossless") == 0, + "re-tap cache key=%s (expect 1000_lossless = 新档位)", g_last_key); + /* sim 账号非会员 → lossless 被 §20 钳顶到 EXHIGH;关键是 eff_level 已按新意图回填 + * (UI 据此显示实际生效档,不再停 BUFFER/「缓冲中」)。 */ + CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH, + "re-tap eff_level=%d (expect EXHIGH=非会员钳顶的新档意图)", player_manager_eff_level()); + g_eof = 1; + wait_state(NCM_PC_STOPPED, 3000); + usleep(200 * 1000); player_manager_stop_thread(); printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n"); diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index ca496fc..2d19242 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/ncm_host b/patch_v2/ncm_app/ncm_host index 42e85ef..bd92a70 100755 Binary files a/patch_v2/ncm_app/ncm_host and b/patch_v2/ncm_app/ncm_host differ