音视频流媒体开发【三十五】FFmpeg+QT播放器4-打通UI到播放器核心的通道

音视频流媒体开发-目录

1 消息队列

1.1 消息对象

typedef struct AVMessage {
    int what;           // 消息类型
    int arg1;           // 参数1
    int arg2;           // 参数2
    void *obj;          // 如果arg1 arg2还不够存储消息则使⽤该参数
    void (*free_l)(void *obj);  // obj的对象是分配的,这⾥要给出函数怎么释放
    struct AVMessage *next; // 下⼀个消息
} AVMessage;

1.2 消息队列对象

typedef struct MessageQueue {   // 消息队列
    AVMessage *first_msg, *last_msg;    // 消息头,消息尾部
    int nb_messages;    // 有多少个消息
    int abort_request;  // 请求终⽌消息队列
    SDL_mutex *mutex;   // 互斥量
    SDL_cond *cond;     // 条件变量
    AVMessage *recycle_msg; // 消息循环使⽤
    int recycle_count;  // 循环的次数,利⽤局部性原理
    int alloc_count;    // 分配的次数
} MessageQueue;

框架

image.png

1.3 消息队列api

// 释放msg的obj资源
void msg_free_res(AVMessage *msg);
// 私有插⼊消息
int msg_queue_put_private(MessageQueue *q, AVMessage *msg);
//插⼊消息
int msg_queue_put(MessageQueue *q, AVMessage *msg);
// 初始化消息
void msg_init_msg(AVMessage *msg);
// 插⼊简单消息,只带消息类型,不带参数
void msg_queue_put_simple1(MessageQueue *q, int what);
// 插⼊简单消息,只带消息类型,只带1个参数
void msg_queue_put_simple2(MessageQueue *q, int what, int arg1);
// 插⼊简单消息,只带消息类型,带2个参数
void msg_queue_put_simple3(MessageQueue *q, int what, int arg1, int arg2)
// 释放msg的obj资源
void msg_obj_free_l(void *obj);
//插⼊消息,带消息类型,带2个参数,带obj
void msg_queue_put_simple4(MessageQueue *q, int what, int arg1, int arg2, void *obj, int obj_len);
// 消息队列初始化
void msg_queue_init(MessageQueue *q);
// 消息队列flush,清空所有的消息
void msg_queue_flush(MessageQueue *q);
// 消息销毁
void msg_queue_destroy(MessageQueue *q);
// 消息队列终⽌
void msg_queue_abort(MessageQueue *q);
// 启⽤消息队列
void msg_queue_start(MessageQueue *q);
// 读取消息
/* return < 0 if aborted, 0 if no msg and > 0 if msg. */
int msg_queue_get(MessageQueue *q, AVMessage *msg, int block);
// 消息删除 把队列⾥同⼀消息类型的消息全删除掉
void msg_queue_remove(MessageQueue *q, int what);

2 类名规划和接⼝设计

2.1 类名规划

IjkMediaPlayer FFplayer VideoState

[图片上传失败...(image-ed05f8-1679839403363)]

image.png

int MainWind::message_loop(void *arg)

ui 和播放器核⼼直接的交互有以下⼏种⽅式:

  1. ui直接调⽤IjkMediaPlayer的接⼝
  2. ui发送消息给消息循环线程,然后调⽤IjkMediaPlayer的接⼝
  3. IjkMediaPlayer发消息给消息循环线程,线程调⽤ui的接⼝。

有部分消息是UI和IjkMediaPlayer都有处理,有部分消息只是IjkMediaPlayer要处理。⽐如:

  • FFP_MSG_PREPARED: IjkMediaPlayer收到该消息后将mp_state_播放器状态设置为MP_STATE_PREPARED,⽽UI收到该消息后则知道资源已经准备好,可以调⽤start开始请求播放。
  • FFP_REQ_START: IjkMediaPlayer收到该消息后调⽤ffp_start_l()触发播放,并将mp_state_设置为MP_STATE_STARTED。

2.2 接⼝函数

重点和难点接⼝解析

难点,以下五个接⼝的作⽤:

  • ijkmp_create
  • ijkmp_destroy
  • ijkmp_prepare_async
  • ijkmp_start
  • ijkmp_stop
播放:
  • ijkmp_create
  • ijkmp_set_data_source
  • ijkmp_prepare_async
  • 然后等待消息MP_STATE_PREPARED再调⽤ijkmp_start启动播放。
