本文为作者原创,转载请注明出处: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. 视频播放器基本原理

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



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

解协议

将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如 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 源码流程分析

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

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表暂停,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 增加音频重采样,修复部分音频格式无法正常播放的问题

FFmpeg简易播放器的实现-音频播放的更多相关文章

  1. 搭建rtmp直播流服务之4:videojs和ckPlayer开源播放器二次开发(播放rtmp、hls直播流及普通视频)

    前面几章讲解了使用 nginx-rtmp搭建直播流媒体服务器; ffmpeg推流到nginx-rtmp服务器; java通过命令行调用ffmpeg实现推流服务; 从数据源获取,到使用ffmpeg推流, ...

  2. html 音乐 QQ播放器 外链 代码 播放器 外链 代码

    韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha QQ播放器 外链 代码 播放器 外链 代码 ======== 歌曲链接 QQ播放器 外链 ...

  3. Android播放器推荐:可以播放本地音乐、视频、在线播放音乐、视频、网络收音机等

    下载链接:http://www.eoeandroid.com/forum.php?mod=attachment&aid=MTAxNTczfGMyNjNkMzFlfDEzNzY1MzkwNTR8 ...

  4. EasyPlayer RTSP Windows(with ActiveX/OCX插件)播放器支持H.265播放与抓图功能

    EasyPlayer作为业界一款比较优秀的RTSP播放器,一直深受用户的好评,经过了近3年的开发和迭代,从一开始的简单PC版本的RTSP播放功能,到如今支持PC(支持ocx插件).Android.iO ...

  5. Android 仿百度网页音乐播放器圆形图片转圈播放效果

    百度网页音乐播放器的效果  如下 : http://www.baidu.com/baidu?word=%E4%B8%80%E7%9B%B4%E5%BE%88%E5%AE%89%E9%9D%99& ...

  6. flash播放器插件与flash播放器的区别

    flash插件是一个网页ActiveX控件,而flash播放器是一个exe的可执行程序.前者用于播放网页中的falsh动画,而后者用于播放本地swf格式文件.

  7. 22_Android中的本地音乐播放器和网络音乐播放器的编写,本地视频播放器和网络视频播放器,照相机案例,偷拍案例实现

    1 编写以下案例: 当点击了"播放"之后,在手机上的/mnt/sdcard2/natural.mp3就会播放. 2 编写布局文件activity_main.xml <Line ...

  8. 关于Ckpalyer播放器的MP4无法播放问题

    此文是从网上摘要的 有时在本地使用ckplayer来播放视频,flv格式非常容易的就播放了,但是使用mp4格式却显示:加载失败.为什么呢?        首页看下你i的本地站点MIME类型中,是否增加 ...

  9. Java模拟音乐播放器 暂停与重新播放——线程如何控制另外一个线程的状态

    package com.example.Thread; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEve ...

随机推荐

  1. node linux

    在linux下安装nodejs 教程:http://my.oschina.net/blogshi/blog/260953 连接linux服务器,supervisor bin/www,断开连接,服务器还 ...

  2. linux 查看、关闭 ssh pts/n登录的用户

    1.查看登录用户: [root@TiaoBan- bin]# w :: up days, :, users, load average: 1.90, 1.75, 1.84 USER TTY FROM ...

  3. Circles and Pi

    Circles and Pi Introduction id: intro-1 For as long as human beings exist, we have looked to the sky ...

  4. Sql递归关联情况,With作为开头条件。

    with Test_Recursion(Id,ParentId)AS(select Id,ParentId  from [V_KPI_DetailsActivities] where ParentId ...

  5. socket粗解

    百度定义:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket. Socket通信流程: 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一 ...

  6. sublime text 文件打开时回调一些函数

    需求:公司服务端脚本以 .s 结尾的文件,也按 js 语法识别,方便查看函数定义. 每次都 ss:js 比较麻烦,所以写个插件. import sublime, sublime_plugin clas ...

  7. 利用Python做绝地科学家(外挂篇)

    i春秋作家:奶权 前言  玩吃鸡时间长的鸡友们 应该都知道现在的游戏环境非常差 特别在高端局 神仙满天飞 搞得很多普通玩家非常没有游戏体验  因为吃鸡的火爆 衍生出了一条巨大的外挂利益链 导致市面上出 ...

  8. Docker - Docker与Vagrant的区别

    Docker Docker - HomePage Wiki - Docker Docker简介 Overview Docker 是一个开源的应用容器引擎,基于 Go 语言并遵从 Apache2.0 协 ...

  9. fastjson 反序列化漏洞笔记,比较乱

    现在思路还是有点乱,希望后面能重新写 先上pon.xml 包 <?xml version="1.0" encoding="UTF-8"?> < ...

  10. df换行问题解决

    df换行问题解决 df是linux下用来查磁盘空间的命令,而在使用了LVM分区或网络挂载的情况下,再用df取分区的使用率时,发现有些分区显示换行了,这样会导致通过脚本取的数据不对. [root@ ]# ...