360路由器不断切换网络,360路由器更换网络设置

首页 > 实用技巧 > 作者:YD1662023-10-30 17:21:49

一、前言

自适应流切换属于多路流切换的方式中的一种,ExoPlayer作为MediaCodec使用的集大成者,不仅具备通过MergingMediaSource实现不同流的组合切换,同样也具备基于MGEG-DASH、HLS、smoothing-stream 协议的的自适应流切换。当然,在项目中每种方案的选型都要充分考虑团队条件。

主要区别如下:

二、基础知识点

前言的内容对于初学ExoPlayer开发者而言还是有些抽象,下面我们梳理一下ExoPlayer的关键类,方便理解本篇内容。

三、自适应流切换分析

3.1 原理图

360路由器不断切换网络,360路由器更换网络设置(1)

在不同网速时自动切换为兼容当前bitrate的媒体流,匹配条件一般在自适应流的清单文件中就已经提前设定了,保证当前网络的bitrate大于清单协议中媒体流的最低bandWidth,就可以切换到指定的媒体流Track。

通过原理图我们可以了解到以下信息:

注意:之所以强调默认情况,一个重要的原因是ExopPlayer具备高度可扩展性,我们可以通过修改部分代码实现其他行为。

3.2 核心逻辑

核心逻辑主要为:

3.2.1 自适应流清单文件解析

ExoPlayer中支持DASH、HLS、Smoothing-Stream协议,我们这里以HLS和DASH协议进行流程分析,毕竟目前使用Smoothing-Stream也就微软自己为主。接下来先看看HLS和DASH的清单文件,方便我们后续测试。

3.2.1.1 hls 协议清单文件

#EXTM3U #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 1",AUTOSELECT=YES,DEFAULT=YES #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 2",AUTOSELECT=NO,DEFAULT=NO,URI="alternate_audio_aac/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/eng/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="en",URI="subtitles/eng_forced/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/fra/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="fr",URI="subtitles/fra_forced/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/spa/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="es",URI="subtitles/spa_forced/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語 (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="ja",URI="subtitles/jpn_forced/prog_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d",RESOLUTION=416x234,AUDIO="bipbop_audio",SUBTITLES="subs" gear1/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,CODECS="avc1.4d400d",URI="gear1/iframe_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs" gear2/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,CODECS="avc1.4d401e",URI="gear2/iframe_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=960x540,AUDIO="bipbop_audio",SUBTITLES="subs" gear3/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,CODECS="avc1.4d401f",URI="gear3/iframe_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1280x720,AUDIO="bipbop_audio",SUBTITLES="subs" gear4/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,CODECS="avc1.4d401f",URI="gear4/iframe_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs" gear5/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,CODECS="avc1.4d401f",URI="gear5/iframe_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS="mp4a.40.2",AUDIO="bipbop_audio",SUBTITLES="subs" gear0/prog_index.m3u8

3.2.1.2 DASH协议清单文件

<?xml version="1.0" encoding="UTF-8"?> <!--Generated with https://github.com/google/shaka-packager version 97fc982-release--> <MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" minBufferTime="PT2S" type="static" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" mediaPresentationDuration="PT734S"> <Period id="0"> <AdaptationSet id="0" contentType="audio" lang="en"> <Representation id="0" bandwidth="131596" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100"> <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/> <BaseURL>tears_audio_eng.mp4</BaseURL> <SegmentBase indexRange="745-1664" timescale="44100"> <Initialization range="0-744"/> </SegmentBase> </Representation> </AdaptationSet> <AdaptationSet id="1" contentType="video" maxWidth="1920" maxHeight="856" frameRate="12288/512" par="38:17"> <Representation id="1" bandwidth="769255" codecs="avc1.42c01e" mimeType="video/mp4" sar="852:857" width="320" height="142"> <BaseURL>tears_h264_baseline_240p_800.mp4</BaseURL> <SegmentBase indexRange="827-1602" timescale="12288"> <Initialization range="0-826"/> </SegmentBase> </Representation> <Representation id="2" bandwidth="1774254" codecs="avc1.4d401f" mimeType="video/mp4" sar="2242:2249" width="854" height="380"> <BaseURL>tears_h264_main_480p_2000.mp4</BaseURL> <SegmentBase indexRange="829-1604" timescale="12288"> <Initialization range="0-828"/> </SegmentBase> </Representation> <Representation id="3" bandwidth="7203938" codecs="avc1.4d4028" mimeType="video/mp4" sar="855:857" width="1280" height="570"> <BaseURL>tears_h264_main_720p_8000.mp4</BaseURL> <SegmentBase indexRange="830-1605" timescale="12288"> <Initialization range="0-829"/> </SegmentBase> </Representation> <Representation id="4" bandwidth="18316946" codecs="avc1.64002a" mimeType="video/mp4" sar="856:857" width="1920" height="856"> <BaseURL>tears_h264_high_1080p_20000.mp4</BaseURL> <SegmentBase indexRange="832-1607" timescale="12288"> <Initialization range="0-831"/> </SegmentBase> </Representation> </AdaptationSet> </Period> </MPD>

