FFmpeg简易播放器的实现3-音频播放

简介: 基于 FFmpeg 和 SDL 实现的简易视频播放器,主要分为读取视频文件解码和调用 SDL 播放两大部分。本实验仅研究音频播放的实现方式,不考虑视频。

作者:叶余

来源:https://www.cnblogs.com/leisure_chn/p/10068490.html


基于 FFmpeg 和 SDL 实现的简易视频播放器,主要分为读取视频文件解码和调用 SDL 播放两大部分。本实验仅研究音频播放的实现方式,不考虑视频。

FFmpeg 简易播放器系列文章如下:

[1]. FFmpeg简易播放器的实现1-最简版

[2]. FFmpeg简易播放器的实现2-视频播放

[3]. FFmpeg简易播放器的实现3-音频播放

[4]. FFmpeg简易播放器的实现4-音视频播放

[5]. FFmpeg简易播放器的实现5-音视频同步

1. 视频播放器基本原理

下图引用自 “雷霄骅,视音频编解码技术零基础学习方法”,因原图太小,看不太清楚,故重新制作了一张图片。

image.png

如下内容引用自 “雷霄骅,视音频编解码技术零基础学习方法”:

解协议

将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如 HTTP,RTMP,或是 MMS 等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用 RTMP 协议传输的数据,经过解协议操作后,输出 FLV 格式的数据。

解封装

将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。

解码

将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含 AAC,MP3,AC-3 等等,视频的压缩编码标准则包含 H.264,MPEG2,VC-1 等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如 YUV420P,RGB 等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如 PCM 数据。

音视频同步

根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。

2. 简易播放器的实现-音频播放

2.1 实验平台

实验平台:  openSUSE Leap 42.3  
FFmpeg版本:4.1  
SDL版本:   2.0.9

FFmpeg 开发环境搭建可参考 “ffmpeg开发环境构建

2.2 源码流程分析

本实验仅播放视频文件中的声音,而不显示图像。源码流程参考如下:

image.png

2.3 源码清单

使用如下命令下载源码:

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/player_audio

2.4 关键过程

几个关键函数的说明直接写在代码注释里:

2.4.1 开启音频处理子线程

// B2. 打开音频设备并创建音频处理线程
// B2.1 打开音频设备,获取SDL设备支持的音频参数actual_spec(期望的参数是wanted_spec,实际得到actual_spec)
// 1) SDL提供两种使音频设备取得音频数据方法:
//    a. push,SDL以特定的频率调用回调函数,在回调函数中取得音频数据
//    b. pull,用户程序以特定的频率调用SDL_QueueAudio(),向音频设备提供数据。此种情况wanted_spec.callback=NULL
// 2) 音频设备打开后播放静音,不启动回调,调用SDL_PauseAudio(0)后启动回调,开始正常播放音频
wanted_spec.freq = p_codec_ctx->sample_rate;    // 采样率
wanted_spec.format = AUDIO_S16SYS;              // S表带符号,16是采样深度,SYS表采用系统字节序
wanted_spec.channels = p_codec_ctx->channels;   // 声道数
wanted_spec.silence = 0;                        // 静音值
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;    // SDL声音缓冲区尺寸,单位是单声道采样点尺寸x通道数
wanted_spec.callback = sdl_audio_callback;      // 回调函数,若为NULL,则应使用SDL_QueueAudio()机制
wanted_spec.userdata = p_codec_ctx;             // 提供给回调函数的参数
if (SDL_OpenAudio(&wanted_spec, &actual_spec) < 0)
{
    printf("SDL_OpenAudio() failed: %s\n", SDL_GetError());
    goto exit4;
}
// B2.2 根据SDL音频参数构建音频重采样参数
// wanted_spec是期望的参数,actual_spec是实际的参数,wanted_spec和auctual_spec都是SDL中的参数。
// 此处audio_param是FFmpeg中的参数,此参数应保证是SDL播放支持的参数,后面重采样要用到此参数
// 音频帧解码后得到的frame中的音频格式未必被SDL支持,比如frame可能是planar格式,但SDL2.0并不支持planar格式,
// 若将解码后的frame直接送入SDL音频缓冲区,声音将无法正常播放。所以需要先将frame重采样(转换格式)为SDL支持的模式,
// 然后送再写入SDL音频缓冲区
s_audio_param_tgt.fmt = AV_SAMPLE_FMT_S16;
s_audio_param_tgt.freq = actual_spec.freq;
s_audio_param_tgt.channel_layout = av_get_default_channel_layout(actual_spec.channels);;
s_audio_param_tgt.channels =  actual_spec.channels;
s_audio_param_tgt.frame_size = av_samples_get_buffer_size(NULL, actual_spec.channels, 1, s_audio_param_tgt.fmt, 1);
s_audio_param_tgt.bytes_per_sec = av_samples_get_buffer_size(NULL, actual_spec.channels, actual_spec.freq, s_audio_param_tgt.fmt, 1);
if (s_audio_param_tgt.bytes_per_sec <= 0 || s_audio_param_tgt.frame_size <= 0)
{
    printf("av_samples_get_buffer_size failed\n");
    goto exit4;
}
s_audio_param_src = s_audio_param_tgt;

