题记:前几篇介绍了视频播放的基本播放流程,本篇介绍播放中的控制,包括播放关闭、暂停、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
实际指向AVBuffer
的buffer
- 引用计数相关方法
av_packet_ref
和av_packet_unref
packet数据的引用+1 和 -1av_frame_ref
和av_frame_unref
frame数据的引用+1 和 -1av_packet_free
、av_frame_free
释放对象,数据的引用 -1
- AVFrame和AVPacket内存如下
完播
读到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 循环中
进阶控制
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自增操作
- seek操作会在每一个PacketQueue中插入一个
serial机制
整个play中处理很多serial,如下图:
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
由上FrameQueue
、Clock
与Decoder
都可以直接或间接获取到基准的(PacketQueue->serial
),判断Packet
、Frame
与Clock
的 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);