[音视频] FFplay 解码学习笔记

963 阅读12分钟

[音视频] FFplay 解码学习笔记

这里先简单介绍一下 FFplay 的解码逻辑:

FFplay 中有三种 Packet Queue,分别是对应的是 Video, AudioSubtitlePacket 队列,在 read thread 线程中去读取 Packet 并写入到对应的队列中去,解码线程(video threadaudio threadsubtitle thread)去读取 Packet 并完成解码。

FFplay 中还有三种 Frame Queue,分别对应的是 Video, AudioSubtitleFrame 队列,Packet Queue 对应的是需要解码的数据,而 Frame Queue 就表示已经解码好的数据,解码好的数据就需要等待渲染线程去读取 Frame 并完成渲染,视频的渲染是在主线程中,通过 video_refresh() 方法完成渲染(前面的介绍音画同步的文章中有说过这个方法[音视频] FFmpeg 时间系统和 FFPlay 音画同步逻辑);音频的渲染在回调方法 sdl_audio_callback() 方法中。

read thread

以下是 read thread 工作的流程图:

ffplay-read-thread.png

他的工作的方法是 read_thread():

static int read_thread(void *arg)
{
    // ...
    // 为 format context 分配内存
    ic = avformat_alloc_context();
    // ...
    // 打开对应的文件或者网络 IO 流
    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
    
    // ...

    // 查找需要处理的 stream
    for (i = 0; i < ic->nb_streams; i++) {
        AVStream *st = ic->streams[i];
        enum AVMediaType type = st->codecpar->codec_type;
        st->discard = AVDISCARD_ALL;
        if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
            if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
                st_index[type] = i;
    }
    
    // ...

    /* open the streams */
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        // 开始音频流解码
        stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
    }

    ret = -1;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        // 开始视频流解码
        ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
    }
    if (is->show_mode == SHOW_MODE_NONE)
        is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;

    if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
        // 开始字幕流解码
        stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
    }

    // ...

    for (;;) {
        if (is->abort_request)
            // 退出读取 packet
            break;

        // 开始或者暂停 packet 的读取
        if (is->paused != is->last_paused) {
            is->last_paused = is->paused;
            if (is->paused)
                is->read_pause_return = av_read_pause(ic);
            else
                av_read_play(ic);
        }
#if CONFIG_RTSP_DEMUXER || CONFIG_MMSH_PROTOCOL
        if (is->paused &&
                (!strcmp(ic->iformat->name, "rtsp") ||
                 (ic->pb && !strncmp(input_filename, "mmsh:", 5)))) {
            /* wait 10 ms to avoid trying to get another packet */
            /* XXX: horrible */
            SDL_Delay(10);
            continue;
        }
#endif
        // 处理 seek 请求
        if (is->seek_req) {
            int64_t seek_target = is->seek_pos;
            int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
            int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
// FIXME the +-2 is due to rounding being not done in the correct direction in generation
//      of the seek_pos/seek_rel variables

            ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
            if (ret < 0) {
                // seek 失败
                av_log(NULL, AV_LOG_ERROR,
                       "%s: error while seeking\n", is->ic->url);
            } else {
                // seek 成功,清空上次缓存的 packet,同时 packet 队列的 serial 会加 1.
                if (is->audio_stream >= 0)
                    packet_queue_flush(&is->audioq);
                if (is->subtitle_stream >= 0)
                    packet_queue_flush(&is->subtitleq);
                if (is->video_stream >= 0)
                    packet_queue_flush(&is->videoq);


                // 重置内部时钟,serial 修改为 0.
                if (is->seek_flags & AVSEEK_FLAG_BYTE) {
                   set_clock(&is->extclk, NAN, 0);
                } else {
                   set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
                }
            }
            is->seek_req = 0;
            is->queue_attachments_req = 1;
            is->eof = 0;
            // 如果当前是暂停状态,恢复暂停
            if (is->paused)
                step_to_next_frame(is);
        }
        if (is->queue_attachments_req) {
            // 获取单帧的视频流(常见的是音乐文件)
            if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {
                if ((ret = av_packet_ref(pkt, &is->video_st->attached_pic)) < 0)
                    goto fail;
                // packet 队列只塞了一个可用 packet
                packet_queue_put(&is->videoq, pkt);
                // 塞入一个空的 packet 表示流结束了,空的 pkt 的 stream id 对应的就是 video stream id.
                packet_queue_put_nullpacket(&is->videoq, pkt, is->video_stream);
            }
            is->queue_attachments_req = 0;
        }

        /* if the queue are full, no need to read more */
        // 判断 packet 队列是否已经满了,满了的话,等 10ms 重试
        // 已满的逻辑判断(满足以下其一就表示满了):
        // 1. 不是无限 buffer,并且三种 packet 队列占用内存和达到 15MB
        // 2. 三种 packet 队列都已经满了,队列满的逻辑是单帧视频流或者 packet 的时长达到 1s
        if (infinite_buffer<1 &&
              (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
            || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
                stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
                stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
            /* wait 10 ms */
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        }

        // 这里表示已经播放完成了,如果需要循环播放的话,seek 到初始位置,如果需要退出的话就直接退出.
        if (!is->paused &&
            (!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
            (!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {
            if (loop != 1 && (!loop || --loop)) {
                stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
            } else if (autoexit) {
                ret = AVERROR_EOF;
                goto fail;
            }
        }
        ret = av_read_frame(ic, pkt);
        if (ret < 0) {
            // 读取失败.


            if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
                // 多媒体文件流读取完毕

                // 往每个 packet 队列中塞入一个空的 packet
                if (is->video_stream >= 0)
                    packet_queue_put_nullpacket(&is->videoq, pkt, is->video_stream);
                if (is->audio_stream >= 0)
                    packet_queue_put_nullpacket(&is->audioq, pkt, is->audio_stream);
                if (is->subtitle_stream >= 0)
                    packet_queue_put_nullpacket(&is->subtitleq, pkt, is->subtitle_stream);
                is->eof = 1;
            }
            if (ic->pb && ic->pb->error) {
                if (autoexit)
                    goto fail;
                else
                    break;
            }
            // 等 10ms 再去读 packet
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        } else {
            is->eof = 0;
        }
        /* check if packet is in play range specified by user, then queue, otherwise discard */
        stream_start_time = ic->streams[pkt->stream_index]->start_time;
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        // 当前的 pts 是否在对应的播放 range 中,如果不在 range 中会被丢掉,不进入队列
        pkt_in_play_range = duration == AV_NOPTS_VALUE ||
                (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
                av_q2d(ic->streams[pkt->stream_index]->time_base) -
                (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
                <= ((double)duration / 1000000);
        // 将获取到的 packet 存入对应的 packet 队列,packet 的 serial 和队列的 serial 一致
        if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
            packet_queue_put(&is->audioq, pkt);
        } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
                   && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
            packet_queue_put(&is->videoq, pkt);
        } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
            packet_queue_put(&is->subtitleq, pkt);
        } else {
            av_packet_unref(pkt);
        }
    }

    ret = 0;
 fail:
    // ...
    return 0;
}

打开文件或者网络流

这部分代码相对比较简单,就不多说了。

初始化对应的解码器和开启解码线程

当获取到需要处理的流类型(FFplay 默认处理视频流,音频流和字幕流,我们只关注视频流和音频流)后,然后为对应的流初始化解码器和创建一个解码线程,处理这个逻辑的是 stream_component_open() 方法:

/* open a given stream. Return 0 if OK */
static int stream_component_open(VideoState *is, int stream_index)
{
    // ...

    // 为解码器分配内存
    avctx = avcodec_alloc_context3(NULL);
    if (!avctx)
        return AVERROR(ENOMEM);

    // 设置流参数到解码器
    ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
    if (ret < 0)
        goto fail;
    avctx->pkt_timebase = ic->streams[stream_index]->time_base;
   
    // 查找对应的解码器
    codec = avcodec_find_decoder(avctx->codec_id);

    // ...
    
    // 设置解码最大线程数
    avctx->lowres = stream_lowres;

    // ... 
    switch (avctx->codec_type) {
    
    case AVMEDIA_TYPE_AUDIO:
        // ...

        /* prepare audio output */
        // 请求 audio 渲染
        if ((ret = audio_open(is, &ch_layout, sample_rate, &is->audio_tgt)) < 0)
            goto fail;
        
        // ...

        // 初始化 audiodec, 它的 serial 设置为 -1
        if ((ret = decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread)) < 0)
            goto fail;
        if (is->ic->iformat->flags & AVFMT_NOTIMESTAMPS) {
            is->auddec.start_pts = is->audio_st->start_time;
            is->auddec.start_pts_tb = is->audio_st->time_base;
        }
        // 开始音频解码,线程为 audio_thread,packet 的队列的 serial 会执行加一, 现在的队列序号就是 1(默认为 0).
        if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
            goto out;
        SDL_PauseAudioDevice(audio_dev, 0);
        break;
    case AVMEDIA_TYPE_VIDEO:
        is->video_stream = stream_index;
        is->video_st = ic->streams[stream_index];

        // 初始化 videodec, 它的 serial 设置为 -1
        if ((ret = decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread)) < 0)
            goto fail;
        // 开始视频解码,线程为 video_thread,packet 的队列的 serial 会执行加一, 现在的队列序号就是 1(默认为 0).
        if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
            goto out;
        is->queue_attachments_req = 1;
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        // ...
    default:
        break;
    }
    goto out;

