自定义播放器系列

第一章 视频渲染
第二章 音频(push)播放(本章)
第三章 音频(pull)播放
第四章 实现时钟同步
第五章 实现通用时钟同步
第六章 实现播放器


文章目录

  • 自定义播放器系列
  • 前言
  • 一、ffmpeg解码
    • 1、打开输入流
    • 2、查找音频流
    • 3、打开解码器
    • 4、解码
    • 5、重采样
    • 6、销毁资源
  • 二、sdl播放
    • 1、初始化sdl
    • 2、打开音频设备
    • 3、播放(push)
    • 4、销毁资源
  • 三、队列长度控制
    • 1、问题
      • (1)、写入较快的情况
      • (2)、写入较慢的情况
    • 2、 解决方法
      • (1)、使用pid
  • 四、完整代码
    • 1、代码(不含pid)
    • 2、项目
  • 总结

前言

使用ffmpeg解码音频并使用sdl播放,网上还是有一些例子的,大多都不是特别完善,比如打开音频设备、音频重采样、使用push的方式播放音频等,都是有不少细节需要注意处理。尤其是使用push的方式播放音频,流程很简单完全可以使用单线程实现,但是队列数据长度比较难控制控制。而且有时想要快速搭建一个demo时,总是要重新编写不少代码,比较不方便,所以在这里提供一个完善的例子,可以直接拷贝拿来使用。


一、ffmpeg解码

ffmpeg解码的流程是比较经典且通用的,基本上是文件、网络流、本地设备都是一模一样的流程。

1、打开输入流

首先需要打开输入流,输入流可以是文件、rtmp、rtsp、http等。

AVFormatContext* pFormatCtx = NULL;
const char* input="test.mp4";
//打开输入流
avformat_open_input(&pFormatCtx, input, NULL, NULL) ;
//查找输入流信息
avformat_find_stream_info(pFormatCtx, NULL) ;

2、查找音频流

因为是音频播放,所以需要找到输入流中的音频流。通过遍历判断codec_type 为AVMEDIA_TYPE_AUDIO值的流。音频流有可能有多个的,这里我们取第一个。

//视频流的下标
int  audioindex = -1;
for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {audioindex = i;break;}

3、打开解码器

通过输入流的信息获取到解码器参数然后查找到响应解码器,最后打开解码器即可。

AVCodecContext* pCodecCtx = NULL;
const AVCodec* pCodec = NULL;
//初始化解码上下文
pCodecCtx=avcodec_alloc_context3(NULL);
//获取解码参数
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[audioindex]->codecpar)
//查找解码器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
//打开解码器
avcodec_open2(pCodecCtx, pCodec, &opts)

打开解码器时可以使用多线程参数优化解码速度。

AVDictionary* opts = NULL;
//使用多线程解码
if (!av_dict_get(opts, "threads", NULL, 0))av_dict_set(&opts, "threads", "auto", 0);

4、解码

解码的流程就是读取输入流的包,对包进行解码,获取解码后的帧。

AVPacket packet;
AVFrame* pFrame = av_frame_alloc();
//读取包
while (av_read_frame(pFormatCtx, &packet) == 0)
{   //发送包avcodec_send_packet(pCodecCtx, &packet);//接收帧while (avcodec_receive_frame(pCodecCtx, pFrame) == 0){//取得解码后的帧pFrameav_frame_unref(pFrame);}av_packet_unref(&packet);
}

解码有个细节是需要注意的,即当av_read_frame到文件尾结束后,需要再次调用avcodec_send_packet传入NULL或者空包flush出里面的缓存帧。下面是完善的解码流程

while (1)
{int gotPacket = av_read_frame(pFormatCtx, &packet) == 0;if (!gotPacket || packet.stream_index == audioindex)//!gotPacket:未获取到packet需要将解码器的缓存flush,所以还需要进一次解码流程。{//发送包if (avcodec_send_packet(pCodecCtx, &packet) < 0){printf("Decode error.\n");av_packet_unref(&packet);goto end;}//接收解码的帧while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {//取得解码后的帧pFrameav_frame_unref(pFrame);}}av_packet_unref(&packet);if (!gotPacket)break;
}

5、重采样

当遇到音频格式或采样率、声道数与输出目标不一致时,就需要进行重采样了,重采样通常放在解码循环中。

