聊聊我使用ffmpeg时总结的最佳实践

  张一帆   2021年11月23日


FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations.

部门的项目涉及到视频处理的功能,公司里负责存储服务的同学并没有这方面的能力支持(虽然他们也正在建设此方面的能力,因为公司内部需要视频处理的业务越来越多了。),所以我们只能硬着头皮开搞ffmpeg。曾经在看游戏的时候短暂的接触过半年多ffmpeg,但那个时候解决问题的能力不足,靠别人帮助的情况比较多。这么多年过去了,自己在开发领域的驾轻就熟,让我有了底气接了这摊子事儿。那么接下来我就总结下这几个月对ffmpeg的学习和使用吧。

一、框架选择 - jave

项目是java的项目,借助github和google,了解到目前处理ffmpeg的框架最好用的是一个叫做jave的开源项目。jave其实就是一个对ffmpeg包装的java库。目前jave的版本已经发展到了3.2.0,只需要在项目中引用jave-all-deps即可,他包括了jave-core和不同平台的ffmpeg脚本。

  <dependency>
 <groupId>ws.schild</groupId>
 <artifactId>jave-all-deps</artifactId>
 <version>3.2.0</version>
</dependency>

由于我们公司的maven库的阿里镜像源超时的原因(那个时候找不到人来解决这个问题。),导致我用不到3.2.0版本的jave,我能获取到最新的版本就是2.7.3。这就导致了我在使用2.7.3时发现了问题去github上提concat mp4 not working properly. version 2.7.3 ,开发者明确表示他没有资源去解决老版本的问题。反正,经过各种方面的努力,我也找到了我们公司管理maven库的同学,在他的帮助下我可以获取到3.2.0版本的jave了。但是,jave框架在视频拼接的处理上做的实在够差,我提了很多类似拼接的issue,比如The ffmpeg version included in 2.7.3 is a vry old one, which might cause the issue. 。开发者在我的追问下已经不再回答我了。还有一次给他提了一个多个视频拼接时监控代码会报NPE问题的issue,when lots of videos to concat . EncoderProgressListernr get a NPE Exception,感觉开发者实在忙不过来了,转而求助我对此框架的修复。

最终,拼接视频的代码我自己写了。格式转换的用了jave的。总的来说,jave可以帮助门外汉快速上手,因为我们可以通过看他的代码一步一步了解他封装ffmpeg时使用的方法以及他在遇到不同操作情况时是采用的什么方案。但是,当你慢慢了解ffmpeg的时候,jave框架会因为他的封装不够扩展而让开发举步维艰。

