转:ffplay audio输出线程分析

本文深入剖析FFmpeg中SDL音频输出机制,详述了ffplay如何通过SDL实现实时音频输出,包括缓冲区管理、音频数据重采样及音量调整等关键环节。

转自:https://zhuanlan.zhihu.com/p/44139512

ffplay的audio输出同样也是通过SDL实现的。

同样地,本文主要介绍audio输出相关内容,且尽量不涉及音视频同步知识,音视频同步将在专门一篇分析。

audio的输出在SDL下是被动输出,即在开启SDL会在需要输出时,回调通知,在回调函数中,SDL会告知要发送多少的数据。(关于SDL音频输出可以参考这篇:https://www.jianshu.com/p/b006e9e9caa6

但是在ffmpeg解码一个AVPacket的音频到AVFrame后,在AVFrame中存储的音频数据大小与SDL回调所需要的数据大概率是不相等的,这就需要再增加一级缓冲区解决问题。

在audio输出时,主要模型如下图:

 

在这个模型中,sdl通过sdl_audio_callback函数向ffplay要音频数据,ffplay将sampq中的数据通过audio_decode_frame函数取出,放入is->audio_buf,然后送出给sdl。在后续回调时先找audio_buf要数据,数据不足的情况下,再调用audio_decode_frame补充audio_buf

注意 audio_decode_frame这个函数名很具有迷惑性,实际上,这个函数是没有解码功能的!这个函数主要是处理sampq到audio_buf的过程,最多只是执行了重采样。

了解了大致的audio输出模型后,再看详细代码。

先看打开sdl音频输出的代码:

//代码在stream_component_open
//先不看filter相关代码,即默认CONFIG_AVFILTER宏为0

//从avctx(即AVCodecContext)中获取音频格式参数
sample_rate    = avctx->sample_rate;
nb_channels    = avctx->channels;
channel_layout = avctx->channel_layout;

//调用audio_open打开sdl音频输出,实际打开的设备参数保存在audio_tgt,返回值表示输出设备的缓冲区大小
if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
    goto fail;
is->audio_hw_buf_size = ret;
is->audio_src = is->audio_tgt;

//初始化audio_buf相关参数
is->audio_buf_size  = 0;
is->audio_buf_index = 0;

由于不同的音频输出设备支持的参数不同,音轨的参数不一定能被输出设备支持(此时就需要重采样了),audio_tgt就保存了输出设备参数。

audio_open是ffplay封装的函数,会优先尝试请求参数能否打开输出设备,尝试失败后会自动查找最佳的参数重新尝试。不再具体分析。

audio_src一开始与audio_tgt是一样的,如果输出设备支持音轨参数,那么audio_src可以一直保持与audio_tgt一致,否则将在后面代码中自动修正为音轨参数,并引入重采样机制。

最后初始化了几个audio_buf相关的参数。这里介绍下audio_buf相关的几个变量:

  • audio_buf: 从要输出的AVFrame中取出的音频数据(PCM),如果有必要,则对该数据重采样。
  • audio_buf_size: audio_buf的总大小
  • audio_buf_index: 下一次可读的audio_buf的index位置。
  • audio_write_buf_size:audio_buf已经输出的大小,即audio_buf_size - audio_buf_index

 

audio_open函数内,通过通过SDL_OpenAudioDevice注册sdl_audio_callback函数为音频输出的回调函数。那么,主要的音频输出的逻辑就在sdl_audio_callback函数内了。

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
    VideoState *is = opaque;
    int audio_size, len1;

    audio_callback_time = av_gettime_relative();

    while (len > 0) {//循环发送,直到发够所需数据长度
        //如果audio_buf消耗完了,就调用audio_decode_frame重新填充audio_buf
        if (is->audio_buf_index >= is->audio_buf_size) {
           audio_size = audio_decode_frame(is);
           if (audio_size < 0) {
                /* if error, just output silence */
               is->audio_buf = NULL;
               is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
           } else {
               if (is->show_mode != SHOW_MODE_VIDEO)
                   update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
               is->audio_buf_size = audio_size;
           }
           is->audio_buf_index = 0;
        }

        //根据缓冲区剩余大小量力而行
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = len;
        //根据audio_volume决定如何输出audio_buf
        if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
            memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
        else {
            memset(stream, 0, len1);
            if (!is->muted && is->audio_buf)
                SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);
        }
        //调整各buffer
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;
    }

    is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
    /* Let's assume the audio driver that is used by SDL has two periods. */
    if (!isnan(is->audio_clock)) {//更新audclk
        set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }
}

