音视频学习笔记九——从0开始的播放器之播放控制

118 阅读6分钟

题记:前几篇介绍了视频播放的基本播放流程,本篇介绍播放中的控制,包括播放关闭、暂停、Seek、倍速播放等。可以结合音视频Demo或ffplay进行理解。本章结束后关于播放部分就告一段落。

基本控制

Stop

关闭播放器,通常需要结束线程和释放资源。

结束线程

  • 关闭音频SDL_CloseAudio
  • 其他线程通过设置标识abort_request实现
    • ffplay中有两类标识
    • 全局 VideoState->abort_request
      • 控制解封装线程,视频线程
    • 队列 PacketQueue->abort_request,每个PacketQueue都包含一个
      • 控制解码线程
    • loop循环中,先判断是否abort状态,如果检测到abort状态,结束线程。
  • 解放SDL_CondWait状态,(涉及到的地方后文有介绍)。

资源释放

  • FFmpeg对象释放 // avformat avformat_close_input(&formatContext) // 解码器 avcodec_close(vCodecContext); avcodec_free_context(&vCodecContext); // SwrContext swr_free(&swrCtx);

  • Frame和Packet队列中对象释放

    • AVFrame和AVPacket内存如下
      • 引用计数设计
      • AVBuffer是实际存储对象,数据存在buffer中,refcount是引用计数
      • AVBufferRef中的buffer实际指向 AVBufferbuffer
    • 引用计数相关方法
      • av_packet_refav_packet_unref packet数据的引用+1 和 -1
      • av_frame_refav_frame_unref frame数据的引用+1 和 -1
      • av_packet_freeav_frame_free 释放对象,数据的引用 -1
    AVFrame管理.jpg

完播

读到packet失败时,根据返回值或是avio_feof判断是否播放完毕。标记eof,并向packet队列中插入一个空的packet(数据位nullptr)。

  • break会进入fail流程,即pb->error,走失败流程
  • 其他情况,进入下一次循环。即播放结束,线程的循环还在维护,等待新的操作
ret = av_read_frame(ic, pkt);
if (ret < 0) {
    if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
        if (is->video_stream >= 0)
            packet_queue_put_nullpacket(&is->videoq, is->video_stream);
        if (is->audio_stream >= 0)
            packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
        if (is->subtitle_stream >= 0)
            packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
        is->eof = 1;
    }
    if (ic->pb && ic->pb->error)
        break;
    SDL_LockMutex(wait_mutex);
    SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
    SDL_UnlockMutex(wait_mutex);
    continue;
}

此时各个线程状态:

  • 解封装线程(av_read_frame),维持循环
    • 等待continue_read_thread信号,超时10ms
    • 发生seek时,拖动了进度条,发送continue_read_thread信号
  • 解码线程循环 会卡到SDL_CondWait(q->cond, q->mutex)
  • 播放线程 会卡到SDL_CondWait(f->cond, f->mutex)
  • 此时如果要关闭流程,非常必要的步骤 decoder_abort中调用 SDL_CondSignal
static void decoder_abort(Decoder *d, FrameQueue *fq)
{
    packet_queue_abort(d->queue); // 会调用 SDL_CondSignal(q->cond)
    frame_queue_signal(fq); // 调用 SDL_CondSignal(f->cond)
    SDL_WaitThread(d->decoder_tid, NULL);
    d->decoder_tid = NULL;
    packet_queue_flush(d->queue);
}

暂停&恢复

暂停&恢复的主要代码在stream_toggle_pause

static void stream_toggle_pause(VideoState *is) {
    if (is->paused) {
        is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
        if (is->read_pause_return != AVERROR(ENOSYS)) {
            is->vidclk.paused = 0;
        }
        set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
    }
    set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial);
    is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
}

主要设置点:

  • is->paused状态
    • 解封装线程、解码线程、视频播放线程,读到这个状态,直接continue
    • 线程loop,每个周期等待10ms,等待其他操作。
  • 时钟paused状态
    • 时钟时间,如果paused,直接返回pts
  • 状态改变
    • 更新外部时钟状态,用当前时钟,主要是更新last_updated
    • 视频时钟在 paused->play,更新last_updated,以及frame_timer
      • 更新视频时钟 last_updated
      • 更新frame_timer表示当前帧开始显示时间,具体操作: +暂时时间
    • 音频时钟没有更新,是因为音频时钟更新其实依赖SDL回调

