​同步播放音视频的时候,《FFmpeg开发实战:从零基础到短视频上线》一书第10章的示例程序playsync.c采取一边遍历一边播放的方式,在源文件的音频流和视频流交错读取的情况下,该方式可以很好地实现同步播放功能。

但个别格式的音频流和视频流是分开存储的,前面一大段放了所有的音频帧,后面一大段放了所有的视频帧,并非音频帧与视频帧交错存储的模式。对于这种格式,playsync.c播放时先放完所有的声音,这期间画面是空白的;再快速放完所有的视频画面,这期间没有声音,显然播放过程是有问题的。

若想纠正playsync.c的播放问题,就得重新设计音视频的同步播放机制,不能采取一边遍历一边播放的方式,而要先把音频帧和视频帧都读到缓存队列中,再依次检查音频与视频的时间戳,从而决定在哪个时刻才播放对应时间戳的音视频。具体到代码实现上,需要补充下列几点改造。

1、除了已有的视频处理线程和视频包队列之外,还要增加声明音频处理线程和音频包队列,当然音频包队列配套的队列锁也要补充声明。增补后的声明代码如下所示:

SDL_mutex *audio_list_lock = NULL; // 声明一个音频包队列锁,防止线程间同时操作包队列
SDL_Thread *audio_thread = NULL; // 声明一个音频处理线程
PacketQueue packet_audio_list; // 存放音频包的队列
SDL_mutex *video_list_lock = NULL; // 声明一个视频包的队列锁,防止线程间同时操作包队列
SDL_Thread *video_thread = NULL; // 声明一个视频处理线程
PacketQueue packet_video_list; // 存放视频包的队列

2、在程序初始化的时候,不但要创建视频处理线程和视频队列的互斥锁,还要创建音频处理线程和音频队列的互斥锁。修改后的初始化代码如下所示:

audio_list_lock = SDL_CreateMutex(); // 创建互斥锁,用于调度队列
// 创建SDL线程,指定任务处理函数,并返回线程编号
audio_thread = SDL_CreateThread(thread_work_audio, "thread_work_audio", NULL);
if (!audio_thread) {
    av_log(NULL, AV_LOG_ERROR, "sdl create audio thread occur error\n");
    return -1;
}
video_list_lock = SDL_CreateMutex(); // 创建互斥锁,用于调度队列
// 创建SDL线程,指定任务处理函数,并返回线程编号
video_thread = SDL_CreateThread(thread_work_video, "thread_work_video", NULL);
if (!video_thread) {
    av_log(NULL, AV_LOG_ERROR, "sdl create video thread occur error\n");
    return -1;
}

3、对音视频文件遍历数据包时,不能立即渲染音频,而要把音频包加入音频队列,把视频包加入视频队列,由两个处理线程根据时间戳来调度具体的播放进度。另外,在所有数据包都遍历完之后,视频包队列可能还有剩余的数据,所以程序末尾得轮询视频包队列,直至所有视频帧都渲染结束才算完成播放。据此修改音视频文件的遍历与轮询代码如下所示:

while (av_read_frame(in_fmt_ctx, packet) >= 0) { // 轮询数据包
    if (packet->stream_index == audio_index) { // 音频包需要解码
        SDL_LockMutex(audio_list_lock); // 对音频队列锁加锁
        push_packet(&packet_audio_list, *packet); // 把音频包加入队列
        SDL_UnlockMutex(audio_list_lock); // 对音频队列锁解锁
    } else if (packet->stream_index == video_index) { // 视频包需要解码
        SDL_LockMutex(video_list_lock); // 对视频队列锁加锁
        push_packet(&packet_video_list, *packet); // 把视频包加入队列
        SDL_UnlockMutex(video_list_lock); // 对视频队列锁解锁
        if (!has_audio) { // 不存在音频流
            SDL_Delay(interval); // 延迟若干时间,单位毫秒
        }
    }
    if (play_video_frame() == -1) { // 播放视频画面
        goto __QUIT;
    }
}
while (!is_empty(packet_video_list)) { // 播放剩余的视频画面
    if (play_video_frame() == -1) {
        goto __QUIT;
    }
    SDL_Delay(5); // 延迟若干时间,单位毫秒
}