停⽌:
  • 先调⽤ijkmp_stop
  • 再调⽤ijkmp_destroy (ijkplayer⾥⾯是通过release调⽤destroy)

下⾯详细说明这五个接⼝的具体作⽤:

  • ijkmp_create

    • 创建IjkMediaPlayer
    • 创建FFPlayer
      • 初始化消息队列msg_queue_init
      • 初始化FFPlayer的成员变量
      • 刷新队列msg_queue_flush
    • 保存ui传⼊的回调msg_loop函数
    • 初始化mutex
    • 最终如果失败则调⽤destroy_p
  • ijkmp_destroy

    • 停⽌msg_loop线程
    • ffp_destroy_p销毁FFPlayer
      • stream_close
        • 请求abort_request
        • packet_queue_abort
        • stream_component_close 关闭audio、video、subtitle
        • avformat_close_input 关闭解复⽤器
        • packet_queue_destroy销毁audio、video、subtitle包队列
        • frame_queue_destory销毁audio、video、subtitle帧队列
      • 销毁消息队列msg_queue_destroy
    • 释放mutex
    • 释放⾃⼰ delete this
  • ijkmp_prepare_async

    • 状态设置为MP_STATE_ASYNC_PREPARING(正在准备),那什么时候状态转为MP_STATE_PREPARED(已经准备)。
      • 在FFPlayer的read_thread线程 解复⽤分析完码流情况、初始化完对应的解码器、⾳视频输出后,先调⽤ toggle_pause设置系统处于暂停播放状态,然后发送FFP_MSG_PREPARED。
      • IjkMediaPlayer收到FFP_MSG_PREPARED消息后,把状态设置为MP_STATE_PREPARED
      • UI收到FFP_MSG_PREPARED消息后,调⽤IjkMediaPlayer的start接⼝,开始正常播放。
    • 启动消息队列msg_queue_start
    • 创建msg_loop线程
    • 调⽤FFplayer的prepare_async_l
      • 调⽤stream_open
        • 分配VideoState
        • 保存filename到VideoState
        • frame_queue_init初始化audio、video、subtitle帧队列
        • packet_queue_init初始化audio、video、subtitle包队列
        • 创建continue_read_thread解复⽤读取线程条件变量
        • init_clock初始化audio、video、ext时钟
        • 设置⾳量
        • 创建视频刷新线程video_refresh_thread
        • 创建数据读取线程read_thread
          • ⼀ 准备⼯作
            • ⅰ. avformat_alloc_context 创建上下⽂
            • ⅱ. ic->interrupt_callback.callback = decode_interrupt_cb;
            • ⅲ. avformat_open_input打开媒体⽂件
            • ⅳ. avformat_find_stream_info 读取媒体⽂件的包获取更多的stream信息
            • ⅴ. 检测是否指定播放起始时间,如果指定时间则seek到指定位置avformat_seek_file
            • ⅵ. 查找查找AVStream,讲对应的index值记录到st_index[AVMEDIA_TYPE_NB];
                1. 根据⽤户指定来查找流avformat_match_stream_specifier
                1. 使⽤av_find_best_stream查找流
            • ⅶ. 从待处理流中获取相关参数,设置显示窗⼝的宽度、⾼度及宽⾼⽐
            • ⅷ. stream_component_open打开⾳频、视频、字幕解码器,并创建相应的解码线程以及进⾏对应输出参数的初始化。
          • ⼆ For循环读取数据
            • ⅰ. 检测是否退出
            • ⅱ. 检测是否暂停/继续
            • ⅲ. 检测是否需要seek
            • ⅳ. 检测video是否为attached_pic
            • ⅴ. 检测队列是否已经有⾜够数据
            • ⅵ. 检测码流是否已经播放结束
              • 1. 是否循环播放
              • 2. 是否⾃动退出
            • ⅶ. 使⽤av_read_frame读取数据包
            • ⅷ. 检测数据是否读取完毕
            • ⅸ. 检测是否在播放范围内
            • X. 到这步才将数据插⼊对应的队列
          • 三 退出线程处理
            • ⅰ. 如果解复⽤器有打开则关闭avformat_close_input
            • ⅱ. 消耗互斥量wait_mutex
        • 保存filename
  • ijkmp_start

    • ijkmp_start_l
      • 先检测当前的状态是否可以转为start,⽐如当前处于MP_STATE_IDLE、MP_STATE_INITIALIZED状态肯定是不能转为start状态的
      • 删除队列⾥的FFP_REQ_START消息,避免START消息请求重复
      • 删除队列⾥的FFP_REQ_PAUSE消息,因为现在是要START,所以如果队列⾥还有PAUSE消息,则队列⾥的PAUSE消息没有必要再被处理,因为接下来就要执⾏START。
      • 发送FFP_REQ_START消息
        • IjkMediaPlayer的循环⾥,ijkmp_get_msg处理FFP_REQ_START,然后调⽤ffp_start_l触发播放。
          • 本质⽽⾔,最终是调⽤toggle_pause实现“暂停->播放”的切换
  • ijkmp_stop

    • ijkmp_stop_l
      • 先检测当前的状态是否可以执⾏stop,⽐如MP_STATE_IDLE状态就没有必要调⽤stop
      • 删除队列⾥的FFP_REQ_START/PAUSE消息,都已经要stop了,队列⾥⾯的start、pause消息已经没有意义。
      • 调⽤FFPlayer的ffp_stop_l
        • 先请求abort_request = 1,因为我们的packet queue、frame queue都需要abort退出
        • 然后暂停输出toggle_pause
        • msg_queue_abort消息队列也不允许再插⼊消息

