feat(ncm §20): 音质体系全量——八档UI/VIP检测缓存/逐级降级下首恢复/fee门禁/默认极高HQ
- core: 9档枚举(顺序=降级链, DOLBY映射保留不进UI/链) + level_key(sky/sky_c51)
+ level_gate(CVIP/SVIP门槛) + song_url surround档附加immerseType=c51
+ song_url_auto全档表降级(out_level回填/响应level静默降档采信/try_fn可注入)
+ vip_info(weapi v2优先v1兜底) + parse_vip_info离线可测 + NCM_ERR_VIP(-15)
- app/logic/account.c(新): account.json会员态缓存(uid/nick/vip/双expire/checked_at)
刷新=登录后/超1天/到期后各一次不轮询; account_vip本地到期降档视图
- player_manager: 用户档按会员钳顶后逐级降级(不硬拒,缓存键仍用户档),
fee∈{1,4}且非会员→NCM_ERR_VIP零请求(缓存命中优先), eff_level回填
- UI: 设置页八档(规格+门槛+未解锁标注,允许选定), 播放页时间行中央
生效档/已降级至xx小字, VIP专享文案; 默认档Hi-Res→极高HQ(存量设置保留)
- 验证(0真实请求): degrade_sim 90例矩阵全PASS; player_flow_sim P1-P8全PASS;
test_ncm_core vip解析9例宿主+qemu全PASS; 窗口/分页/缓存/封面/audiod sim全PASS;
mipsel -Wall -Wextra 0 warning; 6页离屏rc=0。未部署真机(留主会话)
This commit is contained in:
1 parent
ab4dfb4f9c
commit
bdd9e898f4
23 files changed
+1026
-70
No files matched your search
@@ -1,5 +1,5 @@
|
||||
/* play/player_manager.c — 播放线程:URL 获取(Hi-res 逐档降级)→缓存(命中直接播/否则流式落盘)→
|
||||
* minimp3 解码→audio_out 直写。UI 不卡(网络+解码全在此线程)。
|
||||
/* play/player_manager.c — 播放线程:URL 获取(全档表逐级降级)→缓存(命中直接播/否则流式落盘)→
|
||||
* minimp3/dr_flac 解码→audio_out 直写。UI 不卡(网络+解码全在此线程)。
|
||||
*
|
||||
* 模型:一个持久的播放线程。外层循环取「目标曲目」,run_song() 负责一首歌的
|
||||
* 「下载(缓冲) → 解码 → 输出 → 结束/被指令打断」。暂停=冻结(停读停写停推 pos,状态置 PAUSED);
|
||||
@@ -9,6 +9,8 @@
|
||||
* §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 不发请求。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
@@ -44,6 +46,9 @@ int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu);
|
||||
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)
|
||||
{
|
||||
@@ -86,6 +91,7 @@ void player_manager_play(int song_idx)
|
||||
g_play.dl_done = 0;
|
||||
g_play.dl_total = 0;
|
||||
g_play.seq++;
|
||||
g_play.eff_level = -1; /* 新播放意图:生效档未定(song_local_file 回填) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
@@ -120,6 +126,7 @@ void player_manager_next(int dir)
|
||||
g_play.dl_done = 0;
|
||||
g_play.dl_total = 0;
|
||||
g_play.seq++;
|
||||
g_play.eff_level = -1; /* 切歌:生效档随新曲重估(下首恢复用户档,§20) */
|
||||
}
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
@@ -146,16 +153,24 @@ static int download_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
}
|
||||
|
||||
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
|
||||
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。 */
|
||||
* 同歌同档命中不重复下载;换档=新条目,旧档留给 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_str(level));
|
||||
snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level));
|
||||
}
|
||||
|
||||
/* 得到本地音频路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。
|
||||
* 返回 NCM_OK 或负错误码。id 由调用方(run_song)从播放曲快照取,与窗口解耦(§17)。 */
|
||||
static int song_local_file(const char *id, char *path, size_t psz, int64_t *bytes)
|
||||
* 返回 NCM_OK 或负错误码。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, char *path, size_t psz, int64_t *bytes)
|
||||
{
|
||||
const char *id = s->id;
|
||||
char key[64];
|
||||
audio_cache_key(key, sizeof key, id, g_level);
|
||||
|
||||
@@ -163,20 +178,51 @@ static int song_local_file(const char *id, char *path, size_t psz, int64_t *byte
|
||||
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) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) { *bytes = (int64_t)st.st_size; return NCM_OK; }
|
||||
if (stat(path, &st) == 0) {
|
||||
*bytes = (int64_t)st.st_size;
|
||||
/* 生效档推断(缓存键=用户档,降级产物归用户档键,无法精确还原):
|
||||
* mp3 实体 + 用户档 > 极高 → 必为降级产物,标 EXHIGH;其余按用户档显示。
|
||||
* (flac 实体视为与用户档一致;hires→lossless 档内降级会高估显示,已知取舍。) */
|
||||
const char *dot = strrchr(path, '.');
|
||||
int eff = g_level;
|
||||
if (dot && ncm_streq(dot, "mp3") && g_level > NCM_LEVEL_EXHIGH)
|
||||
eff = NCM_LEVEL_EXHIGH;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = eff;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
return NCM_OK;
|
||||
}
|
||||
}
|
||||
/* 旧键温和兼容(§19 前存量:无档位后缀的 song_<id>[.]mp3,含历史双点名):命中即播,
|
||||
* 不主动改名/删除,由 LRU 自然淘汰。 */
|
||||
if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) { *bytes = (int64_t)st.st_size; return NCM_OK; }
|
||||
if (stat(path, &st) == 0) {
|
||||
*bytes = (int64_t)st.st_size;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = (g_level > NCM_LEVEL_EXHIGH) ? NCM_LEVEL_EXHIGH : g_level;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
return NCM_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/* 播放地址:从 g_level 降级,接受 mp3 与 flac(§19:dr_flac 落地后无损/Hi-Res 真实生效) */
|
||||
/* 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;
|
||||
|
||||
/* 播放地址:用户档先按会员权限钳顶,再逐档降级(§20 全档表),接受 mp3 与 flac */
|
||||
int want = g_level;
|
||||
int vip_cap = account_level_cap();
|
||||
if (want > vip_cap) want = vip_cap;
|
||||
ncm_play_url_t u;
|
||||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, g_level, &u);
|
||||
int eff = -1;
|
||||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, want, &u, &eff);
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = (eff >= 0) ? eff : want;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
/* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */
|
||||
const char *ext = "mp3";
|
||||
if (ncm_streq(u.type, "flac")) ext = "flac";
|
||||
@@ -260,11 +306,12 @@ static void run_song(int idx)
|
||||
|
||||
int64_t file_bytes = 0;
|
||||
char path[512];
|
||||
int rc = song_local_file(s.id, path, sizeof path, &file_bytes);
|
||||
int rc = song_local_file(&s, path, sizeof path, &file_bytes);
|
||||
if (rc != NCM_OK) {
|
||||
if (!still_current(idx)) return; /* 下载期间已切歌:静默放弃(缓存已落盘不亏) */
|
||||
const char *m = (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
|
||||
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
|
||||
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;
|
||||
}
|
||||
@@ -295,8 +342,9 @@ static void run_song(int idx)
|
||||
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",
|
||||
idx, s.id, ncm_dec_format(dec), rate, ch, bits, (long long)dur);
|
||||
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];
|
||||
|
||||
Reference in new issue
Block a user