feat(ncm): 网易云客户端 v1 UI+播放链路 (agent-205) — os.meow.ncm: 登录页(真实unikey二维码+轮询状态机)/我的歌单(listview)/歌单详情/播放页(封面+seek+滚动歌词+竖横屏切换)/设置(音质默认Hi-res+缓存上限默认10G); 播放线程(缓存LRU+minimp3→ALSA UAPI直开hw:0,0); 媒体会话MSG_META上报+PLAYCTL接收(首真实媒体源)
ncm 1e331964 (text 764KB); 0 warning; 宿主离屏6页截图目检+真实QR可扫+qemu-mipsel干净启动+解码446xRT 真机待联调: ALSA时序/803扫码闭环/媒体会话联动/gbk大字库渲染
This commit is contained in:
1 parent
32543ac616
commit
21519ce2e4
23 files changed
+3255
No files matched your search
@@ -0,0 +1,321 @@
|
||||
/* play/player_manager.c — 播放线程:URL 获取(Hi-res 逐档降级)→缓存(命中直接播/否则流式落盘)→
|
||||
* minimp3 解码→audio_out 直写。UI 不卡(网络+解码全在此线程)。
|
||||
*
|
||||
* 模型:一个持久的播放线程。外层循环取「目标曲目」,run_song() 负责一首歌的
|
||||
* 「下载(缓冲) → 解码 → 输出 → 结束/被指令打断」。暂停=冻结(停读停写停推 pos,状态置 PAUSED);
|
||||
* seek=重开解码器+输出并 seek 到目标 ms(字节 offset 近似);下一/上一首=打断当前 run_song 返回外层。
|
||||
* 所有跨线程共享状态在 g_play (mu)。g_st.ctx 的 device_id/cookie/cache 在播放期间稳定(登出会先停播)。
|
||||
* 返回码/错误经 g_play.err/errmsg 供 UI 提示。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include "minimp3_ex.h"
|
||||
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
static int g_want_run = 1; /* 线程运行标志(quit=0) */
|
||||
#define PCM_CHUNK 8192 /* 每块交织样本数(双声道 → 4096 帧) */
|
||||
|
||||
/* ---- 线程内部一次性歌的状态 ---- */
|
||||
static char g_path[512]; /* 本地 mp3 路径(缓存) */
|
||||
static int64_t g_file_bytes;
|
||||
static int g_level; /* 音质档(来自设置) */
|
||||
static int g_cache_gb;
|
||||
|
||||
int player_manager_state(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.state; pthread_mutex_unlock(&g_play.mu); return s; }
|
||||
int player_manager_playing_song(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.cur_song; pthread_mutex_unlock(&g_play.mu); return s; }
|
||||
int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu); p = g_play.pos_ms; pthread_mutex_unlock(&g_play.mu); return p; }
|
||||
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; }
|
||||
|
||||
void player_manager_set_quality(int level, int cache_gb)
|
||||
{
|
||||
g_level = level;
|
||||
g_cache_gb = cache_gb;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.err = 0;
|
||||
ncm_cache_set_limit_gb(&g_st.ctx.cache, cache_gb);
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
void player_manager_play(int song_idx)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.cur_song = song_idx;
|
||||
g_play.cmd = 0;
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
g_play.pos_ms = 0;
|
||||
g_play.seq++;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
void player_manager_pause(int on)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
if (on) { if (g_play.state == NCM_PC_PLAYING) g_play.state = NCM_PC_PAUSED; }
|
||||
else { if (g_play.state == NCM_PC_PAUSED) g_play.state = NCM_PC_PLAYING; }
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
void player_manager_seek(int64_t ms)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.cmd = NCM_CTL_SEEK;
|
||||
g_play.cmd_seek_ms = ms;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
void player_manager_next(int dir)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
int c = g_play.cur_song + dir;
|
||||
if (c < 0) c = 0;
|
||||
if (c >= g_st.song_count) c = g_st.song_count - 1;
|
||||
if (c != g_play.cur_song) {
|
||||
g_play.cur_song = c;
|
||||
g_play.cmd = 0;
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
g_play.pos_ms = 0;
|
||||
g_play.seq++;
|
||||
}
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
void player_manager_stop(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.cur_song = -1;
|
||||
g_play.state = NCM_PC_STOPPED;
|
||||
g_play.pos_ms = 0;
|
||||
g_play.seq++;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
/* ---- 每首歌的播放体(在外层循环内调用)---- */
|
||||
static int download_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
{
|
||||
return ncm_cache_write((ncm_cache_writer_t *)ctx, data, len);
|
||||
}
|
||||
|
||||
/* 得到本地 mp3 路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。
|
||||
* 返回 NCM_OK 或负错误码。 */
|
||||
static int song_local_file(int idx, char *path, size_t psz, int64_t *bytes)
|
||||
{
|
||||
const ncm_song_t *s = &g_st.songs[idx];
|
||||
const char *id = s->id;
|
||||
|
||||
/* 缓存命中 → 离线直接播 */
|
||||
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; }
|
||||
}
|
||||
|
||||
/* 播放地址:从 g_level 降级,只收 mp3 */
|
||||
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);
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||
|
||||
/* 流式下载到缓存 temp → commit(稳妥者:先落盘再播) */
|
||||
ncm_cache_writer_t *w = NULL;
|
||||
rc = ncm_cache_open(&g_st.ctx.cache, id, ".mp3", &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;
|
||||
size_t maxb = (u.size > 0 && u.size < (64LL << 20)) ? (size_t)u.size : (64u << 20);
|
||||
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;
|
||||
}
|
||||
rc = ncm_cache_commit(w);
|
||||
if (rc != 0) return rc;
|
||||
if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) != NCM_OK)
|
||||
return NCM_ERR_NOENT;
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) *bytes = (int64_t)st.st_size;
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
static void set_err(int err, const char *msg)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.err = err;
|
||||
if (msg) { ncm_strlcpy(g_play.errmsg, msg, sizeof g_play.errmsg); }
|
||||
g_play.state = NCM_PC_STOPPED;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
/* 一首歌:下载→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
|
||||
static void run_song(int idx)
|
||||
{
|
||||
int64_t file_bytes = 0;
|
||||
char path[512];
|
||||
int rc = song_local_file(idx, path, sizeof path, &file_bytes);
|
||||
if (rc != NCM_OK) {
|
||||
const char *m = (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
|
||||
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
|
||||
set_err(rc, m);
|
||||
return;
|
||||
}
|
||||
ncm_strlcpy(g_path, path, sizeof g_path);
|
||||
g_file_bytes = file_bytes;
|
||||
|
||||
const ncm_song_t *s = &g_st.songs[idx];
|
||||
int64_t dur = s->duration_ms > 0 ? s->duration_ms : 0;
|
||||
|
||||
mp3dec_ex_t dec;
|
||||
memset(&dec, 0, sizeof dec);
|
||||
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { set_err(NCM_ERR_IO, "decode open failed"); return; }
|
||||
|
||||
/* 音频输出:解码器输出 S16_LE(minimp3 默认);用 44100/2ch 直开 */
|
||||
if (audio_out_open(44100, 2, 16) != 0) { mp3dec_ex_close(&dec); set_err(NCM_ERR_IO, "audio open failed"); return; }
|
||||
|
||||
/* 状态→播放 */
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.state = NCM_PC_PLAYING;
|
||||
g_play.pos_ms = 0;
|
||||
g_play.dur_ms = dur;
|
||||
g_play.err = 0;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
|
||||
mp3d_sample_t pcm[PCM_CHUNK];
|
||||
int64_t frames_done = 0; /* 已输出帧(每通道) */
|
||||
int need_seek_ms = -1;
|
||||
|
||||
for (;;) {
|
||||
if (g_app_quit) break;
|
||||
|
||||
/* 取命令 */
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
int cmd = g_play.cmd; g_play.cmd = 0;
|
||||
int64_t seek_ms = g_play.cmd_seek_ms;
|
||||
int cur = g_play.cur_song;
|
||||
int st = g_play.state;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
|
||||
if (cur != idx) break; /* 切歌 → 回外层 */
|
||||
if (st == NCM_PC_STOPPED) break;
|
||||
|
||||
if (cmd == NCM_CTL_SEEK && seek_ms >= 0) { need_seek_ms = (int)seek_ms; }
|
||||
|
||||
if (st == NCM_PC_PAUSED) { usleep(40 * 1000); continue; } /* 冻结 */
|
||||
|
||||
if (need_seek_ms >= 0) {
|
||||
/* 重开并 seek:关闭后重开(字节 offset 近似) */
|
||||
audio_out_close();
|
||||
mp3dec_ex_close(&dec);
|
||||
memset(&dec, 0, sizeof dec);
|
||||
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { set_err(NCM_ERR_IO, "seek reopen failed"); break; }
|
||||
if (audio_out_open(44100, 2, 16) != 0) { set_err(NCM_ERR_IO, "audio reopen failed"); break; }
|
||||
int64_t target = (g_file_bytes > 0 && dur > 0)
|
||||
? (int64_t)g_file_bytes * need_seek_ms / dur : 0;
|
||||
mp3dec_ex_seek(&dec, (uint64_t)(target > 0 ? target : 0));
|
||||
frames_done = (int64_t)need_seek_ms * 44100 / 1000;
|
||||
need_seek_ms = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t got = mp3dec_ex_read(&dec, pcm, PCM_CHUNK);
|
||||
if (got == 0) break; /* 播完 */
|
||||
int frames = (int)(got / 2);
|
||||
if (audio_out_write(pcm, frames) != frames) break;
|
||||
frames_done += frames;
|
||||
/* 更新进度(~200ms 节流)*/
|
||||
int64_t pos = frames_done * 1000 / 44100;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.pos_ms = pos;
|
||||
if (dur > 0 && pos >= dur) g_play.pos_ms = dur;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
audio_out_close();
|
||||
mp3dec_ex_close(&dec);
|
||||
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
|
||||
}
|
||||
|
||||
/* 外层播放线程循环 */
|
||||
static void *player_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (g_want_run && !g_app_quit) {
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
int cur = g_play.cur_song;
|
||||
int st = g_play.state;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
|
||||
if (cur < 0 || cur >= g_st.song_count || st == NCM_PC_STOPPED) {
|
||||
usleep(60 * 1000);
|
||||
continue;
|
||||
}
|
||||
/* 以本地快照播放(idx 被外层线程独占,无需再锁) */
|
||||
run_song(cur);
|
||||
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
int cur2 = g_play.cur_song;
|
||||
int st2 = g_play.state;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
/* 自然播完且无新命令 → 顺延下一首;否则回到循环等待 */
|
||||
if (cur2 >= 0 && cur2 < g_st.song_count && st2 != NCM_PC_STOPPED && cur2 == cur) {
|
||||
int nx = cur + 1;
|
||||
if (nx < g_st.song_count) { player_manager_next(1); }
|
||||
else { player_manager_stop(); }
|
||||
} else {
|
||||
usleep(50 * 1000);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 为当前曲目拉歌词并解析(UI 线程调):buf 有界,解析后存 lyric 模块。 */
|
||||
int player_manager_load_lyric(void)
|
||||
{
|
||||
lyric_clear();
|
||||
int idx = player_manager_playing_song();
|
||||
if (idx < 0 || idx >= g_st.song_count) return NCM_ERR_ARG;
|
||||
const ncm_song_t *s = &g_st.songs[idx];
|
||||
char *lrc = (char *)malloc(64 * 1024);
|
||||
char *tl = (char *)malloc(32 * 1024);
|
||||
if (!lrc || !tl) { free(lrc); free(tl); return NCM_ERR_OOM; }
|
||||
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, s->id, lrc, 64 * 1024, tl, 32 * 1024);
|
||||
if (rc == NCM_OK) lyric_parse(lrc, tl);
|
||||
free(lrc);
|
||||
free(tl);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int player_manager_start(void)
|
||||
{
|
||||
if (g_thr_on) return 0;
|
||||
g_want_run = 1;
|
||||
g_level = g_st.level;
|
||||
g_cache_gb = g_st.cache_limit_gb;
|
||||
pthread_mutex_init(&g_play.mu, NULL);
|
||||
if (pthread_create(&g_thr, NULL, player_thread, NULL) != 0) return -1;
|
||||
g_thr_on = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void player_manager_stop_thread(void)
|
||||
{
|
||||
g_want_run = 0;
|
||||
g_app_quit = 1;
|
||||
audio_out_close(); /* 打断阻塞写 */
|
||||
if (g_thr_on) {
|
||||
pthread_join(g_thr, NULL);
|
||||
g_thr_on = 0;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user