2.4.2 启动音频回调机制

1
2
3
4
5
// 暂停/继续音频回调处理。参数1表暂停,0表继续。
// 打开音频设备后默认未启动回调处理,通过调用SDL_PauseAudio(0)来启动回调处理。
// 这样就可以在打开音频设备后先为回调函数安全初始化数据,一切就绪后再启动音频回调。
// 在暂停期间,会将静音值往音频设备写。
SDL_PauseAudio(0);

2.4.3 音频回调函数

用户实现的函数,由 SDL 音频处理子线程回调

// 音频处理回调函数。读队列获取音频包,解码,播放
// 此函数被SDL按需调用,此函数不在用户主线程中,因此数据需要保护
// \param[in]  userdata用户在注册回调函数时指定的参数
// \param[out] stream 音频数据缓冲区地址,将解码后的音频数据填入此缓冲区
// \param[out] len    音频数据缓冲区大小,单位字节
// 回调函数返回后,stream指向的音频缓冲区将变为无效
// 双声道采样点的顺序为LRLRLR
void audio_callback(void *userdata, uint8_t *stream, int len)
{
    ...
}

2.4.4 音频包队列读写函数

用户实现的函数,主线程向队列尾部写音频包,SDL 音频处理子线程(回调函数处理)从队列头部取出音频包

// 写队列尾部
int packet_queue_push(packet_queue_t *q, AVPacket *pkt)
{
    ...
}
// 读队列头部
int packet_queue_pop(packet_queue_t *q, AVPacket *pkt, int block)
{
    ...
}

2.4.5 音频解码

音频解码功能封装为一个函数,将一个音频 packet 解码后得到的声音数据传递给输出缓冲区。此处的输出缓冲区 audio_buf 会由上一级调用函数 audio_callback() 在返回时将缓冲区数据提供给音频设备。

