英文原文地址:http://dranger.com/ffmpeg/tutorial05.html

警告

当我第一次做这个教程时,我的所有同步代码都是从ffplay.c中提取的。今天,这是一个完全不同的程序,ffmpeg库(以及ffplay.c本身)的改进已经导致了一些策略的改变。虽然这段代码仍然有效,但看起来不太好,还有很多本教程可以使用的改进。

视频如何同步

所以这一整段时间,我们有一个本质上是无用的电影播放器​​。它播放的视频,是的,它播放音频,是的,但它不是我们所说的电影。那么我们该怎么办?

PTS和DTS

幸运的是,音频和视频流都具有有关您在其中播放内容的速度和时间的信息。音频流具有采样率,视频流具有每秒帧数。但是,如果我们仅仅通过计数帧和乘以帧速率来同步视频,就有可能与音频不同步。相反,来自流的分组可能具有所谓的解码时间戳(DTS)和呈现时间戳(PTS)。要理解这两个值,你需要知道电影的存储方式。一些格式,如MPEG,使用他们所谓的“B”帧(B代表“双向”)。另外两种帧称为“I”帧和“P”帧(“I”为“内”,“P”为“预测”)。我的帧包含完整的图像。 P帧取决于先前的I帧和P帧,并且像diff或deltas。 B帧与P帧相同,但依赖于在它们之前和之后显示的帧中找到的信息!这就解释了为什么我们在调用avcodec_decode_video2之后可能没有完成的框架。

所以我们假设我们有一个电影,并且帧被显示为:I B B P.现在,我们需要知道P中的信息,然后我们才能显示B帧。正因为如此,这些帧可能会像这样存储:这就是为什么我们在每一帧都有一个解码时间戳和一个表示时间戳。解码时间戳告诉我们什么时候需要解码什么东西,而显示时间戳告诉我们什么时候需要显示一些东西。所以,在这种情况下,我们的流可能看起来像这样:

PTS: 1 4 2 3DTS: 1 2 3 4
Stream: I P B B

一般来说,PTS和DTS只有在我们正在播放的流中有B帧时才会有所不同。

当我们从av_read_frame()得到一个包时,它将包含该包内信息的PTS和DTS值。但是我们真正想要的是我们新解码的原始帧的PTS,所以我们知道何时显示它。

幸运的是,FFMpeg为我们提供了一个“尽力而为”的时间戳,你可以通过av_frame_get_best_effort_timestamp()

同步

现在,虽然知道什么时候我们应该展示一个特定的视频帧,但是我们是如何做到的呢?这是这样的想法:在我们展示一个框架之后,我们计算出什么时候应该显示下一个框架。然后,我们只需设置一个新的超时时间,以在该时间段之后再次刷新视频。正如你所期望的那样,我们根据系统时钟检查下一帧的PTS值,看看我们的超时应该有多长。这种方法是有效的,但有两个问题需要处理。

首先是知道下一个PTS的时间。现在,您可能会认为我们可以将视频速率添加到当前的PTS中 - 而您大部分都是正确的。然而,一些视频呼叫要重复帧。这意味着我们应该重复当前帧一定的次数。这可能会导致程序过早显示下一帧。所以我们需要说明这一点。

第二个问题是,随着节目的到来,视频和音频愉快地蹦蹦跳跳,根本没有同步的麻烦。如果一切正常,我们不用担心。但是你的电脑并不完美,许多视频文件也不是。所以我们有三种选择:将音频同步到视频,将视频同步到音频,或同步到外部时钟(如计算机)。现在,我们将把视频同步到音频。

编码:获得帧PTS