当对ffmpeg有了一些最基本的了解后,ffmpeg官方网站(https://ffmpeg.org)就成为了进阶时最信得过的锦囊宝典。

二、FFmpeg官网

FFmpeg官网的左边栏,我们可以下载到ffmpeg最新的命令行。通过Documentation我们可以实时查看目前现有的所有功能,因为这个页面每晚都会重新生成。里面最常用的就是“Command Line Tools Documentation”和“Components Documentation”还有“General Documentation”。

官网同时还提供了IRC和MAIL等联系方式,IRC的频道是#ffmpeg 和 #ffmpeg-devel。两个频道分别面向的是使用者和开发者。我曾经在使用ffmpeg发生各种问题时进到IRC的频道里去寻求帮助,虽然里面会有4百多位处在挂机状态,但不时还是会有一些人进行交流的。我总得看下来,感觉里面说的东西都是C方面的问题,别的问题感觉里面的人不太感兴趣。我就因为又一次遇到了问题在里面提问了,确实有人回应我了,人家要我给出命令行以及错误堆栈。在IRC中你不能直接贴出来代码,需要一些专门托管代码片段的网站专门来制作代码片段连接,这一点有点让我觉得麻烦,可是又没别的好办法。等过了一会我把连接贴出来后,就没人再回应了。总体来说,体验很差,浪费了自己很多宝贵时间。我建议后来的人还是不要在IRC上浪费时间了。

三、音视频流的概念

让我们先看一个视频的详细信息:

  # ffprobe 1_1637395707\&88966_1637395335_1637395635.mp4 -hide_banner
[aac @ 0x7f846a808600] Input buffer exhausted before END element found
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '1_1637395707&88966_1637395335_1637395635.mp4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isommp42
    creation_time   : 2021-11-20T08:07:16.000000Z
    com.android.version: 9
  Duration: 00:04:59.78, start: 0.000000, bitrate: 575 kb/s
  Stream #0:0(eng): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 1280x720, 478 kb/s, SAR 1:1 DAR 16:9, 6.15 fps, 90k tbr, 90k tbn, 180k tbc (default)
   Metadata:
     creation_time   : 2021-11-20T08:07:16.000000Z
     handler_name    : VideoHandle
     vendor_id       : [0][0][0][0]
  Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 96 kb/s (default)
    Metadata:
      creation_time   : 2021-11-20T08:07:16.000000Z
      handler_name    : SoundHandle
      vendor_id       : [0][0][0][0]


视频一共有三种流:音频流,视频流,字幕流。我建议还是去看看FFmpeg官网对这几种结构的解释,介绍的挺全的,比我写的强多了。

一般来说一个视频会有一个视频流和多个音频流及多个字幕流。他们的顺序安排按照约定俗成的顺序是第一个是视频流,对应着Stream #0:0;第二个及多个是音频流,对应着Stream#0:1,字幕流按此规则依次展开(当然,在我做ffmpeg的时候,经历过audio和video反了的情况,这种反了情况如果不去做处理ffmpeg其实会智能帮助挑选的,但是在某些时候就会报错失败。所以建议还是按照这个顺序来)。所以,我们可以从上面的代码片段中看到1_1637395707\&88966_1637395335_1637395635.mp4这个mp4文件包含以下信息:

  • 视频持续时间 00:04:59.78,开始时间是0.000000,码率是575kb/s
  • #0:0是视频流(Video),h264编码,profile等级为baseline,视频格式是yuv420p,分辨率1280*720,平均码率478kb/s,帧率 6.15fps,时间基线 90k。(我列举出来的这几个是挺重要的参数,其他参数我还没有太明白)
  • #0:1是音频流(Audio),aac编码,采样率16000Hz,单声道,码率96kb/s

四、concat

拼接的意思就是,将两段或者多端音视频片段拼接成一个完整的音视频。ffmpeg官网指出,根据不同的情况,有多种方案可以进行选择。

1、拼接的3种方案

(1)concat filter

过滤器法适用于同步视频和音频流的片段,每个视频段都必须具有相同数量的流(包括类型),而且最终输出的视频也会按照这个顺序进行合并。这种方法会对视频进行重新编码,速度慢,耗cpu和内存。

因为有些流不一定会和别的流的时间一致,举例来说一段视频有1个视频流和2个音频流,其中有一个音频流长度很短。那么这个方法就会按照这个视频最长时间的那个流为基准,其他流就会填补上静音。手册里明确说明最后一个视频不能这样。

如果想让合并如预期那样工作,一定要保证所有视频片段的时间戳都是从0开始的。否则会出现,画面和声音不同步的情况。

合并的时候还会遇到不同视频片段中有不同类型,不同采样率,不同通道的音频流。FFmpeg会自动的选择一个值来进行处理。另外,还有一些值是不能被FFmpeg自动填充的。还是需要人为的指定。另外,如果每段视频片段拥有不同的帧率,我们在最终结果视频制作时一定要设定个值,否则制作出来的视频就会按照每段自己的帧率进行合并了,这样的结果就是视频卡顿。

  # ffmpeg -i opening.mkv -i episode.mkv -i ending.mkv -filter_complex \
  '[0:0] [0:1] [0:2] [1:0] [1:1] [1:2] [2:0] [2:1] [2:2]
   concat=n=3:v=1:a=2 [v] [a1] [a2]' \
  -map '[v]' -map '[a1]' -map '[a2]' output.mkv

这个bash脚本的具体意思,怎么使。我看到了一篇CSDN上的文章,直接在这里做个传送门

(2)demuxer

当我们的视频文件不支持文件级别的拼接并且我们想要避免重新编译,就应该采用这种方案。

这种方案,通过读取一个文件里面的视频,将他们一个一个进行拼接。这些视频片段的时间都会被重新调整,第一个视频的开始时间调整为0,后面的视频等待前一个文件拼接完毕再进行拼接。如果他们的流时间长短不一,就会在他们之间产生间隙。

如果他们的duration不同,那就会产生伪影(这个伪影,是翻译软件的翻译,我出现过这个问题,形象讲就是还在说话但是画面不动了。)

  # 文件名为file,内容为
file '/mnt/share/file-1.mp4'
file '/mnt/share/file 2.mp4'
file '/mnt/share/file 3.mp4'

# ffmpg -y -f concat -i file  output.mp4 

(3)protocol

物理上进行拼接的方案,这个方案支持文件级拼接,也是最快的方案。一定要保证拼接的视频文件格式都一样。我最终就是选择用这个方案来进行拼接的,因为这种方案更稳定,更高效。(当然,如果你每个文件都不一样的结构,那这个方案就不行。)

  # ffmpeg -y -i "concat:a.mp4|b.mp4" output.mp4

2、拼接时我遇到的各种问题

(1) channel element 1.0 is not allocated

在采用demuxer拼接方案时,遇到了这个错误。错误的根本原因是拼接的视频文件中的流顺序和第一个文件的流顺序不一致导致的。

demuxer的方案,是以第一个文件作为基准的。如果第一个视频的流顺序 #0:0是video,#0:1是audio,那么后面的所有视频文件的流顺序也得是 #0:0是video,#0:1是audio。这种情况是很可能发生的,因为你的输入可能是别人给你的,别人在操作视频的时候因为个人的疏忽或者经验缺失,有可能在制作的过程中将这种顺序弄混。如果你真遇到了这种情况,通过下面的命令,就能将顺序调整成第一个流是video,第二个流是audio。

  # ffmpeg -i input.mp4 -y output.mp4

(2)Non-monotonous DTS in output stream 0:1; previous: 3277744, current: 3276712; changing to 3277745. This may result in incorrect timestamps in the output file.

首先需要了解一下DTS和PTS是什么意思。简单说,PTS就是这一帧应该在什么时候显示,DTS就是这一帧应该在什么时候进行解码。所以,这个问题是有关于帧的时间的报错。我们可以通过以下三篇文章了解到更多。

《DTS和PTS的解释》《图解DTS和PTS》《I,P,B帧和PTS,DTS的关系》]

