播放器

概述

媒体播放器是嵌入式音频应用中最常见的核心组件之一,用于播放本地或网络上的音频内容。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 控制让用户按需启用"支持格式/第三方库"。

系统架构图

../../_images/media_architecture.svg

系统架构

各层职责说明:

层次

内容

应用层

用户应用代码,调用 Public C API

接口层

interfaces/media/ 下的 4 个头文件,SDK 唯一对外接口

多媒体框架

支持数据获取、解封装、解码、渲染同步

音频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);
}

控制播放器

../../_images/media_state.svg

播放器状态机

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)

通用不可恢复错误(数据源失败、解码异常、 内部状态机异常等)

框架内部错误码,通常与 AUDIO_ERR_* 对应; 用于日志定位,不建议作为业务分支条件

备注

当前对外仅暴露通用错误码 MEDIA_PLAYER_ERROR_UNKNOWN,具体原因(网络断开 / 文件损坏 / 解码失败 / 内存不足等)需结合 extra 字段与 SDK 串口日志判断。

收到 OnError 后播放器会进入 MEDIA_PLAYER_ERROR 状态,必须 ResetDestory 后才能继续使用。

媒体来源

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) 实时上报缓冲进度。

../../_images/media_http_buffering_starting.svg

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,全程无需应用层干预。

../../_images/media_http_buffering_playing.svg

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_UPDATEextra 字段(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 销毁后失效。