现在让我们进入代码来完成这一切。我们将需要添加更多的成员到我们的大结构中,但是我们会根据需要做到这一点。首先让我们看看我们的视频线程。请记住,这是我们拿起解码线程放在队列中的数据包的地方。在这部分代码中我们需要做的是获得由avcodec_decode_video2给予我们的帧的PTS。我们谈到的第一种方式是获取最后一个数据包的DTS,这很简单:

 double pts;for(;;) {if(packet_queue_get(&is->videoq, packet, 1) < 0) {// means we quit getting packetsbreak;}pts = 0;// Decode video framelen1 = avcodec_decode_video2(is->video_st->codec,pFrame, &frameFinished, packet);if(packet->dts != AV_NOPTS_VALUE) {pts = av_frame_get_best_effort_timestamp(pFrame);} else {pts = 0;}pts *= av_q2d(is->video_st->time_base);

如果我们不知道它是什么,我们将PTS设置为0。

那很简单。技术说明:您可能已经注意到我们正在使用int64作为PTS。这是因为PTS存储为整数。该值是一个时间戳,对应于该流的time_base单位中的时间的度量。例如,如果一个数据流每秒有24帧,那么42的PTS将指示如果我们每1/24秒有一个帧(当然不一定是真的),那么帧应该在第42帧的位置, 。

我们可以通过除以帧率将这个值转换成秒。流的time_base值将是1 / framerate(对于fixed-fps内容),因此要在几秒钟内获得PTS,我们乘以time_base。
编码:同步和使用PTS

所以现在我们已经有了我们的PTS。现在我们必须关注上面讨论的两个同步问题。我们将定义一个名为synchronize_video的函数,将PTS更新为与所有内容同步。这个功能也将最终处理那些我们没有得到PTS值的情况。同时我们需要跟踪下一帧的预计时间,以便我们可以正确设置刷新率。我们可以通过使用一个内部的video_clock值来完成这个工作,该值跟踪视频已经过了多少时间。我们将这个值添加到我们的大结构中。

typedef struct VideoState {double          video_clock; // pts of last decoded frame / predicted pts of next decoded frame

这里是synchronize_video函数,这是不言自明的:

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {double frame_delay;if(pts != 0) {/* if we have pts, set video clock to it */is->video_clock = pts;} else {/* if we aren't given a pts, set it to the clock */pts = is->video_clock;}/* update the video clock */frame_delay = av_q2d(is->video_st->codec->time_base);/* if we are repeating a frame, adjust clock accordingly */frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);is->video_clock += frame_delay;return pts;
}

你会注意到我们也在这个函数中考虑了重复的帧。

现在让我们得到我们正确的PTS,并使用queue_picture排队,添加一个新的pts参数:

// Did we get a video frame?if(frameFinished) {pts = synchronize_video(is, pFrame, pts);if(queue_picture(is, pFrame, pts) < 0) {break;}}

关于queue_picture的唯一改变就是我们将这个pts值保存到我们排队的VideoPicture结构中。 所以我们必须在结构中添加一个pts变量并添加一行代码:

typedef struct VideoPicture {...double pts;
}
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {... stuff ...if(vp->bmp) {... convert picture ...vp->pts = pts;... alert queue ...}

所以,现在我们已经把图片排列在我们的图片队列中,并具有适当的PTS值,所以让我们来看看我们的视频刷新功能。你可能还记得上次我们只是伪造了一个80ms的刷新。那么,现在我们要找出如何真正弄清楚。

我们的策略是预测下一个PTS的时间,方法是简单地测量前一个PTS和这个PTS之间的时间。同时,我们需要将视频同步到音频。我们将制作一个音频时钟:一个内部值,用于跟踪我们正在播放的音频的位置。这就像任何MP3播放器上的数字读出。由于我们将视频同步到音频,因此视频线程使用此值来确定它是太遥远还是太遥远。

稍后我们会进行实施。现在让我们假设我们有一个get_audio_clock函数,它会给我们音频时钟上的时间。一旦我们有了这个价值,但是,如果视频和音频不同步,我们该怎么办?简单地尝试通过寻找或跳跃到正确的数据包是愚蠢的。相反,我们只是调整我们为下一次刷新计算的值:如果PTS离音频时间太远,我们将计算的延迟加倍。如果PTS在音频时间之前太远,我们只需尽快刷新。现在我们调整了刷新时间,或者延迟了,我们将通过保持一个正在运行的frame_timer与计算机的时钟进行比较。这个帧计时器将总结我们所有计算的延迟,同时播放电影。换句话说,这个frame_timer是什么时候应该是我们显示下一帧。我们只需将新的延迟添加到帧定时器,将其与计算机时钟的时间进行比较,然后使用该值安排下一次刷新。这可能有点令人困惑,所以仔细研究代码:

void video_refresh_timer(void *userdata) {VideoState *is = (VideoState *)userdata;VideoPicture *vp;double actual_delay, delay, sync_threshold, ref_clock, diff;if(is->video_st) {if(is->pictq_size == 0) {schedule_refresh(is, 1);} else {vp = &is->pictq[is->pictq_rindex];delay = vp->pts - is->frame_last_pts; /* the pts from last time */if(delay <= 0 || delay >= 1.0) {/* if incorrect delay, use previous one */delay = is->frame_last_delay;}/* save for next time */is->frame_last_delay = delay;is->frame_last_pts = vp->pts;/* update delay to sync to audio */ref_clock = get_audio_clock(is);diff = vp->pts - ref_clock;/* Skip or repeat the frame. Take delay into accountFFPlay still doesn't "know if this is the best guess." */sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;if(fabs(diff) < AV_NOSYNC_THRESHOLD) {if(diff <= -sync_threshold) {delay = 0;} else if(diff >= sync_threshold) {delay = 2 * delay;}}is->frame_timer += delay;/* computer the REAL delay */actual_delay = is->frame_timer - (av_gettime() / 1000000.0);if(actual_delay < 0.010) {/* Really it should skip the picture instead */actual_delay = 0.010;}schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));/* show the picture! */video_display(is);/* update queue for next picture! */if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {is->pictq_rindex = 0;}SDL_LockMutex(is->pictq_mutex);is->pictq_size--;SDL_CondSignal(is->pictq_cond);SDL_UnlockMutex(is->pictq_mutex);}} else {schedule_refresh(is, 100);}
}

我们做了一些检查:首先,确保临时秘书处和以前的临时秘书处之间的延迟是有意义的。 如果不是,我们只是猜测并使用最后的延迟。 接下来,我们确保我们有一个同步阈值,因为事情永远不会完全同步。 ffplay的值为0.01。 我们也确保同步阈值永远不会小于PTS值之间的差距。 最后,我们将最小刷新值设置为10毫秒*。*实际上,我们应该跳过帧,但是我们不打算去打扰。

我们在大结构中添加了一堆变量,所以不要忘记检查代码。 另外,不要忘记在stream_component_open中初始化帧定时器和初始的前一帧延迟:

 is->frame_timer = (double)av_gettime() / 1000000.0;is->frame_last_delay = 40e-3;

同步:音频时钟

现在是我们实施音频时钟的时候了。 我们可以更新audio_decode_frame函数中的时钟时间,这是我们对音频进行解码的地方。 现在请记住,我们并不总是每次调用这个函数都要处理一个新的包,所以有两个地方需要更新时钟。 第一个地方是我们得到新数据包的地方:我们只需将音频时钟设置为数据包的PTS。 然后,如果一个数据包有多个帧,我们通过计数样本数量并将它们乘以给定的每秒样本速率来保持音频播放的时间。 所以一旦我们有了这个包:

 /* if update, update the audio clock w/pts */if(pkt->pts != AV_NOPTS_VALUE) {is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;}

一旦我们正在处理数据包:

/* Keep audio_clock up-to-date */pts = is->audio_clock;*pts_ptr = pts;n = 2 * is->audio_st->codec->channels;is->audio_clock += (double)data_size /(double)(n * is->audio_st->codec->sample_rate);

一些细节:函数的模板已经改为包含pts_ptr,所以请确保你改变了。 pts_ptr是我们用来通知audio_callback音频数据包的点的指针。 这将在下次用于同步音频和视频。

现在我们可以最终实现我们的get_audio_clock函数。 想到获取is-> audio_clock的价值并不那么简单。 请注意,我们在每次处理音频PTS时都会设置音频PTS,但是如果您查看audio_callback函数,则需要一段时间才能将所有数据从音频数据包移动到输出缓冲区。 这意味着我们音频时钟的价值可能会遥遥领先。 所以我们必须检查我们还剩下多少。 以下是完整的代码:

double get_audio_clock(VideoState *is) {double pts;int hw_buf_size, bytes_per_sec, n;pts = is->audio_clock; /* maintained in the audio thread */hw_buf_size = is->audio_buf_size - is->audio_buf_index;bytes_per_sec = 0;n = is->audio_st->codec->channels * 2;if(is->audio_st) {bytes_per_sec = is->audio_st->codec->sample_rate * n;}if(bytes_per_sec) {pts -= (double)hw_buf_size / bytes_per_sec;}return pts;
}

你应该能够知道为什么这个功能现在工作;)