想要理解DTS和PTS还要知道I、B、P帧的概念。上面那篇《图解DTS和PTS》中的图片表达的很清楚了。为了解决数据存储问题在I帧和P帧的基础上增加了B帧的概念。有了B帧以后才又有了DTS的概念,因为B帧和P帧的组合可以用更少的存储量表示出同样数量的I帧的画面。我们可以看到图中的B帧只有小人的身体,B帧通过把I帧没有移动的背景和P帧中运动的小人整合起来,通过DTS来控制实际解压出来的画面顺序:I >P>B,这样就达到了同样是看了3个画面但实际存储量远小于3个I帧的数据量。注意,每帧蓝色的背景表示的就是实际节省下来的数据量。

IBP

然后,我们得再了解下采样率是什么,有什么用途。我百度了很多关于采样率的文章,感觉大家都在抄互相的东西,让人看的一头雾水。

首先要确定一点,采样率的意义是模拟信号转成数字信号时的参考。因为根据Nyquist和Shannon定理指出,当采样率大于被采信号最大频率的两倍时才能无损的重建原始的模拟信号。如果采样率低的话,就无法从采样信号中重建,或者可能产生混叠的现象。采样率实际上表明了采集的频率,单位是KHZ,意思就是每秒做了1000次采集。例如:音频CD的采样率是44.1khz,这意味着模拟信号每秒采样44100次,就能在数字信号中还原模拟信号。更多内容可以参考这个《7 Questions About Sample Rate》

所以,问题的关键就是让各文件的采样率一致了。就是因为各文件的采样率不一致导致的,拼接视频时解码的时间3277744之后应该是3277745,但突然变小了 3276712就出现了问题。

(3)多个mp4文件拼接不成功 - Found duplicated MOOV Atom.Found duplicated MOOV Atom.

如果想直接拼接的话,采用concat protocol的方案,最终拼接出来的mp4和第一个视频一模一样。以下两个视频均是同样设备和参数采集的视频a.mp4和b.mp4。两个mp4拼接出来的视频为abd.mp4

  # ffprobe a.mp4 -hide_banner
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'a.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf59.4.101
  Duration: 00:04:59.78, start: 0.000000, bitrate: 680 kb/s
  Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 607 kb/s, 6.15 fps, 6.15 tbr, 82935000.00 tbn, 12.31 tbc (default)
    Metadata:
      handler_name    : VideoHandle
      vendor_id       : [0][0][0][0]
  Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 71 kb/s (default)
    Metadata:
      handler_name    : SoundHandle
      vendor_id       : [0][0][0][0]
      
      
