Compare commits
2
Commits
51281e609f
...
a5f675750b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5f675750b | ||
|
|
a01e5ca18a |
No files matched your search
@@ -2318,3 +2318,77 @@ e767bb75b7f7ec096b9c9dc01791691d REPORT.md # 本批记录(§24)
|
|||||||
- 重试仅覆盖「预缓冲(<128KB)前网络失败」;开播后失败不重连(§4.1)。`STREAM_RETRY_BACKOFF_MS=800`
|
- 重试仅覆盖「预缓冲(<128KB)前网络失败」;开播后失败不重连(§4.1)。`STREAM_RETRY_BACKOFF_MS=800`
|
||||||
为固定值,未做指数退避多档(retry=1 时一档即可)。
|
为固定值,未做指数退避多档(retry=1 时一档即可)。
|
||||||
- 未动 sdk/、meow_launcherd/、vendor/;部署链路同 §13.5 留主会话。
|
- 未动 sdk/、meow_launcherd/、vendor/;部署链路同 §13.5 留主会话。
|
||||||
|
|
||||||
|
## 25. 修复:切音质后重播同一曲恒「缓冲中」——同 idx 再 play 未重启换档(2026-09-01,commit `a01e5ca`)
|
||||||
|
|
||||||
|
> 范围:仅 `patch_v2/ncm_app/`。真机用户反馈:切音质后再播同一首歌,声音已喷出但播放页
|
||||||
|
> 「采样率/格式信息」处恒显示「缓冲中」。风控:真实 NCM 请求 **0 次**(全离线 stub);未部署真机(留主会话)。
|
||||||
|
|
||||||
|
### 25.1 根因(文件:行)
|
||||||
|
|
||||||
|
`app/play/player_manager.c`:**同 idx 的新播放意图不被解码主循环识别**——旧 `still_current`
|
||||||
|
(原 L297-303)与解码主循环(原 L564-577)只认 `cur_song != idx` 与 `state == STOPPED`。
|
||||||
|
|
||||||
|
切音质后重播同一曲 = `player_manager_play(idx)`,而该 idx 恰与在播曲相同,于是 `play` 只改了
|
||||||
|
`player_manager.c` L100 `g_play.seq++`、L95 `g_play.state=BUFFER`,`cur_song` 不变。解码主循环
|
||||||
|
每轮比对 `cur != idx`(不变→不 break)与 `st == STOPPED`(BUFFER→不 break),导致:
|
||||||
|
|
||||||
|
1. **旧曲(旧档位)继续喷声**(audio 写照旧走),而 `g_play.state` 停在 `BUFFER` → UI
|
||||||
|
`plr_draw_cover_dyn` 读 `player_manager_state()==BUFFER` → 恒「缓冲中」(`app/ui/player.c` L268-281);
|
||||||
|
`dl_done/dl_total` 保持 0,缓冲文案退化为无 %、无 KB 的「缓冲中…」(L279)。
|
||||||
|
2. **新档位下载/缓存条目从未发起**(换档 = 新缓存键 `song_<id>_<level>`,本应走新下载)。
|
||||||
|
3. 旧曲播完后外层线程把 `BUFFER` 当「自然播完」→ `player_manager_next(1)`(原 L693)→ **顺延
|
||||||
|
下一首**,新档位永不生效。
|
||||||
|
|
||||||
|
`still_current` 同理漏判:同 idx 重播时 `cur==idx && state==BUFFER` 恒真,下载/预缓冲/物化/
|
||||||
|
解码各护栏均不终止旧 intent。三条路径(缓存命中/边下边播/.part 续传)统一受影响——根因在
|
||||||
|
播放意图层,不在某条取数路径。
|
||||||
|
|
||||||
|
### 25.2 修法(play intent 以 seq 标识,层层判别)
|
||||||
|
|
||||||
|
- 新增 `still_intent(idx, my_seq)`(`player_manager.c` L306):
|
||||||
|
`cur==idx && state==BUFFER && seq==my_seq`——严格强于旧 `still_current`(同 idx 重播时 seq 变
|
||||||
|
即判过期),替换全部 `still_current` 调用点。
|
||||||
|
- `run_song` 入口捕获 `my_seq`(L509),沿 `song_local_file`(L424)/`song_download_attempt`
|
||||||
|
(L319)下传,预缓冲(L386)/重试退避(L482)/物化(L522)/预缓冲后(L540)/解码 open 失败
|
||||||
|
(L556)各护栏改 `still_intent(idx, my_seq)`。
|
||||||
|
- **解码主循环**(L597):`seq != my_seq`(同 idx 新 intent)→ `break` 回外层重跑 `run_song`
|
||||||
|
(换档重播),不再静默续播旧档。
|
||||||
|
- **外层线程**「自然播完」收紧为 `st2 == NCM_PC_PLAYING`(L720):不再把 `BUFFER`(挂起的新
|
||||||
|
intent)当「播完」顺延下一首;同 idx 新 intent 落外层后按当前 `cur/state` 重跑 `run_song(idx)`。
|
||||||
|
|
||||||
|
### 25.3 测试证据(0 真实 NCM 请求)
|
||||||
|
|
||||||
|
| 项 | 方式 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 状态机回归 | **player_flow_sim P1-P11 全量**(缓存命中/下载/.part 续传/重试/门禁,0 网络) | 宿主 + qemu-mipsel **ALL PASS**(零回归) |
|
||||||
|
| 本 bug 复现 | **player_flow_sim 新 P12**(§25):standard 档缓存命中播 song0 → 切 lossless → 同 idx 再 play | 宿主 + qemu **ALL PASS**:状态回 PLAYING、新档位下载发起(downloads=1)、缓存键 `1000_lossless`、eff_level 按非会员钳顶回填 EXHIGH(UI 据此显示实际档位,不再停 BUFFER「缓冲中」)。修前 P12 断言 FAIL(恒 PLAYING 不了、键仍 `1000_standard`、downloads=0、eff=-1) |
|
||||||
|
| 边下边播全矩阵 | stream_sim 原 16 用例 + 2 resume(PCM 金标准) | 宿主 **ALL PASS**(后台任务复核;player_manager 未动 stream_src 链,理论零影响) |
|
||||||
|
| core 回归 | test_ncm_core_host + qemu test_ncm_core_mipsel | PASS |
|
||||||
|
| mipsel 构建 | build_ncm.sh 自有代码 -Wall -Wextra | **0 warning**(set -e 下任何告警即失败) |
|
||||||
|
| 宿主离屏 | build_ncm_host.sh + NCM_PAGE=player MEOW_UI_DUMP | rc=0、渲染正常(仅 sdk `meow_ui_vol.c` 既有告警,与本批无关) |
|
||||||
|
|
||||||
|
**状态→UI 对应**:`plr_draw_cover_dyn`(`app/ui/player.c` L268)按 `player_manager_state()` 直判
|
||||||
|
——`BUFFER` 画「缓冲中…N%」、否则画时间行/采样率。P12 断言 `state==PLAYING` 即等于「UI 显示
|
||||||
|
采样率/时间而非缓冲中」。三条路径的 BUFFER→PLAYING 转换分别由 P1(缓存命中)、P6/P7(边下边播
|
||||||
|
下载)、P9(.part 206 续传)覆盖,P12 补同 idx 重播(换档)路径。
|
||||||
|
|
||||||
|
### 25.4 顺带核实(用户问,未改)
|
||||||
|
|
||||||
|
- **歌单列表「离线缓存」:未落地**——`appstate_load_playlists`(`app/logic/appstate.c` L144)
|
||||||
|
每次进入/刷新都走 `ncm_api_user_playlist` 网络拉取(存 `g_st.playlists[]` 内存),无磁盘离线
|
||||||
|
缓存;断网时返回错误、列表空态。歌曲级元数据有落盘(songmeta,滑窗出窗不丢),但「我的歌单」
|
||||||
|
列表本身无持久化。
|
||||||
|
- **手动刷新按钮:已落地**——home topbar 刷新动作(↻ action0)→ `home_reload()`(`app/ui/home.c`
|
||||||
|
L221/L253,后台 worker 非阻塞);歌单详情页 `playlist_refresh()`(`app/ui/playlist.c` L263)重拉当前页。
|
||||||
|
|
||||||
|
### 25.5 md5(本批产物)
|
||||||
|
|
||||||
|
```
|
||||||
|
28698dd0e097c3de73b243de1dae5417 ncm # mipsel 静态 App(新;未部署,留主会话)
|
||||||
|
26480e69c258d56e17464adf0141d21e ncm_host # 宿主离屏调试二进制(重链)
|
||||||
|
fe6302544ae8e09eb340b257b2731e71 app/play/player_manager.c # §25:seq 化 intent 护栏 + 解码主循环重启
|
||||||
|
d9bfe3d7a6eaa76b00161a4e97196eb8 bench/player_flow_sim.c # 新 P12:同 idx 重播换档重启断言
|
||||||
|
```
|
||||||
|
|
||||||
|
(`REPORT.md` 追加 §25;`WORK_STATE.md` 同步锚点见 devdocs。)
|
||||||
@@ -17,6 +17,12 @@
|
|||||||
* 下载完成由播放线程 commit(rename);缓存命中用 final=size 的静态 stream_src(P1:
|
* 下载完成由播放线程 commit(rename);缓存命中用 final=size 的静态 stream_src(P1:
|
||||||
* 不再 mmap 整文件);http 30s 绝对 deadline 已改空闲超时(P2)。.part 续传/LRU 防淘汰
|
* 不再 mmap 整文件);http 30s 绝对 deadline 已改空闲超时(P2)。.part 续传/LRU 防淘汰
|
||||||
* 留阶段 3。
|
* 留阶段 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 "../ncm_app_internal.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -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));
|
snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 切歌竞态护栏(§18):物化/下载/预缓冲期间用户可能已切歌——只有仍是当前目标且处于
|
/* 同曲重播护栏(§25 切音质后重播同一曲,替代 §18 的 still_current):仅 cur/state 不够——
|
||||||
* BUFFER(本次播放意图未过期)才继续,否则直接回外层(外层会以新 cur 重跑 run_song)。 */
|
* 切音质时 play_manager_play
|
||||||
static int still_current(int idx)
|
* 用的 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);
|
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);
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
@@ -306,7 +316,7 @@ static int still_current(int idx)
|
|||||||
* 返回:NCM_OK=src 就绪(下载在后台继续);DL_RETRYABLE=预缓冲前网络失败(调用方整首重试);
|
* 返回:NCM_OK=src 就绪(下载在后台继续);DL_RETRYABLE=预缓冲前网络失败(调用方整首重试);
|
||||||
* DL_ABORTED=切歌/退出(静默放弃,不重试);其它负数=硬错误(不重试)。
|
* DL_ABORTED=切歌/退出(静默放弃,不重试);其它负数=硬错误(不重试)。
|
||||||
* *real_err 回填预缓冲失败的真实错误码。失败时 ss->w/src/dl_thr 已清理干净。 */
|
* *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,
|
song_src_t *ss, char *path, size_t psz, int64_t *bytes,
|
||||||
int *real_err)
|
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;
|
ss->dl_thr_on = 1;
|
||||||
|
|
||||||
/* 预缓冲:≥128KB(minimp3 强制门槛,F3)才放行 open_cb;final/failed 提前出。
|
/* 预缓冲:≥128KB(minimp3 强制门槛,F3)才放行 open_cb;final/failed 提前出。
|
||||||
* still_current 护栏覆盖本阻塞点:等待期间切歌/停止 → 保留 .part 静默放弃。 */
|
* still_intent 护栏覆盖本阻塞点:等待期间切歌/停止/同曲重播 → 保留 .part 静默放弃。 */
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100);
|
int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100);
|
||||||
if (wr != 0) break;
|
if (wr != 0) break;
|
||||||
if (g_app_quit || !still_current(idx)) {
|
if (g_app_quit || !still_intent(idx, seq)) {
|
||||||
stream_src_abort(ss->src);
|
stream_src_abort(ss->src);
|
||||||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||||||
ncm_cache_abort_part(ss->w); ss->w = NULL; /* 切歌/退出:保留残体供下次续传 */
|
ncm_cache_abort_part(ss->w); ss->w = NULL; /* 切歌/退出:保留残体供下次续传 */
|
||||||
stream_src_close(ss->src); ss->src = 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)) {
|
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=免费听低音质,播放允许)。本地缓存命中优先于门禁(缓存可播不拦)。
|
* (8=免费听低音质,播放允许)。本地缓存命中优先于门禁(缓存可播不拦)。
|
||||||
* - VIP 档上限:用户档超会员权限不硬拒,播放时先钳到会员允许的最高档再逐级降级
|
* - VIP 档上限:用户档超会员权限不硬拒,播放时先钳到会员允许的最高档再逐级降级
|
||||||
* (免去注定空 URL 的高档请求);实际生效档回填 g_play.eff_level。 */
|
* (免去注定空 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)
|
char *path, size_t psz, int64_t *bytes)
|
||||||
{
|
{
|
||||||
memset(ss, 0, sizeof *ss);
|
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",
|
meow_log(MEOW_LOG_WARN, "play: download retry %d/%d id=%s backoff=%dms",
|
||||||
attempt, STREAM_RETRY_MAX, id, STREAM_RETRY_BACKOFF_MS);
|
attempt, STREAM_RETRY_MAX, id, STREAM_RETRY_BACKOFF_MS);
|
||||||
usleep(STREAM_RETRY_BACKOFF_MS * 1000); /* 退避:不轰炸 */
|
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 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 == NCM_OK) return NCM_OK;
|
||||||
if (rc == DL_RETRYABLE) { last_rc = real_err; continue; } /* 重试整首 */
|
if (rc == DL_RETRYABLE) { last_rc = real_err; continue; } /* 重试整首 */
|
||||||
return rc; /* DL_ABORTED / 硬错误:不重试 */
|
return rc; /* DL_ABORTED / 硬错误:不重试 */
|
||||||
@@ -492,12 +502,21 @@ static void run_song(int idx)
|
|||||||
{
|
{
|
||||||
/* §17 滑窗兼容:物化目标曲(窗内命中 / 盘缓存 / song_detail 单曲)并整体快照进
|
/* §17 滑窗兼容:物化目标曲(窗内命中 / 盘缓存 / song_detail 单曲)并整体快照进
|
||||||
* g_play.play_song——之后本曲所有字段读快照,窗口滑出当前播放曲不影响播放。 */
|
* 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;
|
ncm_song_t s;
|
||||||
if (appstate_queue_song(idx, &s) != 0) { /* §23:按 queue_src 取(窗口 或 搜索队列) */
|
if (appstate_queue_song(idx, &s) != 0) { /* §23:按 queue_src 取(窗口 或 搜索队列) */
|
||||||
set_err(NCM_ERR_ARG, "load failed");
|
set_err(NCM_ERR_ARG, "load failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!still_current(idx)) return; /* 物化期间已切歌/停止:不写过期快照 */
|
if (!still_intent(idx, my_seq)) return; /* 物化期间已切歌/重播:不写过期快照 */
|
||||||
pthread_mutex_lock(&g_play.mu);
|
pthread_mutex_lock(&g_play.mu);
|
||||||
g_play.play_song = s;
|
g_play.play_song = s;
|
||||||
g_play.play_song_valid = 1;
|
g_play.play_song_valid = 1;
|
||||||
@@ -506,16 +525,16 @@ static void run_song(int idx)
|
|||||||
song_src_t ss;
|
song_src_t ss;
|
||||||
int64_t file_bytes = 0;
|
int64_t file_bytes = 0;
|
||||||
char path[512];
|
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 (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)"
|
const char *m = (rc == NCM_ERR_VIP) ? "VIP only (fee gate)"
|
||||||
: (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
|
: (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
|
||||||
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
|
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
|
||||||
set_err(rc, m);
|
set_err(rc, m);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!still_current(idx)) { /* 预缓冲完成但已切歌:勿开解码/勿动状态 */
|
if (!still_intent(idx, my_seq)) { /* 预缓冲完成但已切歌/同曲重播:勿开解码/勿动状态 */
|
||||||
song_src_dispose(&ss);
|
song_src_dispose(&ss);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -531,7 +550,7 @@ static void run_song(int idx)
|
|||||||
int64_t total_frames = 0;
|
int64_t total_frames = 0;
|
||||||
if (ncm_dec_open_src(&dec, ss.src, &rate, &ch, &bits, &total_frames) != 0) {
|
if (ncm_dec_open_src(&dec, ss.src, &rate, &ch, &bits, &total_frames) != 0) {
|
||||||
song_src_dispose(&ss);
|
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;
|
set_err(NCM_ERR_IO, "decode open failed"); return;
|
||||||
}
|
}
|
||||||
if (dur <= 0 && total_frames > 0 && rate > 0) dur = total_frames * 1000 / rate;
|
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;
|
int64_t seek_ms = g_play.cmd_seek_ms;
|
||||||
int cur = g_play.cur_song;
|
int cur = g_play.cur_song;
|
||||||
int st = g_play.state;
|
int st = g_play.state;
|
||||||
|
int seq = g_play.seq;
|
||||||
pthread_mutex_unlock(&g_play.mu);
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
|
|
||||||
if (cur != idx) { meow_log(MEOW_LOG_INFO, "play: switch idx=%d -> %d", idx, cur); break; } /* 切歌 → 回外层 */
|
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 (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 (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 cur2 = g_play.cur_song;
|
||||||
int st2 = g_play.state;
|
int st2 = g_play.state;
|
||||||
pthread_mutex_unlock(&g_play.mu);
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
/* 自然播完且无新命令 → 顺延下一首;否则回到循环等待 */
|
/* 自然播完(decode 循环正常退出,state 仍 PLAYING)且无新命令 → 顺延下一首;
|
||||||
|
* 若是因同 idx 重播(seq 变、state=BUFFER)返回,则循环顶会按当前 cur/state 重跑
|
||||||
|
* run_song(§25:切音质后重播同一曲换档生效),**不得**当播完顺延下一首。 */
|
||||||
int qc = appstate_queue_count();
|
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;
|
int nx = cur + 1;
|
||||||
if (nx < qc) { player_manager_next(1); }
|
if (nx < qc) { player_manager_next(1); }
|
||||||
else { player_manager_stop(); }
|
else { player_manager_stop(); }
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
* P4 下载途中切歌 → 旧曲下载完成后**不得**把状态改回 PLAYING/写过期快照
|
* P4 下载途中切歌 → 旧曲下载完成后**不得**把状态改回 PLAYING/写过期快照
|
||||||
* (still_current 护栏;旧代码在切歌后 ~下载完成点 会把 BUFFER 覆盖成 PLAYING);
|
* (still_current 护栏;旧代码在切歌后 ~下载完成点 会把 BUFFER 覆盖成 PLAYING);
|
||||||
* P5 错误后 play(cur) 重试 → 状态重新走 BUFFER→PLAYING,err 清零。
|
* P5 错误后 play(cur) 重试 → 状态重新走 BUFFER→PLAYING,err 清零。
|
||||||
|
* P12 切音质后重播同一曲(§25):同 idx 在播时再 play —— 修前被解码主循环忽略
|
||||||
|
* (旧曲继续出声、state 恒 BUFFER/「缓冲中」,收尾被外层当播完顺延下一首);
|
||||||
|
* 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。
|
||||||
*
|
*
|
||||||
* 编译(宿主,ncm_app 目录下):
|
* 编译(宿主,ncm_app 目录下):
|
||||||
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
* 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());
|
CHECK(player_manager_error() != 0, "err set after retry exhausted (rc=%d)", player_manager_error());
|
||||||
g_dl_fail = 0;
|
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);
|
usleep(200 * 1000);
|
||||||
player_manager_stop_thread();
|
player_manager_stop_thread();
|
||||||
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
|
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user