就是这样了! 继续编译它:

gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

终于! 您可以在自己的电影播放器上观看电影。 下一次,我们将看音频同步,然后我们将讨论寻找教程。

FFmpeg和SDL教程(五):同步视频相关推荐

  1. 详细介绍Qt,ffmpeg 和SDl 教程之间的联系

    Qt与 ffmpeg 与 SDl 教程是本文要介绍的内容,从多个角度介绍本文,运用了qmake,先来看内容. 1.  注释 从" #" 开始,到这一行结束. 2.  指定源文件 1 ...

  2. ffmpeg 和 SDL 教程

    教程1:制作屏幕录像 代码:tutorial01.c 概要 电影文件有很多基本的组成部分.首先,文件本身被称为容器Container,容器的类型决定了信息被存放在文件中的位置.AVI和Quicktim ...

  3. ffmpeg 和 SDL 教程2:输出到屏幕

    2019独角兽企业重金招聘Python工程师标准>>> SDL和视频 为了在屏幕上显示,我们将使用SDL.SDL是Simple Direct Layer的缩写.它是一个出色的多媒体库 ...

  4. ffmpeg和SDL教程 04:创建线程

    2019独角兽企业重金招聘Python工程师标准>>> 概述 前面,我们利用 SDL 的音频处理功能优势增加声音支持.每次我们提供音频支持,需要创建一个函数,供SDL启动线程来回调. ...

  5. 使用ffmpeg和sdl播放视频实现时钟同步

    自定义播放器系列 第一章 视频渲染 第二章 音频(push)播放 第三章 音频(pull)播放 第四章 实现时钟同步(本章) 第五章 实现通用时钟同步 第六章 实现播放器 文章目录 自定义播放器系列 ...

  6. FFmpeg和SDL实现视频播放器之 ⌈音视频同步⌋

    FFmpeg简易播放器流程图 音视频同步的目的是为了使播放的声音和显示的画面保持一致. 视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧: 音频按采样点播放, ...

  7. 基于ffmpeg+SDL 实时播放摄像头视频

    基于ffmpeg+SDL 实时播放摄像头视频 基本流程 udp接收rtp数据流接收一帧数据后,转换为NAL单元送去解码 (这里特别说明一下,我本次用的接口是支持从连续数据流中自动分割出一个个NAL的, ...

  8. ffmpeg源码分析_ffmpeg音视频同步的几种策略

    在前面的文章中,我们介绍了播放器的视频渲染及音频渲染的相关知识,这些都是单独进行的,一旦在现实开发中将视频及音频结合在一起播放就会出现音视频不同步的问题. 下面我们就来分析一下如何解决音视频同步的问题 ...

  9. ffmpeg源码中ffplay音视频同步原理及实现

    音视频指南 文章目录 音视频指南 前言 一.音视频同步简单介绍? 二.基本概念解释 1.为什么需要视频压缩 2.什么是I帧.p帧.b帧 3.什么是DTS,PTS 4.其他概念解释 三.常用同步策略 四 ...

最新文章

  1. Entity Framework 4.1/4.3 之五 (DBContext 之 2 查询功能)
  2. 【python图像处理】两幅图像的合成一幅图像(blending two images)
  3. MySQL三层逻辑架构
  4. 关于windows消息机制的猜想
  5. 知识图谱 图数据库 推理_图数据库的知识表示与推理
  6. LeetCode 365. 水壶问题(最大公约数)
  7. java获取spring数据源_Spring动态注册多数据源的实现方法
  8. PHP:判断客户端是否使用代理服务器及其匿名级别
  9. java定义属性时用this_(转载)深入Java关键字this的用法的总结
  10. Ubuntu18.04安装Gstreamer1.0(六)
  11. 通用的分页存储过程(少量代码实现)
  12. 如何修改 WordPress 的用户默认头像?
  13. win10 两台电脑之间共享桌面及共享文件(手把手教学)
  14. 国美金融贷款Kube-apiserver源码分析(国美金融贷款)
  15. vue下载与安装详细教程
  16. uni-app创建并运行微信小程序项目
  17. 4核处理器_苹果电脑便宜卖!4核i5处理器,480G固态硬盘,带刻录,13.4寸,双系统...
  18. 上海网站排名优化找哪家?清法网络助你一臂之力
  19. 【POJ 1788 --- Building a New Depot】
  20. Excel如何快速将多行数据转为一行

热门文章

  1. Mac 在终端下使用 zcat 报错,改用`gunzip -c`
  2. 西门子plc的上升沿和下降沿是什么意思?
  3. java11 二次发布_第二次Java出题
  4. go语言-时间处理(time.Time)
  5. 【tortoiseSVN】乌龟SVN 文件冲突状态图标无法正常显示或者不显示问题
  6. XJOI——3569-萌新关爱之-C语言的余数
  7. 半同步/半异步和领导者/追随者 有趣的解释
  8. 期待我的西行之旅--后会无期观后感
  9. 弘辽科技:直通车推广新思路,真正实现低价引流
  10. 【flowable】八、flowable流程变量