# ffprobe b.mp4 -hide_banner
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'b.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf59.4.101
  Duration: 00:04:59.20, start: 0.000000, bitrate: 653 kb/s
  Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 582 kb/s, 4.08 fps, 4.08 tbr, 13713750.00 tbn, 8.16 tbc (default)
    Metadata:
      handler_name    : VideoHandle
      vendor_id       : [0][0][0][0]
  Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 70 kb/s (default)
    Metadata:
      handler_name    : SoundHandle
      vendor_id       : [0][0][0][0]
      
# ffmpeg -y -i "concat:a.mp4|b.mp4" abd.mp4
[mov,mp4,m4a,3gp,3g2,mj2 @ 0x7f9575c0fa40] Found duplicated MOOV Atom. Skipped it
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'concat:a.mp4|b.mp4':
  Metadata:
    encoder         : Lavf59.4.101
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
  Duration: 00:04:59.78, start: 0.000000, bitrate: 1332 kb/s
  Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 607 kb/s, 6.15 fps, 6.15 tbr, 82935000.00 tbn (default)
    Metadata:
      handler_name    : VideoHandle
      vendor_id       : [0][0][0][0]
  Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 71 kb/s (default)
    Metadata:
      handler_name    : SoundHandle
      vendor_id       : [0][0][0][0]
Stream mapping:
  Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))
  Stream #0:1 -> #0:1 (aac (native) -> aac (native))
Press [q] to stop, [?] for help
[libx264 @ 0x7f95768062c0] using SAR=1/1
[libx264 @ 0x7f95768062c0] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x7f95768062c0] profile High, level 3.1, 4:2:0, 8-bit
[libx264 @ 0x7f95768062c0] 264 - core 164 r3065 ae03d92 - H.264/MPEG-4 AVC codec - Copyleft 2003-2021 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=18 lookahead_threads=3 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=6 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00
Output #0, mp4, to 'abd.mp4':
  Metadata:
    compatible_brands: isomiso2avc1mp41
    major_brand     : isom
    minor_version   : 512
    encoder         : Lavf59.4.101
  Stream #0:0(eng): Video: h264 (avc1 / 0x31637661), yuv420p(progressive), 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 6.15 fps, 82935000.00 tbn (default)
    Metadata:
      handler_name    : VideoHandle
      vendor_id       : [0][0][0][0]
      encoder         : Lavc59.3.102 libx264
    Side data:
      cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
  Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 69 kb/s (default)
    Metadata:
      handler_name    : SoundHandle
      vendor_id       : [0][0][0][0]
      encoder         : Lavc59.3.102 aac
frame= 1843 fps=191 q=-1.0 Lsize=   23688kB time=00:04:59.84 bitrate= 647.2kbits/s speed=31.1x
video:20986kB audio:2629kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.311686%
[libx264 @ 0x7f95768062c0] frame I:8     Avg QP:11.52  size:147506
[libx264 @ 0x7f95768062c0] frame P:747   Avg QP:16.26  size: 20112
[libx264 @ 0x7f95768062c0] frame B:1088  Avg QP:22.94  size:  4858
[libx264 @ 0x7f95768062c0] consecutive B-frames: 17.1%  9.7%  8.3% 64.9%
[libx264 @ 0x7f95768062c0] mb I  I16..4: 16.2% 26.1% 57.7%
[libx264 @ 0x7f95768062c0] mb P  I16..4:  1.4%  2.4%  1.6%  P16..4: 19.9%  5.1%  3.8%  0.0%  0.0%    skip:65.9%
[libx264 @ 0x7f95768062c0] mb B  I16..4:  0.1%  0.3%  0.2%  B16..8: 19.3%  2.8%  1.0%  direct: 0.9%  skip:75.3%  L0:52.0% L1:40.5% BI: 7.5%
[libx264 @ 0x7f95768062c0] 8x8 transform intra:41.5% inter:26.0%
[libx264 @ 0x7f95768062c0] coded y,uvDC,uvAC intra: 62.9% 46.9% 24.0% inter: 8.0% 4.5% 0.2%
[libx264 @ 0x7f95768062c0] i16 v,h,dc,p: 15% 24% 13% 47%
[libx264 @ 0x7f95768062c0] i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 28% 25% 19%  4%  4%  5%  5%  5%  6%
[libx264 @ 0x7f95768062c0] i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 26% 21% 13%  6%  7%  7%  7%  6%  7%
[libx264 @ 0x7f95768062c0] i8c dc,h,v,p: 51% 19% 21%  8%
[libx264 @ 0x7f95768062c0] Weighted P-Frames: Y:0.0% UV:0.0%
[libx264 @ 0x7f95768062c0] ref P L0: 77.2%  8.7% 10.2%  3.9%
[libx264 @ 0x7f95768062c0] ref B L0: 91.6%  6.8%  1.6%
[libx264 @ 0x7f95768062c0] ref B L1: 97.3%  2.7%
[libx264 @ 0x7f95768062c0] kb/s:573.94
[aac @ 0x7f9576807680] Qavg: 20877.602