两种协议的共同点中我们可以明确的发现又很多共同点:

和其他协议的资源不同的是,由于使用清单文件的原因,基本可以实现在解封装之前就能获取到必要的Format信息。

解析时清单文件时,如果使用的是HLS协议,ExoPlayer内部利用HlsPlaylistParser类作为清单文件解析工具,如果是DASH则使用DashManifestParser解析清单,依次类推,smoothing-stream 使用SsmanifestParser进行将进行清单文件解析。

解析的流程主要如下

3.2.2 Renderer与TrackGroup、Selection选择

在ExoPlayer中,DefaultTrackSelector主要负责这项工作,核心逻辑如下

@Override protected final Pair<@NullableType RendererConfiguration[], @NullableType ExoTrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, @Capabilities int[][][] rendererFormatSupports, @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport, MediaPeriodId mediaPeriodId, Timeline timeline) throws ExoPlaybackException { Parameters parameters; synchronized (lock) { parameters = this.parameters; if (parameters.constrainAudioChannelCountToDeviceCapabilities && Util.SDK_INT >= 32 && spatializer != null) { // Initialize the spatializer now so we can get a reference to the playback looper with // Looper.myLooper(). spatializer.ensureInitialized(this, checkStateNotNull(Looper.myLooper())); } } int rendererCount = mappedTrackInfo.getRendererCount(); // 建立Renderers 与 TrackGroup 映射,注意,我们前面提到的MappedTrackInfo,保存有Renderer和TrackGroup的相关信息 // 这里也会尝试对不同TrackGroup Format进行依据兼容性进行合并分组到definition中 ExoTrackSelection.@NullableType Definition[] definitions = selectAllTracks( mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupport, parameters); //对筛选出来的defintions 二次过滤 applyTrackSelectionOverrides(mappedTrackInfo, parameters, definitions); applyLegacyRendererOverrides(mappedTrackInfo, parameters, definitions); //三次过滤,对Renderer不能使用解除映关系 // Disable renderers if needed. for (int i = 0; i < rendererCount; i ) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i); if (parameters.getRendererDisabled(i) || parameters.disabledTrackTypes.contains(rendererType)) { definitions[i] = null; } } //建立TrackGroup与Selection之间的映射关系,并且传入BandwidthMeter用于获取网速策略的数据 @NullableType ExoTrackSelection[] rendererTrackSelections = trackSelectionFactory.createTrackSelections( definitions, getBandwidthMeter(), mediaPeriodId, timeline); // Initialize the renderer configurations to the default configuration for all renderers with // selections, and null otherwise. //创建Renderer初始化配置 @NullableType RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount]; for (int i = 0; i < rendererCount; i ) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i); boolean forceRendererDisabled = parameters.getRendererDisabled(i) || parameters.disabledTrackTypes.contains(rendererType); boolean rendererEnabled = !forceRendererDisabled && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE || rendererTrackSelections[i] != null); rendererConfigurations[i] = rendererEnabled ? RendererConfiguration.DEFAULT : null; } // Configure audio and video renderers to use tunneling if appropriate. if (parameters.tunnelingEnabled) { //这种属于隧道渲染,具体流程好像是MediaCodec不负责解码,可以将未解码的数据输出到驱动层,由驱动层处理,如ac3音频数据直接输出到AudioTrack中,这方面资料太少,后续在研究。 maybeConfigureRenderersForTunneling( mappedTrackInfo, rendererFormatSupports, rendererConfigurations, rendererTrackSelections); } return Pair.create(rendererConfigurations, rendererTrackSelections); }

这部分逻辑相对很复杂,但是我们com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection.Factory#createTrackSelections要做必要的分析。

在ExoPlayer中默认使用改工厂适配Selection,具体逻辑如下

@Override public final @NullableType ExoTrackSelection[] createTrackSelections( @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter, MediaPeriodId mediaPeriodId, Timeline timeline) { ImmutableList<ImmutableList<AdaptationCheckpoint>> adaptationCheckpoints = getAdaptationCheckpoints(definitions); ExoTrackSelection[] selections = new ExoTrackSelection[definitions.length]; for (int i = 0; i < definitions.length; i ) { @Nullable Definition definition = definitions[i]; if (definition == null || definition.tracks.length == 0) { continue; } //遍历所有的definition,对分组中tracks 数量为1的创建FixedTrackSelection,对存在多个的创建AdaptiveTrackSeletion selections[i] = definition.tracks.length == 1 ? new FixedTrackSelection( definition.group, /* track= */ definition.tracks[0], /* type= */ definition.type) : createAdaptiveTrackSelection( definition.group, definition.tracks, definition.type, bandwidthMeter, adaptationCheckpoints.get(i)); } return selections; }

至此,Renderer 、TrackGroup、selection的映射关系建立完成。

那么这里有个疑问,如果利用MergingMediaSource合并多路流并修改参数,能否也实现AdaptiveTrackSelection,进入试下自适应能力?答案是否定的,因为MergingMediaSource合并的是完整的资源,在使用过程中并不会调用TrackSelection相关方法,当然ExoPlayer也没有实现资源的动态分片。

3.2.3 分片加载

DASH、HLS、Smoothing-Stream 加载分片的时候,单个分片都是用各自的实现的ChunkSource类,但是对于存在多个分片情况,ExoPlayer利用ChunkSampleStream和HlsSampleStreamWrapper将分片队列管理起来,核心方法是continueLoading中的实现,大致相同,这里以HlsSampleStreamWrapper的为参考。

public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading() || loader.hasFatalError()) { return false; } boolean pendingReset = isPendingReset(); //获取资源队列 List<BaseMediaChunk> chunkQueue; //获取上次加载资源时间位置 long loadPositionUs; if (pendingReset) { chunkQueue = Collections.emptyList(); loadPositionUs = pendingResetPositionUs; } else { chunkQueue = readOnlyMediaChunks; loadPositionUs = getLastMediaChunk().endTimeUs; } //获取下一个分片 chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; @Nullable Chunk loadable = nextChunkHolder.chunk; nextChunkHolder.clear(); if (endOfStream) { // 如果没有资源可以加载了,标记加载结束 pendingResetPositionUs = C.TIME_UNSET; loadingFinished = true; return true; } if (loadable == null) { return false; } //下面逻辑是加载状态直接的判断 loadingChunk = loadable; if (isMediaChunk(loadable)) { BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; if (pendingReset) { // Only set the queue start times if we're not seeking to a chunk boundary. If we are // seeking to a chunk boundary then we want the queue to pass through all of the samples in // the chunk. Doing this ensures we'll always output the keyframe at the start of the chunk, // even if its timestamp is slightly earlier than the advertised chunk start time. if (mediaChunk.startTimeUs != pendingResetPositionUs) { primarySampleQueue.setStartTimeUs(pendingResetPositionUs); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.setStartTimeUs(pendingResetPositionUs); } } pendingResetPositionUs = C.TIME_UNSET; } mediaChunk.init(chunkOutput); mediaChunks.add(mediaChunk); } else if (loadable instanceof InitializationChunk) { ((InitializationChunk) loadable).init(chunkOutput); } long elapsedRealtimeMs = loader.startLoading( loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); mediaSourceEventDispatcher.loadStarted( new LoadEventInfo(loadable.loadTaskId, loadable.dataSpec, elapsedRealtimeMs), loadable.type, primaryTrackType, loadable.trackFormat, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, loadable.endTimeUs); return true; }

getNextChunk是continueLoading中的核心方法,继续看代码。

public final void getNextChunk( long playbackPositionUs, long loadPositionUs, List<? extends MediaChunk> queue, ChunkHolder out) { if (fatalError != null) { return; } StreamElement streamElement = manifest.streamElements[streamElementIndex]; if (streamElement.chunkCount == 0) { // There aren't any chunks for us to load. //没有分片可以加载了 out.endOfStream = !manifest.isLive; return; } int chunkIndex; //获取最后一次加载片段的索引 if (queue.isEmpty()) { chunkIndex = streamElement.getChunkIndex(loadPositionUs); } else { chunkIndex = (int) (queue.get(queue.size() - 1).getNextChunkIndex() - currentManifestChunkOffset); if (chunkIndex < 0) { //这个片段不存在,说明加载片段不完整 // This is before the first chunk in the current manifest. fatalError = new BehindLiveWindowException(); return; } } if (chunkIndex >= streamElement.chunkCount) { // This is beyond the last chunk in the current manifest. //这个索引不合法,直接停止加载 out.endOfStream = !manifest.isLive; return; } long bufferedDurationUs = loadPositionUs - playbackPositionUs; long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()]; for (int i = 0; i < chunkIterators.length; i ) { int trackIndex = trackSelection.getIndexInTrackGroup(i); chunkIterators[i] = new StreamElementIterator(streamElement, trackIndex, chunkIndex); } //最关键的地方,这里会根据网速,筛选出下一个分片 trackSelection.updateSelectedTrack( playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, chunkIterators); long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkEndTimeUs = chunkStartTimeUs streamElement.getChunkDurationUs(chunkIndex); long chunkSeekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET; int currentAbsoluteChunkIndex = chunkIndex currentManifestChunkOffset; //这里是重点,获取选下一个需要加载的分片 int trackSelectionIndex = trackSelection.getSelectedIndex(); //获取分片解封装器 ChunkExtractor chunkExtractor = chunkExtractors[trackSelectionIndex]; int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); //绑定要加载的片段信息 out.chunk = newMediaChunk( trackSelection.getSelectedFormat(), dataSource, uri, currentAbsoluteChunkIndex, chunkStartTimeUs, chunkEndTimeUs, chunkSeekTimeUs, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), chunkExtractor); }

3.2.4 网速检测和AdaptiveTrackSelection分片选择

网速检测使用的默认的DefaultBandWidthMeter进行测试,具体原理是监控数据的某一段时间的下载流量,计算出平均网速。AdaptiveTrack Selection#updateSelectedTrack中会利用选择测量数据,重新计算使用哪个队列的数据。

int newSelectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);

核心逻辑就是通过bitrate去做比较

@Override public void updateSelectedTrack( long playbackPositionUs, long bufferedDurationUs, long availableDurationUs, List<? extends MediaChunk> queue, MediaChunkIterator[] mediaChunkIterators) { long nowMs = clock.elapsedRealtime(); long chunkDurationUs = getNextChunkDurationUs(mediaChunkIterators, queue); // Make initial selection if (reason == C.SELECTION_REASON_UNKNOWN) { reason = C.SELECTION_REASON_INITIAL; //初始化或者还没有选择过则直接一次性选择,不走兼容流程 selectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs); return; } int previousSelectedIndex = selectedIndex; @C.SelectionReason int previousReason = reason; int formatIndexOfPreviousChunk = queue.isEmpty() ? C.INDEX_UNSET : indexOf(Iterables.getLast(queue).trackFormat); if (formatIndexOfPreviousChunk != C.INDEX_UNSET) { previousSelectedIndex = formatIndexOfPreviousChunk; previousReason = Iterables.getLast(queue).trackSelectionReason; } //选择匹配bitrate的format int newSelectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs); //判断如果该format所在的Track不在黑名单中,则走兼容逻辑 if (!isBlacklisted(previousSelectedIndex, nowMs)) { // Revert back to the previous selection if conditions are not suitable for switching. Format currentFormat = getFormat(previousSelectedIndex); Format selectedFormat = getFormat(newSelectedIndex); //注意:进入这个地方就是一个坑点,如果切码流失败,原因是这里通过一些条件又给重置回去了 long minDurationForQualityIncreaseUs = minDurationForQualityIncreaseUs(availableDurationUs, chunkDurationUs); if (selectedFormat.bitrate > currentFormat.bitrate && bufferedDurationUs < minDurationForQualityIncreaseUs) { // The selected track is a higher quality, but we have insufficient buffer to safely switch // up. Defer switching up for now. //如果选择的码流大于当前的,但是buffering的数据不够去安全的切换,因此还是选择当前Track newSelectedIndex = previousSelectedIndex; } else if (selectedFormat.bitrate < currentFormat.bitrate && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { // The selected track is a lower quality, but we have sufficient buffer to defer switching // down for now. //选择的码流小于当前的,但是buffer数据是足够,不至于去切换 newSelectedIndex = previousSelectedIndex; } } // If we adapted, update the trigger. //触发选择条件 reason = newSelectedIndex == previousSelectedIndex ? previousReason : C.SELECTION_REASON_ADAPTIVE; selectedIndex = newSelectedIndex; }

上面提到2个坑点,bufferedDurationUs 大小影响切换,因此在项目中有必要规避此问题。

核心点determineIdealSelectedIndex,这里的逻辑就是获取当前带宽,然后匹配Selection中的采样队列

private int determineIdealSelectedIndex(long nowMs, long chunkDurationUs) { //获取带宽数据 long effectiveBitrate = getAllocatedBandwidth(chunkDurationUs); int lowestBitrateAllowedIndex = 0; for (int i = 0; i < length; i ) { if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { Format format = getFormat(i); //获取小于当前网速的Format if (canSelectFormat(format, format.bitrate, effectiveBitrate)) { return i; } else { lowestBitrateAllowedIndex = i; } } } return lowestBitrateAllowedIndex; }

带宽数据获取

private long getTotalAllocatableBandwidth(long chunkDurationUs) { //注意这里将带宽x0.7f,实际上比实际值偏小了,因此设置网速时一定要除以0.7f long cautiousBandwidthEstimate = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); long timeToFirstByteEstimateUs = bandwidthMeter.getTimeToFirstByteEstimateUs(); if (timeToFirstByteEstimateUs == C.TIME_UNSET || chunkDurationUs == C.TIME_UNSET) { //默认情况下回到这里,带宽除以当前播放的速度 (倍速) return (long) (cautiousBandwidthEstimate / playbackSpeed); } float availableTimeToLoadUs = max(chunkDurationUs / playbackSpeed - timeToFirstByteEstimateUs, 0); return (long) (cautiousBandwidthEstimate * availableTimeToLoadUs / chunkDurationUs); }

3.2.5 解码器的复用和重启

由于每种采样队列的分片Format有一些差别,可能需要解码器检测到格式变化。这个时候解码器可能需要重启,当然重启是多路流切换的最基本的做法,但是这种往往会出现卡顿或者短暂的黑屏,体验反而比较差。ExoPlayer对于无论是MergingMediaSource方式的多路流切换还是自适应流的切换导致onInputFormatChanged被调用做了相当多的优化,从而实现解码器的重复利用。

核心逻辑如下:

protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { waitingForFirstSampleInFormat = true; Format newFormat = checkNotNull(formatHolder.format); if (newFormat.sampleMimeType == null) { // If the new format is invalid, it is either a media bug or it is not intended to be played. // See also https://github.com/google/ExoPlayer/issues/8283. throw createRendererException( new IllegalArgumentException(), newFormat, PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED); } setSourceDrmSession(formatHolder.drmSession); inputFormat = newFormat; if (bypassEnabled) { //等到队列数据被清空后初始化 bypassDrainAndReinitialize = true; return null; // Need to drain batch buffer first. } //如果Codec已经被释放或者还没有创建的情况下,重新初始化 if (codec == null) { availableCodecInfos = null; maybeInitCodecOrBypass(); return null; } // We have an existing codec that we may need to reconfigure, re-initialize, or release to // switch to bypass. If the existing codec instance is kept then its operating rate and DRM // session may need to be updated. // Copy the current codec and codecInfo to local variables so they remain accessible if the // member variables are updated during the logic below. MediaCodecAdapter codec = this.codec; MediaCodecInfo codecInfo = this.codecInfo; Format oldFormat = codecInputFormat; if (drmNeedsCodecReinitialization(codecInfo, newFormat, codecDrmSession, sourceDrmSession)) { drainAndReinitializeCodec(); return new DecoderReuseEvaluation( codecInfo.name, oldFormat, newFormat, REUSE_RESULT_NO, DISCARD_REASON_DRM_SESSION_CHANGED); } boolean drainAndUpdateCodecDrmSession = sourceDrmSession != codecDrmSession; Assertions.checkState(!drainAndUpdateCodecDrmSession || Util.SDK_INT >= 23); //对比新旧解码器的重用的条件,这里代码太多,不在深入讨论。 DecoderReuseEvaluation evaluation = canReuseCodec(codecInfo, oldFormat, newFormat); @DecoderDiscardReasons int overridingDiscardReasons = 0; switch (evaluation.result) { case REUSE_RESULT_NO: //不重用,直接重启 drainAndReinitializeCodec(); break; case REUSE_RESULT_YES_WITH_FLUSH: //可以重复使用,但需要清除队列,这种情况下需要调用MediaCodec.flush if (!updateCodecOperatingRate(newFormat)) { overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED; } else { codecInputFormat = newFormat; if (drainAndUpdateCodecDrmSession) { if (!drainAndUpdateCodecDrmSessionV23()) { overridingDiscardReasons |= DISCARD_REASON_WORKAROUND; } } else if (!drainAndFlushCodec()) { overridingDiscardReasons |= DISCARD_REASON_WORKAROUND; } } break; case REUSE_RESULT_YES_WITH_RECONFIGURATION: //可以重用,但是需要重新注册信息,如果是视频,需要重新注册SPS、PPS信息,具体信息在 //com.google.android.exoplayer2.Format.Builder#initializationData 中,如果音频一般是资源类型 if (!updateCodecOperatingRate(newFormat)) { overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED; } else { codecReconfigured = true; codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; codecNeedsAdaptationWorkaroundBuffer = codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION && newFormat.width == oldFormat.width && newFormat.height == oldFormat.height); codecInputFormat = newFormat; if (drainAndUpdateCodecDrmSession && !drainAndUpdateCodecDrmSessionV23()) { overridingDiscardReasons |= DISCARD_REASON_WORKAROUND; } } break; case REUSE_RESULT_YES_WITHOUT_RECONFIGURATION: //直接重用解码器,无需注册任何信息 if (!updateCodecOperatingRate(newFormat)) { overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED; } else { codecInputFormat = newFormat; if (drainAndUpdateCodecDrmSession && !drainAndUpdateCodecDrmSessionV23()) { overridingDiscardReasons |= DISCARD_REASON_WORKAROUND; } } break; default: throw new IllegalStateException(); // Never happens. } if (evaluation.result != REUSE_RESULT_NO && (this.codec != codec || codecDrainAction == DRAIN_ACTION_REINITIALIZE)) { // Initial evaluation indicated reuse was possible, but codec re-initialization was triggered. // The reasons are indicated by overridingDiscardReasons. return new DecoderReuseEvaluation( codecInfo.name, oldFormat, newFormat, REUSE_RESULT_NO, overridingDiscardReasons); } return evaluation; }

3.2.6 完成切换

通过上述必要的逻辑,就能实现分片的切换,当然,每个部分代码量实在太多包括,资源加载部分也是一个核心环节,这里就不在继续分析了。

但是如何验证切换完成了,实际上是有回调的,参考下面接口实现。

com.google.android.exoplayer2.audio.AudioRendererEventListener#onAudioInputFormatChanged(com.google.android.exoplayer2.Format) com.google.android.exoplayer2.video.VideoRendererEventListener#onVideoInputFormatChanged(com.google.android.exoplayer2.Format)

四、实验

4.1 实验目的

实现手动切换分片

4.2 实验方法

自动以AdaptiveTrackSelection#Factory或者自定义BandwidthMeter,这里我们选择后者,因为改动较小。

4.2.1实现QmBandwidthMeter

private long bitrateEstimate; private long specificBitrate = C.TIME_UNSET; @Override public synchronized long getBitrateEstimate() { //如果用户没有指定比特率,则使用默认的,如果指定了,则使用用户的设备 if(specificBitrate == C.RATE_UNSET_INT) { return bitrateEstimate; } return specificBitrate; }

4.2.2 设置到Builder中

//创建带宽测量类 bandwidthMeter = new QmBandwidthMeter .Builder(getApplicationContext()) .build(); //缓冲控制类 DefaultLoadControl loadControl = new DefaultLoadControl.Builder() .setAllocator(new DefaultAllocator(true,C.DEFAULT_BUFFER_SEGMENT_SIZE / 2)) .setBufferDurationsMs( 10_000, //低于20秒的缓冲就开始加载数据,不一定会buffer 20_000, //一次加载最多加载20秒的数据 DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) .build(); ExoPlayer.Builder playerBuilder = new ExoPlayer.Builder(/* context= */ this) .setLoadControl(loadControl) .setBandwidthMeter(bandwidthMeter) .setMediaSourceFactory(createMediaSourceFactory()); setRenderersFactory( playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false)); player = playerBuilder.build();

注意:这里设置缓冲控制类的前文已经说过, 分片选择存在坑点,由于bufferedDurationUs值过大,可能造成降码流失效, AdaptiveTrackSelection可能会重置会原来的selectionIndex为上一个分片Track,导致降码流失败,有必要做以下兼容。

4.2.3 以切换为下面的Hls分片为例子,实现从1920x1080 -> 640x360的切换

链接地址:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs"gear5/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs"gear2/prog_index.m3u8

在起播后5s后设置带宽

【1】起播时设置带宽1924009/0.7f

【2】起播10s后设置带宽577610/0.7f

//起播时 bandwidthMeter.setSpecificBitrate((long) Math.ceil(1924009/0.7f)); //播放一段时间后 bandwidthMeter.setSpecificBitrate((long) Math.ceil(577610/0.7f));

注意: 这里除以0.7f上面也说了,是因为带宽冗余机制造成的。

验证方法,实现下面的回调

com.google.android.exoplayer2.audio.AudioRendererEventListener#onAudioInputFormatChanged(com.google.android.exoplayer2.Format) com.google.android.exoplayer2.video.VideoRendererEventListener#onVideoInputFormatChanged(com.google.android.exoplayer2.Format)

4.3 实验结果

符合预期,成功实现了降码流

五、总结

ExoPlayer不仅支持多路流合并方式切换,也支持自适应流切换,具备高度可定制化的能力,因此,对于体验要求较高的场景,可完全通过修改自适应流相关接口实现更加顺滑的多路流切换。

ExoPlayer自适应流切换如果要改造的为用户所能选择的方式,需要修改BandwidthMeter和AdaptiveTrackSelection的一些参数。还有就是分片长度、分片IDR帧对齐、编码格式一致(尽可能解码器复用)是需要考虑的因素,另外分片可能切换等待时间较长,也是需要注意的问题。

作者:winterstian

来源:微信公众号:腾讯音乐技术团队

出处:https://mp.weixin.qq.com/s/624xdCpZmioFZlfAhwvxxg

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.