FFmpeg开发笔记(八):ffmpeg解码音频并使用SDL同步音频播放

简介: FFmpeg开发笔记(八):ffmpeg解码音频并使用SDL同步音频播放

前言

  ffmpeg解码音频之后进行播放,本篇使用SDL播放ffmpeg解码音频流转码后的pcm。


FFmpeg解码音频

  FFmpeg解码音频的基本流程请参照:《FFmpeg开发笔记(七):ffmpeg解码音频保存为PCM并使用软件播放


SDL播放音频

  SDL播放音频的基本流程请参照:《SDL开发笔记(二):音频基础介绍、使用SDL播放音频


ffmpeg音频同步

  ffmpeg同步包含音频、视频、字幕等等,此处描述的同步是音频的同步。

基本流程

  

同步关键点

  不改变播放速度的前提下,音频的播放相对容易,本文章暂时未涉及到音视频双轨或多轨同步。

  解码音频后,时间间隔还是计算一下,主要是控制解码的间隔,避免解码过快导致缓存区溢出导致异常。

  解码音频进行重采样之后可以得到指定采样率、声道、数据类型的固定参数,使用SDL用固定参数打开音频,将解码的数据扔向缓存区即可。因为解码的时候其数据量与采样率是对应的,播放的时候也是扔入对应的数据量,所以再不改变音频采样率的前提下,我们是可以偷懒不做音频同步的。

  压入数据缓存区,可以根据播放的回调函数之后数据缓存区的大小进行同步解码压入音频,但是音频与视频不同,音频卡顿的话对音频播放的效果将会大打折扣,导致音频根本无法被顺利的播放,非常影响用户体验,实测需要保留一倍以上的预加载的音频缓冲区,否则等需要的是再加载就已经晚了。

  音频更为复杂的操作涉及到倍速播放、音调改变等等,后续文章会有相应的文章说明。


ffmpeg音频同步相关结构体详解

AVCodecContext

  该结构体是编码上下文信息,对文件流进行探测后,就能得到文件流的具体相关信息了,关于编解码的相关信息就在此文件结构中。

  与同步视频显示相关的变量在此详解一下,其他的可以自行去看ffmpeg源码上对于结构体AVCodecContext的定义。

struct AVCodecContext {
    AVMediaType codec_type;         // 编码器的类型,如视频、音频、字幕等等
    AVCodec *codec;                // 使用的编码器
    enum AVSampleFormat sample_fmt; // 音频采样点的数据格式
    int sample_rate;                // 每秒的采样率
    int channels;                   // 音频通道数据量
    uint64_t channel_layout;        // 通道布局 
} AVCodecContext;

SwrContext

  重采样的结构体,最关键的是几个参数,输入的采样频率、通道布局、数据格式,输出的采样频率、通道布局、数据格式。

此结构是不透明的。这意味着如果要设置选项,必须使用API,无法直接将属性当作结构体变量进行设置。

  • 属性“out_ch_layout”:输入的通道布局,需要通过通道转换函数转换成通道布局枚举,其通道数与通道布局枚举的值是不同的,
  • 属性“out_sample_fmt”:输入的采样点数据格式,解码流的数据格式即可。
  • 属性“out_sample_rate”:输入的采样频率,解码流的采样频率。
  • 属性“in_ch_layout”:输出的通道布局。
  • 属性“in_sample_fmt”:输出的采样点数据格式。
      
  • 属性“in_sample_rate”:输出的采样频率。


Demo源码