除了上述的三大块改造,尚有下面四个函数要补充修改:

thread_work_audio函数:这是音频处理线程新增的工作函数,主要从音频包队列取数据,然后解码为音频帧再重采样,并将重采样的结果数据送给扬声器。

thread_work_video函数:这是视频处理线程原有的工作函数,除了给视频包队列及其对应的互斥锁改名之外,其他代码照搬即可。

play_video_frame函数:这是播放视频画面的新增函数,就是把原来SDL渲染画面的代码块重新包装成独立的函数,方便多次调用罢了。

release函数:这是释放音视频资源的函数,与之前的释放代码相比,主要增加了音频处理线程的等待操作,以及音频队列锁的销毁操作。

上述修改后的代码已经附在了《FFmpeg开发实战:从零基础到短视频上线》一书第10章的源码chapter10/playsync2.c,这个c代码是playsync.c的改进版,能够正常播放音频流和视频流分开存储的视频文件。

接着执行下面的编译命令。

gcc playsync2.c -o playsync2 -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -I/usr/local/sdl2/include -L/usr/local/sdl2/lib -lsdl2 -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后执行以下命令启动测试程序,期望播放视频文件fuzhou.mp4。

./playsync2 ../fuzhou.mp4

程序运行完毕,发现控制台输出以下的日志信息。

Success open input_file ../fuzhou.mp4.
out_sample_rate=44100, out_nb_samples=1024
thread_work_video
video_index 0
thread_work_audio
audio_index 0
……
9216 10240 11264 12288 13312 14336 15360 16384 17408 18432 19456 20480 21504 22528 23552 24576 25600 26624 27648 28672 29696 30720 31744 32768 33792 34816 35840 36864 37888 38912 39936 ……
Close window.
begin release audio resource
audio_thread audio_status=0
end release audio resource
begin release video resource
video_thread video_status=0
end release video resource
Quit SDL.

同时弹出SDL窗口播放视频画面,并且扬声器传来了阵阵歌声,表示上述代码正确实现了同步播放音视频的功能。

FFmpeg开发笔记(十九)FFmpeg开启两个线程分别解码音视频的更多相关文章

  1. FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放

    前言   ffmpeg播放rtsp网络流和摄像头流.   Demo   使用ffmpeg播放局域网rtsp1080p海康摄像头:延迟0.2s,存在马赛克     使用ffmpeg播放网络rtsp文件流 ...

  2. FFmpeg开发笔记(十):ffmpeg在ubuntu上的交叉编译移植到海思HI35xx平台

    FFmpeg和SDL开发专栏(点击传送门) 上一篇:<FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放>下一篇:敬请期待   前言   将ffmpeg移植到海思H ...

  3. FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  4. FFmpeg开发笔记(四):ffmpeg解码的基本流程详解

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  5. FFmpeg开发笔记(三):ffmpeg介绍、windows编译以及开发环境搭建

    前言   本篇章是对之前windows环境的补充,之前windows的是无需进行编译的,此篇使用源码进行编译,版本就使用3.4.8.   FFmpeg简介   FFmpeg是领先的多媒体框架,能够解码 ...

  6. python3.4学习笔记(十九) 同一台机器同时安装 python2.7 和 python3.4的解决方法

    python3.4学习笔记(十九) 同一台机器同时安装 python2.7 和 python3.4的解决方法 同一台机器同时安装 python2.7 和 python3.4不会冲突.安装在不同目录,然 ...

  7. Django笔记十九之manager用法介绍

    本文首发于微信公众号:Hunter后端 原文链接:Django笔记十九之manager用法介绍 首先介绍一下 manager 的使用场景,比如我们有一些表级别的,需要重复使用的功能,都可以使用 man ...

  8. 开启两个线程,一个线程打印A~Z,一个线程打印1~52的数据

    开启两个线程,一个线程打印A-Z,一个线程打印1-52的数据 import java.util.concurrent.locks.Condition; import java.util.concurr ...

  9. FFmpeg开发笔记(二)搭建Windows系统的开发环境

    由于Linux系统比较专业,个人电脑很少安装Linux,反而大都安装Windows系统,因此提高了FFmpeg的学习门槛,毕竟在Windows系统搭建FFmpeg的开发环境还是比较麻烦的.不过若有已经 ...

  10. FFmpeg开发笔记(一)搭建Linux系统的开发环境

    对于初学者来说,如何搭建FFmpeg的开发环境是个不小的拦路虎,因为FFmpeg用到了许多第三方开发包,所以要先编译这些第三方源码,之后才能给FFmpeg集成编译好的第三方库.不过考虑到刚开始仅仅调用 ...