⽐如什么时候该调⽤create创建IjkMediaPlayer,create接⼝本质上做了哪些操作,对于播放器我们⼀直说要划分。

⽬前的接⼝设计
IjkMediaPlayer();
int ijkmp_create(std::function<int(void *)> msg_loop);
int ijkmp_destroy();
// 设置要播放的url
int ijkmp_set_data_source(const char *url);
// 准备播放
int ijkmp_prepare_async();
// 触发播放
int ijkmp_start();
// 停⽌
int ijkmp_stop();
// 暂停
int ijkmp_pause();
// seek到指定位置
int ijkmp_seek_to(long msec);
// 获取播放状态
int ijkmp_get_state();
// 是不是播放中
bool ijkmp_is_playing();
// 当前播放位置
long ijkmp_get_current_position();
// 总⻓度
long ijkmp_get_duration();
// 已经播放的⻓度
long ijkmp_get_playable_duration();
// 设置循环播放
void ijkmp_set_loop(int loop);
// 获取是否循环播放
int ijkmp_get_loop();
// 读取消息
int ijkmp_get_msg(AVMessage *msg, int block);
// 设置⾳量
void ijkmp_set_playback_volume(float volume);

2.3 代码实现步骤

IjkMediaPlayer类
IjkMediaPlayer成员变量
 // 互斥量
    std::mutex mutex_;
    // 真正的播放器
    FFPlayer *ffplayer_ = NULL;
    //函数指针, 指向创建的message_loop,即消息循环函数
//   int (*msg_loop)(void*);
    std::function<int(void *)> msg_loop_ = NULL; // ui处理消息的循环
    //消息机制线程
    std::thread *msg_thread_; // 执⾏msg_loop
//   SDL_Thread _msg_thread;
    //字符串,就是⼀个播放url
    char *data_source_;
    //播放器状态,例如prepared,resumed,error,completed等
    int mp_state_;  // 播放状态
IjkMediaPlayer成员函数
  • ijkmp_create
  • ijkmp_destroy
  • ijkmp_prepare_async
  • ijkmp_start
  • ijkmp_stop
FFPlayer类
FFPlayer成员变量
std::function<int(const Frame *)> video_refresh_callback_ = NULL;
/* ffplay context */
VideoState *is;
const char* wanted_stream_spec[AVMEDIA_TYPE_NB];
FFPlayer成员函数
int ffp_create();
void ffp_reset_internal();
void ffp_destroy_p()
/* playback controll */
int       ffp_prepare_async_l(const char *file_name);
int       ffp_start_l();
int       ffp_stop_l();

消息循环线程

  • 创建线程
  • UI消息循环处理逻辑
  • IjkMediaPlayer消息循环处理逻辑
