feat(ncm §21): 边下边播阶段1+2——stream_src流式源+decoder双路open_cb+下载线程预缓冲/stall联动+P2 HTTP空闲超时+cache fd写者+stream_sim全矩阵PCM金标准(0真实请求,未部署)
This commit is contained in:
1 parent
dbee37e1d6
commit
9e25db77cc
18 files changed
+1335
-136
No files matched your search
@@ -1,8 +1,19 @@
|
||||
/* play/decoder.c — 格式分发解码实现(接口见 decoder.h,REPORT §19)。
|
||||
/* play/decoder.c — 格式分发解码实现(接口见 decoder.h,REPORT §19;§21 切 stream_src 回调模式)。
|
||||
*
|
||||
* 两后端都建在 stream_src 上(边下边播 .tmp 与缓存命中静态源同套读回调):
|
||||
* mp3 = mp3dec_ex_open_cb(MP3D_SEEK_TO_BYTE | MP3D_DO_NOT_SCAN) —— F3 红线:
|
||||
* 不带 DO_NOT_SCAN 时 open_cb 对无 Xing tag 的流全扫描建索引,read_cb 阻塞
|
||||
* 语义下 open 直接卡到整首下完。代价 dec->samples=0,时长走 meta(现状已是)。
|
||||
* flac = drflac_open(stream_src 回调)。dr_flac 读回调语义「返回实际读到的字节数」,
|
||||
* stream_src 的严格 fread 实现(填满才返回)是其超集,直接兼容。
|
||||
* 缓存命中(ncm_dec_open 整文件路径)也走同一 cb 路径——不再 mmap(MAP_POPULATE)
|
||||
* 整文件(P1 修复:旧 mp3dec_ex_open 在 Linux 预 fault 全部页,RSS ≈ 整文件)。
|
||||
*
|
||||
* mp3 细节:minimp3_ex 的 dec.info(hz/channels)在首次 mp3dec_ex_read 后才保证有效,
|
||||
* 而调用方开 audio_out 前就要 rate/ch —— open 时先预读一批进 stash,read 先吐 stash。
|
||||
* seek(mp3):关+重开(MP3D_SEEK_TO_BYTE 索引在 open 时重建)再按字节近似 seek,同旧语义。
|
||||
* seek(mp3):MP3D_SEEK_TO_BYTE 下 mp3dec_ex_seek 只是 offset=pos + io->seek(pos)(F4,
|
||||
* 无需索引、无需关重开),stream_src 的 seek 回调只改 read_pos——目标超已下载区时
|
||||
* 后续 read 自然阻塞等待数据到达。
|
||||
*/
|
||||
#include "decoder.h"
|
||||
#include "minimp3_ex.h"
|
||||
@@ -11,6 +22,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define DEC_STASH_FRAMES 4096 /* open 预读上限(帧),与播放循环单块读同量级 */
|
||||
|
||||
@@ -18,11 +30,13 @@ enum { DEC_MP3 = 0, DEC_FLAC = 1 };
|
||||
|
||||
struct ncm_decoder {
|
||||
int kind;
|
||||
char path[512];
|
||||
stream_src_t *src; /* 读端(ncm_dec_open 自建静态源时 src_owned=1) */
|
||||
int src_owned;
|
||||
int rate, ch, bits;
|
||||
int64_t total_frames; /* 0=未知 */
|
||||
/* mp3 */
|
||||
mp3dec_ex_t mp3;
|
||||
mp3dec_io_t mp3_io; /* 生命周期须覆盖 mp3(minimp3 内部存指针) */
|
||||
int16_t *stash; /* 预读样本(交织 s16),stash_n 个样本 */
|
||||
size_t stash_n, stash_off;
|
||||
/* flac */
|
||||
@@ -30,48 +44,74 @@ struct ncm_decoder {
|
||||
};
|
||||
|
||||
/* magic 探测:首 4 字节 "fLaC" → flac;其余(ID3 / 0xFF 帧同步 / 未知)→ mp3
|
||||
*(未知格式留给 minimp3 在 open 时报错,不在本层加第三种分支)。 */
|
||||
static int sniff_kind(const char *path)
|
||||
*(未知格式留给 minimp3 在 open 时报错,不在本层加第三种分支)。
|
||||
* 流模式下 peek 读已落盘前缀(预缓冲门槛 ≥128KB 已满足,或 final 截断有多少读多少)。 */
|
||||
static int sniff_kind(stream_src_t *src)
|
||||
{
|
||||
unsigned char m[4] = {0};
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (f) { size_t n = fread(m, 1, 4, f); fclose(f); if (n < 4) return DEC_MP3; }
|
||||
if (m[0] == 'f' && m[1] == 'L' && m[2] == 'a' && m[3] == 'C') return DEC_FLAC;
|
||||
size_t n = stream_src_peek(src, m, 4, 0);
|
||||
if (n >= 4 && m[0] == 'f' && m[1] == 'L' && m[2] == 'a' && m[3] == 'C') return DEC_FLAC;
|
||||
return DEC_MP3;
|
||||
}
|
||||
|
||||
int ncm_dec_open(ncm_decoder_t **out, const char *path,
|
||||
int *rate, int *ch, int *bits, int64_t *total_frames)
|
||||
/* dr_flac 回调签名适配:seek 的 origin 是枚举、tell 用 drflac_int64(long long),
|
||||
* 与 stream_src 的 int/int64_t 形参在 C 里不兼容,包一层。 */
|
||||
static drflac_bool32 dec_flac_seek(void *ud, int offset, drflac_seek_origin origin)
|
||||
{
|
||||
if (!out || !path) return -1;
|
||||
ncm_decoder_t *d = (ncm_decoder_t *)calloc(1, sizeof *d);
|
||||
if (!d) return -1;
|
||||
d->kind = sniff_kind(path);
|
||||
snprintf(d->path, sizeof d->path, "%s", path);
|
||||
return (drflac_bool32)stream_src_drflac_seek(ud, offset, (int)origin);
|
||||
}
|
||||
static drflac_bool32 dec_flac_tell(void *ud, drflac_int64 *cursor)
|
||||
{
|
||||
int64_t c = 0;
|
||||
int r = stream_src_drflac_tell(ud, &c);
|
||||
*cursor = (drflac_int64)c;
|
||||
return (drflac_bool32)r;
|
||||
}
|
||||
|
||||
static int dec_open_on_src(ncm_decoder_t *d)
|
||||
{
|
||||
if (d->kind == DEC_FLAC) {
|
||||
d->flac = drflac_open_file(path, NULL);
|
||||
if (!d->flac) { free(d); return -1; }
|
||||
d->flac = drflac_open(stream_src_drflac_read, dec_flac_seek,
|
||||
dec_flac_tell, d->src, NULL);
|
||||
if (!d->flac) return -1;
|
||||
d->rate = (int)d->flac->sampleRate;
|
||||
d->ch = (int)d->flac->channels;
|
||||
/* 位宽映射(AUDIOD §4 归一交给 SDK/audiod):≤16 → S16;>16 → S32 满幅 */
|
||||
d->bits = (d->flac->bitsPerSample <= 16) ? 16 : 32;
|
||||
d->total_frames = (int64_t)d->flac->totalPCMFrameCount;
|
||||
} else {
|
||||
if (mp3dec_ex_open(&d->mp3, path, MP3D_SEEK_TO_BYTE) != 0) { free(d); return -1; }
|
||||
d->mp3_io.read = stream_src_read_cb;
|
||||
d->mp3_io.read_data = d->src;
|
||||
d->mp3_io.seek = stream_src_seek_cb;
|
||||
d->mp3_io.seek_data = d->src;
|
||||
if (mp3dec_ex_open_cb(&d->mp3, &d->mp3_io,
|
||||
MP3D_SEEK_TO_BYTE | MP3D_DO_NOT_SCAN) != 0) return -1;
|
||||
d->bits = 16;
|
||||
d->stash = (int16_t *)malloc(DEC_STASH_FRAMES * 2 * sizeof(int16_t));
|
||||
if (!d->stash) { mp3dec_ex_close(&d->mp3); free(d); return -1; }
|
||||
/* 预读首块:填 dec.info(hz/channels)+ 预热 stash */
|
||||
if (!d->stash) { mp3dec_ex_close(&d->mp3); return -1; }
|
||||
/* 预读首块:填 dec.info(hz/channels)+ 预热 stash。
|
||||
* 流模式下此处可能阻塞到下载推进(read_cb 阻塞语义),属预期。 */
|
||||
size_t got = mp3dec_ex_read(&d->mp3, d->stash, DEC_STASH_FRAMES * 2);
|
||||
d->stash_n = got;
|
||||
d->rate = d->mp3.info.hz;
|
||||
d->ch = d->mp3.info.channels;
|
||||
if (d->rate <= 0 || d->ch <= 0) { /* 首块即失败/空流 */
|
||||
mp3dec_ex_close(&d->mp3); free(d->stash); free(d); return -1;
|
||||
mp3dec_ex_close(&d->mp3); free(d->stash); d->stash = NULL; return -1;
|
||||
}
|
||||
d->total_frames = (d->ch > 0) ? (int64_t)(d->mp3.samples / (uint64_t)d->ch) : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ncm_dec_open_src(ncm_decoder_t **out, stream_src_t *src,
|
||||
int *rate, int *ch, int *bits, int64_t *total_frames)
|
||||
{
|
||||
if (!out || !src) return -1;
|
||||
ncm_decoder_t *d = (ncm_decoder_t *)calloc(1, sizeof *d);
|
||||
if (!d) return -1;
|
||||
d->src = src;
|
||||
d->kind = sniff_kind(src);
|
||||
if (dec_open_on_src(d) != 0) { ncm_dec_close(d); return -1; }
|
||||
|
||||
if (d->ch < 1 || d->ch > 2) { ncm_dec_close(d); return -1; } /* v1 只接 单/双声道 */
|
||||
*out = d;
|
||||
@@ -82,6 +122,22 @@ int ncm_dec_open(ncm_decoder_t **out, const char *path,
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ncm_dec_open(ncm_decoder_t **out, const char *path,
|
||||
int *rate, int *ch, int *bits, int64_t *total_frames)
|
||||
{
|
||||
if (!out || !path) return -1;
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) return -1;
|
||||
stream_src_t *src = NULL;
|
||||
if (stream_src_open_static(&src, path, (uint64_t)st.st_size) != 0) return -1;
|
||||
if (ncm_dec_open_src(out, src, rate, ch, bits, total_frames) != 0) {
|
||||
stream_src_close(src);
|
||||
return -1;
|
||||
}
|
||||
(*out)->src_owned = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t ncm_dec_read(ncm_decoder_t *d, void *buf, int64_t cap_frames)
|
||||
{
|
||||
if (!d || !buf || cap_frames <= 0) return 0;
|
||||
@@ -117,10 +173,8 @@ int ncm_dec_seek_ms(ncm_decoder_t *d, int64_t ms, int64_t file_bytes, int64_t du
|
||||
drflac_uint64 frame = (drflac_uint64)(ms * d->rate / 1000);
|
||||
return drflac_seek_to_pcm_frame(d->flac, frame) ? 0 : -1;
|
||||
}
|
||||
/* mp3:关+重开(重建字节索引)再按字节近似 */
|
||||
mp3dec_ex_close(&d->mp3);
|
||||
memset(&d->mp3, 0, sizeof d->mp3);
|
||||
if (mp3dec_ex_open(&d->mp3, d->path, MP3D_SEEK_TO_BYTE) != 0) return -1;
|
||||
/* mp3:MP3D_SEEK_TO_BYTE 下 seek 只是 offset=pos + io->seek(pos)(F4),
|
||||
* 无需索引、无需关重开;目标超已下载区时 read 自然阻塞等待。 */
|
||||
d->stash_n = d->stash_off = 0;
|
||||
int64_t target = (file_bytes > 0 && dur_ms > 0) ? file_bytes * ms / dur_ms : 0;
|
||||
return mp3dec_ex_seek(&d->mp3, (uint64_t)(target > 0 ? target : 0));
|
||||
@@ -131,6 +185,7 @@ void ncm_dec_close(ncm_decoder_t *d)
|
||||
if (!d) return;
|
||||
if (d->kind == DEC_FLAC) { if (d->flac) drflac_close(d->flac); }
|
||||
else { mp3dec_ex_close(&d->mp3); free(d->stash); }
|
||||
if (d->src_owned) stream_src_close(d->src);
|
||||
free(d);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#define NCM_DECODER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "stream_src.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -15,14 +16,22 @@ extern "C" {
|
||||
|
||||
typedef struct ncm_decoder ncm_decoder_t;
|
||||
|
||||
/* 打开并探测流参数:rate/ch/bits/total_frames 经指针回填实际值(mp3 恒 16bit;
|
||||
* total_frames 0=未知)。返回 0=成功。 */
|
||||
/* 在 stream_src 上打开并探测流参数(边下边播/缓存命中统一入口,§21):
|
||||
* magic 经 stream_src_peek 探测;mp3=mp3dec_ex_open_cb(MP3D_SEEK_TO_BYTE|MP3D_DO_NOT_SCAN,
|
||||
* F3 红线:不带 DO_NOT_SCAN 会全流扫描阻塞到下载结束);flac=drflac_open 回调模式。
|
||||
* src 由调用方持有(dec_close 不关闭 src)。rate/ch/bits/total_frames 经指针回填
|
||||
* (mp3 恒 16bit;total_frames 0=未知)。返回 0=成功。 */
|
||||
int ncm_dec_open_src(ncm_decoder_t **out, stream_src_t *src,
|
||||
int *rate, int *ch, int *bits, int64_t *total_frames);
|
||||
/* 整文件路径便捷入口:内部建 final=size 的静态 stream_src(dec_close 一并释放)。
|
||||
* 解码同样走 cb 路径(不 mmap 整文件,P1 修复),输出与 ncm_dec_open_src 一致。 */
|
||||
int ncm_dec_open(ncm_decoder_t **out, const char *path,
|
||||
int *rate, int *ch, int *bits, int64_t *total_frames);
|
||||
/* 读最多 cap_frames 帧到 buf(交织,样本位宽=open 返回的 bits;cap 以帧计,
|
||||
* buf 容量须 ≥ cap_frames*ch*(bits/8))。返回实际帧数,0=EOF。 */
|
||||
int64_t ncm_dec_read(ncm_decoder_t *d, void *buf, int64_t cap_frames);
|
||||
/* seek 到 ms。flac=精确 PCM 帧;mp3=按 file_bytes*ms/dur_ms 字节近似(同旧语义)。 */
|
||||
/* seek 到 ms。flac=精确 PCM 帧;mp3=按 file_bytes*ms/dur_ms 字节近似(同旧语义)。
|
||||
* 流模式下 seek 目标超已下载区时读端自然阻塞等待(F4),不报错。 */
|
||||
int ncm_dec_seek_ms(ncm_decoder_t *d, int64_t ms, int64_t file_bytes, int64_t dur_ms);
|
||||
void ncm_dec_close(ncm_decoder_t *d);
|
||||
/* "mp3" / "flac"(日志/UI 用)。 */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* play/player_manager.c — 播放线程:URL 获取(全档表逐级降级)→缓存(命中直接播/否则流式落盘)→
|
||||
* minimp3/dr_flac 解码→audio_out 直写。UI 不卡(网络+解码全在此线程)。
|
||||
/* play/player_manager.c — 播放线程:URL 获取(全档表逐级降级)→缓存(命中直接播/否则边下边播)→
|
||||
* minimp3/dr_flac 解码(stream_src 回调)→audio_out 直写。UI 不卡(网络+解码全在此线程)。
|
||||
*
|
||||
* 模型:一个持久的播放线程。外层循环取「目标曲目」,run_song() 负责一首歌的
|
||||
* 「下载(缓冲) → 解码 → 输出 → 结束/被指令打断」。暂停=冻结(停读停写停推 pos,状态置 PAUSED);
|
||||
@@ -11,6 +11,12 @@
|
||||
* 音频缓存键 = 歌曲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。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
@@ -19,6 +25,7 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include "decoder.h"
|
||||
#include "stream_src.h"
|
||||
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
@@ -92,6 +99,9 @@ void player_manager_play(int song_idx)
|
||||
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;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
@@ -127,6 +137,9 @@ void player_manager_next(int dir)
|
||||
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;
|
||||
}
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
@@ -143,13 +156,77 @@ void player_manager_stop(void)
|
||||
}
|
||||
|
||||
/* ---- 每首歌的播放体(在外层循环内调用)---- */
|
||||
static int download_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
|
||||
/* 边下边播每首曲的源对象(§21):读端 stream_src + 下载路径写端/线程。
|
||||
* run_song 栈上分配,下载线程 join 后才返回,生命周期安全。 */
|
||||
#define PREBUFFER_BYTES (128 * 1024) /* 预缓冲门槛 = minimp3 MINIMP3_IO_SIZE(F3 实测强制) */
|
||||
|
||||
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 分母) */
|
||||
} song_src_t;
|
||||
|
||||
/* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 +
|
||||
* 锁内推进 written + broadcast 唤醒读端 + 进度反馈。 */
|
||||
static int stream_dl_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
{
|
||||
/* 下载进度反馈(UI「缓冲中… N%」,§18):回调在播放线程,锁内仅累加。 */
|
||||
song_src_t *ss = (song_src_t *)ctx;
|
||||
if (stream_src_aborted(ss->src)) return 1; /* 切歌/停止:中止 http */
|
||||
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 ncm_cache_write((ncm_cache_writer_t *)ctx, data, len);
|
||||
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";
|
||||
req.extra_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.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
|
||||
req.timeout_ms = 15000; /* 绝对段(连接/握手/头)上限;body 空闲超时 15s(P2) */
|
||||
|
||||
ncm_http_resp_t resp;
|
||||
int rc = ncm_http_download(&req, stream_dl_cb, ss, ss->maxb, &resp);
|
||||
if (rc == NCM_OK && resp.status != 200) rc = NCM_ERR_HTTP;
|
||||
ss->dl_rc = rc;
|
||||
/* 结束通知:成功 final=全长;失败/中止 final=截断点+failed(读端播到截断点自然结束,F5) */
|
||||
stream_src_finish(ss->src, rc == NCM_OK ? 0 : rc);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 收尾(播放线程,解码循环退出后调用):中止在途下载并 join → commit(rename 转正) 或
|
||||
* abort(unlink .tmp)。阶段 2 语义:中断即丢弃;.part 续传/LRU 防淘汰留阶段 3。 */
|
||||
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,此处已近曲终) */
|
||||
if (ncm_cache_commit(ss->w) == 0)
|
||||
ncm_cache_evict(&g_st.ctx.cache); /* 写后检查:新落盘可能推过上限 */
|
||||
} else {
|
||||
ncm_cache_abort(ss->w);
|
||||
}
|
||||
ss->w = NULL;
|
||||
}
|
||||
if (ss->src) { stream_src_close(ss->src); ss->src = NULL; }
|
||||
}
|
||||
|
||||
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
|
||||
@@ -160,50 +237,63 @@ 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));
|
||||
}
|
||||
|
||||
/* 得到本地音频路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。
|
||||
* 返回 NCM_OK 或负错误码。s 为播放曲快照(run_song 物化,与窗口解耦 §17)。
|
||||
/* 切歌竞态护栏(§18):物化/下载/预缓冲期间用户可能已切歌——只有仍是当前目标且处于
|
||||
* BUFFER(本次播放意图未过期)才继续,否则直接回外层(外层会以新 cur 重跑 run_song)。 */
|
||||
static int still_current(int idx)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
int ok = (g_play.cur_song == idx) && (g_play.state == NCM_PC_BUFFER);
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
return 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, char *path, size_t psz, int64_t *bytes)
|
||||
static int song_local_file(const ncm_song_t *s, int idx, 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);
|
||||
|
||||
/* 缓存命中 → 离线直接播(新键两扩展名都试;档位=用户设定档,降级产物也归该档键) */
|
||||
/* 缓存命中 → 静态 stream_src(新键两扩展名都试;档位=用户设定档,降级产物也归该档键) */
|
||||
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) {
|
||||
struct stat st;
|
||||
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;
|
||||
}
|
||||
hit = 1;
|
||||
} else if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
|
||||
/* 旧键温和兼容(§19 前存量:无档位后缀的 song_<id>[.]mp3,含历史双点名):
|
||||
* 命中即播,不主动改名/删除,由 LRU 自然淘汰。 */
|
||||
hit = 2;
|
||||
}
|
||||
/* 旧键温和兼容(§19 前存量:无档位后缀的 song_<id>[.]mp3,含历史双点名):命中即播,
|
||||
* 不主动改名/删除,由 LRU 自然淘汰。 */
|
||||
if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
|
||||
if (hit) {
|
||||
struct stat st;
|
||||
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;
|
||||
}
|
||||
if (stat(path, &st) != 0) return NCM_ERR_NOENT;
|
||||
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;
|
||||
/* 生效档推断(缓存键=用户档,降级产物归用户档键,无法精确还原):
|
||||
* 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;
|
||||
}
|
||||
|
||||
/* fee 门禁:VIP 才能听(1=VIP 歌曲 / 4=付费数字专辑)且非会员 → 不请求 url。
|
||||
@@ -222,50 +312,63 @@ static int song_local_file(const ncm_song_t *s, char *path, size_t psz, int64_t
|
||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = (eff >= 0) ? eff : want;
|
||||
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
/* 落盘扩展名: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";
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
|
||||
/* 写前检查:池大小超限先按 LRU 淘汰(此线程尚未开 temp,无在途文件被误删风险) */
|
||||
ncm_cache_evict(&g_st.ctx.cache);
|
||||
|
||||
/* 流式下载到缓存 temp → commit(稳妥者:先落盘再播) */
|
||||
ncm_cache_writer_t *w = NULL;
|
||||
rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &w);
|
||||
/* 边下边播(§21):下载线程写 .tmp(write(2) 直写),解码线程 pread 同一文件 */
|
||||
rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &ss->w);
|
||||
if (rc != NCM_OK) return rc;
|
||||
|
||||
ncm_http_req_t req;
|
||||
memset(&req, 0, sizeof req);
|
||||
req.url = u.url;
|
||||
req.method = "GET";
|
||||
req.extra_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.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
|
||||
req.timeout_ms = 30000;
|
||||
|
||||
ncm_http_resp_t resp;
|
||||
if (stream_src_open_stream(&ss->src, ncm_cache_writer_tmp_path(ss->w)) != 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;
|
||||
/* 上限:Hi-Res flac 单曲可达数百 MB(192k/24bit 5min ≈345MB),flac 上限 1GB;mp3 维持 64MB。
|
||||
* size 可信时按 size 限幅(防超额),未知按格式默认上限。 */
|
||||
size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20);
|
||||
size_t maxb = (u.size > 0 && (uint64_t)u.size < cap) ? (size_t)u.size : cap;
|
||||
rc = ncm_http_download(&req, download_cb, w, maxb, &resp);
|
||||
if (rc != NCM_OK || resp.status != 200) {
|
||||
ncm_cache_abort(w);
|
||||
return (rc != NCM_OK) ? rc : NCM_ERR_HTTP;
|
||||
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 (pthread_create(&ss->dl_thr, NULL, dl_thread, ss) != 0) {
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return NCM_ERR_IO;
|
||||
}
|
||||
rc = ncm_cache_commit(w);
|
||||
if (rc != 0) return rc;
|
||||
/* 写后检查:新落盘可能把池推过上限 → 再按 LRU 淘汰最旧(刚写的 mtime 最新,存活) */
|
||||
ncm_cache_evict(&g_st.ctx.cache);
|
||||
if (ncm_cache_lookup(&g_st.ctx.cache, key, ext, path, psz) != NCM_OK)
|
||||
return NCM_ERR_NOENT;
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) *bytes = (int64_t)st.st_size;
|
||||
ss->dl_thr_on = 1;
|
||||
|
||||
/* 预缓冲:≥128KB(minimp3 强制门槛,F3)才放行 open_cb;final/failed 提前出。
|
||||
* still_current 护栏覆盖本阻塞点:等待期间切歌/停止 → 中止下载静默放弃。 */
|
||||
for (;;) {
|
||||
int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100);
|
||||
if (wr != 0) break;
|
||||
if (g_app_quit || !still_current(idx)) {
|
||||
stream_src_abort(ss->src);
|
||||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return NCM_ERR_IO; /* 调用方 still_current 复核后静默返回 */
|
||||
}
|
||||
}
|
||||
if (stream_src_aborted(ss->src) ||
|
||||
(stream_src_failed(ss->src) && stream_src_written(ss->src) < PREBUFFER_BYTES)) {
|
||||
/* 中止,或首开声前失败(written < 预缓冲门槛):abort .tmp
|
||||
*(阶段 2 不保留不重试;重试策略留阶段 3)。已下过门槛的断流照常放行——
|
||||
* 读端播到截断点自然结束(F5)。 */
|
||||
int frc = stream_src_failed(ss->src);
|
||||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return frc ? frc : NCM_ERR_NET;
|
||||
}
|
||||
/* final < 门槛(小文件/截断):照样放行,open_cb 读到的就是全部 */
|
||||
*bytes = ss->total > 0 ? ss->total : (int64_t)stream_src_written(ss->src);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
@@ -278,17 +381,7 @@ static void set_err(int err, const char *msg)
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
/* 切歌竞态护栏(§18):物化/下载期间用户可能已切歌——只有仍是当前目标且处于 BUFFER
|
||||
* (本次播放意图未过期)才继续,否则直接回外层(外层会以新 cur 重跑 run_song)。 */
|
||||
static int still_current(int idx)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
int ok = (g_play.cur_song == idx) && (g_play.state == NCM_PC_BUFFER);
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* 一首歌:下载→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
|
||||
/* 一首歌:源就绪(缓存/下载线程+预缓冲)→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
|
||||
static void run_song(int idx)
|
||||
{
|
||||
/* §17 滑窗兼容:物化目标曲(窗内命中 / 盘缓存 / song_detail 单曲)并整体快照进
|
||||
@@ -304,35 +397,43 @@ static void run_song(int idx)
|
||||
g_play.play_song_valid = 1;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
|
||||
song_src_t ss;
|
||||
int64_t file_bytes = 0;
|
||||
char path[512];
|
||||
int rc = song_local_file(&s, path, sizeof path, &file_bytes);
|
||||
int rc = song_local_file(&s, idx, &ss, path, sizeof path, &file_bytes);
|
||||
if (rc != NCM_OK) {
|
||||
if (!still_current(idx)) return; /* 下载期间已切歌:静默放弃(缓存已落盘不亏) */
|
||||
if (!still_current(idx)) 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)) return; /* 下载完成但已切歌:勿开解码/勿动状态 */
|
||||
if (!still_current(idx)) { /* 预缓冲完成但已切歌:勿开解码/勿动状态 */
|
||||
song_src_dispose(&ss);
|
||||
return;
|
||||
}
|
||||
ncm_strlcpy(g_path, path, sizeof g_path);
|
||||
g_file_bytes = file_bytes;
|
||||
|
||||
int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0;
|
||||
|
||||
/* 解码分发(§19):magic 探测 fLaC→dr_flac / 其它→minimp3;rate/ch/bits 按实际流。 */
|
||||
/* 解码分发(§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(&dec, path, &rate, &ch, &bits, &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 元数据等待)已切歌 */
|
||||
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); set_err(NCM_ERR_IO, "audio open failed"); return;
|
||||
ncm_dec_close(dec); song_src_dispose(&ss);
|
||||
set_err(NCM_ERR_IO, "audio open failed"); return;
|
||||
}
|
||||
|
||||
/* 状态→播放 */
|
||||
@@ -351,6 +452,8 @@ static void run_song(int idx)
|
||||
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;
|
||||
|
||||
for (;;) {
|
||||
if (g_app_quit) break;
|
||||
@@ -385,7 +488,8 @@ static void run_song(int idx)
|
||||
}
|
||||
|
||||
if (need_seek_ms >= 0) {
|
||||
/* seek:flac 精确帧 / mp3 关+重开按字节近似(decoder 内处理);输出重开 */
|
||||
/* 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) {
|
||||
@@ -397,8 +501,19 @@ static void run_song(int idx)
|
||||
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) { /* 播完 */
|
||||
if (frames == 0) { /* 播完(下载中断时=截断点,F5) */
|
||||
meow_log(MEOW_LOG_INFO, "play: done idx=%d frames=%lld", idx, (long long)frames_done);
|
||||
break;
|
||||
}
|
||||
@@ -409,6 +524,28 @@ static void run_song(int idx)
|
||||
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);
|
||||
}
|
||||
}
|
||||
/* 更新进度(~200ms 节流)*/
|
||||
int64_t pos = rate > 0 ? frames_done * 1000 / rate : 0;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
@@ -419,6 +556,8 @@ static void run_song(int idx)
|
||||
|
||||
audio_out_close();
|
||||
ncm_dec_close(dec);
|
||||
/* 下载完成由播放线程 commit(rename) / 中断 abort(阶段 2 语义,§21) */
|
||||
song_src_dispose(&ss);
|
||||
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
/* play/stream_src.c — 边下边播共享流源实现(接口与契约见 stream_src.h,REPORT §21)。
|
||||
*
|
||||
* 关键实测事实(NCM_STREAM_PLAY.md,红线):
|
||||
* F7 read_cb 必须严格 fread 语义(填满才返回,真 EOF 才短读),否则 minimp3 假播完;
|
||||
* F5 写端中断(final=截断点)→ 读端短读 → 解码干净结束于截断点;
|
||||
* F4 seek 到未下载区只是改 read_pos,read 自然阻塞等待,数据到后继续出声。
|
||||
*
|
||||
* 等待/唤醒:pthread condvar + 100ms timedwait 兜底(防丢唤醒)。
|
||||
* 再缓冲迟滞(§4.2):单次调用累计等待 >500ms 后,要求 written-read_pos >= 64KB 才放行
|
||||
* (防「每追上 1 帧就出声」的爆音感);final/failed/abort 时不受限。
|
||||
*/
|
||||
#include "stream_src.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define WAIT_SLICE_MS 100 /* cond timedwait 兜底片长 */
|
||||
#define REBUFFER_AFTER_MS 500 /* 单次调用累计等待超此值后启用再缓冲迟滞 */
|
||||
#define REBUFFER_MIN (64 * 1024) /* 迟滞放行门槛(≈1.6s@320kbps) */
|
||||
#define STALL_MS 1000 /* 单次调用等待超此值 → 置 stalled(播放线程 pause 联动) */
|
||||
|
||||
struct stream_src {
|
||||
int fd; /* 只读 fd(pread;写端另有自己的写 fd) */
|
||||
pthread_mutex_t mu;
|
||||
pthread_cond_t cv; /* written/final/failed/abort 变化时 broadcast */
|
||||
uint64_t written; /* 已落盘字节(单调增,写端推进) */
|
||||
uint64_t final; /* 0=下载中;>0=最终大小(完成=全长,中断=截断点) */
|
||||
int failed; /* <0=下载/IO 错误码 */
|
||||
int abort; /* 切歌/停止:读端按 EOF 处理,写端回调返回 NZ */
|
||||
uint64_t read_pos; /* 解码逻辑读指针(seek 回调改写;允许 > written) */
|
||||
/* 统计 */
|
||||
uint64_t wait_ms_total;
|
||||
int underruns;
|
||||
int stalled;
|
||||
};
|
||||
|
||||
static int64_t mono_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static int src_alloc(stream_src_t **out, int fd)
|
||||
{
|
||||
stream_src_t *s = (stream_src_t *)calloc(1, sizeof *s);
|
||||
if (!s) { close(fd); return -1; }
|
||||
s->fd = fd;
|
||||
pthread_mutex_init(&s->mu, NULL);
|
||||
pthread_cond_init(&s->cv, NULL);
|
||||
*out = s;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stream_src_open_stream(stream_src_t **out, const char *tmp_path)
|
||||
{
|
||||
if (!out || !tmp_path) return -1;
|
||||
int fd = open(tmp_path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
return src_alloc(out, fd); /* written=0/final=0:下载中 */
|
||||
}
|
||||
|
||||
int stream_src_open_static(stream_src_t **out, const char *path, uint64_t size)
|
||||
{
|
||||
if (!out || !path) return -1;
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
int rc = src_alloc(out, fd);
|
||||
if (rc == 0) {
|
||||
(*out)->written = size;
|
||||
(*out)->final = size ? size : 1; /* 空文件也置 final(read 立即 EOF) */
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
void stream_src_close(stream_src_t *s)
|
||||
{
|
||||
if (!s) return;
|
||||
if (s->fd >= 0) close(s->fd);
|
||||
pthread_mutex_destroy(&s->mu);
|
||||
pthread_cond_destroy(&s->cv);
|
||||
free(s);
|
||||
}
|
||||
|
||||
void stream_src_note_written(stream_src_t *s, uint64_t n)
|
||||
{
|
||||
if (!s || !n) return;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
s->written += n;
|
||||
pthread_cond_broadcast(&s->cv);
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
}
|
||||
|
||||
void stream_src_finish(stream_src_t *s, int err)
|
||||
{
|
||||
if (!s) return;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
s->final = s->written ? s->written : 1; /* 0 字节下载也视为结束(final=1 > written=0) */
|
||||
if (err) s->failed = err;
|
||||
pthread_cond_broadcast(&s->cv);
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
}
|
||||
|
||||
void stream_src_abort(stream_src_t *s)
|
||||
{
|
||||
if (!s) return;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
s->abort = 1;
|
||||
pthread_cond_broadcast(&s->cv);
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
}
|
||||
|
||||
int stream_src_aborted(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 1;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
int a = s->abort;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return a;
|
||||
}
|
||||
|
||||
int stream_src_wait(stream_src_t *s, uint64_t need, int timeout_ms)
|
||||
{
|
||||
if (!s) return -1;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
if (s->written >= need) { pthread_mutex_unlock(&s->mu); return 1; }
|
||||
if (s->final || s->failed || s->abort) { pthread_mutex_unlock(&s->mu); return -1; }
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_nsec += (long)timeout_ms * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
pthread_cond_timedwait(&s->cv, &s->mu, &ts);
|
||||
int r;
|
||||
if (s->written >= need) r = 1;
|
||||
else if (s->final || s->failed || s->abort) r = -1;
|
||||
else r = 0;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return r;
|
||||
}
|
||||
|
||||
size_t stream_src_peek(stream_src_t *s, void *buf, size_t n, uint64_t off)
|
||||
{
|
||||
if (!s || !buf || !n) return 0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
uint64_t avail = (s->written > off) ? s->written - off : 0;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
if (!avail) return 0;
|
||||
if (n > avail) n = (size_t)avail;
|
||||
ssize_t r = pread(s->fd, buf, n, (off_t)off);
|
||||
return r > 0 ? (size_t)r : 0;
|
||||
}
|
||||
|
||||
/* 核心读:严格 fread 语义。minimp3(MP3D_READ_CB)与 dr_flac(drflac_read_proc)
|
||||
* 共用——dr_flac 接受「实际读到的字节数」,尽力填满是其超集,两契约同满足。 */
|
||||
static size_t src_read(void *buf, size_t size, void *ud)
|
||||
{
|
||||
stream_src_t *s = (stream_src_t *)ud;
|
||||
size_t done = 0;
|
||||
int64_t waited = 0; /* 本次调用累计等待 ms */
|
||||
int waited_this_call = 0; /* 本次调用是否已进入过等待(underrun 按调用计一次) */
|
||||
pthread_mutex_lock(&s->mu);
|
||||
while (done < size) {
|
||||
uint64_t avail = (s->written > s->read_pos) ? s->written - s->read_pos : 0;
|
||||
/* 再缓冲迟滞:本轮累计等过 >500ms 后,前方不足 64KB 且未结束 → 继续等
|
||||
*(仅本轮尚未产出字节时下闸;已有产出则尽力填满)。 */
|
||||
int gated = (done == 0 && avail > 0 && waited > REBUFFER_AFTER_MS &&
|
||||
avail < REBUFFER_MIN && !s->final && !s->failed && !s->abort);
|
||||
if (avail > 0 && !gated) {
|
||||
size_t n = (size - done) < (size_t)avail ? (size - done) : (size_t)avail;
|
||||
uint64_t pos = s->read_pos;
|
||||
pthread_mutex_unlock(&s->mu); /* pread 期间放锁,不阻塞写端 broadcast */
|
||||
ssize_t r = pread(s->fd, (char *)buf + done, n, (off_t)pos);
|
||||
pthread_mutex_lock(&s->mu);
|
||||
if (r <= 0) { /* 盘 IO 错:按中断收尾(真 EOF 语义) */
|
||||
if (!s->failed) s->failed = -3; /* NCM_ERR_IO(本层不依赖 core 头) */
|
||||
s->final = s->written ? s->written : 1;
|
||||
pthread_cond_broadcast(&s->cv);
|
||||
break;
|
||||
}
|
||||
s->read_pos += (uint64_t)r;
|
||||
done += (size_t)r;
|
||||
continue;
|
||||
}
|
||||
if (s->final || s->failed || s->abort) break; /* 真 EOF:短读/0 返回 */
|
||||
/* 追上下载:记 underrun(每次 read 调用至多计一次——minimp3 一次 refill 常先给
|
||||
* 部分数据再等待,done==0 口径会漏记;按「本调用发生了等待」计数) */
|
||||
if (!waited_this_call) { waited_this_call = 1; s->underruns++; }
|
||||
int64_t t0 = mono_ms();
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_nsec += WAIT_SLICE_MS * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
pthread_cond_timedwait(&s->cv, &s->mu, &ts);
|
||||
int64_t dt = mono_ms() - t0;
|
||||
if (dt > 0) { waited += dt; s->wait_ms_total += (uint64_t)dt; }
|
||||
if (waited > STALL_MS) s->stalled = 1;
|
||||
}
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return done;
|
||||
}
|
||||
|
||||
size_t stream_src_read_cb(void *buf, size_t size, void *ud) { return src_read(buf, size, ud); }
|
||||
|
||||
int stream_src_seek_cb(uint64_t pos, void *ud)
|
||||
{
|
||||
stream_src_t *s = (stream_src_t *)ud;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
s->read_pos = pos; /* 允许 > written:读端自然阻塞等待(F4) */
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t stream_src_drflac_read(void *ud, void *buf, size_t bytes_to_read)
|
||||
{
|
||||
return src_read(buf, bytes_to_read, ud);
|
||||
}
|
||||
|
||||
int stream_src_drflac_seek(void *ud, int offset, int origin)
|
||||
{
|
||||
stream_src_t *s = (stream_src_t *)ud;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
if (origin == STREAM_SRC_SEEK_SET) {
|
||||
s->read_pos = (offset < 0) ? 0 : (uint64_t)offset;
|
||||
} else if (origin == STREAM_SRC_SEEK_END) {
|
||||
/* dr_flac open 期用 SEEK_END+tell 取文件总长做元数据校验;下载中不知总长,
|
||||
* 按 dr_flac 约定返回 FALSE(hasKnownFileSize=false → 跳过该校验)。
|
||||
* 静态源/已完成的下载 final 已知 → 正常定位。 */
|
||||
if (!s->final) { pthread_mutex_unlock(&s->mu); return 0; }
|
||||
int64_t np = (int64_t)s->final + offset;
|
||||
if (np < 0) { pthread_mutex_unlock(&s->mu); return 0; }
|
||||
s->read_pos = (uint64_t)np;
|
||||
} else {
|
||||
int64_t np = (int64_t)s->read_pos + offset;
|
||||
if (np < 0) { pthread_mutex_unlock(&s->mu); return 0; } /* drflac_bool32 false */
|
||||
s->read_pos = (uint64_t)np;
|
||||
}
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return 1; /* drflac_bool32 true */
|
||||
}
|
||||
|
||||
int stream_src_drflac_tell(void *ud, int64_t *cursor)
|
||||
{
|
||||
stream_src_t *s = (stream_src_t *)ud;
|
||||
if (!cursor) return 0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
*cursor = (int64_t)s->read_pos;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void stream_src_stats(stream_src_t *s, int *underruns, uint64_t *wait_ms)
|
||||
{
|
||||
if (!s) return;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
if (underruns) *underruns = s->underruns;
|
||||
if (wait_ms) *wait_ms = s->wait_ms_total;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
}
|
||||
|
||||
int stream_src_take_stalled(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
int v = s->stalled;
|
||||
s->stalled = 0;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return v;
|
||||
}
|
||||
|
||||
uint64_t stream_src_written(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
uint64_t w = s->written;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return w;
|
||||
}
|
||||
|
||||
uint64_t stream_src_final(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 1;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
uint64_t f = s->final;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return f;
|
||||
}
|
||||
|
||||
int stream_src_failed(stream_src_t *s)
|
||||
{
|
||||
if (!s) return -1;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
int f = s->failed;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return f;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* play/stream_src.h — 边下边播共享流源(REPORT §21,方案 NCM_STREAM_PLAY A:写穿缓存)。
|
||||
*
|
||||
* 模型:下载线程把字节顺序写进 .tmp(ncm_cache 写者 write(2) 直写,不走 stdio 缓冲),
|
||||
* 解码线程经本对象 pread 同一文件——落盘的文件本身就是下载缓冲,内存里无第二份拷贝。
|
||||
* 写端每写完一块调用 stream_src_note_written() 推进可读边界并 broadcast 唤醒读端;
|
||||
* 下载结束(完成/中断)stream_src_finish() 置 final;切歌/停止 stream_src_abort()。
|
||||
*
|
||||
* 读回调契约(实测红线 F7):stream_src_read_cb 严格 fread 语义——填满才返回,
|
||||
* 真 EOF(final/failed/abort 且数据耗尽)才短读;否则 minimp3 把短读当 EOF 假播完。
|
||||
* dr_flac 回调语义不同(返回实际读到的字节数):本对象同样先尽力填满再返回,
|
||||
* 两种契约同时满足。
|
||||
*
|
||||
* 本层只依赖 pthread/pread/write,不依赖 core(保持 app/play 层自含)。
|
||||
*/
|
||||
#ifndef NCM_STREAM_SRC_H
|
||||
#define NCM_STREAM_SRC_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct stream_src stream_src_t;
|
||||
|
||||
/* seek 原点(与 drflac_seek_origin 同值,避免本头依赖 dr_flac.h)。 */
|
||||
enum { STREAM_SRC_SEEK_SET = 0, STREAM_SRC_SEEK_CUR = 1, STREAM_SRC_SEEK_END = 2 };
|
||||
|
||||
/* 下载路径:tmp_path 已由 ncm_cache_open 创建(写端持有自己的写 fd);
|
||||
* 本函数只开只读 fd(pread 与写端 write 无偏移冲突),written 从 0 起由写端推进。 */
|
||||
int stream_src_open_stream(stream_src_t **out, const char *tmp_path);
|
||||
/* 缓存命中路径:完整文件,written=final=size,read 永不阻塞(同一套读回调)。 */
|
||||
int stream_src_open_static(stream_src_t **out, const char *path, uint64_t size);
|
||||
void stream_src_close(stream_src_t *s);
|
||||
|
||||
/* ---- 写端(下载线程) ---- */
|
||||
/* 数据已落盘(ncm_cache_write 成功)后调用:推进可读边界 + 唤醒读端。 */
|
||||
void stream_src_note_written(stream_src_t *s, uint64_t n);
|
||||
/* 下载结束:err==0=完成(final=written),否则中断(final=written + failed=err)。
|
||||
* 之后 read_cb 数据耗尽即短读(真 EOF),解码自然结束于截断点(F5)。 */
|
||||
void stream_src_finish(stream_src_t *s, int err);
|
||||
/* 切歌/停止:唤醒所有等待,read 立即按 EOF 短读返回;下载回调应随后返回 NZ 中止 http。 */
|
||||
void stream_src_abort(stream_src_t *s);
|
||||
int stream_src_aborted(stream_src_t *s); /* 下载回调每块开头廉价检查 */
|
||||
|
||||
/* ---- 预缓冲 ---- */
|
||||
/* 等 written >= need;final/failed/abort 提前返回。timeout_ms 为单次等待上限
|
||||
* (调用方在循环里套切歌护栏)。返回 1=满足,0=超时,<0=已结束/失败/中止。 */
|
||||
int stream_src_wait(stream_src_t *s, uint64_t need, int timeout_ms);
|
||||
|
||||
/* 同步 peek(pread,不动 read_pos):magic 探测用。off+n 超出 written 的部分不读,
|
||||
* 返回实际读到字节数。 */
|
||||
size_t stream_src_peek(stream_src_t *s, void *buf, size_t n, uint64_t off);
|
||||
|
||||
/* ---- minimp3 回调(MP3D_READ_CB / MP3D_SEEK_CB) ---- */
|
||||
size_t stream_src_read_cb(void *buf, size_t size, void *ud);
|
||||
int stream_src_seek_cb(uint64_t pos, void *ud);
|
||||
|
||||
/* ---- dr_flac 回调(drflac_read_proc / drflac_seek_proc / drflac_tell_proc) ---- */
|
||||
size_t stream_src_drflac_read(void *ud, void *buf, size_t bytes_to_read);
|
||||
int stream_src_drflac_seek(void *ud, int offset, int origin); /* origin=STREAM_SRC_SEEK_* */
|
||||
int stream_src_drflac_tell(void *ud, int64_t *cursor);
|
||||
|
||||
/* ---- 统计 / 停滞联动 ---- */
|
||||
/* underrun=read_cb 追上下载(无数据可给)的等待次数;wait_ms=累计等待 ms。 */
|
||||
void stream_src_stats(stream_src_t *s, int *underruns, uint64_t *wait_ms);
|
||||
/* 单次 read 等待 >1s 置位(播放线程下一轮对 audiod pause(1),恢复后 pause(0))。
|
||||
* 取走即清零。 */
|
||||
int stream_src_take_stalled(stream_src_t *s);
|
||||
|
||||
uint64_t stream_src_written(stream_src_t *s);
|
||||
uint64_t stream_src_final(stream_src_t *s); /* 0=下载中;>0=最终大小(完成=全长,中断=截断点) */
|
||||
int stream_src_failed(stream_src_t *s); /* 0=无错误;<0=下载/IO 错误码 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* NCM_STREAM_SRC_H */
|
||||
Reference in new issue
Block a user