sdl_audio_callback函数是一个典型的缓冲区输出过程,看代码和注释应该可以理解。具体看3个细节:

  • 输出audio_buf到stream,如果audio_volume为最大音量,则只需memcpy复制给stream即可。否则,可以利用SDL_MixAudioFormat进行音量调整和混音
  • 如果audio_buf消耗完了,就调用audio_decode_frame重新填充audio_buf。接下来会继续分析audio_decode_frame函数
  • set_clock_at更新audclk时,audio_clock是当前audio_buf的显示结束时间(pts+duration),由于audio driver本身会持有一小块缓冲区,典型地,会是两块交替使用,所以有2 * is->audio_hw_buf_size.(这里为何还要加audio_write_buf_size,表示不能理解。有理解的希望能赐教)

 

接下来看下audio_decode_frame(省略重采样代码):

static int audio_decode_frame(VideoState *is)
{
    int data_size, resampled_data_size;
    int64_t dec_channel_layout;
    av_unused double audio_clock0;
    int wanted_nb_samples;
    Frame *af;

    if (is->paused)//暂停状态,返回-1,sdl_audio_callback会处理为输出静音
        return -1;

    do {//1. 从sampq取一帧,必要时丢帧
        if (!(af = frame_queue_peek_readable(&is->sampq)))
            return -1;
        frame_queue_next(&is->sampq);
    } while (af->serial != is->audioq.serial);

    //2. 计算这一帧的字节数
    data_size = av_samples_get_buffer_size(NULL, af->frame->channels,
                                           af->frame->nb_samples,
                                           af->frame->format, 1);

    //[]计算dec_channel_layout,用于确认是否需要重新初始化重采样(难道af->channel_layout不可靠?不理解)
    dec_channel_layout =
        (af->frame->channel_layout && af->frame->channels == av_get_channel_layout_nb_channels(af->frame->channel_layout)) ?
        af->frame->channel_layout : av_get_default_channel_layout(af->frame->channels);
    wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);

    //[]判断是否需要重新初始化重采样
    if (af->frame->format        != is->audio_src.fmt            ||
        dec_channel_layout       != is->audio_src.channel_layout ||
        af->frame->sample_rate   != is->audio_src.freq           ||
        (wanted_nb_samples       != af->frame->nb_samples && !is->swr_ctx)) {
        //……
    }

    //3. 获取这一帧的数据
    if (is->swr_ctx) {//[]如果初始化了重采样,则对这一帧数据重采样输出
    }else {
        is->audio_buf = af->frame->data[0];
        resampled_data_size = data_size;
    }

    audio_clock0 = is->audio_clock;//audio_clock0用于打印调试信息

    //4. 更新audio_clock,audio_clock_serial
    /* update the audio clock with the pts */
    if (!isnan(af->pts))
        is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;
    else
        is->audio_clock = NAN;
    is->audio_clock_serial = af->serial;

    return resampled_data_size;//返回audio_buf的数据大小
}

audio_decode_frame并没有真正意义上的decode代码,最多是进行了重采样。主流程有以下步骤:

  1. 从sampq取一帧,必要时丢帧。如发生了seek,此时serial会不连续,就需要丢帧处理
  2. 计算这一帧的字节数。通过av_samples_get_buffer_size可以方便计算出结果
  3. 获取这一帧的数据。对于frame格式和输出设备不同的,需要重采样;如果格式相同,则直接拷贝指针输出即可。总之,需要在audio_buf中保存与输出设备格式相同的音频数据
  4. 更新audio_clock,audio_clock_serial。用于设置audclk.

在省略了重采样代码后看,相对容易理解。

至此,音频输出的主要代码就分析完了。中间我们省略了filter和resample相关的代码,有研究后再补充。