struct SwrContext* swr_ctx = NULL;
enum AVSampleFormatforceFormat = AV_SAMPLE_FMT_FLT;
uint8_t* data;
size_t dataSize;
if (forceFormat != pCodecCtx->sample_fmt|| spec.freq!= pFrame->sample_rate|| spec.channels!= pFrame->channels)//重采样
{//计算输入采样数int out_count = (int64_t)pFrame->nb_samples * spec.freq / pFrame->sample_rate + 256;//计算输出数据大小int out_size = av_samples_get_buffer_size(NULL, spec.channels, out_count, forceFormat, 0);//输入数据指针const uint8_t** in = (const uint8_t**)pFrame->extended_data;//输出缓冲区指针uint8_t** out = &outBuffer;int len2 = 0;if (out_size < 0) {av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");goto end;}if (!swr_ctx)//初始化重采样对象{swr_ctx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(spec.channels), forceFormat, spec.freq, pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_if (!swr_ctx|| swr_init(swr_ctx) < 0) {av_log(NULL, AV_LOG_ERROR, "swr_alloc_set_opts() failed\n");goto end;}                       }       if (!outBuffer)//申请输出缓冲区{outBuffer = (uint8_t*)av_mallocz(out_size);}//执行重采样len2 = swr_convert(swr_ctx, out, out_count, in, pFrame->nb_samples);if (len2 < 0) {av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");goto end;}//取得输出数据data = outBuffer;//输出数据长度dataSize = av_samples_get_buffer_size(0, spec.channels, len2, forceFormat, 1);
}

6、销毁资源

使用完成后需要释放资源。

//销毁资源
if (pFrame)
{if (pFrame->format != -1){av_frame_unref(pFrame);}av_frame_free(&pFrame);
}
if (packet.data)
{av_packet_unref(&packet);
}
if (pCodecCtx)
{avcodec_close(pCodecCtx);avcodec_free_context(&pCodecCtx);
}
if (pFormatCtx)avformat_close_input(&pFormatCtx);
if (pFormatCtx)avformat_free_context(pFormatCtx);
swr_free(&swr_ctx);
av_dict_free(&opts);
if (outBuffer)av_free(outBuffer);

二、sdl播放

1、初始化sdl

使用sdl前需要在最开始初始化sdl,全局只需要初始化一次即可。

if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf("Could not initialize SDL - %s\n", SDL_GetError());return -1;
}

2、打开音频设备

建议使用SDL_OpenAudioDevice打开设备,使用SDL_OpenAudio的话samples设置可能不生效,不利于push的方式播放。

SDL_AudioSpec wanted_spec, spec;
int audioId = 0;
//打开设备
wanted_spec.channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout);
wanted_spec.freq = pCodecCtx->sample_rate;
wanted_spec.format = AUDIO_F32SYS;
wanted_spec.silence = 0;
wanted_spec.samples = FFMAX(512, 2 << av_log2(wanted_spec.freq / 30));
wanted_spec.callback = NULL;
wanted_spec.userdata = NULL;
audioId = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, 1);
if (audioId < 2)
{printf("Open audio device error!\n");goto end;
}
//开启播放
SDL_PauseAudioDevice(audioId, 0);

3、播放(push)

我们采用push的方式播放,即调用SDL_QueueAudio,将音频数据写入sdl内部维护的队列中,sdl会按照一定的频率读取队列数据并写入带音频设备。

SDL_QueueAudio(audioId, data, dataSize);

4、销毁资源

使用完成后需要销毁资源,如下所示,SDL_Quit并不是必要的,通常是程序退出才需要调用,这个时候调不调已经无所谓了。

if (audioId >= 2)SDL_CloseAudioDevice(audioId);
SDL_Quit();

三、队列长度控制

使用push(SDL_QueueAudio)的方式播放音频,通常会遇到一个问题:应该以什么频率往队列写入多少数据?如何保持队列长度稳定,且不会因为数据过少导致声音卡顿。通用以定量的方式是不可行的,基本都会出现数据量少卡顿或队列长度不断增长。这时候我们需要能够动态的控制队列长度,数据少了就写入快一些,数据过多就写入慢一些。

1、问题

写入过快或者慢都会出现问题。

(1)、写入较快的情况

写入过快时队列长度不受控制的增长,如果播放时间足够长就会导致out of memory。

(2)、写入较慢的情况