void FFmpegManager::testDecodeAudioPlay()
{
    QString fileName = "E:/testFile2/1.mp3";        // 输入解码的文件
    QFile file("D:/1.pcm");
    AVFormatContext *pAVFormatContext = 0;          // ffmpeg的全局上下文,所有ffmpeg操作都需要
    AVStream *pAVStream = 0;                        // ffmpeg流信息
    AVCodecContext *pAVCodecContext = 0;            // ffmpeg编码上下文
    AVCodec *pAVCodec = 0;                          // ffmpeg编码器
    AVPacket *pAVPacket = 0;                        // ffmpag单帧数据包
    AVFrame *pAVFrame = 0;                          // ffmpeg单帧缓存
    SwrContext *pSwrContext = 0;                    // ffmpeg音频转码
    SDL_AudioSpec sdlAudioSpec;                     // sdk音频结构体,用于打开音频播放器
    int ret = 0;                                    // 函数执行结果
    int audioIndex = -1;                            // 音频流所在的序号
    int numBytes = 0;                               // 音频采样点字节数
    uint8_t * outData[8] = {0};                        // 音频缓存区(不带P的)
    int dstNbSamples = 0;                           // 解码目标的采样率
    int outChannel = 0;                             // 重采样后输出的通道
    AVSampleFormat outFormat = AV_SAMPLE_FMT_NONE;  // 重采样后输出的格式
    int outSampleRate = 0;                          // 重采样后输出的采样率
    pAVFormatContext = avformat_alloc_context();    // 分配
    pAVPacket = av_packet_alloc();                  // 分配
    pAVFrame = av_frame_alloc();                    // 分配
    if(!pAVFormatContext || !pAVPacket || !pAVFrame)
    {
        LOG << "Failed to alloc";
        goto END;
    }
    // 步骤一:注册所有容器和编解码器(也可以只注册一类,如注册容器、注册编码器等)
    av_register_all();
    // 步骤二:打开文件(ffmpeg成功则返回0)
    LOG << "文件:" << fileName << ",是否存在:" << QFile::exists(fileName);
    ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), 0, 0);
    if(ret)
    {
        LOG << "Failed";
        goto END;
    }
    // 步骤三:探测流媒体信息
    ret = avformat_find_stream_info(pAVFormatContext, 0);
    if(ret < 0)
    {
        LOG << "Failed to avformat_find_stream_info(pAVCodecContext, 0)";
        goto END;
    }
    // 步骤四:提取流信息,提取视频信息
    for(int index = 0; index < pAVFormatContext->nb_streams; index++)
    {
        pAVCodecContext = pAVFormatContext->streams[index]->codec;
        pAVStream = pAVFormatContext->streams[index];
        switch (pAVCodecContext->codec_type)
        {
        case AVMEDIA_TYPE_UNKNOWN:
            LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_UNKNOWN";
            break;
        case AVMEDIA_TYPE_VIDEO:
            LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_VIDEO";
            break;
        case AVMEDIA_TYPE_AUDIO:
            LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_AUDIO";
            audioIndex = index;
            break;
        case AVMEDIA_TYPE_DATA:
            LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_DATA";
            break;
        case AVMEDIA_TYPE_SUBTITLE:
            LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_SUBTITLE";
            break;
        case AVMEDIA_TYPE_ATTACHMENT:
            LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_ATTACHMENT";
            break;
        case AVMEDIA_TYPE_NB:
            LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_NB";
            break;
        default:
            break;
        }
        // 已经找打视频品流
        if(audioIndex != -1)
        {
            break;
        }
    }
    if(audioIndex == -1 || !pAVCodecContext)
    {
        LOG << "Failed to find video stream";
        goto END;
    }
    // 步骤五:对找到的音频流寻解码器
    pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
    if(!pAVCodec)
    {
        LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):"
            << pAVCodecContext->codec_id;
        goto END;
    }
    // 步骤六:打开解码器
    ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
    if(ret)
    {
        LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
        goto END;
    }
    // 打印
    LOG << "解码器名称:" <<pAVCodec->name << endl
        << "通道数:" << pAVCodecContext->channels << endl
        << "通道布局:" << av_get_default_channel_layout(pAVCodecContext->channels) << endl
        << "采样率:" << pAVCodecContext->sample_rate << endl
        << "采样格式:" << pAVCodecContext->sample_fmt;
    outChannel = 2;
    outSampleRate = 44100;
    outFormat = AV_SAMPLE_FMT_S16;
    // 步骤七:获取音频转码器并设置采样参数初始化
    pSwrContext = swr_alloc_set_opts(0,                                 // 输入为空,则会分配
                                     av_get_default_channel_layout(outChannel),
                                     outFormat,                         // 输出的采样频率
                                     outSampleRate,                     // 输出的格式
                                     av_get_default_channel_layout(pAVCodecContext->channels),
                                     pAVCodecContext->sample_fmt,       // 输入的格式
                                     pAVCodecContext->sample_rate,      // 输入的采样率
                                     0,
                                     0);
    ret = swr_init(pSwrContext);
    if(ret < 0)
    {
        LOG << "Failed to swr_init(pSwrContext);";
        goto END;
    }
    // 最大缓存区,1152个采样样本,16字节,支持最长8个通道
    outData[0] = (uint8_t *)av_malloc(1152 * 2 * 8);
    ret = SDL_Init(SDL_INIT_AUDIO);
    // SDL步骤一:初始化音频子系统
    ret = SDL_Init(SDL_INIT_AUDIO);
    if(ret)
    {
        LOG << "Failed";
        return;
    }
    // SDL步骤二:打开音频设备
    sdlAudioSpec.freq = outSampleRate;
    sdlAudioSpec.format = AUDIO_S16LSB;
    sdlAudioSpec.channels = outChannel;
    sdlAudioSpec.silence = 0;
    sdlAudioSpec.samples = 1024;
    sdlAudioSpec.callback = callBack_fillAudioData;
    sdlAudioSpec.userdata = 0;
    ret = SDL_OpenAudio(&sdlAudioSpec, 0);
    if(ret)
    {
        LOG << "Failed";
        return;
    }
    SDL_PauseAudio(0);
    _audioBuffer = (uint8_t *)malloc(102400);
    file.open(QIODevice::WriteOnly | QIODevice::Truncate);
    // 步骤八:读取一帧数据的数据包
    while(av_read_frame(pAVFormatContext, pAVPacket) >= 0)
    {
        if(pAVPacket->stream_index == audioIndex)
        {
            // 步骤九:将封装包发往解码器
            ret = avcodec_send_packet(pAVCodecContext, pAVPacket);
            if(ret)
            {
                LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret;
                break;
            }
            // 步骤十:从解码器循环拿取数据帧
            while(!avcodec_receive_frame(pAVCodecContext, pAVFrame))
            {
                // nb_samples并不是每个包都相同,遇见过第一个包为47,第二个包开始为1152的
//                LOG << pAVFrame->nb_samples;
                // 步骤十一:获取每个采样点的字节大小
                numBytes = av_get_bytes_per_sample(outFormat);
                // 步骤十二:修改采样率参数后,需要重新获取采样点的样本个数
                dstNbSamples = av_rescale_rnd(pAVFrame->nb_samples,
                                              outSampleRate,
                                              pAVCodecContext->sample_rate,
                                              AV_ROUND_ZERO);
                // 步骤十三:重采样
                swr_convert(pSwrContext,
                            outData,
                            dstNbSamples,
                            (const uint8_t **)pAVFrame->data,
                            pAVFrame->nb_samples);
                // 第一次显示
                static bool show = true;
                if(show)
                {
                    LOG << numBytes << pAVFrame->nb_samples << "to" << dstNbSamples;
                    show = false;
                }
                // 缓存区大小,小于一次回调获取的4097就得提前添加,否则声音会开盾
                while(_audioLen > 4096 * 1)
//                while(_audioLen > 4096 * 0)
                {
                    SDL_Delay(1);
                }
                _mutex.lock();
                memcpy(_audioBuffer + _audioLen, outData[0], numBytes * dstNbSamples * outChannel);
                file.write((const char *)outData[0], numBytes * dstNbSamples * outChannel);
                _audioLen += numBytes * dstNbSamples * outChannel;
                _mutex.unlock();
            }
            av_free_packet(pAVPacket);
        }
    }
