媒体手册

领域知识

本教程http://dranger.com/ffmpeg/ffmpeg.html是构建一些领域知识的一个很好的介绍。请记住,本教程已经相当陈旧,一些ffmpeg函数已被弃用--但基本内容仍然有效。

在FFmpeg基本代码中有ffplay.c播放器--这是查看如何管理事物的一个非常好的方法。特别是,使用了一些较新的FFmpeg函数,而当前的pyglet媒体代码仍然使用现在已弃用的函数。

当前代码体系结构

媒体代码概述如下:

来源

在介质/源文件夹中找到。

Source S代表包含媒体信息的数据。它们可以来自磁盘,也可以在内存中创建。一个 Source 的职责是读取或生成音频和/或视频数据,然后提供它。 producer

FFmpegStreamingSource

的一种实现 StreamingSourceFFmpegSource 。它实现了 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>