随机推荐

  1. RowHammer 攻击:内存的隐形威胁

    今天看了一篇 IT 之家关于 AMD 处理器受 RowHammer 内存攻击影响的报道,心血来潮了解了一下 RowHammer 攻击的原理,把了解到的知识记录下来. RowHammer 攻击是一种相对 ...

  2. C++原子操作与内存序 1

    问题 #include<iostream> #include<thread> int main() { int sum = 0; auto f = [&sum]() { ...

  3. #树上差分 or 01-Trie#洛谷 6623 [省选联考 2020 A 卷] 树

    题目 分析(01trie) 考虑用trie做需要满足什么操作:加入某个数.01-Trie的合并.全局加一. 主要是全局加一比较难做,考虑改变的地方就是 \(X*2^T+2^T-1\). 把01-Tri ...

  4. #网络流,树状数组#JZOJ 4020 Revolution with JZOJ 4018 Magic

    CF297E Mystic Carvings=JZOJ 4018 Magic JZOJ 4020 Revolution 题目 有一个\(n*m(n,m\leq 20)\)的网格图 这格子有收益当且仅当 ...

  5. 【FAQ】调用应用内支付SDK时报错,如何用tag对问题进行排查和分析

    华为应用内支付服务(In-App Purchases,IAP)为开发者提供便捷的应用内支付体验和简便的接入流程.开发者的应用集成IAP SDK后,调用IAP SDK接口,启动IAP收银台,即可实现应用 ...

  6. 详解Java Chassis 3与Spring Cloud的互操作

    本文分享自华为云社区<Java Chassis 3技术解密:与Spring Cloud的互操作>,作者: liubao68. Java Chassis 3一个很重要的设计原则:利用架构的韧 ...

  7. MogDB-opengauss中的聚集与分组操作

    MogDB/opengauss 中的聚集与分组操作 COUNT:对结果集中的元组数量进行计数,如果是 COUNT(*),那么会统计所有元组(包括 NULL 值)的数量,如果是 COUNT(colnam ...

  8. 重新点亮shell————什么是shell[一]

    前言 这里简介一下什么是shell. 写linux和shell 系列是为了后面的docker 系列的整理,本来想直接整理k8s的,但是呢,想想docker 系列整理完了的话,那么整理k8s系列就没有那 ...

  9. JavaWeb技术JSP连接数据库操作

    "感谢您阅读本篇博客!如果您觉得本文对您有所帮助或启发,请不吝点赞和分享给更多的朋友.您的支持是我持续创作的动力,也欢迎留言交流,让我们一起探讨技术,共同成长!谢谢!" 0X01 ...

  10. 【Oracle】使用exit,return,continue跳出循环

    [Oracle]使用exit,return,continue跳出循环 exit是结束循环,但还会继续执行程序包中其他的内容 return则是直接中断整个程序 continue的作用是终止本次循环,开始 ...