线程状态:

  • 解封装线程(av_read_frame)SDL_Delay(10);
  • 解码线程循环 会因为FrameQueue满或者PaketQueue为空进入阻塞状态。abort时需要SDL_Signal
  • 视频播放线程 video_refresh_thread 10ms循环
  • 音频播放线程 callback audio_decode_frame返回-1 循环中
解码阻塞.jpg

进阶控制

Seek、快进、快退

播放视频中的拖动进度条,或者快进快退都是通过seek实现的。主要对应的方法 avformat_seek_file或者av_seek_frame(精密度没有前一个方法高)。函数原型:

int avformat_seek_file(
    AVFormatContext *s,     // 格式上下文(封装层)
    int stream_index,       // 目标流索引(用于参考定位的流,如视频流),-1自动选择默认
    int64_t min_ts,         // 允许的最小时间戳(下限)
    int64_t ts,             // 目标时间戳(期望定位的时间点)
    int64_t max_ts,         // 允许的最大时间戳(上限)
    int flags               // Seek标志(如关键帧对齐、字节定位等)
);

avformat_seek_file函数的调用都在read_thread中:

  • 还未进入循环,如命令行一开始设置了开始时间。
  • 循环中设置seek_req标记,启用seek

调用seek后,原来的AVFrame和AVPacket都失效了,此时需要进行操作

  • PacketQueue的flush操作,清理掉没有出来的packet。
  • 对于已经解码的Frame,使用serial机制来同步
    • seek操作会在每一个PacketQueue中插入一个flush_pkt
    • PacketQueue在接收到flush_pkt,serial自增操作

serial机制

整个play中处理很多serial,如下图: serial.jpg

serial解析

  • PacketQueue->serial 在收到flush_pkt 自增,是基准标记
  • Packet->serial是在入队列时刻的 PacketQueue->serial
  • Decoder->pkt_serial是当前处理的Packet->serial
    • Decoder->queue指向PacketQueue,间接获取
  • Frame->serial是当时的Decoder->pkt_serial
  • FrameQueue->pktq指向PacketQueue,所以可以获取到PacketQueue->serial
  • Clock中有两个结构
    • queue_serial指向PacketQueue->serial
    • serial 是更新clock时的Frame->serial

由上FrameQueueClockDecoder都可以直接或间接获取到基准的(PacketQueue->serial),判断PacketFrameClock的 serial是否有效,于是实现seek同步。

倍速播放

倍速播放如0.5倍或2倍速播放,ffplay中只有在外部时钟同步时有作用,如check_external_clock_speed

static void check_external_clock_speed(VideoState *is) {
   if (is->video_stream >= 0 && is->videoq.nb_packets <= EXTERNAL_CLOCK_MIN_FRAMES ||
       is->audio_stream >= 0 && is->audioq.nb_packets <= EXTERNAL_CLOCK_MIN_FRAMES) {
       set_clock_speed(&is->extclk, FFMAX(EXTERNAL_CLOCK_SPEED_MIN, is->extclk.speed - EXTERNAL_CLOCK_SPEED_STEP));
   } else if ((is->video_stream < 0 || is->videoq.nb_packets > EXTERNAL_CLOCK_MAX_FRAMES) &&
              (is->audio_stream < 0 || is->audioq.nb_packets > EXTERNAL_CLOCK_MAX_FRAMES)) {
       set_clock_speed(&is->extclk, FFMIN(EXTERNAL_CLOCK_SPEED_MAX, is->extclk.speed + EXTERNAL_CLOCK_SPEED_STEP));
   } else {
       double speed = is->extclk.speed;
       if (speed != 1.0)
           set_clock_speed(&is->extclk, speed + EXTERNAL_CLOCK_SPEED_STEP * (1.0 - speed) / fabs(1.0 - speed));
   }
}

主要通过get_clock实现,外部时钟时会同时影响视频和音频播放

  • 视频通过延长或者缩短展示时长实现
  • 音频通过重采样实现,参考音视频同步
static double get_clock(Clock *c)
{
    xxx
    double time = av_gettime_relative() / 1000000.0;
    return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
}

其他同步倍速播放补充

如果要实现视频、音频主导的倍速需要修改代码

  • 视频同步,相对简单,根据当前速度和帧播放时长,重新计算进度,同步到其他
  • 音频同步,可以通过添加滤镜实现,如atempo
// 初始化音频滤镜
AVFilterContext *atempo_ctx;
avfilter_graph_create_filter(&atempo_ctx, avfilter_get_by_name("atempo"), "atempo", "tempo=2.0", NULL, filter_graph);