播放器
概述
媒体播放器是嵌入式音频应用中最常见的核心组件之一,用于播放本地或网络上的音频内容。Media Framework 提供了一个统一的 MediaPlayer 接口,定义了"创建—设置数据源—准备—播放控制—释放"这一完整的播放生命周期,使应用层无需关心底层是哪种容器格式、哪种解码器。
与裸调用各类解码库相比,MediaPlayer 在内部封装了多协议的数据源接入、容器解封装、音频解码、渲染同步以及自动缓冲等管线,并通过事件回调向上层报告状态变化、缓冲进度和错误信息。同时框架基于 Kconfig 进行了模块化裁剪,可以按业务实际需要勾选所需的格式和第三方解码库,最大限度控制 ROM/RAM 占用,使其能够适配不同算力等级的 SoC。
功能特性
Media Framework 在轻量级 IoT 设备上提供了接近桌面/移动端水平的播放能力:
统一抽象的 C 接口:MediaPlayer 屏蔽容器格式与数据源差异。无论是播放 LittleFS 中的提示音 WAV、Flash 中的 MP3 铃声,还是来自云端的 HTTPS AAC 流,应用层代码几乎一致。
覆盖主流音频格式:支持 WAV、MP3、AAC、M4A、FLAC、AMR、OGG(Vorbis / Opus)的解封装与解码;每种格式都可独立裁剪。
多种媒体来源:支持本地文件系统(LittleFS / FAT / USB / SD)、HTTP/HTTPS 流式播放、以及面向客户的自定义 StreamSource。
同步与异步两种准备模式:Prepare 适合短文件场景;PrepareAsync 在后台完成首部解析与缓冲,避免阻塞主线程,明显改善网络播放的首帧体感。
完整的播放控制语义:Start / Pause / Stop / Seek / Loop / SetVolume / SetSpeed,以及 GetCurrentTime / GetDuration / IsPlaying 等查询接口。
基于回调的事件机制:OnStateChanged、OnInfo、OnError 三个通道分别报告状态机迁移、缓冲信息、不可恢复错误,便于实现 UI 联动与重试逻辑。
可裁剪的轻量编译:菜单化的 Config 控制让用户按需启用"支持格式/第三方库"。
系统架构图
系统架构
各层职责说明:
层次 |
内容 |
|---|---|
应用层 |
用户应用代码,调用 Public C API |
接口层 |
|
多媒体框架 |
支持数据获取、解封装、解码、渲染同步 |
音频HAL |
I2S 驱动、AMP 控制,由 AudioService 抽象 |
芯片 |
硬件 |
应用层只需包含 interfaces/media/ 下的头文件,链接多媒体框架即可完成集成。
支持的格式
MediaPlayer 播放器支持以下音频格式:
音频格式 |
描述 |
文件类型 |
|---|---|---|
AAC |
支持标准采样率从 8kHz 到 96kHz 的单声道/立体声内容 |
ADTS raw AAC (.aac) |
MP3 |
单声道/立体声 8kbps ~ 320kbps 固定码率(CBR)或可变码率(VBR) |
MP3 (.mp3) |
PCM/WAVE |
8 位、16 位、24 位和浮点线性 PCM,采样率从 8kHz 到 96kHz 的原始 PCM 录音 |
WAVE (.wav) |
FLAC |
单声道/立体声(不支持多声道),采样率最高可达 48kHz,建议 16 位; 支持无抖动 24 位 |
FLAC (.flac) |
M4A |
支持标准采样率从 8kHz 到 96kHz 的单声道/立体声内容 |
MPEG-4 (.m4a) |
OGG-Opus |
低延迟语音编解码,码率 6 ~ 510 kbps,适合实时通信与语音消息 |
Ogg (.ogg) |
OGG-Vorbis |
开源通用音频格式,音质接近 AAC,适合音乐与播客 |
Ogg (.ogg) |
AMR |
AMR-NB(窄带)/ AMR-WB(宽带)语音编解码,适合语音消息与通话录音 |
AMR (.amr) |
配置
Media Framework 通过 Kconfig 进行模块化裁剪,可按业务需要勾选音频格式与第三方解码库,以平衡功能与固件体积。
基础配置
在工程目录执行 ./menuconfig.py,进入以下路径:
Audio Config --->
[*] Enable Audio Framework # AUDIO_FWK_MENU
Select Audio Interfaces --->
(X) Mixer # 推荐 Mixer 模式
[*] Enable Media Player # MEDIA_PLAYER_MENU(自动 select AUDIO_FWK_MENU)
MEDIA_PLAYER_MENU 会自动选中 AUDIO_FWK_MENU,无需手动勾选。Audio Framework 提供底层 AudioService,必须先于 Media Player 初始化(调用 AudioService_Init())。
音频格式配置
Audio Config --->
[*] Enable Media Player --->
Media Formats ---> # 按需勾选
[*] WAV
[*] MP3
[*] AAC
[*] M4A
[*] FLAC
[*] AMR
[*] OGG --->
OGG Codec --->
[*] Vorbis
[*] Opus
每个格式对应独立的 demux + codec 模块,勾选后自动选中所需第三方库。仅保留实际需要的格式可显著减少固件体积。
第三方库配置
Kconfig 选项 |
库名 |
用途 |
|---|---|---|
THIRD_PARTY_HAAC_MENU |
FDK-AAC |
AAC、M4A 格式解码 |
THIRD_PARTY_FLAC_MENU |
libFLAC |
FLAC 无损格式解码 |
THIRD_PARTY_OPUS_MENU |
libopus |
Opus 编解码 |
THIRD_PARTY_TREMOLO_MENU |
Tremolo |
OGG Vorbis 解码 |
THIRD_PARTY_GSM_MENU |
libgsm |
AMR-NB/WB 解码 |
第三方库在勾选对应格式时由 Kconfig 自动选中,也可在 Third Party Libraries 菜单中手动配置。
使用入门
本章按"创建—填充数据源—准备—播放控制—释放"的顺序介绍 MediaPlayer 的完整使用流程。
创建播放器
任何使用 MediaPlayer 的代码都需要先初始化音频服务,再分配播放器实例。AudioService_Init 负责唤起底层 HAL、启动渲染线程、加载默认音效配置;该接口建议在系统启动早期统一调用一次:
#include "media/media_player.h"
#include "audio/audio_service.h"
AudioService_Init(); // 整个进程生命周期内调用一次即可
MediaPlayer *player = MediaPlayer_Create();
MediaPlayer_Create 返回的实例此时处于 IDLE 状态。建议在创建后立即调用 MediaPlayer_SetCallback() 注册回调,否则后续 Prepare / Start 过程中产生的早期事件(如 PREPARED、BUFFERING_START)会因为没有监听者而被丢弃。
设置数据源
播放器对"数据从哪里来"提供两种互斥的接入方式。URL 方式适用于绝大多数场景,前缀决定底层走哪条数据通路:
MediaPlayer_SetDataSource(player, "lfs://res/welcome.mp3"); // LittleFS
MediaPlayer_SetDataSource(player, "fat://music/song.flac"); // FAT / SD
MediaPlayer_SetDataSource(player, "http://example.com/audio.aac"); // HTTP
MediaPlayer_SetDataSource(player, "https://cdn.example.com/song.m4a"); // HTTPS+TLS
当数据并非来自 URL(例如蓝牙音频帧、加密容器、外部 buffer),可以通过实现 StreamSource 接口接入自定义源:
StreamSource *source = MyDataSource_Create(buffer, length);
MediaPlayer_SetStreamSource(player, source);
无论哪种方式,必须在 IDLE 状态下设置数据源。如果需要切歌,要先调用 MediaPlayer_Reset() 把状态机带回 IDLE。
准备播放器
方式 |
函数 |
阻塞 |
完成通知 |
适用场景 |
|---|---|---|---|---|
同步 |
MediaPlayer_Prepare() |
是 |
函数返回即完成 |
调试、简单场景 |
异步 |
MediaPlayer_PrepareAsync() |
否 |
OnStateChanged(MEDIA_PLAYER_PREPARED) |
生产代码,避免阻塞 |
PrepareAsync 提速原理:调用后播放器立即进入 PREPARING 状态,SDK 在内部线程异步完成网络连接、格式探测、解码器初始化。应用线程不阻塞,准备完成通过回调通知。所谓提速并不是底层准备本身变快,而是把阻塞从调用线程转移到了 looper 线程。对于网络流,Prepare 可能需要数百毫秒到数秒来完成 TCP 握手、TLS 协商和初始缓冲,这段时间内 UI 线程或业务主循环若同步等待会出现明显的卡顿。PrepareAsync 立即返回后,应用可以继续刷新 UI、响应按键、做播放过渡动画,等到准备完成再切到播放态,从用户感知层面显著降低了"按下播放键到出声"的时延。网络播放强烈建议使用 PrepareAsync;本地短文件用同步版本则代码更简洁。
异步准备示例(网络流推荐):
static volatile int g_prepared = 0;
void OnStateChanged(const MediaPlayerCallback *cb,
const MediaPlayer *player, int state)
{
if (state == MEDIA_PLAYER_PREPARED) {
g_prepared = 1;
}
}
MediaPlayer_PrepareAsync(player);
while (!g_prepared) { /* 实际应用用信号量替代轮询 */
rtos_time_delay_ms(20);
}
MediaPlayer_Start(player);
同步准备示例(本地小文件推荐):
if (MediaPlayer_Prepare(player) == AUDIO_OK) {
MediaPlayer_Start(player);
}
控制播放器
播放器状态机
API |
有效状态 |
说明 |
|---|---|---|
MediaPlayer_Start() |
PREPARED / PAUSED / REWIND_COMPLETE |
开始或恢复播放 |
MediaPlayer_Pause() |
STARTED |
暂停,保留当前位置 |
MediaPlayer_Stop() |
STARTED / PAUSED / PLAYBACK_COMPLETE |
停止播放 |
MediaPlayer_Seek(msec) |
PREPARED / STARTED / PAUSED |
跳转到指定毫秒位置 |
MediaPlayer_SetLooping(1) |
任意有效状态 |
循环播放 |
MediaPlayer_SetVolume(l, r) |
STARTED 及之后 |
设置左右声道音量(0.0–1.0) |
MediaPlayer_SetSpeed(spd, pitch) |
STARTED |
变速变调(1.0 = 正常) |
MediaPlayer_GetCurrentTime(&ms) |
STARTED / PAUSED |
获取当前播放位置(毫秒) |
MediaPlayer_GetDuration(&ms) |
PREPARED 及之后 |
总时长,直播流返回 -1 |
MediaPlayer_IsPlaying() |
任意 |
1 = 正在播放 |
MediaPlayer_Reset() |
STOPPED 及之后 |
重置到 IDLE,可重新 SetDataSource |
有效状态转换表
当前状态 |
允许的操作 |
目标状态 |
|---|---|---|
IDLE |
SetDataSource / SetStreamSource |
IDLE |
IDLE |
Prepare |
PREPARED |
IDLE |
PrepareAsync |
PREPARING |
IDLE |
Destroy |
— |
PREPARING |
Prepare 完成 |
PREPARED |
PREPARED |
Start |
STARTED |
PREPARED |
Reset |
IDLE |
STARTED |
Pause |
PAUSED |
STARTED |
Stop |
STOPPED |
STARTED |
Seek 完成 |
REWIND_COMPLETE |
STARTED |
播放完成 |
PLAYBACK_COMPLETE |
REWIND_COMPLETE |
Start |
STARTED |
PAUSED |
Start |
STARTED |
PAUSED |
Stop |
STOPPED |
STOPPED |
Reset |
IDLE |
PLAYBACK_COMPLETE |
Reset |
IDLE |
ERROR |
Reset |
IDLE |
典型完整播放流程:
AudioService_Init();
MediaPlayer *player = MediaPlayer_Create();
/* cb 必须在播放器整个生命周期内保持有效,禁止使用栈变量;
建议堆分配或使用静态/全局变量。需在 MediaPlayer_Destory 之后再 free。 */
MediaPlayerCallback *cb = malloc(sizeof(MediaPlayerCallback));
cb->OnStateChanged = OnStateChanged;
cb->OnInfo = OnInfo;
cb->OnError = OnError;
MediaPlayer_SetCallback(player, cb);
MediaPlayer_SetDataSource(player, "lfs://audio/demo.mp3");
MediaPlayer_PrepareAsync(player);
/* 等待 PREPARED 回调 ... */
MediaPlayer_Start(player);
int64_t duration = 0;
MediaPlayer_GetDuration(player, &duration);
/* 播放结束(收到 PLAYBACK_COMPLETE 回调)后 */
MediaPlayer_Stop(player);
/* 等待 STOPPED 回调 ... */
MediaPlayer_Reset(player);
MediaPlayer_Destory(player);
free(cb);
释放播放器
释放顺序必须严格遵守:Stop → (等 STOPPED) → Reset → Destory。
MediaPlayer_Stop(player);
/* OnStateChanged(MEDIA_PLAYER_STOPPED) 后 */
MediaPlayer_Reset(player);
MediaPlayer_Destory(player);
/* StreamSource 必须在 Destory 之后释放 */
if (stream_source) {
MyStreamSource_Destroy((MyStreamSource *)stream_source);
}
free(cb);
备注
StreamSource 的 Destory 必须在 MediaPlayer_Destory() 返回后执行,否则 SDK 仍可能在访问数据。
播放器事件
MediaPlayer 通过 MediaPlayerCallback 结构体向应用层上报三类事件:状态变更、播放信息、播放错误。本章介绍回调注册方式、注意事项及三种回调的具体使用。
监听播放事件
填充 MediaPlayerCallback 结构体并在 SetDataSource() 之前调用 MediaPlayer_SetCallback() 。
/* cb 的生命周期必须 ≥ 播放器实例:禁止使用栈变量;
建议堆分配或使用静态/全局变量,并在 MediaPlayer_Destory 之后再 free。 */
MediaPlayerCallback *cb = malloc(sizeof(MediaPlayerCallback));
cb->OnStateChanged = my_on_state_changed;
cb->OnInfo = my_on_info;
cb->OnError = my_on_error;
MediaPlayer_SetCallback(player, cb);
回调使用注意事项
设置时机
必须在
MediaPlayer_Prepare/MediaPlayer_PrepareAsync调用之前设置回调,否则可能错过MEDIA_PLAYER_PREPARED等关键状态。MediaPlayerCallback结构体必须在播放器整个生命周期内保持有效,不要使用栈变量。
线程安全
三个回调均由 Player Core 内部线程上抛,不在调用方线程。在回调中若访问应用层共享数据,需自行加锁。
同一个播放器实例的回调是串行的,不会并发触发。
内存管理
回调参数中的 listener / player 指针由框架持有,回调返回前一直有效;不要释放、不要保存到回调外的长期容器。
若需要在回调中向其他线程传递事件,请通过队列/消息复制传递,而不是保存指针。
避免死锁
回调上下文持有播放器内部消息循环,禁止在回调中同步调用
MediaPlayer_Stop/MediaPlayer_Reset/MediaPlayer_Destory,否则会与内部状态机互锁。如需基于事件控制播放器,请通过应用层任务/消息异步执行。
回调函数应尽快返回:耗时操作(文件 IO、网络、UI 刷新)必须丢到自己的工作线程。
播放状态变更 OnStateChanged
void OnStateChanged(const MediaPlayerCallback *cb,
const MediaPlayer *player, int state)
{
switch (state) {
case MEDIA_PLAYER_IDLE: /* 0 - 初始/Reset 后 */ break;
case MEDIA_PLAYER_PREPARING: /* 1 - PrepareAsync 进行中 */ break;
case MEDIA_PLAYER_PREPARED: /* 2 - 准备完成 */ break;
case MEDIA_PLAYER_STARTED: /* 3 - 播放中 */ break;
case MEDIA_PLAYER_PAUSED: /* 4 - 已暂停 */ break;
case MEDIA_PLAYER_STOPPED: /* 5 - 已停止 */ break;
case MEDIA_PLAYER_PLAYBACK_COMPLETE: /* 6 - 播放结束 (EOS) */ break;
case MEDIA_PLAYER_REWIND_COMPLETE: /* 7 - Seek 完成 */ break;
case MEDIA_PLAYER_ERROR: /* 8 - 不可恢复错误 */ break;
}
}
state 取值见 enum MediaPlayerStates:
值 |
含义 |
触发场景 |
|---|---|---|
MEDIA_PLAYER_IDLE (0) |
空闲 |
创建后 / Reset 后 |
MEDIA_PLAYER_PREPARING (1) |
准备中 |
PrepareAsync 调用后立即上报 |
MEDIA_PLAYER_PREPARED (2) |
已准备好 |
Prepare 完成,可以 Start |
MEDIA_PLAYER_STARTED (3) |
播放中 |
Start 成功 |
MEDIA_PLAYER_PAUSED (4) |
已暂停 |
Pause 成功 |
MEDIA_PLAYER_STOPPED (5) |
已停止 |
Stop 成功 |
MEDIA_PLAYER_PLAYBACK_COMPLETE (6) |
播放完成 |
文件播完且未循环 |
MEDIA_PLAYER_REWIND_COMPLETE (7) |
Seek 完成 |
Rewind 完成 |
MEDIA_PLAYER_ERROR (8) |
错误 |
后续会再触发OnError |
播放信息提示 OnInfo
void OnInfo(const MediaPlayerCallback *cb,
const MediaPlayer *player, int info, int extra)
{
switch (info) {
case MEDIA_PLAYER_INFO_BUFFERING_START: /* 0 - 缓冲开始(播放内部暂停) */ break;
case MEDIA_PLAYER_INFO_BUFFERING_END: /* 1 - 缓冲结束(播放恢复) */ break;
case MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE: /* 2 - extra = 缓冲百分比 */ break;
case MEDIA_PLAYER_INFO_NOT_REWINDABLE: /* 3 - 直播流,不支持 Seek */ break;
}
}
info 取值见 enum MediaPlayerInfos:
值 |
含义 |
extra |
|---|---|---|
MEDIA_PLAYER_INFO_BUFFERING_START (0) |
缓冲开始(数据不足) |
保留 |
MEDIA_PLAYER_INFO_BUFFERING_END (1) |
缓冲结束(恢复播放) |
保留 |
MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE (2) |
缓冲进度更新 |
已缓冲百分比 (0–100) |
MEDIA_PLAYER_INFO_NOT_REWINDABLE (3) |
当前数据源不可 Seek(如 live 流) |
保留 |
应用层可据此显示"正在缓冲"提示或禁用进度条拖拽。
播放错误 OnError
void OnError(const MediaPlayerCallback *cb,
const MediaPlayer *player, int error, int extra)
{
if (error == MEDIA_PLAYER_ERROR_UNKNOWN) {
/* 记录日志,通知业务层重试或提示用户 */
}
}
error 取值见 enum MediaPlayerErrors:
值 |
含义 |
extra |
|---|---|---|
MEDIA_PLAYER_ERROR_UNKNOWN (0) |
通用不可恢复错误(数据源失败、解码异常、 内部状态机异常等) |
框架内部错误码,通常与 |
备注
当前对外仅暴露通用错误码 MEDIA_PLAYER_ERROR_UNKNOWN,具体原因(网络断开 / 文件损坏 / 解码失败 / 内存不足等)需结合 extra 字段与 SDK 串口日志判断。
收到 OnError 后播放器会进入 MEDIA_PLAYER_ERROR 状态,必须 Reset 或 Destory 后才能继续使用。
媒体来源
MediaPlayer 支持三大类媒体来源:本地文件(LittleFS / FAT / VFS 等)、网络流(HTTP/HTTPS)以及应用自定义的 StreamSource。
本地文件
通过文件系统前缀区分存储介质(是否可用取决于硬件连接与对应驱动配置):
前缀 |
文件系统 |
示例 |
|---|---|---|
lfs:// |
LittleFS(内置 Flash) |
lfs://audio/prompt.wav |
fat:// |
FAT(SD 卡 / USB 大容量存储) |
fat://music/song.mp3 |
vfs:// |
VFS(多 FS 统一挂载点) |
vfs://usb/song.flac |
备注
SD 卡与 USB 存储设备需要先在 SDK 侧挂载到对应的 FATFS/VFS 路径后才能访问;USB 音频设备另需启用 USB Host + Mass Storage 配置。
URL 形式:
MediaPlayer_SetDataSource(player, "lfs://audio/alarm.wav");
MediaPlayer_SetDataSource(player, "fat://sdcard/music.flac");
文件 Source 还支持以查询参数指定子区间播放(仅本地文件有效):
MediaPlayer_SetDataSource(player, "lfs://music/song.mp3?offset=1024&size=204800");
网络文件 HTTP/HTTPS
支持 http:// 与 https:// 前缀,需要在 SDK 中启用网络与 HTTP 客户端组件。
MediaPlayer_SetDataSource(player, "http://music.example.com/song.mp3");
MediaPlayer_SetDataSource(player, "https://music.example.com/song.mp3");
MediaPlayer_PrepareAsync(player); /* 强烈建议异步 */
HTTP 流缓冲
HTTP 流缓冲分为两个独立阶段,分别由不同的阈值参数控制:
阶段一:准备阶段(Prepare / PrepareAsync)
调用 PrepareAsync() 后,SDK 在后台建立 HTTP 连接并持续接收数据。当已缓冲时长超过 buffering_initial_time 阈值时,才触发 OnStateChanged(PREPARED) 通知应用可以开始播放。期间 SDK 通过 OnInfo(BUFFERING_INFO_UPDATE, percent) 实时上报缓冲进度。
HTTP 缓冲机制 — 准备阶段
备注
关键参数:buffering_initial_time(准备阶段初始缓冲阈值)
默认值:5000 ms — 准备阶段缓冲累计达到 5 秒后,SDK 才上报 PREPARED 状态
建议设置:直播流推荐 5000 ms 以上以保证启播稳定;点播文件可设置更小值以加快首帧出帧
准备阶段全程通过
OnInfo(BUFFERING_INFO_UPDATE, percent)持续上报缓冲进度,应用层可用于显示加载 UI阈值未达成前 SDK 不会回调 PREPARED,应用层不应在 PREPARED 之前调用
Start()
阶段二:播放阶段(Playing)
播放过程中若网络中断导致缓冲不足,SDK 自动内部暂停并触发 BUFFERING_START 回调;继续下载数据,当缓冲时长恢复超过 buffering_resume_time 阈值后,SDK 自动恢复播放并触发 BUFFERING_END,全程无需应用层干预。
HTTP 缓冲机制 — 播放阶段
备注
关键参数:buffering_resume_time(播放阶段恢复缓冲阈值)
默认值:2000 ms — 缓冲积累超过 2 秒后,SDK 自动恢复播放并触发 BUFFERING_END 回调
建议设置:直播流推荐 2000 ms 以上;点播文件可设置更小值以快速恢复
缓冲不足时 SDK 内部自动暂停解码并上报 BUFFERING_START,全程无需应用层轮询
弱网恢复:内置 TCP 重连机制,连接断开时自动重试,
read_retry_time_ms控制重试等待时间
默认参数表:
参数 |
默认值 |
说明 |
|---|---|---|
缓冲区大小 |
256 KB |
网络接收缓冲容量上限 |
buffering_initial_time |
5000 ms |
准备阶段初始缓冲阈值(达到后触发 PREPARED) |
buffering_resume_time |
2000 ms |
播放阶段恢复缓冲阈值(达到后触发 BUFFERING_END) |
Socket 连接超时 |
500 ms |
TCP 连接建立超时 |
发送超时 |
2000 ms |
HTTP 请求发送超时 |
接收超时 |
2000 ms |
HTTP 数据接收超时 |
读取重试间隔 |
5000 ms |
网络中断后重试等待时间 |
缓冲期间的回调序列:
OnInfo(MEDIA_PLAYER_INFO_BUFFERING_START) — SDK 内部暂停(缓冲不足)
OnInfo(MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE, percent) — 缓冲进度实时上报
OnInfo(MEDIA_PLAYER_INFO_BUFFERING_END) — 缓冲充足,自动恢复播放
检测和监控实时播放
通过
MEDIA_PLAYER_INFO_BUFFERING_START/MEDIA_PLAYER_INFO_BUFFERING_END显示"缓冲中"UI 状态。通过
MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE的extra字段(0–100)刷新缓冲进度条。直播流无固定时长:
MediaPlayer_GetDuration()返回 -1,并会上报MEDIA_PLAYER_INFO_NOT_REWINDABLE,应用层应屏蔽 Seek 操作。播放期间持续
MEDIA_PLAYER_INFO_BUFFERING_START表示当前网络带宽不足,建议提示用户或下调码率。
直播流(Icecast / HTTP Live)不支持随机访问,SDK 检测后通过回调通知:
void OnInfo(const MediaPlayerCallback *cb,
const MediaPlayer *player, int info, int extra)
{
if (info == MEDIA_PLAYER_INFO_NOT_REWINDABLE) {
g_is_live_stream = 1; /* 禁用进度条,不调用 Seek() */
}
if (info == MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE) {
printf("Buffering: %d%%\n", extra);
}
}
自定义数据源 StreamSource
StreamSource 允许应用提供任意内存中的音频数据,SDK 通过函数指针按需读取。
typedef struct StreamSource StreamSource;
struct StreamSource {
int32_t (*CheckPrepared)(const StreamSource *source);
ssize_t (*ReadAt)(const StreamSource *source, off_t offset, void *data, size_t size);
int32_t (*GetLength)(const StreamSource *source, off_t *size);
};
实现要点
CheckPrepared:用于在 Prepare 阶段确认上层数据通路已就绪。返回
AUDIO_OK表示可以开始读取,返回AUDIO_ERR_NO_INIT让框架报告失败。ReadAt:按
offset拷贝最多size字节到data, 返回实际读取字节数。特殊返回值:0:到达流末尾。
STREAM_SOURCE_EOF:流结束(与 0 等效,由实现选择)。
STREAM_SOURCE_READ_AGAIN:暂时无数据,请求框架稍后重试(适合实时拉流)。
< 0 其他值:硬错误。
GetLength:写入流总长度到
*size:已知长度:返回
AUDIO_OK。不可知(直播):返回
STREAM_SOURCE_UNKNOWN_LENGTH,框架将禁用 Seek 并通过MEDIA_PLAYER_INFO_NOT_REWINDABLE通知 App。
使用示例
#include "media/stream_source.h"
typedef struct {
StreamSource base; /* 必须为第一个成员 */
const char *data;
int data_length;
int all_ready; /* 1 = 所有数据就绪 */
} MySource;
static int32_t my_check_prepared(const StreamSource *s)
{
MySource *src = (MySource *)s;
return src->data ? AUDIO_OK : AUDIO_ERR_NO_INIT;
}
static ssize_t my_read_at(const StreamSource *s, off_t offset,
void *buf, size_t size)
{
MySource *src = (MySource *)s;
if (offset >= src->data_length) {
if (!src->all_ready)
return (ssize_t)STREAM_SOURCE_READ_AGAIN;
return (ssize_t)STREAM_SOURCE_EOF;
}
size_t avail = (size_t)(src->data_length - (int)offset);
if (size > avail) size = avail;
memcpy(buf, src->data + offset, size);
return (ssize_t)size;
}
static int32_t my_get_length(const StreamSource *s, off_t *out)
{
MySource *src = (MySource *)s;
*out = src->data_length;
return src->all_ready ? STREAM_SOURCE_OK : STREAM_SOURCE_UNKNOWN_LENGTH;
}
StreamSource *MySource_Create(const char *data, int len)
{
MySource *src = calloc(1, sizeof(MySource));
src->base.CheckPrepared = my_check_prepared;
src->base.ReadAt = my_read_at;
src->base.GetLength = my_get_length;
src->data = data;
src->data_length = len;
src->all_ready = 1;
return (StreamSource *)src;
}
/* 使用方式 */
StreamSource *src = MySource_Create(audio_buf, audio_buf_len);
MediaPlayer_SetStreamSource(player, src);
MediaPlayer_PrepareAsync(player);
注意事项
注意项 |
说明 |
|---|---|
随机访问 |
SDK 可能以任意顺序调用ReadAt(seek 后会回到之前 offset),必须支持随机访问 |
READ_AGAIN |
数据暂未到达时返回 STREAM_SOURCE_READ_AGAIN,SDK 轮询重试,避免长时间持锁 |
未知长度 |
GetLength 返回 STREAM_SOURCE_UNKNOWN_LENGTH 时 SDK 以流式模式处理;追加完成后返回 STREAM_SOURCE_OK |
生命周期 |
StreamSource 对象必须在 MediaPlayer_Destory() 返回后才能释放 |
线程安全 |
ReadAt 、 GetLength 在 SDK 内部线程调用,访问共享字段时需加锁 |
播放器命令
player 命令工具是基于 Media SDK 的命令行调试工具,可通过串口终端直接测试播放功能。
启用路径(menuconfig):
Audio Config --->
CONFIG AUDIO CMD --->
[*] player
命令参数:
参数 |
说明 |
示例 |
|---|---|---|
-f <path/url> |
指定播放文件路径或 HTTP URL |
-f lfs://audio/test.mp3 |
-s 0/1 |
是否使用 StreamSource 模式 |
-s 1 |
-v <float> |
设置初始音量(0.0 ~ 1.0) |
-v 0.8 |
示例:
player -f http://192.168.1.100/test.mp3
player -f lfs://audio/alarm.wav
player -f buffer -s 1
常见问题
Q: 播放长文件时内存不足?
A: 使用流式播放,SDK 内部以分块方式解码,无需将整个文件加载到内存。仅启用实际需要的格式(Kconfig 裁剪)。
Q: 无法跳转播放位置?
A: 部分流媒体格式(直播流、某些 HTTP 服务器)不支持跳转。检查是否收到 MEDIA_PLAYER_INFO_NOT_REWINDABLE,若收到则禁用 Seek。对于本地文件,确认文件格式完整(如 MP3 VBR 文件需有完整 Xing/VBRI 头)。
Q: PrepareAsync 超时,未收到 PREPARED 回调?
A: HTTP 场景下 prepared_timeout_ms 默认 2000 ms,弱网时可增大。StreamSource 场景确认 CheckPrepared 返回 AUDIO_OK(数据已就绪)。
Q: 如何实现连续播放(播放列表)?
A: 在 OnStateChanged(MEDIA_PLAYER_PLAYBACK_COMPLETE) 中通过信号量通知业务线程,执行:Stop → (等 STOPPED) → Reset → SetDataSource → PrepareAsync → (等 PREPARED) → Start。不要在回调线程中直接执行这些操作。
API 参考
本章列出 MediaPlayer 对外 API 的完整签名、参数与返回值。
生命周期
函数 |
说明 |
|---|---|
MediaPlayer *MediaPlayer_Create(void) |
创建实例,返回 NULL 表示失败 |
void MediaPlayer_Destory(MediaPlayer *player) |
销毁并释放资源 |
int32_t MediaPlayer_Reset(MediaPlayer *player) |
重置到 IDLE 状态 |
数据源
MediaPlayer_SetDataSource
int32_t MediaPlayer_SetDataSource(MediaPlayer *player, const char *url);
参数 |
说明 |
|---|---|
player |
MediaPlayer 实例 |
url |
文件路径或 HTTP/HTTPS URL |
返回值 |
含义 |
|---|---|
AUDIO_OK |
成功 |
AUDIO_ERR_INVALID_OPERATION |
非 IDLE 状态 |
AUDIO_ERR_UNKNOWN_ERROR |
内部错误 |
MediaPlayer_SetStreamSource
int32_t MediaPlayer_SetStreamSource(MediaPlayer *player, StreamSource *source);
参数 |
说明 |
|---|---|
player |
MediaPlayer 实例 |
source |
用户实现的 StreamSource 指针 |
返回值同 SetDataSource。
准备
函数 |
说明 |
|---|---|
int32_t MediaPlayer_Prepare(MediaPlayer *player) |
同步准备,阻塞至 PREPARED 或出错 |
int32_t MediaPlayer_PrepareAsync(MediaPlayer *player) |
异步准备,立即返回,结果通过 OnStateChanged 通知 |
播放控制
函数 |
参数说明 |
有效状态 |
|---|---|---|
int32_t MediaPlayer_Start(MediaPlayer*) |
— |
PREPARED / PAUSED / REWIND_COMPLETE |
int32_t MediaPlayer_Pause(MediaPlayer*) |
— |
STARTED |
int32_t MediaPlayer_Stop(MediaPlayer*) |
— |
STARTED / PAUSED / PLAYBACK_COMPLETE |
int32_t MediaPlayer_Seek(MediaPlayer*, int64_t msec) |
msec: 目标位置(毫秒) |
PREPARED / STARTED / PAUSED |
int32_t MediaPlayer_SetLooping(MediaPlayer*, int8_t loop) |
loop: 1=循环,0=单次 |
任意 |
int32_t MediaPlayer_SetVolume(MediaPlayer*, float l, float r) |
0.0–1.0 |
STARTED 及之后 |
int32_t MediaPlayer_SetSpeed(MediaPlayer*, float s, float p) |
1.0=正常 |
STARTED |
状态查询
函数 |
说明 |
|---|---|
int32_t MediaPlayer_GetCurrentTime(MediaPlayer*, int64_t *msec) |
当前播放位置(毫秒) |
int32_t MediaPlayer_GetDuration(MediaPlayer*, int64_t *msec) |
总时长(毫秒),直播流返回 -1 |
int MediaPlayer_IsPlaying(MediaPlayer*) |
1=播放中,0=未播放 |
回调
void MediaPlayer_SetCallback(MediaPlayer *player, MediaPlayerCallback *callbacks);
MediaPlayerCallback 结构体:
struct MediaPlayerCallback {
void (*OnStateChanged)(const MediaPlayerCallback*, const MediaPlayer*, int state);
void (*OnInfo)(const MediaPlayerCallback*, const MediaPlayer*, int info, int extra);
void (*OnError)(const MediaPlayerCallback*, const MediaPlayer*, int error, int extra);
};
StreamSource
struct StreamSource {
int32_t (*CheckPrepared)(const StreamSource *source);
ssize_t (*ReadAt)(const StreamSource *source, off_t offset, void *data, size_t size);
int32_t (*GetLength)(const StreamSource *source, off_t *size);
};
StreamSource 返回值:
常量 |
值 |
含义 |
|---|---|---|
STREAM_SOURCE_OK |
0 |
成功 |
STREAM_SOURCE_READ_AGAIN |
-1001 |
数据暂不可用,请稍后重试 |
STREAM_SOURCE_FAIL |
-1002 |
操作失败 |
STREAM_SOURCE_EOF |
-1003 |
已到数据末尾 |
STREAM_SOURCE_UNKNOWN_LENGTH |
-1004 |
数据总长度未知 |
Parcel
函数 |
说明 |
|---|---|
Parcel *Parcel_Create(void) |
创建空 Parcel |
void Parcel_Destroy(Parcel *p) |
销毁并释放 |
Parcel_WriteBool / Parcel_ReadBool |
bool 读写 |
Parcel_WriteInt8 / Parcel_ReadInt8 |
8 位有符号整数读写 |
Parcel_WriteInt16 / Parcel_ReadInt16 |
16 位有符号整数读写 |
Parcel_WriteInt32 / Parcel_ReadInt32 |
32 位有符号整数读写 |
Parcel_WriteInt64 / Parcel_ReadInt64 |
64 位有符号整数读写 |
Parcel_WriteUint8 / Parcel_ReadUint8 |
8 位无符号整数读写 |
Parcel_WriteUint16 / Parcel_ReadUint16 |
16 位无符号整数读写 |
Parcel_WriteUint32 / Parcel_ReadUint32 |
32 位无符号整数读写 |
Parcel_WriteUint64 / Parcel_ReadUint64 |
64 位无符号整数读写 |
Parcel_WriteFloat / Parcel_ReadFloat |
float |
Parcel_WriteDouble / Parcel_ReadDouble |
double |
Parcel_WritePointer / Parcel_ReadPointer |
指针数值存储 |
Parcel_WriteBuffer(p, data, size) |
写入任意字节块 |
void *Parcel_ReadBuffer(p, length) |
读取字节块(返回 Parcel 内部指针) |
Parcel_WriteCString(p, str) |
写入 null 结尾字符串(含长度前缀) |
char *Parcel_ReadCString(p) |
读取字符串(Parcel 拥有,不可 free) |
备注
读写顺序必须一致; ReadBuffer / ReadCString 返回的指针由 Parcel 持有,Parcel 销毁后失效。