fail:
    avcodec_free_context(&avctx);
out:
    av_channel_layout_uninit(&ch_layout);
    av_dict_free(&opts);

    return ret;
}

上面的代码比较简单就不多说了,后续单独分析 video_threadaudio_thread

循环读取 Packet 写入到队列中

  • 检查是否请求退出

  • 检查暂停或者恢复状态

  • 检查 seek 请求 这里需要注意,完成 seek 后会清空 Packet Queue 中的缓存,并将 serial 加 1,当后续解码过程发现 serial 改变后,也会做一些缓存清除的工作。

  • 检查 attachments 状态
    这里介绍一下 attachements:像很多的音乐文件他们都是包含专辑封面图的,这类的视频流有一个特点就是只有一帧视频。可以通过以下代码来判断这种情况:

    is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC
    

    可以直接通过流中的 attached_pic 参数来直接完成读取,然后添加到 Packet Queue 中去,添加空的 packet 表示这个流已经读取完毕。

  • 检查 Packet Queue 是否已经满了
    已满的逻辑判断(满足以下其一就表示满了):

  1. 不是无限 buffer,并且三种 packet 队列占用内存和达到 15MB
  2. 三种 packet 队列都已经满了,队列满的逻辑是单帧视频流或者 packet 的时长达到 1s
  • 检查播放是否已经完成

  • 读取 Packet 将读取到的 Packet 存入对应的队列中,然后进入下一次循环