END:
    file.close();
    LOG << "释放回收资源";
    SDL_CloseAudio();
    SDL_Quit();
    if(outData[0])
    {
        av_free(outData[0]);
        outData[0] = 0;
        LOG << "av_free(outData)";
    }
    if(pSwrContext)
    {
        swr_free(&pSwrContext);
        pSwrContext = 0;
    }
    if(pAVFrame)
    {
        av_frame_free(&pAVFrame);
        pAVFrame = 0;
        LOG << "av_frame_free(pAVFrame)";
    }
    if(pAVPacket)
    {
        av_free_packet(pAVPacket);
        pAVPacket = 0;
        LOG << "av_free_packet(pAVPacket)";
    }
    if(pAVCodecContext)
    {
        avcodec_close(pAVCodecContext);
        pAVCodecContext = 0;
        LOG << "avcodec_close(pAVCodecContext);";
    }
    if(pAVFormatContext)
    {
        avformat_close_input(&pAVFormatContext);
        avformat_free_context(pAVFormatContext);
        pAVFormatContext = 0;
        LOG << "avformat_free_context(pAVFormatContext)";
    }
}
void FFmpegManager::callBack_fillAudioData(void *userdata, uint8_t *stream, int len)
{
    SDL_memset(stream, 0, len);
    _mutex.lock();
    if(_audioLen == 0)
    {
        _mutex.unlock();
        return;
    }
    LOG << _audioLen << len;
    len = (len > _audioLen ? _audioLen : len);
    SDL_MixAudio(stream, _audioBuffer, len, SDL_MIX_MAXVOLUME);
    _audioLen -= len;
    memmove(_audioBuffer, _audioBuffer + len, _audioLen);
    _mutex.unlock();
    // 每次加载4096
//    LOG << _audioLen << len;
}


