1
FFmpeg-ffplay 播放器分析(1).md
作者:Changing Lin
- 2022-11-01 广东
本文字数:3700 字
阅读完需:约 12 分钟
1.背景:
本文主要分析 ffplay 播放器的基础框架,从而理解代码结构和工作原理,同时,理解时间戳和音视频同步等知识点,掌握 ffmpeg 基础开发能力。
2.正文
1.main 方法
/* Called from the main */
int main(int argc, char **argv){
avformat_network_init(); // 初始化ffmpeg框架
SDL_Init (flags);
SDL_CreateWindow
SDL_CreateRenderer
SDL_GetRendererInfo
is = stream_open(input_filename, file_iformat);
event_loop(is);
}
复制代码
2.stream_open 方法
static VideoState *stream_open(const char *filename, AVInputFormat *iformat){
frame_queue_init
packet_queue_init
init_clock
SDL_CreateThread(read_thread, "read_thread", is); // 新建一个名字叫read_thread的子线程
}
复制代码
3.event_loop 方法
static void event_loop(VideoState *cur_stream)
for (;;) {
refresh_loop_wait_event(cur_stream, &event); // 监听各种输入事件,移动进度,快进快退,退出,鼠标事件等
switch (event.type) {
case SDL_KEYDOWN:
switch (event.key.keysym.sym) {
case SDLK_f:
break;
case SDLK_p:
case SDLK_SPACE:
break;
case SDLK_m:
break;
case SDLK_KP_MULTIPLY:
case SDLK_0:
break;
case SDLK_KP_DIVIDE:
case SDLK_9:
break;
case SDLK_s: // S: Step to next frame
break;
case SDLK_a:
break;
case SDLK_v:
break;
case SDLK_c:
break;
case SDLK_t:
break;
case SDLK_w:
break;
case SDLK_PAGEUP:
break;
case SDLK_PAGEDOWN:
break;
case SDLK_LEFT:
goto do_seek;
case SDLK_RIGHT:
goto do_seek;
case SDLK_UP:
goto do_seek;
case SDLK_DOWN:
do_seek:
break;
default:
break;
}
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEMOTION:
break;
case SDL_WINDOWEVENT:
break;
case SDL_QUIT:
case FF_QUIT_EVENT:
break;
default:
break;
}
}
}
复制代码
4.read_thread 子线程
static int read_thread(void *arg){
AVFormatContext * ic = avformat_alloc_context();
avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
av_format_inject_global_side_data(ic);
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
for (;;) {
ret = av_read_frame(ic, pkt);
if (ret < 0) {
packet_queue_put_nullpacket(&is->videoq, is->video_stream);
or packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
or packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
continue;
}
packet_queue_put(&is->audioq, pkt);
or packet_queue_put(&is->videoq, pkt);
or packet_queue_put(&is->subtitleq, pkt);
}
}
复制代码
5.stream_component_open 方法
static int stream_component_open(VideoState *is, int stream_index){
avctx = avcodec_alloc_context3(NULL);
codec = avcodec_find_decoder(avctx->codec_id);
ret = avcodec_open2(avctx, codec, &opts)
switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)
break;
case AVMEDIA_TYPE_VIDEO:
decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)
break;
case AVMEDIA_TYPE_SUBTITLE:
break;
}
}
复制代码
6.decoder_init/decoder_start 方法
d->decoder_tid = SDL_CreateThread(fn, thread_name, arg); // 启动子线程
复制代码
7.video_decoder 子线程
static int video_thread(void *arg){
for (;;) {
ret = get_video_frame(is, frame);
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
}
}
复制代码
8.queue_picture 方法
static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial){
av_frame_move_ref(vp->frame, src_frame);
frame_queue_push(&is->pictq)
}
复制代码
9.get_video_frame 方法
static int get_video_frame(VideoState *is, AVFrame *frame){
got_picture = decoder_decode_frame(&is->viddec, frame, NULL);
if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
if (frame->pts != AV_NOPTS_VALUE) {
double diff = dpts - get_master_clock(is);
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&
diff - is->frame_last_filter_delay < 0 &&
is->viddec.pkt_serial == is->vidclk.serial &&
is->videoq.nb_packets) {
is->frame_drops_early++; // 计算是否丢帧,会不会是追帧策略?
av_frame_unref(frame);
got_picture = 0;
}
}
}
}
复制代码
10.decoder_decode_frame 方法
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
for (;;) {
ret = avcodec_receive_frame(d->avctx, frame);
packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial)
avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)
}
}
复制代码
11.refresh_loop_wait_event 方法
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
double remaining_time = 0.0;
SDL_PumpEvents();
while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
SDL_ShowCursor(0);
cursor_hidden = 1;
}
if (remaining_time > 0.0)
av_usleep((int64_t)(remaining_time * 1000000.0));
remaining_time = REFRESH_RATE;
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
video_refresh(is, &remaining_time);
SDL_PumpEvents();
}
}
复制代码
12.video_refresh->video_display->video_image_display 方法
static void video_refresh(void *opaque, double *remaining_time){
video_display(is);
}
static void video_display(VideoState *is)
{
if (!is->width)
video_open(is);
if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
video_audio_display(is);
else if (is->video_st)
video_image_display(is);
SDL_RenderPresent(renderer);
}
static void video_image_display(VideoState *is){
vp = frame_queue_peek_last(&is->pictq);
upload_texture
SDL_RenderCopyEx
SDL_RenderCopy
}
复制代码
3.总结
3.1 多线程
通过梳理,ffplay 播放器总共开了 5 个线程
主线程:循环监听输入事件,从 FrameQueue 中获取每一帧 Frame,渲染画面或播放音频
read_thread:启动下面 3 个子线程,循环从 Stream 中读取每一个 AVPacket,并且放到 PacketQueue 里面
video_thread:循环从 PacketQueue 里面获取一个 AVPacket,并且解码成一个 Frame,计算是否丢帧策略,放到 FrameQueue 里面
audio_thread:如上,只不过 AVPacket 的类型是 audio,也就是音频包、音频帧
subtitle_thread:如上,只不过 AVPacket 的类型是 subtitle,也就是字幕包、字幕帧
3.2 时间戳:
音视频同步
快进、快退、从指定位置开始播放
划线
评论
复制
发布于: 刚刚阅读数: 4
版权声明: 本文为 InfoQ 作者【Changing Lin】的原创文章。
原文链接:【http://xie.infoq.cn/article/e7427a6488785db64ffcc2b5b】。文章转载请联系作者。
Changing Lin
关注
获得机遇的手段远超于固有常规之上~ 2020-04-29 加入
我能做的,就是调整好自己的精神状态,以最佳的面貌去面对那些未曾经历过得事情,对生活充满热情和希望。
评论