1. Предисловие
Адаптивное переключение потоков — один из методов многопотокового переключения. ExoPlayer, как мастер MediaCodec, не только имеет возможность реализовать комбинированное переключение разных потоков через MergingMediaSource, но и имеет возможность переключения на основе MGEG-DASH. HLS и протоколы сглаживания потока. Адаптивное переключение потоков. Конечно, условия команды должны полностью учитываться при выборе каждого варианта проекта.
Основные различия заключаются в следующем:
2. Базовые знания
Содержание предисловия все еще немного абстрактно для новых разработчиков ExoPlayer. Давайте разберемся с ключевыми классами ExoPlayer, чтобы облегчить понимание содержания этой статьи.
3. Анализ адаптивного переключения потоков
3.1 Принципиальная схема
Автоматическое переключение на медиапоток, совместимый с текущим битрейтом на разных скоростях сети. Условия соответствия обычно задаются заранее в файле манифеста адаптивного потока. Убедитесь, что битрейт текущей сети превышает минимальную ширину полосы пропускания. медиапоток в протоколе манифеста. Переключиться на указанный медиапоток Track.
Из принципиальной схемы мы можем узнать следующую информацию:
Примечание. Важной причиной подчеркнуть ситуацию по умолчанию является то, что ExopPlayer обладает высокой расширяемостью, и мы можем добиться другого поведения, изменив некоторые коды.
3.2 Основная логика
Основная логика в основном:
3.2.1 Анализ файла манифеста адаптивного потока
ExoPlayer поддерживает протоколы DASH, HLS и Smoothing-Stream. Здесь мы используем протоколы HLS и DASH для анализа процессов. В конце концов, сама Microsoft — единственная, кто использует 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=СУБТИТРЫ,GROUP-ID="subs",NAME="Японский",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility" .transcribes-разговорный-диалог, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=СУБТИТРЫ,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>
Мы явно можем найти много общего между двумя протоколами:
В отличие от ресурсов других протоколов, благодаря использованию файлов манифеста, в принципе возможно получить необходимую информацию о формате перед декапсуляцией.
При анализе файла манифеста, если используется протокол HLS, ExoPlayer внутренне использует класс HlsPlaylistParser в качестве инструмента анализа файла манифеста. Если это DASH, для анализа манифеста используется DashManifestParser, и т. д. Smoothing-stream использует SsmanifestParser для анализа. файл манифеста.
Процесс анализа в основном выглядит следующим образом.
3.2.2 Модуль рендеринга, группа треков и выбор
В 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();
// Создание рендереров и TrackGroup При сопоставлении обратите внимание, что MappedTrackInfo, о котором мы упоминали ранее, сохраняет информацию, связанную с Renderer и TrackGroup.
// Здесь мы также попробуем разные TrackGroup Формат объединяется и группируется в определения на основе совместимости.
ExoTrackSelection.@NullableType Definition[] definitions =
selectAllTracks(
mappedTrackInfo,
rendererFormatSupports,
rendererMixedMimeTypeAdaptationSupport,
parameters);
//Для отфильтрованных определений Вторичная фильтрация
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.
//Создаем конфигурацию инициализации рендерера
@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. Конкретная логика следующая.
@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;
}
//Проходим по всем определениям и сравниваем треки в группе Создайте фиксированныйTrackSelection, если количество равно 1, и создайте AdaptiveTrackSelection, если их несколько.
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 и выделением завершено.
Итак, вот вопрос. Если вы используете 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);
Основная логика заключается в сравнении битрейта.
@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;
}
//Выбираем формат, соответствующий битрейту
int newSelectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
//Оцениваем, нет ли трека, на котором находится формат, в черном списке, тогда используем логику совместимости
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.
//Если выбранный поток кода больше текущего, но данных буферизации недостаточно для безопасного переключения, поэтому текущий трек все равно выбран.
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.
//Выбранный поток кода меньше текущего, но буфера данных достаточно, чтобы не пришлось переключаться.
newSelectedIndex = previousSelectedIndex;
}
}
// If we adapted, update the trigger.
//условие выбора триггера
reason =
newSelectedIndex == previousSelectedIndex ? previousReason : C.SELECTION_REASON_ADAPTIVE;
selectedIndex = newSelectedIndex;
}
Выше упомянуты две ловушки. Размер bufferedDurationUs влияет на переключение, поэтому в проекте необходимо избегать этой проблемы.
Основной момент — определениеIdealSelectedIndex. Логика здесь заключается в том, чтобы получить текущую пропускную способность и затем сопоставить очередь выборки в 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);
//Получаем формат, который меньше текущей скорости сети
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 Мультиплексирование декодера и перезапуск
Поскольку формат фрагмента каждой очереди выборки несколько отличается, декодеру может потребоваться обнаружение изменений формата. В это время может потребоваться перезапуск декодера. Конечно, перезапуск — это самый простой способ переключения между несколькими потоками. Однако это часто приводит к зависаниям или кратковременному черному экрану, что оставляет желать лучшего. ExoPlayer провел значительную оптимизацию для вызова onInputFormatChanged из-за многопоточного переключения в методе MergingMediaSource или адаптивного переключения потоков, тем самым позволяя повторно использовать декодеры.
Основная логика заключается в следующем:
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.
}
//Если кодек выпущен или не создан, инициализируем его повторно
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. Экспериментируйте
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 Установить «Строитель»
//Создаем класс измерения пропускной способности
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 секунд, не обязательно буфер
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 слишком велико, поток даункодирования может завершиться неудачно. поток не удался, необходимо сделать следующую совместимость.
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
Установите пропускную способность через 5 секунд после начала трансляции
【1】Установите пропускную способность 1924009/0,7f при запуске трансляции.
【2】Установите пропускную способность 577610/0,7f через 10 секунд после начала трансляции.
//При запуске трансляции
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 Результаты экспериментов
В соответствии с ожиданиями поток сокращения кода был успешно реализован.
5. Резюме
ExoPlayer не только поддерживает переключение режима слияния нескольких потоков, но также поддерживает адаптивное переключение потоков и обладает широкими возможностями настройки. Таким образом, для сценариев с более высокими требованиями к опыту можно полностью добиться более плавного переключения нескольких потоков путем изменения интерфейсов, связанных с адаптивным потоком. переключение потока.
Если адаптивное переключение потоков ExoPlayer должно быть преобразовано в метод, который могут выбирать пользователи, необходимо изменить некоторые параметры BandwidthMeter и AdaptiveTrackSelection. Существуют также факторы, которые необходимо учитывать: длина фрагмента, выравнивание кадра IDR фрагмента и согласованный формат кодирования (повторное использование декодера). Кроме того, ожидание переключения фрагмента может занять много времени, что также является проблемой. требует внимания.