解码

以下是解码一帧音频帧或者视频帧的流程图:

ffplay-decode-one-frame.png

video_thread:

static int video_thread(void *arg)
{
    // ...

    for (;;) {
        // 解码
        ret = get_video_frame(is, frame);
        if (ret < 0)
            goto the_end;
        if (!ret)
            continue;

        // ...

        while (ret >= 0) {
            //...
            
            // 将解码后的 frame 插入 frame 队列
            ret = queue_picture(is, frame, pts, duration, fd ? fd->pkt_pos : -1, is->viddec.pkt_serial);
            av_frame_unref(frame);
            if (is->videoq.serial != is->viddec.pkt_serial)
                break;
        }

        if (ret < 0)
            goto the_end;
    }
 the_end:
    avfilter_graph_free(&graph);
    av_frame_free(&frame);
    return 0;
}


// 返回值等于 1 成功, 0 流读取结束,-1 主动退出.
static int get_video_frame(VideoState *is, AVFrame *frame)
{
    int got_picture;

    if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
        return -1;

    // ...

    return got_picture;
}

audio_thread:

static int audio_thread(void *arg)
{
    // ...

    do {
        // 解码
        if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)
            goto the_end;

        if (got_frame) {
            // 解码成功
            // ...

            while ((ret = av_buffersink_get_frame_flags(is->out_audio_filter, frame, 0)) >= 0) {
                // ...
                
                // 从 frame 队列中获取可用于写入的 frame,如果没有可用的就阻塞.
                if (!(af = frame_queue_peek_writable(&is->sampq)))
                    goto the_end;

                af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
                af->pos = fd ? fd->pkt_pos : -1;
                af->serial = is->auddec.pkt_serial;
                af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});

                // 移动 frame 数据的引用
                av_frame_move_ref(af->frame, frame);
                // 入 frame 队列
                frame_queue_push(&is->sampq);

                if (is->audioq.serial != is->auddec.pkt_serial)
                    break;
            }
            if (ret == AVERROR_EOF)
                is->auddec.finished = is->auddec.pkt_serial;
        }
    } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
 the_end:
    // ...
}

无论是视频解码还是音频解码,它们最终调用的解码方法都是 decoder_decode_frame()