int audio_decode_frame(AVCodecContext *p_codec_ctx, AVPacket *p_packet, uint8_t *audio_buf, int buf_size)
{
    AVFrame *p_frame = av_frame_alloc();
    int frm_size = 0;
    int res = 0;
    int ret = 0;
    int nb_samples = 0;             // 重采样输出样本数
    uint8_t *p_cp_buf = NULL;
    int cp_len = 0;
    bool need_new = false;
    res = 0;
    while (1)
    {
        need_new = false;
        // 1 接收解码器输出的数据,每次接收一个frame
        ret = avcodec_receive_frame(p_codec_ctx, p_frame);
        if (ret != 0)
        {
            if (ret == AVERROR_EOF)
            {
                printf("audio avcodec_receive_frame(): the decoder has been fully flushed\n");
                res = 0;
                goto exit;
            }
            else if (ret == AVERROR(EAGAIN))
            {
                //printf("audio avcodec_receive_frame(): output is not available in this state - "
                //       "user must try to send new input\n");
                need_new = true;
            }
            else if (ret == AVERROR(EINVAL))
            {
                printf("audio avcodec_receive_frame(): codec not opened, or it is an encoder\n");
                res = -1;
                goto exit;
            }
            else
            {
                printf("audio avcodec_receive_frame(): legitimate decoding errors\n");
                res = -1;
                goto exit;
            }
        }
        else
        {
            // s_audio_param_tgt是SDL可接受的音频帧数,是main()中取得的参数
            // 在main()函数中又有“s_audio_param_src = s_audio_param_tgt”
            // 此处表示:如果frame中的音频参数 == s_audio_param_src == s_audio_param_tgt,那音频重采样的过程就免了(因此时s_audio_swr_ctx是NULL)
            //      否则使用frame(源)和s_audio_param_src(目标)中的音频参数来设置s_audio_swr_ctx,并使用frame中的音频参数来赋值s_audio_param_src
            if (p_frame->format         != s_audio_param_src.fmt            ||
                p_frame->channel_layout != s_audio_param_src.channel_layout ||
                p_frame->sample_rate    != s_audio_param_src.freq)
            {
                swr_free(&s_audio_swr_ctx);
                // 使用frame(源)和is->audio_tgt(目标)中的音频参数来设置is->swr_ctx
                s_audio_swr_ctx = swr_alloc_set_opts(NULL,
                                                     s_audio_param_tgt.channel_layout, 
                                                     s_audio_param_tgt.fmt, 
                                                     s_audio_param_tgt.freq,
                                                     p_frame->channel_layout,           
                                                     p_frame->format, 
                                                     p_frame->sample_rate,
                                                     0,
                                                     NULL);
                if (s_audio_swr_ctx == NULL || swr_init(s_audio_swr_ctx) < 0)
                {
                    printf("Cannot create sample rate converter for conversion of %d Hz %s %d channels to %d Hz %s %d channels!\n",
                            p_frame->sample_rate, av_get_sample_fmt_name(p_frame->format), p_frame->channels,
                            s_audio_param_tgt.freq, av_get_sample_fmt_name(s_audio_param_tgt.fmt), s_audio_param_tgt.channels);
                    swr_free(&s_audio_swr_ctx);
                    return -1;
                }
                // 使用frame中的参数更新s_audio_param_src,第一次更新后后面基本不用执行此if分支了,因为一个音频流中各frame通用参数一样
                s_audio_param_src.channel_layout = p_frame->channel_layout;
                s_audio_param_src.channels       = p_frame->channels;
                s_audio_param_src.freq           = p_frame->sample_rate;
                s_audio_param_src.fmt            = p_frame->format;
            }
            if (s_audio_swr_ctx != NULL)        // 重采样
            {
                // 重采样输入参数1:输入音频样本数是p_frame->nb_samples
                // 重采样输入参数2:输入音频缓冲区
                const uint8_t **in = (const uint8_t **)p_frame->extended_data;
                // 重采样输出参数1:输出音频缓冲区尺寸
                // 重采样输出参数2:输出音频缓冲区
                uint8_t **out = &s_resample_buf;
                // 重采样输出参数:输出音频样本数(多加了256个样本)
                int out_count = (int64_t)p_frame->nb_samples * s_audio_param_tgt.freq / p_frame->sample_rate + 256;
                // 重采样输出参数:输出音频缓冲区尺寸(以字节为单位)
                int out_size  = av_samples_get_buffer_size(NULL, s_audio_param_tgt.channels, out_count, s_audio_param_tgt.fmt, 0);
                if (out_size < 0)
                {
                    printf("av_samples_get_buffer_size() failed\n");
                    return -1;
                }
                if (s_resample_buf == NULL)
                {
                    av_fast_malloc(&s_resample_buf, &s_resample_buf_len, out_size);
                }
                if (s_resample_buf == NULL)
                {
                    return AVERROR(ENOMEM);
                }
                // 音频重采样:返回值是重采样后得到的音频数据中单个声道的样本数
                nb_samples = swr_convert(s_audio_swr_ctx, out, out_count, in, p_frame->nb_samples);
                if (nb_samples < 0) {
                    printf("swr_convert() failed\n");
                    return -1;
                }
                if (nb_samples == out_count)
                {
                    printf("audio buffer is probably too small\n");
                    if (swr_init(s_audio_swr_ctx) < 0)
                        swr_free(&s_audio_swr_ctx);
                }
                // 重采样返回的一帧音频数据大小(以字节为单位)
                p_cp_buf = s_resample_buf;
                cp_len = nb_samples * s_audio_param_tgt.channels * av_get_bytes_per_sample(s_audio_param_tgt.fmt);
            }
            else    // 不重采样
            {
                // 根据相应音频参数,获得所需缓冲区大小
                frm_size = av_samples_get_buffer_size(
                        NULL, 
                        p_codec_ctx->channels,
                        p_frame->nb_samples,
                        p_codec_ctx->sample_fmt,
                        1);
                printf("frame size %d, buffer size %d\n", frm_size, buf_size);
                assert(frm_size <= buf_size);
                p_cp_buf = p_frame->data[0];
                cp_len = frm_size;
            }
            // 将音频帧拷贝到函数输出参数audio_buf
            memcpy(audio_buf, p_cp_buf, cp_len);
            res = cp_len;
            goto exit;
        }
        // 2 向解码器喂数据,每次喂一个packet
        if (need_new)
        {
            ret = avcodec_send_packet(p_codec_ctx, p_packet);
            if (ret != 0)
            {
                printf("avcodec_send_packet() failed %d\n", ret);
                av_packet_unref(p_packet);
                res = -1;
                goto exit;
            }
        }
    }
exit:
    av_frame_unref(p_frame);
    return res;
}

