根因:ncm_api_song_url_auto 无账户感知,依赖调用方按会员钳顶;若账户 VIP 态未就绪/
错钳到 EXHIGH,降级链从 EXHIGH 起步,LOSSLESS(无损 SQ) 被跳过,直接掉「标准」。
修法:新增 ncm_api_song_url_auto_av[_f](§20.3 权威形态):fee∈{1,4} 且非 CVIP →
NCM_ERR_VIP 零请求;按 account_vip 自钳顶(NONE→EXHIGH/CVIP→HIRES/SVIP→SURROUND);
只试账户可访问档(ncm_api_level_gate<=vip);落在最近可用档。原 auto/auto_f 行为零回归。
逐级尝试重构为共享 level_attempt(type/url 接受+静默降档采信)。
测试(bench/degrade_sim.c):fee门禁5例 + auto_av矩阵 vip3×desired9×avail9=243例全PASS
+ 真机复现(CVIP+Hi-Res+有SQ无HiRes→落无损SQ flac);原90例零回归。
验证:test_ncm_core_host+qemu-mipsel 全PASS;build_core/build_ncm -Wall -Wextra 0 warning。
新 ncm md5 1064a1d686390cc159f18296ee2980ee(未部署,留主会话)。
遗留:player_manager.c 仍调 auto(不传vip/fee),应改请 auto_av(account_vip()+fee);
真机若仍掉标准需另查旧缓存 song_<id>_hires.mp3 复用或旧二进制。
251 lines
13 KiB
C
251 lines
13 KiB
C
/* bench/degrade_sim.c — song_url_auto 全档降级链离线矩阵(§20,0 真实请求)。
|
||
*
|
||
* 驱动真实 core/ncm_api.c 的 ncm_api_song_url_auto_f(注入假 try_fn,按「可用下限」
|
||
* 模型应答:level <= avail_from 才给 url)。覆盖:
|
||
* ① desired × avail_from 全矩阵(9 档 × 9 档 + 全不可用):命中档 = min(desired, avail_from),
|
||
* 请求数 = desired - 命中 + 1(逐级试,不多不少);
|
||
* ② desired 越界钳制(DOLBY=16 → SURROUND;负数 → STANDARD);
|
||
* ③ url 在但 type 未知(如 aac)→ 跳过继续降;
|
||
* ④ type 缺失时按 url 扩展名收 flac;
|
||
* ⑤ out_level 回填值与命中档一致(「下首恢复用户档」由调用方重传 desired 保证,
|
||
* 链内不记忆——每次调用独立从 desired 起步)。
|
||
*
|
||
* 编译(宿主,ncm_app 目录下,先跑过 build_core.sh 产出 out/core-host/ 全部对象):
|
||
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -Icore -Ivendor/jsmn \
|
||
* bench/degrade_sim.c out/core-host/全部.o \
|
||
* -Lout/mbedtls/host -lmbedtls -lmbedx509 -lmbedcrypto -o degrade_sim -lpthread -lm
|
||
* 运行:./degrade_sim (PASS/FAIL 退出码)
|
||
*/
|
||
#include "ncm.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
static int g_fail = 0;
|
||
#define CHECK(cond, fmt, ...) do { \
|
||
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
|
||
} while (0)
|
||
|
||
/* ---- 假逐档取址:level <= g_avail_from 给 url(type 可配),记录每档请求次数 ---- */
|
||
static int g_avail_from; /* 可用下限;-1=全档不可用 */
|
||
static const char *g_type = "mp3"; /* 应答 type(""=缺省看扩展名) */
|
||
static const char *g_resp_level = NULL; /* 非 NULL:响应 data[0].level 固定串(服务端静默降档模拟) */
|
||
static int g_calls[NCM_LEVEL_SURROUND + 1];
|
||
|
||
static int fake_try(const char *dev, const char *ck, const char *id, int level,
|
||
ncm_play_url_t *out)
|
||
{
|
||
(void)dev; (void)ck; (void)id;
|
||
if (level >= 0 && level <= NCM_LEVEL_SURROUND) g_calls[level]++;
|
||
memset(out, 0, sizeof *out);
|
||
if (level > g_avail_from) return NCM_OK; /* 该档不可用:url 空 */
|
||
snprintf(out->url, sizeof out->url, "http://sim/%lld.%s", (long long)12345,
|
||
(level >= NCM_LEVEL_LOSSLESS) ? "flac" : "mp3");
|
||
snprintf(out->type, sizeof out->type, "%s", g_type);
|
||
if (g_resp_level) snprintf(out->level, sizeof out->level, "%s", g_resp_level);
|
||
out->br = 320000;
|
||
out->size = 9000000;
|
||
return NCM_OK;
|
||
}
|
||
|
||
static int g_bad_type_from = -1; /* >=0: 该档以上给 aac(未知格式应跳过) */
|
||
static int fake_try_badtype(const char *dev, const char *ck, const char *id, int level,
|
||
ncm_play_url_t *out)
|
||
{
|
||
int r = fake_try(dev, ck, id, level, out);
|
||
if (r == NCM_OK && out->url[0] && level >= g_bad_type_from)
|
||
snprintf(out->type, sizeof out->type, "aac");
|
||
return r;
|
||
}
|
||
|
||
static void reset_calls(void) { memset(g_calls, 0, sizeof g_calls); }
|
||
static int total_calls(void)
|
||
{
|
||
int n = 0;
|
||
for (int i = 0; i <= NCM_LEVEL_SURROUND; i++) n += g_calls[i];
|
||
return n;
|
||
}
|
||
|
||
/* ---- §20.3 账户/fee 感知降级链(auto_av_f)期望模型 ----
|
||
* cap(vip):NONE→EXHIGH / CVIP→HIRES / SVIP→SURROUND(与 account.c account_level_cap 同口径)。
|
||
* start = clamp(desired, STANDARD, cap);只试 gate(lv)<=vip 的档;落在 lv<=avail 的最近可用档。 */
|
||
static int cap_for_vip(int vip) {
|
||
switch (vip) {
|
||
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
|
||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||
default: return NCM_LEVEL_EXHIGH;
|
||
}
|
||
}
|
||
static int expect_land(int vip, int desired, int avail) {
|
||
int cap = cap_for_vip(vip);
|
||
int start = desired > cap ? cap : desired;
|
||
if (start < NCM_LEVEL_STANDARD) start = NCM_LEVEL_STANDARD;
|
||
for (int lv = start; lv >= NCM_LEVEL_STANDARD; lv--)
|
||
if (ncm_api_level_gate(lv) <= vip && lv <= avail) return lv;
|
||
return -1;
|
||
}
|
||
static int expect_tried(int vip, int desired, int best) {
|
||
int cap = cap_for_vip(vip);
|
||
int start = desired > cap ? cap : desired;
|
||
if (start < NCM_LEVEL_STANDARD) start = NCM_LEVEL_STANDARD;
|
||
int n = 0;
|
||
for (int lv = start; lv >= NCM_LEVEL_STANDARD; lv--)
|
||
if (ncm_api_level_gate(lv) <= vip && (best < 0 || lv >= best)) n++;
|
||
return n;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
ncm_play_url_t u;
|
||
int eff;
|
||
|
||
printf("== 1) desired × avail_from 全矩阵 ==\n");
|
||
int cases = 0, bad = 0;
|
||
for (int desired = NCM_LEVEL_STANDARD; desired <= NCM_LEVEL_SURROUND; desired++) {
|
||
for (int avail = NCM_LEVEL_STANDARD; avail <= NCM_LEVEL_SURROUND; avail++) {
|
||
reset_calls();
|
||
g_avail_from = avail; g_type = "mp3";
|
||
int rc = ncm_api_song_url_auto_f("dev", "ck", "1", desired, &u, &eff, fake_try);
|
||
int want = (avail < desired) ? avail : desired; /* 期望命中 = min */
|
||
int ok = (rc == NCM_OK) && (eff == want) && u.url[0] &&
|
||
(total_calls() == desired - want + 1);
|
||
/* 高档(>命中)各恰好 1 次,低档不请求 */
|
||
for (int lv = want + 1; lv <= desired; lv++) ok = ok && (g_calls[lv] == 1);
|
||
for (int lv = 0; lv < want; lv++) ok = ok && (g_calls[lv] == 0);
|
||
cases++;
|
||
if (!ok) {
|
||
bad++;
|
||
printf(" FAIL desired=%d(%s) avail=%d -> rc=%d eff=%d calls=%d (want %d/%d)\n",
|
||
desired, ncm_api_level_str(desired), avail, rc, eff, total_calls(),
|
||
want, desired - want + 1);
|
||
g_fail = 1;
|
||
}
|
||
}
|
||
/* 全不可用 → NOTFOUND,且恰好 desired+1 次请求 */
|
||
reset_calls();
|
||
g_avail_from = -1; g_type = "mp3";
|
||
int rc = ncm_api_song_url_auto_f("dev", "ck", "1", desired, &u, &eff, fake_try);
|
||
int ok = (rc == NCM_ERR_NOTFOUND) && (eff == -1) &&
|
||
(total_calls() == desired - NCM_LEVEL_STANDARD + 1);
|
||
cases++;
|
||
if (!ok) {
|
||
bad++;
|
||
printf(" FAIL desired=%d all-unavail -> rc=%d eff=%d calls=%d\n",
|
||
desired, rc, eff, total_calls());
|
||
g_fail = 1;
|
||
}
|
||
}
|
||
CHECK(bad == 0, "matrix %d cases all pass (desired 9 x avail 9 + 全不可用 9)", cases);
|
||
|
||
printf("== 2) desired 越界钳制 ==\n");
|
||
reset_calls();
|
||
g_avail_from = NCM_LEVEL_SURROUND; g_type = "mp3";
|
||
CHECK(ncm_api_song_url_auto_f("d", "c", "1", NCM_LEVEL_DOLBY, &u, &eff, fake_try) == NCM_OK &&
|
||
eff == NCM_LEVEL_SURROUND && total_calls() == 1 && g_calls[NCM_LEVEL_SURROUND] == 1,
|
||
"desired=DOLBY(16) clamped to SURROUND (eff=%d calls=%d)", eff, total_calls());
|
||
reset_calls();
|
||
CHECK(ncm_api_song_url_auto_f("d", "c", "1", -7, &u, &eff, fake_try) == NCM_OK &&
|
||
eff == NCM_LEVEL_STANDARD && total_calls() == 1,
|
||
"desired=-7 clamped to STANDARD (eff=%d)", eff);
|
||
|
||
printf("== 3) 未知 type 跳过继续降 ==\n");
|
||
reset_calls();
|
||
g_avail_from = NCM_LEVEL_SURROUND; g_bad_type_from = NCM_LEVEL_HIRES;
|
||
/* hires 及以上 url 在但 type=aac → 跳过,落在 lossless */
|
||
CHECK(ncm_api_song_url_auto_f("d", "c", "1", NCM_LEVEL_SKY, &u, &eff, fake_try_badtype)
|
||
== NCM_OK && eff == NCM_LEVEL_LOSSLESS,
|
||
"aac above hires -> land lossless (eff=%d)", eff);
|
||
|
||
printf("== 4) type 缺失按 url 扩展名收 flac ==\n");
|
||
reset_calls();
|
||
g_avail_from = NCM_LEVEL_LOSSLESS; g_type = "";
|
||
CHECK(ncm_api_song_url_auto_f("d", "c", "1", NCM_LEVEL_LOSSLESS, &u, &eff, fake_try)
|
||
== NCM_OK && strcmp(u.type, "flac") == 0,
|
||
"type empty + .flac url -> type backfilled flac");
|
||
|
||
printf("== 5) out_level 每次独立(下首恢复用户档语义) ==\n");
|
||
reset_calls();
|
||
g_avail_from = NCM_LEVEL_STANDARD; g_type = "mp3";
|
||
ncm_api_song_url_auto_f("d", "c", "1", NCM_LEVEL_SURROUND, &u, &eff, fake_try);
|
||
int first_eff = eff;
|
||
reset_calls();
|
||
g_avail_from = NCM_LEVEL_SURROUND;
|
||
ncm_api_song_url_auto_f("d", "c", "2", NCM_LEVEL_SURROUND, &u, &eff, fake_try);
|
||
CHECK(first_eff == NCM_LEVEL_STANDARD && eff == NCM_LEVEL_SURROUND,
|
||
"prev degraded to standard, next song back at surround (eff %d -> %d)",
|
||
first_eff, eff);
|
||
|
||
printf("== 6) 响应 level 静默降档采信(data[0].level < 请求档) ==\n");
|
||
reset_calls();
|
||
g_avail_from = NCM_LEVEL_SURROUND; g_type = "flac"; g_resp_level = "exhigh";
|
||
CHECK(ncm_api_song_url_auto_f("d", "c", "1", NCM_LEVEL_HIRES, &u, &eff, fake_try) == NCM_OK &&
|
||
eff == NCM_LEVEL_EXHIGH,
|
||
"req hires, resp level=exhigh -> eff=exhigh (got %d)", eff);
|
||
reset_calls();
|
||
g_resp_level = "sky"; /* sky 族有歧义:不采信,保守取请求档 */
|
||
CHECK(ncm_api_song_url_auto_f("d", "c", "1", NCM_LEVEL_SURROUND, &u, &eff, fake_try) == NCM_OK &&
|
||
eff == NCM_LEVEL_SURROUND,
|
||
"req surround, resp level=sky (歧义) -> eff=surround (got %d)", eff);
|
||
g_resp_level = NULL;
|
||
|
||
printf("== 7) auto_av_f fee 门禁(§20.4:fee∈{1,4} 且非 CVIP → 零请求 NCM_ERR_VIP)==\n");
|
||
reset_calls(); g_avail_from = NCM_LEVEL_SURROUND; g_type = "mp3";
|
||
CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_NONE, 1, NCM_LEVEL_HIRES, &u, &eff, fake_try)
|
||
== NCM_ERR_VIP && total_calls() == 0,
|
||
"fee=1 + NONE -> NCM_ERR_VIP (calls=%d)", total_calls());
|
||
CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_NONE, 4, NCM_LEVEL_HIRES, &u, &eff, fake_try)
|
||
== NCM_ERR_VIP && total_calls() == 0,
|
||
"fee=4 + NONE -> NCM_ERR_VIP (calls=%d)", total_calls());
|
||
CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_CVIP, 1, NCM_LEVEL_HIRES, &u, &eff, fake_try)
|
||
== NCM_OK, "fee=1 + CVIP -> 放行");
|
||
CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_NONE, 8, NCM_LEVEL_HIRES, &u, &eff, fake_try)
|
||
== NCM_OK, "fee=8 + NONE -> 放行(免费听低音质)");
|
||
CHECK(ncm_api_song_url_auto_av_f("d", "c", "1", NCM_VIP_NONE, 0, NCM_LEVEL_HIRES, &u, &eff, fake_try)
|
||
== NCM_OK, "fee=0 + NONE -> 放行");
|
||
|
||
printf("== 8) auto_av_f 全矩阵: account_vip 3 x desired 9 x avail 9(钳顶+门槛+最近可用档)==\n");
|
||
int av_bad = 0, av_cases = 0;
|
||
for (int vip = NCM_VIP_NONE; vip <= NCM_VIP_SVIP; vip++) {
|
||
for (int desired = NCM_LEVEL_STANDARD; desired <= NCM_LEVEL_SURROUND; desired++) {
|
||
for (int avail = NCM_LEVEL_STANDARD; avail <= NCM_LEVEL_SURROUND; avail++) {
|
||
reset_calls();
|
||
g_avail_from = avail; g_type = "mp3";
|
||
int rc = ncm_api_song_url_auto_av_f("dev", "ck", "1", vip, 0, desired, &u, &eff, fake_try);
|
||
int want = expect_land(vip, desired, avail);
|
||
int ok = (want >= 0 && rc == NCM_OK && eff == want && u.url[0] &&
|
||
total_calls() == expect_tried(vip, desired, want));
|
||
av_cases++;
|
||
if (!ok) {
|
||
av_bad++;
|
||
printf(" FAIL vip=%d desired=%d(%s) avail=%d -> rc=%d eff=%d calls=%d (want %d/%d)\n",
|
||
vip, desired, ncm_api_level_str(desired), avail, rc, eff, total_calls(),
|
||
want, expect_tried(vip, desired, want));
|
||
g_fail = 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
CHECK(av_bad == 0, "auto_av matrix %d cases all pass (vip 3 x desired 9 x avail 9)", av_cases);
|
||
|
||
printf("== 9) 真机场景复现:CVIP + 设置Hi-Res + 歌曲「有SQ无Hi-Res」→ 落无损SQ ==\n");
|
||
reset_calls();
|
||
g_avail_from = NCM_LEVEL_LOSSLESS; /* 歌曲只提供到 SQ(及更低)→ 无 Hi-Res */
|
||
g_type = "flac"; /* SQ 以 flac 下发 */
|
||
int rc9 = ncm_api_song_url_auto_av_f("d", "c", "10", NCM_VIP_CVIP, 0, NCM_LEVEL_HIRES,
|
||
&u, &eff, fake_try);
|
||
CHECK(rc9 == NCM_OK && eff == NCM_LEVEL_LOSSLESS && strcmp(u.type, "flac") == 0,
|
||
"CVIP Hi-Res、有SQ无HiRes -> 落无损SQ (rc=%d eff=%d(%s) type=%s)",
|
||
rc9, eff, ncm_api_level_str(eff), u.type);
|
||
/* 对照:若账户VIP态未就绪(NONE),钳顶把Hi-Res压到极高,SQ被跳过(旧app clamp即此路径) */
|
||
reset_calls();
|
||
g_avail_from = NCM_LEVEL_LOSSLESS; g_type = "flac";
|
||
int rcn = ncm_api_song_url_auto_av_f("d", "c", "10", NCM_VIP_NONE, 0, NCM_LEVEL_HIRES,
|
||
&u, &eff, fake_try);
|
||
CHECK(rcn == NCM_OK && eff == NCM_LEVEL_EXHIGH,
|
||
"NONE+Hi-Res -> 钳到极高EXHIGH (rc=%d eff=%d) — 旧app clamp正是此路径致SQ被跳过",
|
||
rcn, eff);
|
||
|
||
printf(g_fail ? "== DEGRADE SIM: FAIL ==\n" : "== DEGRADE SIM: ALL PASS ==\n");
|
||
return g_fail;
|
||
}
|