<think> 嗯,用户这次想要的是FFmpeg从命令行到数据流的源码分析内容。看起来ta是个开发者或者音视频技术研究者,需要深入理解FFmpeg的内部工作机制。从问题表述来看,ta应该已经具备一定FFmpeg使用基础,现在想深入源码层。 我注意到用户引用了四篇FFmpeg相关的技术文档片段:包括结构体分析、ffmpeg.c源码解析、架构设计和ffplay参数解析。这些引用很有价值,特别是引用[2]直接提到了ffmpeg.c的主函数流程,这正是命令行处理的核心入口。 用户上次的问题也是关于FFmpeg源码架构的,这次聚焦在“命令行到数据流”这个具体路径上。需要重点解释从用户输入命令到音视频数据流被处理的完整过程。 考虑从这几个层面展开: 1. 命令解析阶段:ffmpeg_parse_options()如何拆解命令行参数,这是用户最直接的操作入口 2. 初始化阶段:编解码器注册、数据结构准备 3. 核心流程:transcode()函数里的输入输出流处理闭环 4. 数据流:解复用、解码、滤镜、编码、复用的完整流水线 5. 关键数据结构:AVFormatContext、AVStream这些容器如何承载数据流 特别要注意引用[3]提到的模块化设计思想,在解释流程时要突出各模块的协作关系。比如解复用器(libavformat)和编解码器(libavcodec)如何通过AVPacket和AVFrame交互。 用户可能还想知道数据流经过的每个模块的可扩展性,比如如何通过AVFilter实现自定义处理。这正好对应引用[3]的滤镜系统描述。 在回答中会避免陷入太细节的函数调用,而是把握主干流程。最后考虑附上几个进阶问题,引导用户深入理解性能优化和定制开发方向。 </think> 以下是FFmpeg从命令行输入到数据流处理的源码分析全流程解析,结合核心源码结构(基于FFmpeg 4.2.2版本): --- ### **一、命令行入口与初始化** **源码文件**:`fftools/ffmpeg.c` **主函数调用栈**: ```c main(int argc, char **argv) ├─ avdevice_register_all() // 注册所有输入输出设备 ├─ show_banner() // 打印版本信息 ├─ ffmpeg_parse_options() // 关键:解析命令行参数 └─ transcode() // 核心码入口 ``` --- ### **二、命令行参数解析流程** **函数**:`ffmpeg_parse_options()` **关键步骤**: 1. **参数分类处理** ```c while ((opt = getopt(argc, argv, "i:y:f:...")) != -1) { switch (opt) { case 'i': input_files = optarg; // 输入文件 case 'y': force_height = atoi(optarg); // 强制设置分辨率(如用户示例) case 'f': format = optarg; // 强制封装格式 ... } } ``` 2. **构建过滤器图**(Filtergraph) 通过`-vf`/`-af`参数解析为`FilterGraph`结构体[^3] --- ### **三、核心码流程:`transcode()`** **函数调用栈**: ```c transcode() ├─ open_input_file() // 打开输入文件 │ └─ avformat_open_input() // 解复用器初始化 ├─ open_output_file() // 打开输出文件 │ └─ avformat_alloc_output_context2() ├─ init_input_stream() // 初始化输入流 │ ├─ avcodec_find_decoder() // 查找解码器 │ └─ avcodec_open2() // 打开解码器 ├─ transcode_init() // 初始化码器 ├─ transcode_step() // 执行码循环 └─ close_files() // 资源释放 ``` --- ### **四、数据流处理关键路径** #### 1. **解复用(Demux)** - **数据结构**:`AVFormatContext` (包含`AVStream`数组) - **源码逻辑**: ```c while (av_read_frame(ifmt_ctx, &pkt) >= 0) { // 读取数据包 AVStream *in_stream = ifmt_ctx->streams[pkt.stream_index]; // 将pkt送入解码队列 } ``` #### 2. **解码(Decode)** - **函数**:`decode_video()`/`decode_audio()` - **关键调用**: ```c ret = avcodec_send_packet(dec_ctx, pkt); // 发送数据包到解码器 while (ret >= 0) { ret = avcodec_receive_frame(dec_ctx, frame); // 获取解码后的帧 if (ret == AVERROR_EOF) break; // 将frame送入滤镜或编码器 } ``` #### 3. **滤镜处理(Filtering)** - **数据结构**:`FilterGraph`、`AVFilterContext` - **流程**: ```c av_buffersrc_add_frame(filter_ctx, frame); // 帧输入滤镜 while (av_buffersink_get_frame_flags(sink_ctx, frame) >= 0) { // 处理过滤后的帧 } ``` #### 4. **编码(Encode)与复用(Mux)** - **编码**: ```c avcodec_send_frame(enc_ctx, frame); // 发送帧到编码器 avcodec_receive_packet(enc_ctx, pkt); // 获取编码后的包 ``` - **复用**: ```c av_interleaved_write_frame(ofmt_ctx, pkt); // 写入输出文件 ``` --- ### **五、关键数据结构关系** ```mermaid graph TD A[命令行参数] --> B[AVFormatContext] B --> C[AVStream] C --> D[AVCodecContext] D --> E[AVPacket/AVFrame] E --> F[AVFilterGraph] F --> G[编码器AVCodecContext] G --> H[输出AVFormatContext] ``` --- ### **六、性能优化关键点** 1. **异步处理**:通过多线程解码/编码(`avcodec_open2`时设置`thread_count`) 2. **零拷贝**:使用`hwaccel`硬件加速(如`-hwaccel cuda`)[^1] 3. **内存优化**:复用`AVFrame`和`AVPacket`对象池
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值