写入过慢则会导致队列数据不足,sdl会自动补充静音包,呈现出来的结果就是播放的声音断断续续的。

2、 解决方法

(1)、使用pid

比较简单的动态控制算法就是pid了,我们只需要根据当前队列的长度计算出需要调整的延时,即能够控制队列长度:(示例)

//目标队列长度
double targetSize;
//当前队列长度
int size;
error_p = targetSize - size;
error_i += error_p;
error_d = error_p - error_dp;
error_dp = error_p;
size = (kp * error_p + ki * error_i + kd * error_d);
//将targetSize - size转换成时长就是延时。
double delay;

效果预览:
目标队列长度是49152bytes,基本在可控范围内波动


四、完整代码

1、代码(不含pid)

将上述代码合并起来形成一个完整的音频解码播放流程:
示例的sdk版本:ffmpeg 4.3、sdl2
windows、linux都可以正常运行


#include <stdio.h>
#include <SDL.h>
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#undef main
int main(int argc, char** argv) {const char* input = "test_music.wav";enum AVSampleFormat forceFormat;AVFormatContext* pFormatCtx = NULL;AVCodecContext* pCodecCtx = NULL;const AVCodec* pCodec = NULL;AVDictionary* opts = NULL;AVPacket packet;AVFrame* pFrame = NULL;struct SwrContext* swr_ctx = NULL;uint8_t* outBuffer = NULL;int    audioindex = -1;int exitFlag = 0;int isLoop = 1;SDL_AudioSpec wanted_spec, spec;int audioId = 0;memset(&packet, 0, sizeof(AVPacket));//初始化SDLif (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf("Could not initialize SDL - %s\n", SDL_GetError());return -1;}//打开输入流if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {printf("Couldn't open input stream.\n");goto end;}//查找输入流信息if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {printf("Couldn't find stream information.\n");goto end;}//获取音频流for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {audioindex = i;break;}if (audioindex == -1) {printf("Didn't find a audio stream.\n");goto end;}//创建解码上下文pCodecCtx = avcodec_alloc_context3(NULL);if (pCodecCtx == NULL){printf("Could not allocate AVCodecContext\n");goto end;}//获取解码器if (avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[audioindex]->codecpar) < 0){printf("Could not init AVCodecContext\n");goto end;}pCodec = avcodec_find_decoder(pCodecCtx->codec_id);if (pCodec == NULL) {printf("Codec not found.\n");goto end;}//使用多线程解码if (!av_dict_get(opts, "threads", NULL, 0))av_dict_set(&opts, "threads", "auto", 0);//打开解码器if (avcodec_open2(pCodecCtx, pCodec, &opts) < 0) {printf("Could not open codec.\n");goto end;}if (pCodecCtx->sample_fmt == AV_SAMPLE_FMT_NONE){printf("Unknown sample foramt.\n");goto end;}if (pCodecCtx->sample_rate <= 0 || av_get_channel_layout_nb_channels(pFormatCtx->streams[audioindex]->codecpar->channels) <= 0){printf("Invalid sample rate or channel count!\n");goto end;}//打开设备wanted_spec.channels = pFormatCtx->streams[audioindex]->codecpar->channels;wanted_spec.freq = pCodecCtx->sample_rate;wanted_spec.format = AUDIO_F32SYS;wanted_spec.silence = 0;wanted_spec.samples = FFMAX(512, 2 << av_log2(wanted_spec.freq / 30));wanted_spec.callback = NULL;wanted_spec.userdata = NULL;audioId = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, 1);if (audioId < 2){printf("Open audio device error!\n");goto end;}switch (spec.format){case AUDIO_S16SYS:forceFormat = AV_SAMPLE_FMT_S16;break;case    AUDIO_S32SYS:forceFormat = AV_SAMPLE_FMT_S32;break;case    AUDIO_F32SYS:forceFormat = AV_SAMPLE_FMT_FLT;break;default:printf("audio device format was not surported!\n");goto end;break;}pFrame = av_frame_alloc();SDL_PauseAudioDevice(audioId, 0);
start:while (!exitFlag){//读取包int gotPacket = av_read_frame(pFormatCtx, &packet) == 0;if (!gotPacket || packet.stream_index == audioindex)//!gotPacket:未获取到packet需要将解码器的缓存flush,所以还需要进一次解码流程。{//发送包if (avcodec_send_packet(pCodecCtx, &packet) < 0){printf("Decode error.\n");av_packet_unref(&packet);goto end;}//接收解码的帧while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {uint8_t* data;size_t dataSize;if (forceFormat != pCodecCtx->sample_fmt || spec.freq != pFrame->sample_rate || spec.channels != pFrame->channels)//重采样{//计算输入采样数int out_count = (int64_t)pFrame->nb_samples * spec.freq / pFrame->sample_rate + 256;//计算输出数据大小int out_size = av_samples_get_buffer_size(NULL, spec.channels, out_count, forceFormat, 0);//输入数据指针const uint8_t** in = (const uint8_t**)pFrame->extended_data;//输出缓冲区指针uint8_t** out = &outBuffer;int len2 = 0;if (out_size < 0) {av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");goto end;}if (!swr_ctx)//初始化重采样对象{swr_ctx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(spec.channels), forceFormat, spec.freq, av_get_default_channel_layout(pFormatCtx->streams[audioindex]->codecpar->channels), pCodecCtx->sample_fmt, pCodecCtx->sample_rate, 0, NULL);if (!swr_ctx || swr_init(swr_ctx) < 0) {av_log(NULL, AV_LOG_ERROR, "swr_alloc_set_opts() failed\n");goto end;}}if (!outBuffer)//申请输出缓冲区{outBuffer = (uint8_t*)av_mallocz(out_size);}//执行重采样len2 = swr_convert(swr_ctx, out, out_count, in, pFrame->nb_samples);if (len2 < 0) {av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");goto end;}//取得输出数据data = outBuffer;//输出数据长度dataSize = av_samples_get_buffer_size(0, spec.channels, len2, forceFormat, 1);}else{data = pFrame->data[0];dataSize = av_samples_get_buffer_size(pFrame->linesize, pFrame->channels, pFrame->nb_samples, forceFormat, 0);}//写入数据SDL_QueueAudio(audioId, data, dataSize);//延时,按照数据长度,-1是防止写入过慢卡顿SDL_Delay((dataSize) * 1000.0 / (spec.freq * av_get_bytes_per_sample(forceFormat) * spec.channels) - 1);int size = SDL_GetQueuedAudioSize(audioId);printf("queue size:%dbytes\n", size);}}av_packet_unref(&packet);if (!gotPacket){//循环播放时flush出缓存帧后需要调用此方法才能重新解码。avcodec_flush_buffers(pCodecCtx);break;}}if (!exitFlag){if (isLoop){//定位到起点if (avformat_seek_file(pFormatCtx, -1, 0, 0, 0, AVSEEK_FLAG_FRAME) >= 0){goto start;}}}
end://销毁资源if (pFrame){if (pFrame->format != -1){av_frame_unref(pFrame);}av_frame_free(&pFrame);}if (packet.data){av_packet_unref(&packet);}if (pCodecCtx){avcodec_close(pCodecCtx);avcodec_free_context(&pCodecCtx);}if (pFormatCtx)avformat_close_input(&pFormatCtx);if (pFormatCtx)avformat_free_context(pFormatCtx);swr_free(&swr_ctx);av_dict_free(&opts);if (outBuffer)av_free(outBuffer);if (audioId >= 2)SDL_CloseAudioDevice(audioId);SDL_Quit();return 0;
}

2、项目

包含pid的完整代码,项目为vs2022,目录中包含makefile,windows、linux都可以正常运行。
下载链接


总结

以上就是今天要讲的内容,总的来说,使用ffmpeg解码音频sdl播放流程是基本与视频一致的,而且使用push的方式,相对与pull的方式,不需要使用额外的队列以及条件变量做访问控制。但是音频队列数据长度的控制也是一个难点,虽然本文使用pid达到了目的,但长度还是存在动态波动,需要继续调参或者调整计算逻辑或者采取其他方式去优化。

使用ffmpeg解码音频sdl(push)播放相关推荐

  1. FFmpeg开发笔记(七):ffmpeg解码音频保存为PCM并使用软件播放

    若该文为原创文章,转载请注明原文出处 本文章博客地址:https://blog.csdn.net/qq21497936/article/details/108799279 各位读者,知识无穷而人力有穷 ...

  2. 老调重弹之ffmpeg解码音频

    老调重弹之ffmpeg解码音频 接着之前的视频解码,在之前的基础上加上音频解码,还是使用SDL. 首先找到音频流 与找视频流时一样,在avformat_find_stream_info之后,遍历一下A ...

  3. ffmpeg 解码音频(aac、mp3)输出pcm文件

    ffmpeg 解码音频(aac.mp3)输出pcm文件 播放pcm可以参考: ffplay -ar 48000 -ac 2 -f f32le out.pcm main.c #include <s ...

  4. ffmpeg解码视频文件并播放

    最近学习了一下如何使用ffmpeg解码音视频,网上的教程挺多但是也挺杂的,搞了好几天,明白了ffmpeg解码音视频的大体流程,这里记录一下ffmpeg解码视频并播放音视频的例子,但并没有做音频.视频播 ...

  5. FFmpeg解码音频代码

    工程请访问DecodeTest.用c++写的,使用FFmpeg-4.1.0.比较完善的是OfficalDecodeAudio.cpp,它是我研究了一下ffplay.c的源码得来的.该代码按照统一的44 ...

  6. FFMPEG小白-day10(sdl项目播放视频进阶)

    首先,向雷神,雷霄骅致敬! 星河战队.png 我们上篇文章已经介绍了sdl的播放视频的知识了,但是不知道试验过的同学有没有发现,我们在播放视频时的状态是这样的 细心的同学应该能看到,鼠标现在是加载中的 ...

  7. iOS 音视频开发:Audio Unit播放FFmpeg解码的音频

    本文档描述了iOS播放经FFmpeg解码的音频数据的编程步骤,具体基于Audio Toolbox框架的Audio Session和Audio Unit框架提供的接口实现.在iOS 7及以上平台Audi ...

  8. ffmpeg 找不到bin_FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放

    若该文为原创文章,转载请注明原文出处 本文章博客地址:https://blog.csdn.net/qq21497936/article/details/109603499 各位读者,知识无穷而人力有穷 ...

  9. FFmpeg进行音频的解码和播放

    音频编码 音频数字化主要有压缩与非压缩(pcm)两种方式. 非压缩编码(PCM)PCM音频编码 PCM通过抽样.量化.编码三个步骤将连续变化的模拟信号转换为数字编码. 当采样频率fs.max大于信号中 ...

最新文章

  1. 7.0、Android Studio命令行工具
  2. 计算机网络系统中hn是,中南大学计算机网络作业1.pdf
  3. vue 如何获取图片的原图尺寸_公众号封面图片尺寸是多少?如何在公众号里制作封面图?...
  4. CPU瓶颈(五)--过度编译与不必要重复编译的解决方案
  5. 【网页保存为PDF】pdfkit神器网页自动保存PDF文件
  6. 打开计算机任务栏有桌面没,如何解决Win7任务栏不显示打开的窗口的问题
  7. 压力单位MPa、Psi和bar之间换算公式
  8. 模型结构可视化神器——Netron(支持tf, caffe, keras,mxnet等多种框架)
  9. English写作-如何运用such as、for example、etc.、i.e.
  10. 详解物理学四大神兽————麦克斯韦妖
  11. 16.Linux环境搭建虚拟网络
  12. 未来虚拟经济的雏形,SecondLife,全新的概念。。。
  13. ios金融现金贷App产品上架心得分享
  14. 滤波器入门RLC电路BSF
  15. 基于Springboot的高校食堂管理系统
  16. od机考真题-求n阶方阵里的所有数的和
  17. 02 【ArcGIS JS API + eCharts系列】实现二、三维迁徙图的绘制
  18. navicat导出数据库表结构及数据
  19. 嵌入式人工智能,理念还是噱头?
  20. xilinx和altera 资源对比

热门文章

  1. python爬虫需要cookie_python爬虫(六) Cookie
  2. 【笔记】播放器 - mpv - 使用、配置
  3. 英雄联盟手游服务器维护中,《英雄联盟手游》serveriscurrentlyundermaintenance解决攻略...
  4. 如何配置DAVINCI内存
  5. Spring整合日志框架Log4j2
  6. 数据库迁移 | DBMotion v23.04 支持异地多活
  7. 照片相框软件有什么?照片相框怎么加技巧分享
  8. Swift UIImageView 相框
  9. 训练集、验证集、测试集的作用和意义
  10. Pikachu的渗透测试