播放器

概述

多媒体架构是指为应用程序提供可使用的媒体接口的一整套多媒体系统。该架构包含名为 MediaPlayer 的播放器组件,该播放器可用于控制音频文件的播放。

MediaPlayer 播放器支持不同种类的音频源:

  • 保存在 Flash 中的音频文件

  • 保存在内存缓冲区中的音频数据

  • Http/Https 数据流

  • 自定义数据源

MediaPlayer 播放器支持以下音频格式:

音频格式

描述

文件类型

AAC

支持标准采样率从 8kHz 到 96kHz 的单声道/立体声内容

ADTS raw AAC (.aac)

MPEG-4 (.m4a)

MP3

单声道/立体声 8kbps ~320kbps 固定码率(CBR)或可变码率(VBR)

MP3 (.mp3)

PCM/WAVE

8位、16位、24位和浮点线性 PCM,采样率从8kHz到96kHz的原始 PCM 录音

WAVE (.wav)

FLAC

单声道/立体声(不支持多声道),采样率最高可达到48kHz, 建议使用16位音频 无抖动的24位音频

FLAC (.flac)

Opus

Ogg (.ogg)

Vorbis

Ogg (.ogg)

架构

应用程序根据下图与媒体进行交互:

../../_images/media_architecture.svg

媒体架构

Playback 状态

媒体的播放是通过状态机进行管理的。播放状态如下图所示:

../../_images/media_playback_states.svg

MediaPlayer 对象支持的播放控制操作如上图所示,其中:

  • 椭圆表示 MediaPlayer 对象的可能状态

  • 带有单箭头的弧线表示驱动对象状态转换的同步方法调用

  • 带有双箭头的弧线表示驱动对象状态转换的异步方法调用

从图中可以看出,某些操作只有在播放器处于特定状态时才有效。如果在错误的状态下执行操作,系统可能会引发其他不良行为。在某些特定状态下调用某些函数可能会触发状态转换,请参阅下表以了解详细信息。

函数

有效状态

说明

MediaPlayer_Start

{Prepared,

Started, Paused,

PlaybackCompleted }

在有效状态下调用此函数将切换到Start状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_Stop

{Prepared, Started,

Stopped,Paused,

PlaybackCompleted}

在有效状态下调用此函数将切换到Stopped状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_Pause

{Started, Paused,

PlaybackCompleted}

在有效状态下调用此函数将切换到Paused状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_Reset