我们注意,其中有个报错是Found duplicated MOOV Atom. ,MOOV Atom是个什么东西呢?我们可以参考这篇文章:理解 MPEG 的 moov atom,通过这篇文章我们了解到mp4是个封装容器,一个视频文件只能有一个MOOV Atom,现在两个mp4文件要进行拼接,就会有两个MOOV Atom。所以,ffmpeg就会选择第一个文件作为最终文件直接输出出来,而后面的都会选择跳过。

所以,想用concat方法直接去拼接mp4文件的想法应该立刻停止。我们可以选择其他方案继续进行,比如ts类型文件就是少数可以这么做的类型之一。ts文件是在DVD标准上存储的视频流文件。它使用的是标准的MPEG-2(.MPEG)来压缩视频数据。我们只需要记住ts文件主要存储的是流媒体,流媒体可以直接拼。

最后,我的方案就是:先转成ts文件,然后再按照concat protocol拼接起来。完美,perfect。此种方法耗费cpu资源和少量内存资源。可以说,是最佳方案,而且健壮性很强。我为什么说健壮性很强呢,因为下面的问题。

(4)使用filter方案,无缘无故的停止ffmpeg线程

之前,我拼接时采用的方案是filter,功能一切正常,直到有一天系统拼接失败数量直线上升,我立刻进行排查。我发现了好多源视频都没有声音,后来跟端上的同学询问,可能是端上加载别的sdk导致音频流丢失了,这就导致了我的拼接系统拼不出来了。因为filter方案,是需要自己map每个视频的声音和视频流的,如果音频流没了的话就会造成意想不到的错误。

(5)Could not find codec parameters for stream 0 XXXX Consider increasing the value for the ‘analyzeduration’ and ‘probesize’ options

ffmpeg在avformat_find_stream_info中会读取一部分源文件的音视频数据,来分析文件信息。其中有两个参数来控制:probesize和analyzeduration。该函数的作用是通过读取一定时间内或一定长度内的字节码流数据来分析码流的基本信息。比如编码信息,时长,码率,帧率等等。如果这两个值设置的过小就会增加此函数的耗时,严重的可能会导致读取数据量不足,从而无法解析出来关键信息。这样就会导致播放失败或者有音频没有视频,有视频没有音频的问题。

我就是因为没有设置这两个值,端上采集的视频由于电视的cpu持续飙高,视频中的关键信息被延迟写入到视频内,我的ffmpeg以默认的值去读取视频而没有获取到关键信息,最终拼接出来的视频有声音但是没有图像。

五、ANULLSRC(空源)

我需要做一个黑屏没有声音的视频放在拼接视频的最前面,通过查询ffmpeg的手册。用以下方法能够制作出:

  # ffmpeg -f lavfi -i color=size=1280x720:rate=25:color=black -f lavfi -i anullsrc=channel_layout=mono:sample_rate=16000 \
# -metadata:s:a:0 language=eng -metadata:s:v:0 language=eng \
# -video_track_timescale 12800k \
# -vcodec h264-acodec aac  \
# -threads 1 -t 30 \
# -y abc.mp4;

这里面有几个参数我需要说明下。

  • -f lavfi 代表输入是一个虚拟输入源。-i 后面的就是这个是什么样的输入源
  • Language = eng 代表的是元数据中视频或者音频的stream是英文。
  • -video_track_timescale 设置视频tbn,tbn是容器的时间基准。经过测试,他的作用是如果设置的过大视频就会以几倍速的速度快速播放。如果设置的小就会以原来的几分之一的速度播放。最终,会造成和音频流不同步。

因为,空源视频是放在所有视频之前的,所以tbn这个值一定要设置好。因为后面的所有视频都以它为基准进行拼接。