UI MainWind消息循环
int MainWind::message_loop(void *arg)
{
    IjkMediaPlayer *mp = (IjkMediaPlayer *)arg;
    // 线程循环
    qDebug() << "message_loop into";
    while (1) {
        AVMessage msg;
        //取消息队列的消息,如果没有消息就阻塞,直到有消息被发到消息队列。
        int retval = mp->ijkmp_get_msg(&msg, 1);    // 主要处理Java->C的消息

        if (retval < 0)
            break;
        switch (msg.what) {
            case FFP_MSG_FLUSH:
                qDebug() << __FUNCTION__ << " FFP_MSG_FLUSH";
            break;
        case FFP_MSG_PREPARED:
            std::cout << __FUNCTION__ << " FFP_MSG_PREPARED" <<std::endl;
            mp->ijkmp_start();
            break;
        default:
           qDebug()  << __FUNCTION__ << " default " << msg.what ;
          break;
       }

        msg_free_res(&msg);

//       qDebug() << "message_loop sleep, mp:" << mp;
        // 先模拟线程运⾏
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
   }
    qDebug() << "message_loop leave";
}
IjkMediaPlayer消息循环
int IjkMediaPlayer::ijkmp_get_msg(AVMessage *msg, int block)
{
    while (1) {
        int continue_wait_next_msg = 0;

        //取消息,如果没有消息则阻塞。
        int retval = msg_queue_get(&ffplayer_->msg_queue_, msg, block);

        if (retval <= 0)        // -1 abort, 0 没有消息
            return retval;
        switch (msg->what) {
            case FFP_MSG_PREPARED:
            std::cout << __FUNCTION__ << " FFP_MSG_PREPARED" <<std::endl;
            break;
            case FFP_REQ_START:
            std::cout << __FUNCTION__ << " FFP_REQ_START" << std::endl;
            continue_wait_next_msg = 1;
            break;
          default:
            std::cout << __FUNCTION__ << " default " << msg->what <<std::endl;
            break;
       }

        if (continue_wait_next_msg) {
            msg_free_res(msg);
            continue;
       }

        return retval;
   }

     return -1;
}

3 补充知识

参考1:Android中MediaPlayer的setDataSource⽅法的使⽤
https://blog.csdn.net/yanlinembed/article/details/51887642

ijkmp_set_data_source的设计来源于Android的MediaPlayer,可以通过重载接⼝提供不同的资源类型。

MediaPlayer的setDataSource()⽅法主要有四种:

Sets the data source as a content Uri.
@param context the Context to use when resolving the Uri
@param uri the Content URI of the data you want to play public void setDataSource(Context context, Uri uri)

Sets the data source (file-path or http/rtsp URL) to use.
@param path the path of the file, or the http/rtsp URL of the stream you want to play public void setDataSource(String path)

Sets the data source (FileDescriptor) to use. It is the caller’s responsibility to close the file descriptor. It is safe to do so as soon as this call returns.
@param fd the FileDescriptor for the file you want to play public void setDataSource(FileDescriptor fd)
Sets the data source (FileDescriptor) to use. The FileDescriptor must be seekable (N.B. a LocalSocket is not seekable). It is the caller’s responsibility to close the file descriptor. It is safe to do so as soon as this call returns.
@param fd the FileDescriptor for the file you want to play
@param offset the offset into the file where the data to be played starts, in bytes
@param length the length in bytes of the data to be played public void setDataSource(FileDescriptor fd, long offset, long length)

1. 播放应⽤的资源⽂件

法1. 直接调⽤create函数实例化⼀个MediaPlayer对象,播放位于res/raw/test.mp3⽂件
MediaPlayer  mMediaPlayer = MediaPlayer.create(this, R.raw.test);

法2. test.mp3放在res/raw/⽬录下,使⽤setDataSource(Context context, Uri uri)
mp = new MediaPlayer();
Uri setDataSourceuri = Uri.parse("android.[resource://com.android.sim/"+R.raw.test);](resource://com.android.sim/)
mp.setDataSource(this, uri);

说明:此种⽅法是通过res转换成uri然后调⽤setDataSource()⽅法,需要注意格式
Uri.parse("android.resource://[应⽤程序包名Application packagename]/"+R.raw.播放⽂件名);

例⼦中的包名为com.android.sim,播放⽂件名为:test;特别注意包名后的"/"。

法3\. test.mp3⽂件放在assets⽬录下,使⽤setDataSource(FileDescriptor fd, longoffset, long length)
AssetManager assetMg = this.getApplicationContext().getAssets();
AssetFileDescriptor fileDescriptor = assetMg.openFd("test.mp3");  
mp.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(), fileDescriptor.getLength());
  1. 播放存储设备的资源⽂件
MediaPlayer mediaPlayer = new MediaPlayer();  
mediaPlayer.setDataSource("/mnt/sdcard/test.mp3");
  1. 播放远程的资源⽂件
Uri uri = Uri.parse("http://**");  
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(Context, uri);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容