{Idle, Unprepared,

Prepared, Started,

Paused, Stopped,

PlaybackCompleted, Error

调用此函数将重置对象。

MediaPlayer_GetCurrentTime

{Idle, Unprepared,

Prepared, Started,

Paused, Stopped,

PlaybackCompleted}

在有效状态下调用此函数不会改变状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_GetDuration

{Prepared, Started,

Paused, Stopped,

PlaybackCompleted}

在有效状态下调用此函数不会改变状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_IsPlaying

{Idle, Unprepared,

Prepared, Started,

Paused, Stopped,

PlaybackCompleted}

在有效状态下调用此函数不会改变状态,而在 无效状态下调用将切换到错误状态。

MediaPlayer_SetSource

{Idle}

在有效状态下调用此函数不会改变状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_SetDataSource

{Idle}

在有效状态下调用此函数将切换到Unprepared状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_Prepare

{Unprepared, Stopped}

在有效状态下调用此函数将切换到prepared状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_PrepareAsync

{Unprepared, Stopped}

在有效状态下调用此函数将切换到Preparing状态, 而在无效状态下调用将切换到错误状态。

MediaPlayer_SetCallbacks

any

调用此函数不会改变状态。

回调函数

媒体提供了一个注册函数 MediaPlayer_SetCallback(struct MediaPlayer *player, struct MediaPlayerCallback *callbacks) 供应用程序在播放或流媒体时监控状态变化和运行时错误。应用程序需要实现 MediaPlayerCallback 中定义的接口。

相关接口描述如下:

void (*OnMediaPlayerStateChanged)(const struct MediaPlayerCallback *listener, const struct MediaPlayer *player, int state)

通知用于监控播放器状态的变化。

参数 state 是 MediaPlayerStates 中的一种。MediaPlayerStates 的值描述如下:

MediaPlayerStates的值

说明

MEDIA_PLAYER_IDLE

表示初始状态

MEDIA_PLAYER_PREPARING

表示播放器正在准备中

MEDIA_PLAYER_PREPARED

表示播放器已经准备就绪

MEDIA_PLAYER_STARTED

表示播放器已经开始播放

MEDIA_PLAYER_PAUSED

表示播放器已经暂停

MEDIA_PLAYER_STOPPED

表示播放器已经停止

MEDIA_PLAYER_PLAYBACK_COMPLETE

表示播放器已经完成播放

MEDIA_PLAYER_ERROR

表示播放器出现错误

void (*OnMediaPlayerInfo)(const struct MediaPlayerCallback *listener, const struct MediaPlayer *player, int info, int extra)

通知用于监视一些播放器信息。

参数 info 表示信息的类型,参数 extra 表示信息代码。例如,在缓冲状态下, extra 的值是缓冲数据的百分比。MediaPlayerInfos 的值描述如下:

MediaPlayerInfos的值

说明

MEDIA_PLAYER_INFO_BUFFERING_START | MediaPlayer 正在内部暂时暂停播放,以便缓冲更多数据

MEDIA_PLAYER_INFO_BUFFERING_END | MediaPlayer 在填充缓冲区后正恢复播放

MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE | MediaPlayer 更新缓冲数据百分比

void (*OnMediaPlayerError)(const struct MediaPlayerCallback *listener, const struct MediaPlayer *player, int error, int extra)

通知监视器发生了一些播放器错误。

参数 error 表示错误类型。MediaPlayerErrors 的值描述如下:

MediaPlayerInfos的值

说明

MEDIA_PLAYER_ERROR_UNKNOWN

表示播放器发生错误

StreamSource 结构体

媒体支持播放客户实现的数据源。应用程序需要实现结构体 StreamSource 中定义的接口。

相关接口描述如下:

rt_status_t (*CheckPrepared)(const StreamSource *source);
  1. 检查数据源初始化是否成功。

    ssize_t (*ReadAt)(const StreamSource *source, off_t offset, void *data, size_t size);
    
  2. 从客户数据源读取数据,并返回读取的字节数;如果失败,则返回错误值。

    rt_status_t (*GetLength)(const StreamSource *source, off_t *size);
    
  3. 获取数据源的完整长度。

流缓冲原理

对于 HTTP 流媒体源,网络带宽会影响播放效果。良好的用户体验应包含以下功能:

  • 当下载速度过慢时,应用程序需要暂停播放并进行提示(例如:弹出一些对话框显示下载百分比)。

  • 当某部分数据下载完成时,应用程序需要恢复播放。

MediaPlayer 为 HTTP 流媒体源设置了一个启动水位,直到缓存了足够的音频数据,播放才会开始。应用程序调用 MediaPlayer_Prepare()MediaPlayer_PrepareAsync() 以准备流媒体源。

  • MediaPlayer_Prepare() 是一个同步函数,应用程序可以在它之后调用 MediaPlayer_Start()

  • MediaPlayer_PrepareAsync() 是一个异步函数, 应用程序在接收到 MediaPlayerCallback.OnMediaPlayerStateChanged(…, …, MEDIA_PLAYER_PREPARED) 之前不可以调用 MediaPlayer_Start()

HTTP 流媒体源启动时的缓冲原理

HTTP 流媒体源在启动时的缓冲原理如下图所示:

../../_images/media_http_source_stream_buffering_principle_during_starting.svg

对于 HTTP 源,MediaPlayer 将持续检查下载的数据,并通过 MediaPlayerCallback.OnMediaPlayerInfo(…, …, MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE) 将已下载的百分比更新给监视器。当下载的数据足够时,MediaPlayer 会触发 MediaPlayerCallback.OnMediaPlayerStateChanged (…, …, MEDIA_PLAYER_PREPARED),应用程序即可开始播放。

HTTP 流媒体源播放过程中的缓冲原理

HTTP 流媒体源播放过程中的缓冲原理如下图所示:

../../_images/media_http_source_stream_buffering_principle_during_playing.svg

在播放过程中,如果剩余的下载数据不足,MediaPlayer 将触发 MediaPlayerCallback.OnMediaPlayerInfo(…, …, MEDIA_PLAYER_INFO_BUFFERING_START) 。应用程序可以更新用户显示界面,向用户指示当前网络状况不佳。在此期间,MediaPlayer 将持续检查下载的数据,并通过 MediaPlayerCallback.OnMediaPlayerInfo(…, …, MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE) 将已下载的百分比更新给监视器。当下载的数据足够时,MediaPlayer 将触发 MediaPlayerCallback.OnMediaPlayerInfo(…, …, MEDIA_PLAYER_INFO_BUFFERING_END) ,应用程序即可恢复播放。

接口

概述

媒体提供了一层接口:

API层

描述

MediaPlayer

用于应用程序播放音频文件的高级API

MediaPlayer API

MediaPlayer API 可以执行基本的播放操作,如开始播放、在播放时暂停音频、查询指定播放的信息,以及注册观察者以监控播放状态的变化。

使用 MediaPlayer

MediaPlayer 支持多种不同的媒体来源,如存储在 Flash 中的本地文件、媒体缓冲数据和流媒体。

  1. 在创建播放时,需要向 MediaPlayer 传递一个表示资源路径的 URL。

    • 对于文件来源,URL 是存储路径。例如: char *url = "lfs://1.wav"

    • 对于缓冲来源,URL 以 "buffer://" 开头。例如: char *url = "buffer://1611496456"1611496456 是指向缓冲数据信息的地址)。

    • 对于 Http/Https 数据流,URL 以 "http://""https://" 开头。例如: char *url = "http://127.0.0.1/2.mp3"char *url = "https://127.0.0.1/2.mp3"

    // source_buffer 代表一个 audio 源
    const unsigned char source_buffer[] = {-1, -15, 76, -128, 41, 63, ...};
    
    char url[64];
    memset(url, 0x00, sizeof(url));
    int buffer[2] = { sizeof(source_buffer), source_buffer };
    sprintf(&url[0], "%s", "buffer://");
    sprintf(&url[strlen(url)], "%d", (int)(&buffer));
    struct MediaPlayer *player = MediaPlayer_Create();
    MediaPlayer_SetSource(player, url);
    
  2. MediaPlayer_SetSource() 之后, 需要通过 MediaPlayer_Prepare()MediaPlayer_PrepareAsync() 来准备播放。

    • MediaPlayer_Prepare() 是一个同步函数, 可以在之后立即调用 MediaPlayer_Start()

      MediaPlayer_Prepare(player);
      MediaPlayer_Start(player);
      
    • MediaPlayer_PrepareAsync() 是一个异步函数,在收到 MediaPlayerCallback.OnMediaPlayerStateChanged(…, …, MEDIA_PLAYER_PREPARED) 之前 不可以 调用 MediaPlayer_Start()

      enum PlayingStatus {
         IDLE,
         PREPARING,
         PREPARED,
         ...
      };
      void OnPlayerStateChanged(const struct MediaPlayerCallback *listener, const struct MediaPlayer *player, int state)
      {
         switch (state) {
         case MEDIA_PLAYER_PREPARED: { //进入异步准备完成阶段
            g_playing_status = PREPARED;
            break;
         }
         ...
      }
      MediaPlayer_PrepareAsync(player);
      g_playing_status = PREPARING;
      while (g_playing_status != PREPARED) {
         OsalMSleep(20);
      }
      MediaPlayer_Start(player);
      
  3. 当 MediaPlayer 准备完毕后进入 Prepared 状态,这意味着你可以调用 MediaPlayer_Start() 让其播放媒体。此时,通过调用 MediaPlayer_Start()MediaPlayer_Pause() 等方法,可以在 Started、Paused 和 PlaybackCompleted 状态之间切换。

    备注

    在调用 MediaPlayer_Stop() 后,除非重新准备 MediaPlayer,否则不能再次调用 MediaPlayer_Start()

  4. 在编写与 MediaPlayer 对象交互的代码时,始终要记住状态图,因为在错误状态下调用其方法是常见的错误原因。

    暂停播放:

    MediaPlayer_Pause(player);
    

    MediaPlayer 可能会消耗宝贵的系统资源。因此,请确保不要长时间持有 MediaPlayer 实例。播放完成后,应始终调用 MediaPlayer_Stop()MediaPlayer_Reset() 释放资源并恢复状态。在调用 MediaPlayer_Reset() 后,需要通过设置源和调用 MediaPlayer_Prepare() 来重新初始化它。

    • 当播放完成收到 MEDIA_PLAYER_PLAYBACK_COMPLETE 之后调用 MediaPlayer_Stop()

      MediaPlayer_Stop(player);
      
    • 当播放结束收到 MEDIA_PLAYER_STOPPED 之后调用 MediaPlayer_Reset()

      MediaPlayer_Reset(player);
      

    此外,你必须释放 MediaPlayer,因为当你的活动没有与用户交互时(除非你在后台播放媒体),持有 MediaPlayer 意义不大。当然,当活动恢复或重新启动时,你需要创建一个新的 MediaPlayer 并在恢复播 放之前重新准备它。以下是你应该如何释放并使 MediaPlayer 为空的步骤:

    MediaPlayer_Destory(player);
    player = NULL;
    

使用 Callback

你可以创建一个 MediaPlayerCallback 实例并将其分配给播放器以监控播放状态的变化。示例如下:

void OnStateChanged(const struct MediaPlayerCallback *listener, const struct MediaPlayer *player, int state)
{
   switch (state) {
      case MEDIA_PLAYER_PREPARED:
         // 进入异步准备完成阶段
         break;
      case MEDIA_PLAYER_PLAYBACK_COMPLETE:
         // 播放完成,需要调用 Stop();
         break;
      case MEDIA_PLAYER_STOPPED:
         // 结束完成,需要调用 Reset();
         break;
      case MEDIA_PLAYER_PAUSED:
         break;
   }
}
void OnInfo(const struct MediaPlayerCallback *listener, const struct MediaPlayer *player, int info, int extra)
{
   switch (info) {
      case MEDIA_PLAYER_INFO_BUFFERING_START:
         // 剩余数据低于水位缓冲,应用程序应暂停播放并弹出提示对话框。
         break;
      case MEDIA_PLAYER_INFO_BUFFERING_END:
         // 现在继续播放。
         break;
      case MEDIA_PLAYER_INFO_BUFFERING_INFO_UPDATE:
         // 在缓冲期间提示缓存缓冲区的百分比。
         break;
   }
}
void OnError(const struct MediaPlayerCallback *listener, const struct MediaPlayer *player, int error, int extra)
{
   // 处理播放错误。
}
struct MediaPlayerCallback *callback = (struct MediaPlayerCallback *)OsalMemCalloc(sizeof (struct MediaPlayerCallback), LOG_TAG, __LINE__);
callback->OnMediaPlayerStateChanged = OnStateChanged;
callback->OnMediaPlayerInfo = OnInfo;
callback->OnMediaPlayerError = OnError;
MediaPlayer_SetCallback(player, callback);

记住释放 MediaPlayerCallback 来防止内存泄露:

OsalMemFree(callback);

使用 StreamSource

如果源不是来自固定的 URL,客户可以基于 StreamSource 实现自己的数据源。 .

typedef struct MyStreamSource MyStreamSource;
struct MyStreamSource {
   StreamSource base;
   char *data;
   int data_length; //当前总源长度;对于未知数据长度的源,数据长度会变化,直到最后一个数据获取等于 1。
   bool unknown_data_length;
   bool last_data_gained;
};

实现 StreamSource 接口:

rt_status_t MyStreamSource_CheckPrepared(const StreamSource *source)
{
   if (!source) {
      return OSAL_ERR_NO_INIT;
   }
   MyStreamSource *stream_source = (MyStreamSource *)source;
   return stream_source->data ? OSAL_OK : OSAL_ERR_NO_INIT;
}
ssize_t MyStreamSource_ReadAt(const StreamSource *source, off_t offset, void *data, size_t size)
{
   if (!source || !data || !size) {
      return (ssize_t)OSAL_ERR_INVALID_OPERATION;
   }
   MyStreamSource *stream_source = (MyStreamSource *)source;
   if (offset >= stream_source->data_length) {
      if (stream_source->unknown_data_length && !stream_source->last_data_gained) {
         return (ssize_t)STREAM_SOURCE_READ_AGAIN;
      }
      return (ssize_t)STREAM_SOURCE_EOF;
   }
   if ((stream_source->data_length - offset) < size) {
      size = stream_source->data_length - offset;
   }
   memcpy(data, stream_source->data + offset, (size_t)size);
   return size;
}
rt_status_t MyStreamSource_GetLength(const StreamSource *source, off_t *size)
{
   if (!source) {
      return STREAM_SOURCE_FAIL;
   }
   MyStreamSource *stream_source = (MyStreamSource *)source;
   *size = stream_source->data_length;
   if (stream_source->unknown_data_length && !stream_source->last_data_gained) {
      return STREAM_SOURCE_UNKNOWN_LENGTH;
   }
   return OSAL_OK;
}
StreamSource *MyStreamSource_Create(char *data, int length)
{
   MyStreamSource *stream_source = OsalMemCalloc(sizeof(MyStreamSource), LOG_TAG, __LINE__);
   stream_source->base.CheckPrepared = MyStreamSource_CheckPrepared;
   stream_source->base.ReadAt = MyStreamSource_ReadAt;
   stream_source->base.GetLength = MyStreamSource_GetLength;
   stream_source->data = data;
   stream_source->data_length = length;
   return (StreamSource *)stream_source;
}
void MyStreamSource_Destroy(MyStreamSource *source)
{
   if (!source) {
      return;
   }
   OsalMemFree((void *)source);
}

以下是您可能使用播放器控制 StreamSource 源播放的方法:

const unsigned char source_buffer[] = {-1, -15, 76, -128, 41, 63, ...};
  // source_buffer 表示一个音频数据源
StreamSource *stream_source = MyStreamSource_Create(source_buffer, sizeof(source_buffer));
struct MediaPlayer *player = MediaPlayer_Create();
MediaPlayer_SetDataSource(player, stream_source);
MediaPlayer_Prepare(player);
MediaPlayer_Start(player);
...
MediaPlayer_Stop(player);
MediaPlayer_Reset(player);
MyStreamSource_Destroy((MyStreamSource *)stream_source);
MediaPlayer_Destory(player);