媒体手册
领域知识
本教程http://dranger.com/ffmpeg/ffmpeg.html是构建一些领域知识的一个很好的介绍。请记住,本教程已经相当陈旧,一些ffmpeg函数已被弃用--但基本内容仍然有效。
在FFmpeg基本代码中有ffplay.c播放器--这是查看如何管理事物的一个非常好的方法。特别是,使用了一些较新的FFmpeg函数,而当前的pyglet媒体代码仍然使用现在已弃用的函数。
当前代码体系结构
媒体代码概述如下:
来源
在介质/源文件夹中找到。
Source
S代表包含媒体信息的数据。它们可以来自磁盘,也可以在内存中创建。一个 Source
的职责是读取或生成音频和/或视频数据,然后提供它。 producer 。
FFmpegStreamingSource
的一种实现 StreamingSource
是 FFmpegSource
。它实现了 Source
通过调用由ctype包装并位于media/Sources/ffmpeg_lib中的FFmpeg函数来实现基类。它们提供了处理媒体流的基本功能,例如打开文件、读取流信息、读取包以及解码音频和视频包。
这个 FFmpegSource
维护两个队列,一个用于音频包,一个用于视频包,具有预定的最大大小。当加载源时,它将从流中读取数据包并填满队列,直到其中一个队列已满。然后它必须停止,因为我们永远不知道我们下一步将从流中获得什么类型的包。它可能是与已填满的队列相同类型的包,在这种情况下,我们将无法存储额外的包。
无论何时当 Player
-信号源的消费者-请求音频数据或视频帧, Source
将从适当的队列中弹出下一个包,对数据进行解码,并将结果返回给播放器。如果这导致音频和视频队列中都有可用空间,它将读取额外的数据包,直到其中一个队列再次满为止。
球员
可在media/player.py中找到
这个 Player
是驱动源头的主要对象。它维护可以按顺序播放的内部源序列或源迭代器。它的职责是发挥、停顿和追根究底。
如果源包含音频,则 Player
将实例化一个 AudioPlayer
通过询问 AudioDriver
要创建适当的 AudioPlayer
对于给定的平台。这个 AudioDriver
是根据可用的驱动程序创建的单例。目前支持的声卡驱动程序有:DirectSound、OpenAL、PulseAudio和XAudio2。还可以使用静音音频驱动程序,该驱动程序会消耗但不播放任何音频。
如果源包含视频,则播放器有一个 get_texture()
方法返回当前视频帧。
这名球员有一个内部 master clock 其用于同步视频和音频。音频同步被委托给 AudioPlayer
。更多信息可在下面找到。视频同步是通过询问 Source
下一个视频时间戳。这个 Player
然后,在pyglet事件上的调度循环调用其 update_texture()
延迟等于下一个视频时间戳和主时钟当前时间之间的差。
什么时候 update_texture()
时,我们将检查与视频时间戳相比,实际主时钟时间是否不会太晚。如果循环非常繁忙,并且无法按时调用函数,则可能会发生这种情况。在这种情况下,该帧将被跳过,直到我们找到具有适合当前主时钟时间的时间戳的帧。
AudioPlayer
在介质/驱动程序中找到
这个 AudioPlayer
负责播放音频数据。它从以下位置读取 Source
,并且可以启动、停止或清除。
为了完成此任务,音频播放器保留对 AudioDriver
Singleton,它提供对所选音频驱动程序的低级功能的访问,并且其 Player
,它与之同步并将事件调度到。
AudioPlayer
S与他们的源头绑在一起 AudioFormat
。一旦创建,它们就不能播放不同格式的音频。
AudioPlayer
S将努力使自己与他们关联的 Player
。这是通过 _get_and_compensate_audio_data
方法。他们估计的音频时间和他们的播放器主时钟之间的最后8个差值将为每个读取的音频数据块存储。如果该值的平均值超过30ms,播放器将开始通过一次丢弃或复制非常少量的样本(默认情况下为12ms)来进行自我更正。如果任何一次测量超过280ms,则假定在应用程序的上下文中可以注意到极端的去同步。如果 AudioPlayer
在主时钟之后运行,则跳过所有这些音频数据并重置测量。当跑步的时候 ahead 超过280毫秒后,除了一次12毫秒的标准拉伸外,什么也不会做。
play
当被指示演奏时, AudioPlayer
将给它的音频后端任何必要的指令,以便开始播放自己。
为了不耗尽数据,它会将自身添加到 PlayerWorkerThread
它的音频驱动程序。该线程通常负责向源请求音频数据,以防止主线程/事件循环锁定I/O操作。这个 PlayerWorkerThread
会定期致电 work 在每一个上 AudioPlayer
。
此方法可以在已经播放时调用,并且在这种情况下不起作用。
stop
此方法会导致 AudioPlayer
停止播放其音频流,或暂停它。可以使用以下命令重新启动 play 之后,这将导致它从停止的地方继续。
此方法应该做的第一件事是将自身从其驱动程序的 PlayerWorkerThread
确保 work 在它停止时不会被调用。
此方法可以在已停止时调用,并且在这种情况下不起作用。
prefill_audio
此方法是从 Player
无论何时 AudioPlayer
即将开始播放,也是在 play 是第一次被调用。第一批数据从这里给出,因为使用单个音频缓冲区的后端可能会在 PlayerWorkerThread
会将适当的音频数据加载到。
此方法预先填充理想的数据量 AudioPlayer
,可在 _buffered_data_ideal_size
。默认情况下,这是以900毫秒的音频形式给出的,具体取决于播放的信号源的音频格式。
work
此方法仅从 PlayerWorkerThread
,尽管它可以通过 prefill_audio 。由于它是从线程调用的,因此很难无错误地实现它。
此方法负责在需要时重新填充音频数据,并且通常负责调度 on_eos()
事件。
实现这种方法有很多陷阱。当该方法运行时,在其他线程中可以自由地发生以下情况:
- 玩家暂停或取消暂停。
音频后台通常接受非播放流/源/等的数据,所以这不是太大的问题。实际上,这不会发生,所有当前实现都包含对
self.driver.worker.remove/add(self)
中的代码段 play/stop 实施。该调用将只返回一次PlayerWorkerThread
是通过一个工作周期来完成的。为了使这些呼叫最可靠,
remove
应该是 stop 实施和add
中的最后一条 play 实施,以确保 work 将不会在玩家属性更改之前运行/不会在玩家属性更改之前启动。- 该玩家即被删除。
为了抗击这一点,
self.driver.worker.remove(self)
在所有实现中都使用,确保删除调用不会干扰 work 方法。- 本机回调运行,从而更改
AudioPlayer
。 使用本地锁来保护某些节。
AudioPlayer
。此锁不应在调用_get_and_compensate_audio_data
,因为这使得整个步骤将加载/解码工作卸载到PlayerWorkerThread
已经过时了。
在伪代码中,实现此方法的一般方式为:
def work():
update_play_cursor()
dispatch_media_events()
if not source_exhausted:
if play_cursor_too_close_to_write_cursor():
get_and_submit_new_audio_data()
if source_exhausted:
update_play_cursor()
else:
return
else:
return
if play_cursor > write_cursor and not has_underrun:
has_underrun = True
dispatch_on_eos()
如果涉及在另一个线程中运行的本机回调,则流程往往不同:
def work():
update_play_cursor()
dispatch_media_events()
if not source_exhausted:
if play_cursor_too_close_to_write_cursor():
get_and_submit_new_audio_data()
if has_underrun:
if source_exhausted:
dispatch_eon_eos()
else:
restart_player()
has_underrun = False
def on_underrun():
if source_exhausted:
dispatch_on_eos()
else:
has_underrun = True
必须非常小心地使用锁保护适当的部分(回调和工作方法都可以访问的任何变量和缓冲区),否则该方法可能会遇到非常不幸的问题,即回调未被调度而支持工作方法,反之亦然,这可能会导致其中一个函数基于现在过时的状态承担/操作。
work
不会因为它被调度了就停止被呼叫 on_eos
。该方法必须确保其源之前没有用完音频数据,才能仅调度此事件一次。
clear
此方法可以 only 在以下情况下被调用 AudioPlayer
不是在玩。它会导致它丢弃所有缓冲的数据,并将其自身重置为干净的初始状态。
delete
此方法将导致 AudioPlayer
停止播放并删除其所有本机资源。与之形成鲜明对比的是 clear ,它可以在任何时候被调用。它可能会被多次调用,并且必须确保它不会删除已经删除的资源。
AudioDriver
在介质/驱动程序中找到
这个 AudioDriver
是平台上可用的低级声音驱动程序的包装。这是个独生子女。它可以创建一个 AudioPlayer
适合当前的 AudioDriver
。
这个 AudioDriver
通常包含一个 PlayerWorkerThread
负责让每个人 AudioPlayer
这就是充满数据的游戏。
这个 AudioDriver
提供了一种 AudioListener
,用于将监听器放置在与每个监听器相同的空间中 AudioPlayer
,启用位置音频。
设备的正常运行 Player
客户端代码以如下方式实例化媒体播放器::
player = pyglet.media.Player()
source = pyglet.media.load(filename)
player.queue(source)
player.play()
当客户端代码运行时 player.play()
:
这个 Player
将检查媒体上是否有音轨。如果是这样,它将实例化一个 AudioPlayer
适用于平台上可用的声卡驱动程序。它将创建一个空的 Texture
如果媒体包含视频帧,并且将计划其 update_texture()
马上就会被叫来。最后,它将启动主时钟。
这个 AudioPlayer
将会开始演奏 as described above 。
当 update_texture()
方法时,将使用主时钟检查下一个视频时间戳。我们允许延迟到帧持续时间。如果主时钟超过该时间,该帧将被跳过。我们将检查以下帧的时间戳,直到找到适合主时钟时间的帧。我们将设置 texture
添加到新的视频帧中。我们将检查下一个视频帧时间戳,并安排新的呼叫 update_texture()
延迟等于下一个视频时间戳和主时钟时间之间的差。
有用的工具
我发现使用二进制ffbe是探索媒体文件内容的一种好方法。以下是一些可能会很有趣和有帮助的事情:
ffprobe samples_v1.01\SampleVideo_320x240_1mb.3gp -show_frames
这将显示有关文件中每一帧的信息。属性可以仅选择音频帧或仅视频帧。 v
用于视频的标志和 a
对于音频。::
ffprobe samples_v1.01\SampleVideo_320x240_1mb.3gp -show_frames -select_streams v
您也可以通过以下方式要求查看帧信息的子集:
ffprobe samples_v1.01\SampleVideo_320x240_1mb.3gp -show_frames
-select_streams v -show_entries frame=pkt_pts,pict_type
最后,您可以使用附加的 compact
标志:
FfProbe Samples_v1.01SampleVideo_320x240_1Mb.3gp-Show_Frame-SELECT_Streams v-Show_Entry Frame=pkt_pt,pict_type-of精简
将视频转换为MKV
ffmpeg -i <original_video> -c:v libx264 -preset slow -profile:v high -crf 18
-coder 1 -pix_fmt yuv420p -movflags +faststart -g 30 -bf 2 -c:a aac -b:a 384k
-profile:a aac_low <outputfilename.mkv>