工程模板v1.4.0

  对应工程模板v1.4.0:增加解码音频转码使用SDL播放

相关文章
|
6天前
|
编解码
FFmpeg开发笔记(三十三)分析ZLMediaKit对H.264流的插帧操作
《FFmpeg开发实战》书中3.4.3节讲解如何将H.264流封装成MP4。H.264流通常以SPS→PPS→IDR帧开始,这一说法通过雷霄骅的H264分析器得到验证。分析器能解析H.264文件但不支持MP4。ZLMediaKit服务器在遇到I帧时会自动插入SPS和PPS配置帧,确保流符合标准格式。若缺少这些帧,客户端拉流时会报错。FFmpeg开发实战:从零基础到短视频上线》书中提供了更多FFmpeg开发细节。
21 0
FFmpeg开发笔记(三十三)分析ZLMediaKit对H.264流的插帧操作
|
13天前
|
编解码 Java Android开发
FFmpeg开发笔记(三十一)使用RTMP Streamer开启APP直播推流
RTMP Streamer是一款开源的安卓直播推流框架,支持RTMP、RTSP和SRT协议,适用于各种直播场景。它支持H264、H265、AV1视频编码和AAC、G711、OPUS音频编码。本文档介绍了如何使用Java版的RTMP Streamer,建议使用小海豚版本的Android Studio (Dolphin)。加载项目时,可添加国内仓库加速依赖下载。RTMP Streamer包含五个模块:app、encoder、rtmp、rtplibrary和rtsp。完成加载后,可以在手机上安装并运行APP,提供多种直播方式。开发者可以从《FFmpeg开发实战:从零基础到短视频上线》获取更多信息。
46 7
FFmpeg开发笔记(三十一)使用RTMP Streamer开启APP直播推流
|
14天前
|
存储 编解码
FFmpeg开发笔记(三十)解析H.264码流中的SPS帧和PPS帧
《FFmpeg开发实战》书中介绍了音视频编码历史,重点讲述H.264的成功在于其分为视频编码层和网络抽象层。H.264帧类型包括SPS(序列参数集,含视频规格参数),PPS(图像参数集,含编码参数)和IDR帧(立即解码刷新,关键帧)。SPS用于计算视频宽高和帧率,PPS存储编码设置,IDR帧则标志新的解码序列。书中还配以图片展示各帧结构详情,完整内容可参考相关书籍。
42 7
FFmpeg开发笔记(三十)解析H.264码流中的SPS帧和PPS帧
|
7天前
|
Web App开发 移动开发 编解码
FFmpeg开发笔记(三十二)利用RTMP协议构建电脑与手机的直播Demo
本文讨论了实时数据传输在互联网中的重要性,如即时通讯和在线直播。一对一通信通常使用WebRTC技术,但一对多直播需要流媒体服务器和特定协议,如RTSP、RTMP、SRT或RIST。RTMP由于其稳定性和早期普及,成为国内直播的主流。文章通过实例演示了如何使用OBS Studio和RTMP Streamer进行RTMP推流,并对比了不同流媒体传输协议的优缺点。推荐了两本关于FFmpeg和Android开发的书籍以供深入学习。
16 0
FFmpeg开发笔记(三十二)利用RTMP协议构建电脑与手机的直播Demo
|
12天前
|
Java Linux
ffmpeg音频格式转换、合成、速率调整
ffmpeg音频格式转换、合成、速率调整
13 2
|
20天前
|
编解码 Windows
FFmpeg开发笔记(二十九)Windows环境给FFmpeg集成libxvid
XviD是开源MPEG-4视频编码器,与DivX相似但后者非开源。早期MP4常使用XviD或DivX编码,现已被H.264取代。在Windows上集成FFmpeg的XviD编解码库libxvid,需访问<https://labs.xvid.com/source/>下载源码,解压后在MSYS环境中配置、编译和安装。之后重新配置FFmpeg,启用libxvid并编译安装。详细步骤包括configure命令、make和make install。成功后,通过`ffmpeg -version`检查是否启用libxvid。更多音视频开发技术可参考《FFmpeg开发实战:从零基础到短视频上线》。
58 0
FFmpeg开发笔记(二十九)Windows环境给FFmpeg集成libxvid
|
21天前
|
编解码 Linux
FFmpeg开发笔记(二十八)Linux环境给FFmpeg集成libxvid
XviD是开源的MPEG-4视频编解码器,曾与DivX一起用于早期MP4视频编码,但现在已被H.264取代。要集成XviD到Linux上的FFmpeg,首先下载源码,解压后配置并编译安装libxvid。接着,在FFmpeg源码目录中,重新配置FFmpeg以启用libxvid,然后编译并安装。成功后,通过`ffmpeg -version`检查是否启用libxvid。详细步骤包括下载、解压libxvid,使用`configure`和`make`命令安装,以及更新FFmpeg配置并安装。
38 2
FFmpeg开发笔记(二十八)Linux环境给FFmpeg集成libxvid
|
26天前
|
移动开发 小程序 视频直播
FFmpeg开发笔记(二十七)解决APP无法访问ZLMediaKit的直播链接问题
本文讲述了在使用ZLMediaKit进行视频直播时,遇到移动端通过ExoPlayer和微信小程序播放HLS直播地址失败的问题。错误源于ZLMediaKit对HTTP地址的Cookie校验导致401无权限响应。通过修改ZLMediaKit源码,注释掉相关鉴权代码并重新编译安装,解决了此问题,使得ExoPlayer和小程序能成功播放HLS视频。详细解决方案及FFmpeg集成可参考《FFmpeg开发实战:从零基础到短视频上线》一书。
43 3
FFmpeg开发笔记(二十七)解决APP无法访问ZLMediaKit的直播链接问题
|
27天前
|
Web App开发 安全 Linux
FFmpeg开发笔记(二十六)Linux环境安装ZLMediaKit实现视频推流
《FFmpeg开发实战》书中介绍轻量级流媒体服务器MediaMTX,但其功能有限,不适合生产环境。推荐使用国产开源的ZLMediaKit,它支持多种流媒体协议和音视频编码标准。以下是华为欧拉系统下编译安装ZLMediaKit和FFmpeg的步骤,包括更新依赖、下载源码、配置、编译、安装以及启动MediaServer服务。此外,还提供了通过FFmpeg进行RTSP和RTMP推流,并使用VLC播放器拉流的示例。
42 3
FFmpeg开发笔记(二十六)Linux环境安装ZLMediaKit实现视频推流
|
28天前
|
编解码 Linux
FFmpeg开发笔记(二十五)Linux环境给FFmpeg集成libwebp
《FFmpeg开发实战》书中指导如何在Linux环境下为FFmpeg集成libwebp以支持WebP图片编解码。首先,从GitHub下载libwebp源码,解压后通过`libtoolize`,`autogen.sh`,`configure`,`make -j4`和`make install`步骤安装。接着,在FFmpeg源码目录中重新配置并添加`--enable-libwebp`选项,然后进行`make clean`,`make -j4`和`make install`以编译安装FFmpeg。最后,验证FFmpeg版本信息确认libwebp已启用。
42 1
FFmpeg开发笔记(二十五)Linux环境给FFmpeg集成libwebp