注意:

[1]. 一个音频 packet 中含有多个完整的音频帧,此函数每次只返回一个 frame,当 avcodec_receive_frame() 指示需要新数据时才调用 avcodec_send_packet() 向编码器发送一个 packet。

[2]. 音频 frame 中的数据格式未必被 SDL 支持,对于不支持的音频 frame 格式,需要进行重采样,转换为 SDL 支持的格式声音才能正常播放

[3]. 解码器内部会有缓冲机制,会缓存一定量的音频帧,不冲洗(flush)解码器的话,缓存帧是取不出来的,未冲洗(flush)解码器情况下,avcodec_receive_frame() 返回 AVERROR(EAGAIN),表示解码器中改取的帧已取完了(当然缓存帧还是在的),需要用 avcodec_send_packet() 向解码器提供新数据。

[4]. 文件播放完毕时,应冲洗(flush)解码器。冲洗(flush)解码器的方法就是调用 avcodec_send_packet(..., NULL),然后按之前同样的方式多次调用 avcodec_receive_frame() 将缓存帧取尽。缓存帧取完后,avcodec_receive_frame() 返回 AVERROR_EOF。

3. 编译与验证

3.1 编译

在源码目录运行:

./compiler.sh

3.2 验证

选用clock.avi测试文件,测试文件下载(右键另存为):clock.avi

查看视频文件格式信息:

ffprobe clock.avi

打印视频文件信息如下:

[avi @ 0x9286c0] non-interleaved AVI
Input #0, avi, from 'clock.avi':
  Duration: 00:00:12.00, start: 0.000000, bitrate: 42 kb/s
    Stream #0:0: Video: msrle ([1][0][0][0] / 0x0001), pal8, 320x320, 1 fps, 1 tbr, 1 tbn, 1 tbc
    Stream #0:1: Audio: truespeech ([34][0][0][0] / 0x0022), 8000 Hz, mono, s16, 8 kb/s

运行测试命令:

./ffplayer clock.avi

可以听到每隔 1 秒播放一次“嘀”声,播放 12 次后播放结束。播放过程只有声音,没有图像窗口。播放正常。

4. 参考资料

[1] 雷霄骅,视音频编解码技术零基础学习方法

[2] 雷霄骅,最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)

[3] SDL WIKI, https://wiki.libsdl.org/

[4] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 03: Playing Sound

5. 修改记录

2018-12-04 V1.0 初稿

2019-01-06 V1.1 增加音频重采样,修复部分音频格式无法正常播放的问题


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

阿里云视频云@凡科快图.png

相关文章
|
存储 编解码 Linux
FFmpeg+SDL播放器开发实践:解析、解码、渲染全流程详解
FFmpeg+SDL播放器开发实践:解析、解码、渲染全流程详解
|
26天前
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
101 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
6月前
|
设计模式 存储 缓存
【ffmpeg C++ 播放器优化实战】优化你的视频播放器:使用策略模式和单例模式进行视频优化
【ffmpeg C++ 播放器优化实战】优化你的视频播放器:使用策略模式和单例模式进行视频优化
206 0
|
Linux API 开发工具
[开源]基于ffmpeg和libvlc的视频剪辑、播放器
[开源]基于ffmpeg和libvlc的视频剪辑、播放器
216 0
|
6月前
|
人机交互 C++
QT + FFmpeg 5.x + x264 + x265 + SDL2 音视频播放器
QT + FFmpeg 5.x + x264 + x265 + SDL2 音视频播放器
222 0

热门文章

最新文章