什么是视频封装

在开始介绍音视频的Track Selection之前,我们先了解一下视频封装,比如常见的mp4文件, ts文件,就是一个视频封装文件,mp4和ts就是一种封装格式,这里之所以选择mp4和ts这两种封装格式,是因为这两种封装格式具有代表性,前者适用于点播,后者常用语广播电视,当然也不是绝对的。

mp4本地文件就不说了,看一下新浪微博的点播视频,用的就是mp4

exo玩沉默游戏(ExoPlayer系列2-Track)(1)

ts流一般用广播电视,IPTV 这个是网上的一个IPTV的源,从里面找了一个CCTV4源。

http://117.169.120.140:8080/live/cctv-4/.m3u8

m3u8文件其实就是一个文本文件,它是苹果出的HLS(HTTP Live Stream)的一部分。

HLS 的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些.

下面就是一个m3u8文件,可以看到里面包含了好几个ts文件,每个ts文件对应的是一小段片段。如果是直播,会定时,请求这个m3u8文件,然后下载里面的ts片段并播放。

#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:1648731453 #EXTINF:10.0,5260052 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133801_133811.ts #EXTINF:10.0,5380372 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133811_133821.ts #EXTINF:10.0,5312504 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133821_133831.ts #EXTINF:10.0,5333936 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133831_133841.ts #EXTINF:10.0,5366272 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133841_133851.ts #EXTINF:10.0,5317204 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133851_133901.ts #EXTINF:10.0,5319272 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133901_133911.ts #EXTINF:10.0,5330928 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133911_133921.ts #EXTINF:10.0,5354052 http://117.169.120.140:8080/wh7f454c46tw2466423005_613384962/live/cctv-4/HD-4000k-1080P-cctv4_20220426_133921_133931.ts

我们都知道,文件的扩展名是用来识别文件类型的。通过给他指定扩展名,我们可以告诉自己,也可以告诉操作系统用什么打开方式打开这个文件。

比如一个mp4后缀名的文件,打开方式里会有一些默认的视频软件。

exo玩沉默游戏(ExoPlayer系列2-Track)(2)

同样一个mp3后缀名的文件,打开方式里也会有一些默认的音频软件。

exo玩沉默游戏(ExoPlayer系列2-Track)(3)

文件的后缀名是可以随便改的,Linux或者mac上可以用file命令查看实际的文件格式。

➜ file audio.mp3 audio.mp3: Audio file with ID3 version 2.3.0, contains:MPEG ADTS, layer III, v1, 320 kbps, 44.1 kHz, Stereo

➜ file run_android_emulator.mp4 run_android_emulator.mp4: ISO Media, MP4 v2 [ISO 14496-14]

它其实是利用文件头标识来进行文件类型判断的,下面是mp3和mp4对应的文件头标识。

扩展名

文件头标识(HEX)

文件描述

MP3

49 44 33

MPEG-1 Audio Layer 3 (MP3) audio file

mp4

00 00 00 18 66 74 79 70 33 67 70 35

MPEG-4 video files

更直观的我们可以通过hexdump命令来查看文件的十六进制形式。

exo玩沉默游戏(ExoPlayer系列2-Track)(4)

一个封装文件或者一个封装格式通常由下面的元素构成:

  • 文件标识头
  • 多媒体信息(多媒体元数据)
  • 音视频(字幕)轨
  • 视频索引块组成

整个解封装的流程:

  1. 读取文件头判断文件格式,比如mp4, ts,然后选择对应的Extractor.
  2. 解析多媒体信息
  3. 解析视频帧的索引块
  4. 最后根据索引去定位并读取音视频数据。

如下图:

exo玩沉默游戏(ExoPlayer系列2-Track)(5)

image-20220426192634200

下面我们用一种稍微直观点的方式,看一下封装文件中包含了哪些信息。

ffprobe是ffmpeg中的一个库,我们借助这个工具可以看一下一个封装文件中的信息。

mp4 format信息

ffprobe -show_format run_android_emulator.mp4 -print_format json

{ Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'run_android_emulator.mp4': Metadata: major_brand : mp42 minor_version : 1 compatible_brands: mp41mp42isom creation_time : 2021-10-30T04:16:21.000000Z Duration: 00:02:24.50, start: 0.000000, bitrate: 2168 kb/s Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 127 kb/s (default) Metadata: creation_time : 2021-10-30T04:16:21.000000Z handler_name : Core Media Audio Stream #0:1(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1280x720 [SAR 1:1 DAR 16:9], 2032 kb/s, 30 fps, 30 tbr, 30k tbn, 60k tbc (default) Metadata: creation_time : 2021-10-30T04:16:21.000000Z handler_name : Core Media Video "format": { "filename": "run_android_emulator.mp4", //文件名称 "nb_streams": 2, //视频av stream的个数 "nb_programs": 0, "format_name": "mov,mp4,m4a,3gp,3g2,mj2", "format_long_name": "QuickTime / MOV", "start_time": "0.000000", "duration": "144.500000", "size": "39162388", "bit_rate": "2168159", "probe_score": 100, "tags": { "major_brand": "mp42", "minor_version": "1", "compatible_brands": "mp41mp42isom", "creation_time": "2021-10-30T04:16:21.000000Z" } } }

mp4 streams信息

ffprobe run_android_emulator.mp4 -show_streams -print_format json

{ Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'run_android_emulator.mp4': Metadata: major_brand : mp42 minor_version : 1 compatible_brands: mp41mp42isom creation_time : 2021-10-30T04:16:21.000000Z Duration: 00:02:24.50, start: 0.000000, bitrate: 2168 kb/s Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 127 kb/s (default) Metadata: creation_time : 2021-10-30T04:16:21.000000Z handler_name : Core Media Audio Stream #0:1(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1280x720 [SAR 1:1 DAR 16:9], 2032 kb/s, 30 fps, 30 tbr, 30k tbn, 60k tbc (default) Metadata: creation_time : 2021-10-30T04:16:21.000000Z handler_name : Core Media Video "streams": [ { "index": 0, "codec_name": "aac", "codec_long_name": "AAC (Advanced Audio Coding)", "profile": "LC", "codec_type": "audio", "codec_time_base": "1/48000", "codec_tag_string": "mp4a", "codec_tag": "0x6134706d", "sample_fmt": "fltp", "sample_rate": "48000", "channels": 2, "channel_layout": "stereo", "bits_per_sample": 0, "r_frame_rate": "0/0", "avg_frame_rate": "0/0", "time_base": "1/48000", "start_pts": 0, "start_time": "0.000000", "duration_ts": 6936000, "duration": "144.500000", "bit_rate": "127307", "max_bit_rate": "128000", "nb_frames": "6776", "disposition": { "default": 1, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0 }, "tags": { "creation_time": "2021-10-30T04:16:21.000000Z", "language": "eng", "handler_name": "Core Media Audio" } }, { "index": 1, "codec_name": "h264", "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", "profile": "High", "codec_type": "video", "codec_time_base": "1/60", "codec_tag_string": "avc1", "codec_tag": "0x31637661", "width": 1280, "height": 720, "coded_width": 1280, "coded_height": 720, "has_b_frames": 0, "sample_aspect_ratio": "1:1", "display_aspect_ratio": "16:9", "pix_fmt": "yuv420p", "level": 31, "color_range": "tv", "color_space": "bt709", "color_transfer": "bt709", "color_primaries": "bt709", "chroma_location": "left", "field_order": "progressive", "refs": 1, "is_avc": "true", "nal_length_size": "4", "r_frame_rate": "30/1", "avg_frame_rate": "30/1", "time_base": "1/30000", "start_pts": 0, "start_time": "0.000000", "duration_ts": 4335000, "duration": "144.500000", "bit_rate": "2032370", "bits_per_raw_sample": "8", "nb_frames": "4335", "disposition": { "default": 1, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0 }, "tags": { "creation_time": "2021-10-30T04:16:21.000000Z", "language": "und", "handler_name": "Core Media Video" } } ] }

ts文件format信息

"format": { "filename": "0_00_192.tts", "nb_streams": 31, "nb_programs": 5, "format_name": "mpegts", "format_long_name": "MPEG-TS (MPEG-2 Transport Stream)", "start_time": "63983.328467", "duration": "193.067422", "size": "735749184", "bit_rate": "30486725", "probe_score": 50 }

ts文件stream信息

ts的stream信息太多了,这里就不贴了。

使用ffmpeg制作多track视频

➜ ffmpeg -i run_android_emulator.mp4 -i video_track.mp4 -i audio.mp3 -i audio2.mp3 -map 0:v -map 1:v -map 2:a -map 3:a -c copy -shortest output_video_audio_track.mp4

上面的命令就是将run_android_emulator.mp4和video_track.mp4这两个视频以及audio.mp3 和 audio2.mp3这两个音频合并成一个mp4视频, 这个过程不需要重新编码,所以速度非常的快。

然后再用ffmpeg查看一下合并成的mp4文件信息:

➜ ffmpeg -i output_video_audio_track.mp4 -hide_banner Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'output_video_audio_track.mp4': Metadata: major_brand : isom minor_version : 512 compatible_brands: isomiso2avc1mp41 encoder : Lavf59.5.100 Duration: 00:00:25.03, start: 0.000000, bitrate: 2923 kb/s Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709), 1280x720 [SAR 1:1 DAR 16:9], 2052 kb/s, 30 fps, 30 tbr, 30k tbn (default) Metadata: handler_name : Core Media Video vendor_id : [0][0][0][0] Stream #0:1[0x2](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x360 [SAR 1:1 DAR 4:3], 413 kb/s, 30 fps, 30 tbr, 30k tbn (default) Metadata: handler_name : (C) 2007 Google Inc. v08.13.2007. vendor_id : [0][0][0][0] Stream #0:2[0x3](und): Audio: mp3 (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 320 kb/s (default) Metadata: handler_name : SoundHandler vendor_id : [0][0][0][0] Stream #0:3[0x4](und): Audio: mp3 (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s Metadata: handler_name : SoundHandler vendor_id : [0][0][0][0]

ExoPlayer track selection

前面介绍了一下音视频文件关于封装的一些基础知识,大概知道了音视频中Track的概念,下面就介绍一下Track Selection并且它在ExoPlayer中的运用。Track Selection决定了播放器去播哪个track, 对应上面的stream。比如一个视频文件里有多个video track, 多个audio track,或者多个字幕track.播视频的时候,你需要选择播哪个video track,哪个audio track和哪个字幕track. 在ExoPlayer使用TrackSelectionParameters 这个类负责Track Selection的一些设置, 也可以给track selection添加限制规则。

ExoPlayer获取 tracks信息

ExoPlayer需要prepare后,才能获取到当前视频可用的tracks信息,可以通过Player.Listener.onTracksInfoChanged

监听tracks信息的变化。这个触发的时机是:

  • 当Player preparation完成。
  • 当可以的tracks或者selected的tracks信息发生变化。
  • 当playlist item发生变化。

player.addListener(new Player.Listener() { @Override public void onTracksInfoChanged(TracksInfo tracksInfo) { for (TracksInfo.TrackGroupInfo groupInfo : tracksInfo.getTrackGroupInfos()) { @C.TrackType int trackType = groupInfo.getTrackType(); boolean trackInGroupIsSelected = groupInfo.isSelected(); boolean trackInGroupIsSupported = groupInfo.isSupported(); Log.d(TAG, "TrackGroupInfo trackType: " trackType); Log.d(TAG, "TrackGroupInfo trackInGroupIsSelected: " trackInGroupIsSelected); Log.d(TAG, "TrackGroupInfo trackInGroupIsSupported: " trackInGroupIsSupported); TrackGroup group = groupInfo.getTrackGroup(); for (int i = 0; i < group.length; i ) { boolean isSupported = groupInfo.isTrackSupported(i); boolean isSelected = groupInfo.isTrackSelected(i); Format trackFormat = group.getFormat(i); Log.d(TAG, "TrackGroup isSupported: " isSupported); Log.d(TAG, "TrackGroup isSelected: " isSelected); Log.d(TAG, "TrackGroup trackFormat: " trackFormat.toString()); } } } });

如果想获取当前的TracksInfo可以使用下面的方法

player.getCurrentTracksInfo()

  • TracksInfo包含了一组TrackGroupInfo` ,每个TrackGroupInfo包含了Track的type, format详情,播放器是否支持当前track,是否被select.进一步的解释是,TrackGroupInfo对应的是一组视频轨、或者音频轨、或者字幕轨。TrackGroupInfo再往下分是TrackGroup,例如TrackGroupInfo对应的是一组视频轨,那么TrackGroup就是这一组视频轨中的某一个视频轨。

exo玩沉默游戏(ExoPlayer系列2-Track)(6)

  • 一个track是supported表示Playe能够decode和render这个track.
  • 一个track是selected表示track selector选择这个track播放,track select的配置放在TrackSelectionParameters中。需要注意如果是多条track的TrackGroup被selected,那么播放器会自适应播放(比如,多个video track 有不同的码率)。注释,虽然TrackGourp是selected,但是其真正select的是group里的一个track select.
修改Track Selection参数

Selection流程可以通过Player.setTrackSelectionParameters方法,设置 TrackSelectionParameters类。这些更新可以在播放前或者播放过程中都可以生效。建议在设置Track Selection的时候,先获取一下当前的配置,然后在当前配置的基础上去修改。

player.setTrackSelectionParameters( player.getTrackSelectionParameters() .buildUpon() .setMaxVideoSizeSd() .setPreferredAudioLanguage("hu") .build());

停止某个Track Type

比如关闭Video track 这个类型,这样就只有音频能播放了,这样就可以试下Audio Only的功能了,比如如果想实现Android版B站有后台播放的功能,直接可以将Video Track设置为Disable就可以。

player.setTrackSelectionParameters( player.getTrackSelectionParameters() .buildUpon() .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_VIDEO)) .build());

还有另外一种方式,可以实现这个功能,通过创建一个空的video overrides, 这个overrides下面再介绍,相当于指定某一个track,其他的track就不会被selected了。

TrackSelectionOverrides overrides = new TrackSelectionOverrides.Builder() .addOverride( new TrackSelectionOverride( disabledTrackGroup, /* select no tracks for this group */ ImmutableList.of())) .build(); player.setTrackSelectionParameters( player.getTrackSelectionParameters() .buildUpon().setTrackSelectionOverrides(overrides).build());

指定某一个特殊的tracks

指定某一个特殊的TrackGroup为Selected Tracks.

TrackSelectionOverrides overrides = new TrackSelectionOverrides.Builder() .setOverrideForType(new TrackSelectionOverride(audioTrackGroup)) .build(); player.setTrackSelectionParameters( player.getTrackSelectionParameters() .buildUpon().setTrackSelectionOverrides(overrides).build());

总结

ExoPlayer支持本地音视频文件的播放,比如上面用ffmpeg生成了一个多track视频文件。同时,ExoPlayer也支持一些流媒体协议比如:DASH,HLS等,如下是ExoPlayer支持的流媒体协议。

exo玩沉默游戏(ExoPlayer系列2-Track)(7)

这些流媒体里同样可以获取到Track信息和Track Selection.具体这里就不详细介绍了,有兴趣的可以研究一下。

参考文献
  • ExoPlayer官方文档
  • 走进音视频的世界
  • 利用文件头标志判断文件类型
  • MpegTS流解复用程序实现
  • ts流格式解析
  • ts格式介绍
  • ffmpeg、ffplay、ffprobe命令使用
  • FFmpeg 视频处理入门教程
  • ExoPlay实现框架
  • Sample地址
,