// 返回值等于 1 成功, 0 流读取结束,-1 主动退出.
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);

    for (;;) {
        // 解码第一帧,packet 队列的 serial 为 1,decoder 的 serial 为 -1
        if (d->queue->serial == d->pkt_serial) {
            do {
                if (d->queue->abort_request)
                    // 已经退出.
                    return -1;

                switch (d->avctx->codec_type) {
                    case AVMEDIA_TYPE_VIDEO:
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {
                            // 解码成功
                            if (decoder_reorder_pts == -1) {
                                frame->pts = frame->best_effort_timestamp;
                            } else if (!decoder_reorder_pts) {
                                frame->pts = frame->pkt_dts;
                            }
                        }
                        break;
                    case AVMEDIA_TYPE_AUDIO:
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {
                            // 解码成功
                            AVRational tb = (AVRational){1, frame->sample_rate};
                            if (frame->pts != AV_NOPTS_VALUE)
                                frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
                            else if (d->next_pts != AV_NOPTS_VALUE)
                                frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
                            if (frame->pts != AV_NOPTS_VALUE) {
                                d->next_pts = frame->pts + frame->nb_samples;
                                d->next_pts_tb = tb;
                            }
                        }
                        break;
                }
                if (ret == AVERROR_EOF) {
                    // 当前流解码已经结束
                    d->finished = d->pkt_serial;
                    avcodec_flush_buffers(d->avctx);
                    return 0;
                }
                if (ret >= 0)
                    // 解码成功
                    return 1;
            } while (ret != AVERROR(EAGAIN)); // ret == EAGAIN 需要读取更多的 packet 来完成解码
        }

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {
                // 不需要读取 packet
                d->packet_pending = 0;
            } else {
                // 需要读取 packet
                // decoder 的旧的 serial
                int old_serial = d->pkt_serial;

                // 如果当前 packet 为空,会阻塞住,等待有可用的 packet,如果返回小于 0 表示退出
                if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
                    return -1;
                // decoder 的 serial 发生了改变
                if (old_serial != d->pkt_serial) {
                    // 清空 decoder 缓存
                    avcodec_flush_buffers(d->avctx);
                    d->finished = 0;
                    d->next_pts = d->start_pts;
                    d->next_pts_tb = d->start_pts_tb;
                }
            }
            // 检查 decoder 和 packet 队列的 serial, 如果不一致表示当前的 packet 不一致,需要重新读取
            if (d->queue->serial == d->pkt_serial)
                break;
            av_packet_unref(d->pkt);
        } while (1);

        if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
            int got_frame = 0;
            ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, d->pkt);
            if (ret < 0) {
                ret = AVERROR(EAGAIN);
            } else {
                if (got_frame && !d->pkt->data) {
                    d->packet_pending = 1;
                }
                ret = got_frame ? 0 : (d->pkt->data ? AVERROR(EAGAIN) : AVERROR_EOF);
            }
            av_packet_unref(d->pkt);
        } else {
            if (d->pkt->buf && !d->pkt->opaque_ref) {
                FrameData *fd;

                d->pkt->opaque_ref = av_buffer_allocz(sizeof(*fd));
                if (!d->pkt->opaque_ref)
                    return AVERROR(ENOMEM);
                fd = (FrameData*)d->pkt->opaque_ref->data;
                fd->pkt_pos = d->pkt->pos;
            }

            if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {
                // 表示下次解码不再需要读取 packet.
                av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
                d->packet_pending = 1;
            } else {
                // 清除 packet 引用
                av_packet_unref(d->pkt);
            }
        }
    }
}

我发现很多人并不会处理 AVERROR(EAGAIN) 返回值,这里有必要解释下。
我们在读取到 packet 后,需要通过 avcodec_send_packet() 方法将需要解码的数据发送给解码器,然后通过 avcodec_receive_frame() 方法来获取已经解码成功的数据。这两个方法都可能会返回 AVERROR(EAGAIN)(取决于编码的方式),如果是 avcodec_send_packet() 返回就表示下一帧的解码不需要再去读 packet,也就是下次的解码不需要调用 avcodec_send_packet()FFplay 中使用 packet_pending 参数来标记;avcodec_receive_frame() 方法表示需要更多的 packet 才能完成当前帧的解码,也就是需要继续调用 avcodec_send_packet() 方法来发送更多的数据来解码。

具体解码过程我就不再赘述了,通过我上面的流程图和 decoder_decode_frame() 注释已经很清楚了。

最后

希望本篇文章对你写一个高性能的播放器有所帮助,我自己也要去重写我自己的播放器了。