Initial hlsmplayer application
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
plugins { id("com.android.library") }
|
||||
|
||||
android {
|
||||
namespace = "androidx.media3.exoplayer.hls"
|
||||
compileSdk = 36
|
||||
defaultConfig { minSdk = 23 }
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":lib-exoplayer"))
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.androidx.concurrent.futures)
|
||||
compileOnly(libs.checkerframework.qual)
|
||||
compileOnly(libs.errorprone.annotations)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2017 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest>
|
||||
<uses-sdk />
|
||||
</manifest>
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DataSourceInputStream;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.datasource.TransferListener;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* A {@link DataSource} that decrypts data read from an upstream source, encrypted with AES-128 with
|
||||
* a 128-bit key and PKCS7 padding.
|
||||
*
|
||||
* <p>Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is
|
||||
* designed specifically for reading whole files as defined in an HLS media playlist. For this
|
||||
* reason the implementation is private to the HLS package.
|
||||
*/
|
||||
/* package */ class Aes128DataSource implements DataSource {
|
||||
|
||||
private final DataSource upstream;
|
||||
private final byte[] encryptionKey;
|
||||
private final byte[] encryptionIv;
|
||||
|
||||
@Nullable private CipherInputStream cipherInputStream;
|
||||
|
||||
/**
|
||||
* @param upstream The upstream {@link DataSource}.
|
||||
* @param encryptionKey The encryption key.
|
||||
* @param encryptionIv The encryption initialization vector.
|
||||
*/
|
||||
public Aes128DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) {
|
||||
this.upstream = upstream;
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.encryptionIv = encryptionIv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void addTransferListener(TransferListener transferListener) {
|
||||
checkNotNull(transferListener);
|
||||
upstream.addTransferListener(transferListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long open(DataSpec dataSpec) throws IOException {
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = getCipherInstance();
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Key cipherKey = new SecretKeySpec(encryptionKey, "AES");
|
||||
AlgorithmParameterSpec cipherIV = new IvParameterSpec(encryptionIv);
|
||||
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
DataSourceInputStream inputStream = new DataSourceInputStream(upstream, dataSpec);
|
||||
cipherInputStream = new CipherInputStream(inputStream, cipher);
|
||||
inputStream.open();
|
||||
|
||||
return C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int read(byte[] buffer, int offset, int length) throws IOException {
|
||||
checkNotNull(cipherInputStream);
|
||||
int bytesRead = cipherInputStream.read(buffer, offset, length);
|
||||
if (bytesRead < 0) {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public final Uri getUri() {
|
||||
return upstream.getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Map<String, List<String>> getResponseHeaders() {
|
||||
return upstream.getResponseHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (cipherInputStream != null) {
|
||||
cipherInputStream = null;
|
||||
upstream.close();
|
||||
}
|
||||
}
|
||||
|
||||
protected Cipher getCipherInstance() throws NoSuchPaddingException, NoSuchAlgorithmException {
|
||||
return Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static com.google.common.io.ByteStreams.toByteArray;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.AdPlaybackState.SkipInfo;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.ParserException;
|
||||
import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.Asset;
|
||||
import androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.AssetList;
|
||||
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** Parses a X-ASSET-LIST JSON object. */
|
||||
/* package */ final class AssetListParser
|
||||
implements ParsingLoadable.Parser<Pair<AssetList, JSONObject>> {
|
||||
|
||||
/** The asset name of the assets array in a X-ASSET-LIST JSON object. */
|
||||
private static final String ASSET_LIST_JSON_NAME_ASSET_ARRAY = "ASSETS";
|
||||
|
||||
/** The asset URI name in a X-ASSET-LIST JSON object. */
|
||||
private static final String ASSET_LIST_JSON_NAME_URI = "URI";
|
||||
|
||||
/** The asset duration name in a X-ASSET-LIST JSON object. */
|
||||
private static final String ASSET_LIST_JSON_NAME_DURATION = "DURATION";
|
||||
|
||||
/** The skip control field name in a X-ASSET-LIST JSON object. */
|
||||
private static final String ASSET_LIST_JSON_NAME_SKIP_CONTROL = "SKIP-CONTROL";
|
||||
|
||||
/** The offset field name in a SKIP-CONTROL JSON object. */
|
||||
private static final String ASSET_LIST_JSON_NAME_OFFSET = "OFFSET";
|
||||
|
||||
/** The label ID field name in a SKIP-CONTROL JSON object. */
|
||||
private static final String ASSET_LIST_JSON_NAME_LABEL_ID = "LABEL-ID";
|
||||
|
||||
@Override
|
||||
public Pair<AssetList, JSONObject> parse(Uri uri, InputStream inputStream) throws IOException {
|
||||
try {
|
||||
JSONObject jsonObject = new JSONObject(new String(toByteArray(inputStream), UTF_8));
|
||||
return new Pair<>(getAssetListFromRawJson(jsonObject), jsonObject);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw ParserException.createForMalformedManifest(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static AssetList getAssetListFromRawJson(JSONObject jsonObject) throws JSONException {
|
||||
if (!jsonObject.has(ASSET_LIST_JSON_NAME_ASSET_ARRAY)) {
|
||||
throw new JSONException("missing " + ASSET_LIST_JSON_NAME_ASSET_ARRAY + " attribute");
|
||||
}
|
||||
ImmutableList.Builder<Asset> assets = new ImmutableList.Builder<>();
|
||||
@Nullable SkipInfo skipInfo = null;
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(ASSET_LIST_JSON_NAME_ASSET_ARRAY);
|
||||
for (int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject assetObject = jsonArray.getJSONObject(i);
|
||||
if (!assetObject.has(ASSET_LIST_JSON_NAME_URI)) {
|
||||
throw new JSONException("missing " + ASSET_LIST_JSON_NAME_URI + " attribute");
|
||||
}
|
||||
if (!assetObject.has(ASSET_LIST_JSON_NAME_DURATION)) {
|
||||
throw new JSONException("missing " + ASSET_LIST_JSON_NAME_DURATION + " attribute");
|
||||
}
|
||||
Uri assetUri = Uri.parse(assetObject.getString(ASSET_LIST_JSON_NAME_URI));
|
||||
long durationUs =
|
||||
(long) (assetObject.getDouble(ASSET_LIST_JSON_NAME_DURATION) * C.MICROS_PER_SECOND);
|
||||
assets.add(new Asset(assetUri, durationUs));
|
||||
}
|
||||
|
||||
if (jsonObject.has(ASSET_LIST_JSON_NAME_SKIP_CONTROL)) {
|
||||
JSONObject skipControlObject = jsonObject.getJSONObject(ASSET_LIST_JSON_NAME_SKIP_CONTROL);
|
||||
long offsetUs = 0;
|
||||
if (skipControlObject.has(ASSET_LIST_JSON_NAME_OFFSET)) {
|
||||
offsetUs =
|
||||
(long) (skipControlObject.getDouble(ASSET_LIST_JSON_NAME_OFFSET) * C.MICROS_PER_SECOND);
|
||||
}
|
||||
long durationUs = C.TIME_UNSET;
|
||||
if (skipControlObject.has(ASSET_LIST_JSON_NAME_DURATION)) {
|
||||
durationUs =
|
||||
(long)
|
||||
(skipControlObject.getDouble(ASSET_LIST_JSON_NAME_DURATION) * C.MICROS_PER_SECOND);
|
||||
}
|
||||
@Nullable String labelId = null;
|
||||
if (skipControlObject.has(ASSET_LIST_JSON_NAME_LABEL_ID)) {
|
||||
labelId = skipControlObject.getString(ASSET_LIST_JSON_NAME_LABEL_ID);
|
||||
}
|
||||
skipInfo = new SkipInfo(offsetUs, durationUs, labelId);
|
||||
}
|
||||
return new AssetList(assets.build(), skipInfo);
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.TimestampAdjuster;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.extractor.Extractor;
|
||||
import androidx.media3.extractor.ExtractorInput;
|
||||
import androidx.media3.extractor.ExtractorOutput;
|
||||
import androidx.media3.extractor.PositionHolder;
|
||||
import androidx.media3.extractor.mp3.Mp3Extractor;
|
||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
||||
import androidx.media3.extractor.text.SubtitleParser;
|
||||
import androidx.media3.extractor.ts.Ac3Extractor;
|
||||
import androidx.media3.extractor.ts.Ac4Extractor;
|
||||
import androidx.media3.extractor.ts.AdtsExtractor;
|
||||
import androidx.media3.extractor.ts.TsExtractor;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* {@link HlsMediaChunkExtractor} implementation that uses ExoPlayer app-bundled {@link Extractor
|
||||
* Extractors}.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtractor {
|
||||
|
||||
private static final PositionHolder POSITION_HOLDER = new PositionHolder();
|
||||
|
||||
@VisibleForTesting /* package */ final Extractor extractor;
|
||||
private final Format multivariantPlaylistFormat;
|
||||
private final TimestampAdjuster timestampAdjuster;
|
||||
private final SubtitleParser.Factory subtitleParserFactory;
|
||||
private final boolean parseSubtitlesDuringExtraction;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param extractor The underlying {@link Extractor}.
|
||||
* @param multivariantPlaylistFormat The {@link Format} obtained from the multivariant playlist.
|
||||
* @param timestampAdjuster A {@link TimestampAdjuster} to adjust sample timestamps.
|
||||
*/
|
||||
public BundledHlsMediaChunkExtractor(
|
||||
Extractor extractor, Format multivariantPlaylistFormat, TimestampAdjuster timestampAdjuster) {
|
||||
this(
|
||||
extractor,
|
||||
multivariantPlaylistFormat,
|
||||
timestampAdjuster,
|
||||
SubtitleParser.Factory.UNSUPPORTED,
|
||||
/* parseSubtitlesDuringExtraction= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param extractor The underlying {@link Extractor}.
|
||||
* @param multivariantPlaylistFormat The {@link Format} obtained from the multivariant playlist.
|
||||
* @param timestampAdjuster A {@link TimestampAdjuster} to adjust sample timestamps.
|
||||
* @param subtitleParserFactory A {@link SubtitleParser.Factory} to be used with WebVTT subtitles.
|
||||
* If the value is null, subtitles will be parsed during decoding, otherwise - during
|
||||
* extraction. Decoding will only work if this subtitleParserFactory supports the provided
|
||||
* multivariantPlaylistFormat.
|
||||
*/
|
||||
// TODO(b/289983417): Once the subtitle-parsing-during-extraction is the only available flow, make
|
||||
// this constructor public and remove parseSubtitlesDuringExtraction parameter
|
||||
/* package */ BundledHlsMediaChunkExtractor(
|
||||
Extractor extractor,
|
||||
Format multivariantPlaylistFormat,
|
||||
TimestampAdjuster timestampAdjuster,
|
||||
SubtitleParser.Factory subtitleParserFactory,
|
||||
boolean parseSubtitlesDuringExtraction) {
|
||||
this.extractor = extractor;
|
||||
this.multivariantPlaylistFormat = multivariantPlaylistFormat;
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
this.subtitleParserFactory = subtitleParserFactory;
|
||||
this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput extractorOutput) {
|
||||
extractor.init(extractorOutput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean read(ExtractorInput extractorInput) throws IOException {
|
||||
return extractor.read(extractorInput, POSITION_HOLDER) == Extractor.RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPackedAudioExtractor() {
|
||||
Extractor underlyingExtractor = extractor.getUnderlyingImplementation();
|
||||
return underlyingExtractor instanceof AdtsExtractor
|
||||
|| underlyingExtractor instanceof Ac3Extractor
|
||||
|| underlyingExtractor instanceof Ac4Extractor
|
||||
|| underlyingExtractor instanceof Mp3Extractor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReusable() {
|
||||
Extractor underlyingExtractor = extractor.getUnderlyingImplementation();
|
||||
return underlyingExtractor instanceof TsExtractor
|
||||
|| underlyingExtractor instanceof FragmentedMp4Extractor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HlsMediaChunkExtractor recreate() {
|
||||
checkState(!isReusable());
|
||||
checkState(
|
||||
extractor.getUnderlyingImplementation() == extractor,
|
||||
"Can't recreate wrapped extractors. Outer type: %s",
|
||||
extractor.getClass());
|
||||
Extractor newExtractorInstance;
|
||||
// LINT.IfChange(extractor_instantiation)
|
||||
if (extractor instanceof WebvttExtractor) {
|
||||
newExtractorInstance =
|
||||
new WebvttExtractor(
|
||||
multivariantPlaylistFormat.language,
|
||||
timestampAdjuster,
|
||||
subtitleParserFactory,
|
||||
parseSubtitlesDuringExtraction);
|
||||
} else if (extractor instanceof AdtsExtractor) {
|
||||
newExtractorInstance = new AdtsExtractor();
|
||||
} else if (extractor instanceof Ac3Extractor) {
|
||||
newExtractorInstance = new Ac3Extractor();
|
||||
} else if (extractor instanceof Ac4Extractor) {
|
||||
newExtractorInstance = new Ac4Extractor();
|
||||
} else if (extractor instanceof Mp3Extractor) {
|
||||
newExtractorInstance = new Mp3Extractor();
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
"Unexpected extractor type for recreation: " + extractor.getClass().getSimpleName());
|
||||
}
|
||||
// LINT.ThenChange(DefaultHlsExtractorFactory.java:extractor_instantiation)
|
||||
return new BundledHlsMediaChunkExtractor(
|
||||
newExtractorInstance,
|
||||
multivariantPlaylistFormat,
|
||||
timestampAdjuster,
|
||||
subtitleParserFactory,
|
||||
parseSubtitlesDuringExtraction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTruncatedSegmentParsed() {
|
||||
extractor.seek(/* position= */ 0, /* timeUs= */ 0);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
|
||||
/** Default implementation of {@link HlsDataSourceFactory}. */
|
||||
@UnstableApi
|
||||
public final class DefaultHlsDataSourceFactory implements HlsDataSourceFactory {
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
/**
|
||||
* @param dataSourceFactory The {@link DataSource.Factory} to use for all data types.
|
||||
*/
|
||||
public DefaultHlsDataSourceFactory(DataSource.Factory dataSourceFactory) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource createDataSource(@C.DataType int dataType) {
|
||||
return dataSourceFactory.createDataSource();
|
||||
}
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static androidx.media3.extractor.ts.TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.FileTypes;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.TimestampAdjuster;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||
import androidx.media3.extractor.Extractor;
|
||||
import androidx.media3.extractor.ExtractorInput;
|
||||
import androidx.media3.extractor.mp3.Mp3Extractor;
|
||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
||||
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
|
||||
import androidx.media3.extractor.text.SubtitleParser;
|
||||
import androidx.media3.extractor.ts.Ac3Extractor;
|
||||
import androidx.media3.extractor.ts.Ac4Extractor;
|
||||
import androidx.media3.extractor.ts.AdtsExtractor;
|
||||
import androidx.media3.extractor.ts.DefaultTsPayloadReaderFactory;
|
||||
import androidx.media3.extractor.ts.TsExtractor;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Default {@link HlsExtractorFactory} implementation. */
|
||||
@UnstableApi
|
||||
public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|
||||
|
||||
// Extractors order is optimized according to
|
||||
// https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ.
|
||||
private static final int[] DEFAULT_EXTRACTOR_ORDER =
|
||||
new int[] {
|
||||
FileTypes.MP4,
|
||||
FileTypes.WEBVTT,
|
||||
FileTypes.TS,
|
||||
FileTypes.ADTS,
|
||||
FileTypes.AC3,
|
||||
FileTypes.AC4,
|
||||
FileTypes.MP3,
|
||||
};
|
||||
|
||||
private final @DefaultTsPayloadReaderFactory.Flags int payloadReaderFactoryFlags;
|
||||
|
||||
private SubtitleParser.Factory subtitleParserFactory;
|
||||
private boolean parseSubtitlesDuringExtraction;
|
||||
private @C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies;
|
||||
|
||||
private final boolean exposeCea608WhenMissingDeclarations;
|
||||
|
||||
/**
|
||||
* Equivalent to {@link #DefaultHlsExtractorFactory(int, boolean) new
|
||||
* DefaultHlsExtractorFactory(payloadReaderFactoryFlags = 0, exposeCea608WhenMissingDeclarations =
|
||||
* true)}
|
||||
*/
|
||||
public DefaultHlsExtractorFactory() {
|
||||
this(/* payloadReaderFactoryFlags= */ 0, /* exposeCea608WhenMissingDeclarations */ true);
|
||||
codecsToParseWithinGopSampleDependencies = C.VIDEO_CODEC_FLAG_H264 | C.VIDEO_CODEC_FLAG_H265;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a factory for HLS segment extractors.
|
||||
*
|
||||
* @param payloadReaderFactoryFlags Flags to add when constructing any {@link
|
||||
* DefaultTsPayloadReaderFactory} instances. Other flags may be added on top of {@code
|
||||
* payloadReaderFactoryFlags} when creating {@link DefaultTsPayloadReaderFactory}.
|
||||
* @param exposeCea608WhenMissingDeclarations Whether created {@link TsExtractor} instances should
|
||||
* expose a CEA-608 track should the multivariant playlist contain no Closed Captions
|
||||
* declarations. If the multivariant playlist contains any Closed Captions declarations, this
|
||||
* flag is ignored.
|
||||
*/
|
||||
public DefaultHlsExtractorFactory(
|
||||
int payloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations) {
|
||||
this.payloadReaderFactoryFlags = payloadReaderFactoryFlags;
|
||||
this.exposeCea608WhenMissingDeclarations = exposeCea608WhenMissingDeclarations;
|
||||
subtitleParserFactory = new DefaultSubtitleParserFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BundledHlsMediaChunkExtractor createExtractor(
|
||||
Uri uri,
|
||||
Format format,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
TimestampAdjuster timestampAdjuster,
|
||||
Map<String, List<String>> responseHeaders,
|
||||
ExtractorInput sniffingExtractorInput,
|
||||
PlayerId playerId)
|
||||
throws IOException {
|
||||
@FileTypes.Type
|
||||
int formatInferredFileType = FileTypes.inferFileTypeFromMimeType(format.sampleMimeType);
|
||||
@FileTypes.Type
|
||||
int responseHeadersInferredFileType =
|
||||
FileTypes.inferFileTypeFromResponseHeaders(responseHeaders);
|
||||
@FileTypes.Type int uriInferredFileType = FileTypes.inferFileTypeFromUri(uri);
|
||||
|
||||
// Defines the order in which to try the extractors.
|
||||
List<Integer> fileTypeOrder =
|
||||
new ArrayList<>(/* initialCapacity= */ DEFAULT_EXTRACTOR_ORDER.length);
|
||||
addFileTypeIfValidAndNotPresent(formatInferredFileType, fileTypeOrder);
|
||||
addFileTypeIfValidAndNotPresent(responseHeadersInferredFileType, fileTypeOrder);
|
||||
addFileTypeIfValidAndNotPresent(uriInferredFileType, fileTypeOrder);
|
||||
for (int fileType : DEFAULT_EXTRACTOR_ORDER) {
|
||||
addFileTypeIfValidAndNotPresent(fileType, fileTypeOrder);
|
||||
}
|
||||
|
||||
// Extractor to be used if the type is not recognized.
|
||||
@Nullable Extractor fallBackExtractor = null;
|
||||
sniffingExtractorInput.resetPeekPosition();
|
||||
for (int i = 0; i < fileTypeOrder.size(); i++) {
|
||||
int fileType = fileTypeOrder.get(i);
|
||||
Extractor extractor =
|
||||
checkNotNull(
|
||||
createExtractorByFileType(fileType, format, muxedCaptionFormats, timestampAdjuster));
|
||||
if (sniffQuietly(extractor, sniffingExtractorInput)) {
|
||||
return new BundledHlsMediaChunkExtractor(
|
||||
extractor,
|
||||
format,
|
||||
timestampAdjuster,
|
||||
subtitleParserFactory,
|
||||
parseSubtitlesDuringExtraction);
|
||||
}
|
||||
if (fallBackExtractor == null
|
||||
&& (fileType == formatInferredFileType
|
||||
|| fileType == responseHeadersInferredFileType
|
||||
|| fileType == uriInferredFileType
|
||||
|| fileType == FileTypes.TS)) {
|
||||
// If sniffing fails, fallback to the file types inferred from context. If all else fails,
|
||||
// fallback to Transport Stream. See https://github.com/google/ExoPlayer/issues/8219.
|
||||
fallBackExtractor = extractor;
|
||||
}
|
||||
}
|
||||
|
||||
return new BundledHlsMediaChunkExtractor(
|
||||
checkNotNull(fallBackExtractor),
|
||||
format,
|
||||
timestampAdjuster,
|
||||
subtitleParserFactory,
|
||||
parseSubtitlesDuringExtraction);
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public DefaultHlsExtractorFactory setSubtitleParserFactory(
|
||||
SubtitleParser.Factory subtitleParserFactory) {
|
||||
this.subtitleParserFactory = subtitleParserFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public DefaultHlsExtractorFactory experimentalParseSubtitlesDuringExtraction(
|
||||
boolean parseSubtitlesDuringExtraction) {
|
||||
this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public DefaultHlsExtractorFactory experimentalSetCodecsToParseWithinGopSampleDependencies(
|
||||
@C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies) {
|
||||
this.codecsToParseWithinGopSampleDependencies = codecsToParseWithinGopSampleDependencies;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>This implementation performs transcoding of the original format to {@link
|
||||
* MimeTypes#APPLICATION_MEDIA3_CUES} if it is supported by {@link SubtitleParser.Factory}.
|
||||
*
|
||||
* <p>To modify the support behavior, you can {@linkplain
|
||||
* #setSubtitleParserFactory(SubtitleParser.Factory) set your own subtitle parser factory}.
|
||||
*/
|
||||
@Override
|
||||
public Format getOutputTextFormat(Format sourceFormat) {
|
||||
if (parseSubtitlesDuringExtraction && subtitleParserFactory.supportsFormat(sourceFormat)) {
|
||||
return sourceFormat
|
||||
.buildUpon()
|
||||
.setSampleMimeType(MimeTypes.APPLICATION_MEDIA3_CUES)
|
||||
.setCueReplacementBehavior(subtitleParserFactory.getCueReplacementBehavior(sourceFormat))
|
||||
.setCodecs(
|
||||
sourceFormat.sampleMimeType
|
||||
+ (sourceFormat.codecs != null ? " " + sourceFormat.codecs : ""))
|
||||
.setSubsampleOffsetUs(Format.OFFSET_SAMPLE_RELATIVE)
|
||||
.build();
|
||||
} else {
|
||||
return sourceFormat;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addFileTypeIfValidAndNotPresent(
|
||||
@FileTypes.Type int fileType, List<Integer> fileTypes) {
|
||||
if (Ints.indexOf(DEFAULT_EXTRACTOR_ORDER, fileType) == -1 || fileTypes.contains(fileType)) {
|
||||
return;
|
||||
}
|
||||
fileTypes.add(fileType);
|
||||
}
|
||||
|
||||
@SuppressLint("SwitchIntDef") // HLS only supports a small subset of the defined file types.
|
||||
@Nullable
|
||||
private Extractor createExtractorByFileType(
|
||||
@FileTypes.Type int fileType,
|
||||
Format format,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
TimestampAdjuster timestampAdjuster) {
|
||||
// LINT.IfChange(extractor_instantiation)
|
||||
switch (fileType) {
|
||||
case FileTypes.WEBVTT:
|
||||
return new WebvttExtractor(
|
||||
format.language,
|
||||
timestampAdjuster,
|
||||
subtitleParserFactory,
|
||||
parseSubtitlesDuringExtraction);
|
||||
case FileTypes.ADTS:
|
||||
return new AdtsExtractor();
|
||||
case FileTypes.AC3:
|
||||
return new Ac3Extractor();
|
||||
case FileTypes.AC4:
|
||||
return new Ac4Extractor();
|
||||
case FileTypes.MP3:
|
||||
return new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);
|
||||
case FileTypes.MP4:
|
||||
return createFragmentedMp4Extractor(
|
||||
subtitleParserFactory,
|
||||
parseSubtitlesDuringExtraction,
|
||||
timestampAdjuster,
|
||||
muxedCaptionFormats,
|
||||
codecsToParseWithinGopSampleDependencies);
|
||||
case FileTypes.TS:
|
||||
return createTsExtractor(
|
||||
payloadReaderFactoryFlags,
|
||||
exposeCea608WhenMissingDeclarations,
|
||||
format,
|
||||
muxedCaptionFormats,
|
||||
timestampAdjuster,
|
||||
subtitleParserFactory,
|
||||
parseSubtitlesDuringExtraction);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
// LINT.ThenChange(BundledHlsMediaChunkExtractor.java:extractor_instantiation)
|
||||
}
|
||||
|
||||
private static TsExtractor createTsExtractor(
|
||||
@DefaultTsPayloadReaderFactory.Flags int userProvidedPayloadReaderFactoryFlags,
|
||||
boolean exposeCea608WhenMissingDeclarations,
|
||||
Format format,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
TimestampAdjuster timestampAdjuster,
|
||||
SubtitleParser.Factory subtitleParserFactory,
|
||||
boolean parseSubtitlesDuringExtraction) {
|
||||
@DefaultTsPayloadReaderFactory.Flags
|
||||
int payloadReaderFactoryFlags =
|
||||
DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM
|
||||
| userProvidedPayloadReaderFactoryFlags;
|
||||
if (muxedCaptionFormats != null) {
|
||||
// The playlist declares closed caption renditions, we should ignore descriptors.
|
||||
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS;
|
||||
} else if (exposeCea608WhenMissingDeclarations) {
|
||||
// The playlist does not provide any closed caption information. We preemptively declare a
|
||||
// closed caption track on channel 0.
|
||||
muxedCaptionFormats =
|
||||
Collections.singletonList(
|
||||
new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608).build());
|
||||
} else {
|
||||
muxedCaptionFormats = Collections.emptyList();
|
||||
}
|
||||
@Nullable String codecs = format.codecs;
|
||||
if (!TextUtils.isEmpty(codecs)) {
|
||||
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
|
||||
// exist. If we know from the codec attribute that they don't exist, then we can
|
||||
// explicitly ignore them even if they're declared.
|
||||
if (!MimeTypes.containsCodecsCorrespondingToMimeType(codecs, MimeTypes.AUDIO_AAC)) {
|
||||
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
|
||||
}
|
||||
if (!MimeTypes.containsCodecsCorrespondingToMimeType(codecs, MimeTypes.VIDEO_H264)) {
|
||||
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
|
||||
}
|
||||
}
|
||||
@TsExtractor.Flags int extractorFlags = 0;
|
||||
if (!parseSubtitlesDuringExtraction) {
|
||||
subtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED;
|
||||
extractorFlags |= TsExtractor.FLAG_EMIT_RAW_SUBTITLE_DATA;
|
||||
}
|
||||
return new TsExtractor(
|
||||
TsExtractor.MODE_HLS,
|
||||
extractorFlags,
|
||||
subtitleParserFactory,
|
||||
timestampAdjuster,
|
||||
new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats),
|
||||
DEFAULT_TIMESTAMP_SEARCH_BYTES);
|
||||
}
|
||||
|
||||
private static FragmentedMp4Extractor createFragmentedMp4Extractor(
|
||||
SubtitleParser.Factory subtitleParserFactory,
|
||||
boolean parseSubtitlesDuringExtraction,
|
||||
TimestampAdjuster timestampAdjuster,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
@C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies) {
|
||||
@FragmentedMp4Extractor.Flags int flags = FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK;
|
||||
if (!parseSubtitlesDuringExtraction) {
|
||||
subtitleParserFactory = SubtitleParser.Factory.UNSUPPORTED;
|
||||
flags |= FragmentedMp4Extractor.FLAG_EMIT_RAW_SUBTITLE_DATA;
|
||||
}
|
||||
flags |=
|
||||
FragmentedMp4Extractor.codecsToParseWithinGopSampleDependenciesAsFlags(
|
||||
codecsToParseWithinGopSampleDependencies);
|
||||
return new FragmentedMp4Extractor(
|
||||
subtitleParserFactory,
|
||||
flags,
|
||||
timestampAdjuster,
|
||||
/* sideloadedTrack= */ null,
|
||||
muxedCaptionFormats != null ? muxedCaptionFormats : ImmutableList.of(),
|
||||
/* additionalEmsgTrackOutput= */ null);
|
||||
}
|
||||
|
||||
private static boolean sniffQuietly(Extractor extractor, ExtractorInput input)
|
||||
throws IOException {
|
||||
boolean result = false;
|
||||
try {
|
||||
result = extractor.sniff(input);
|
||||
} catch (EOFException e) {
|
||||
// Do nothing.
|
||||
} finally {
|
||||
input.resetPeekPosition();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* LRU cache that holds up to {@code maxSize} full-segment-encryption keys. Which each addition,
|
||||
* once the cache's size exceeds {@code maxSize}, the oldest item (according to insertion order) is
|
||||
* removed.
|
||||
*/
|
||||
/* package */ final class FullSegmentEncryptionKeyCache {
|
||||
|
||||
private final LinkedHashMap<Uri, byte[]> backingMap;
|
||||
|
||||
public FullSegmentEncryptionKeyCache(int maxSize) {
|
||||
backingMap =
|
||||
new LinkedHashMap<Uri, byte[]>(
|
||||
/* initialCapacity= */ maxSize + 1, /* loadFactor= */ 1, /* accessOrder= */ false) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<Uri, byte[]> eldest) {
|
||||
return size() > maxSize;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code encryptionKey} cached against this {@code uri}, or null if {@code uri} is
|
||||
* null or not present in the cache.
|
||||
*/
|
||||
@Nullable
|
||||
public byte[] get(@Nullable Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
return backingMap.get(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an entry into the cache.
|
||||
*
|
||||
* @throws NullPointerException if {@code uri} or {@code encryptionKey} are null.
|
||||
*/
|
||||
@Nullable
|
||||
public byte[] put(Uri uri, byte[] encryptionKey) {
|
||||
return backingMap.put(checkNotNull(uri), checkNotNull(encryptionKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code uri} is present in the cache.
|
||||
*
|
||||
* @throws NullPointerException if {@code uri} is null.
|
||||
*/
|
||||
public boolean containsUri(Uri uri) {
|
||||
return backingMap.containsKey(checkNotNull(uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes {@code uri} from the cache. If {@code uri} was present in the cahce, this returns the
|
||||
* corresponding {@code encryptionKey}, otherwise null.
|
||||
*
|
||||
* @throws NullPointerException if {@code uri} is null.
|
||||
*/
|
||||
@Nullable
|
||||
public byte[] remove(Uri uri) {
|
||||
return backingMap.remove(checkNotNull(uri));
|
||||
}
|
||||
}
|
||||
+1410
File diff suppressed because it is too large
Load Diff
+353
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* Copyright 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static androidx.media3.common.C.DATA_TYPE_STEERING_MANIFEST;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsRedundantGroup;
|
||||
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
||||
import androidx.media3.exoplayer.upstream.BandwidthMeter;
|
||||
import androidx.media3.exoplayer.upstream.contentsteering.ContentSteeringTracker;
|
||||
import androidx.media3.exoplayer.upstream.contentsteering.SteeringManifest;
|
||||
import androidx.media3.exoplayer.upstream.contentsteering.SteeringManifestTracker;
|
||||
import androidx.media3.exoplayer.util.ReleasableExecutor;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/** Tracks the content steering states for an HLS stream. */
|
||||
@UnstableApi
|
||||
public final class HlsContentSteeringTracker implements ContentSteeringTracker {
|
||||
|
||||
/** A callback to be notified of {@link HlsContentSteeringTracker} events. */
|
||||
public interface Callback {
|
||||
|
||||
/**
|
||||
* Called when the current pathway is updated.
|
||||
*
|
||||
* @param currentPathwayId The current pathway ID after the update.
|
||||
* @param previousPathwayId The pathway ID before the update, or {@code null} if the call of
|
||||
* this method is the result of starting the tracker.
|
||||
* @param previousPathwayExcludeDurationMs The exclude duration in milliseconds if the update is
|
||||
* due to {@linkplain #excludeCurrentPathway(long) the exclusion of the previous pathway},
|
||||
* or {@link C#TIME_UNSET} if the previous pathway is not excluded.
|
||||
*/
|
||||
void onCurrentPathwayUpdated(
|
||||
String currentPathwayId,
|
||||
@Nullable String previousPathwayId,
|
||||
long previousPathwayExcludeDurationMs);
|
||||
|
||||
/**
|
||||
* Called when a new pathway cloned from an existing pathway becomes available.
|
||||
*
|
||||
* <p>The lists {@code newPlaylistUrls} and {@code basePlaylistUrls} have the equal size, and
|
||||
* each URI in the {@code newPlaylistUrls} is cloned from the base URI in the {@code
|
||||
* basePlaylistUrls} of the same index.
|
||||
*
|
||||
* @param newPathwayId The new pathway ID.
|
||||
* @param basePathwayId The base pathway ID.
|
||||
* @param newPlaylistUrls The list of new playlist URLs.
|
||||
* @param basePlaylistUrls The list of base playlist URLs.
|
||||
*/
|
||||
void onNewPathwayAvailable(
|
||||
String newPathwayId,
|
||||
String basePathwayId,
|
||||
ImmutableList<Uri> newPlaylistUrls,
|
||||
ImmutableList<Uri> basePlaylistUrls);
|
||||
}
|
||||
|
||||
private static final String PATHWAY_PARAM = "_HLS_pathway";
|
||||
private static final String THROUGHPUT_PARAM = "_HLS_throughput";
|
||||
|
||||
private final SteeringManifestTracker steeringManifestTracker;
|
||||
@Nullable private final Callback callback;
|
||||
private final BandwidthMeter bandwidthMeter;
|
||||
private final List<HlsRedundantGroup> variantRedundantGroups;
|
||||
private final List<HlsRedundantGroup> videoRenditionRedundantGroups;
|
||||
private final List<HlsRedundantGroup> audioRenditionRedundantGroups;
|
||||
private final List<HlsRedundantGroup> subtitleRenditionRedundantGroups;
|
||||
private final Set<String> availablePathwayIds;
|
||||
private final Set<String> excludedPathwayIds;
|
||||
private final HandlerWrapper handler;
|
||||
private boolean isActive;
|
||||
private String currentPathwayId;
|
||||
@Nullable private ImmutableList<String> currentPathwayPriority;
|
||||
|
||||
/**
|
||||
* Creates an {@link HlsContentSteeringTracker}.
|
||||
*
|
||||
* @param dataSourceFactory The {@link HlsDataSourceFactory} to create data sources for loading
|
||||
* steering manifests.
|
||||
* @param downloadExecutorSupplier A supplier to obtain a {@link ReleasableExecutor} for
|
||||
* downloading steering manifests, or {@code null}.
|
||||
* @param playlistTracker The {@link HlsPlaylistTracker}.
|
||||
* @param callback A {@link Callback} to receive events, or {@code null}.
|
||||
* @param bandwidthMeter The {@link BandwidthMeter} to obtain throughput estimates.
|
||||
* @param clock The {@link Clock} to schedule handler messages.
|
||||
*/
|
||||
public HlsContentSteeringTracker(
|
||||
HlsDataSourceFactory dataSourceFactory,
|
||||
@Nullable Supplier<ReleasableExecutor> downloadExecutorSupplier,
|
||||
HlsPlaylistTracker playlistTracker,
|
||||
@Nullable Callback callback,
|
||||
BandwidthMeter bandwidthMeter,
|
||||
Clock clock) {
|
||||
this.steeringManifestTracker =
|
||||
new SteeringManifestTracker(
|
||||
() -> dataSourceFactory.createDataSource(DATA_TYPE_STEERING_MANIFEST),
|
||||
downloadExecutorSupplier);
|
||||
this.callback = callback;
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
this.variantRedundantGroups =
|
||||
checkNotNull(playlistTracker.getRedundantGroups(HlsRedundantGroup.VARIANT));
|
||||
this.videoRenditionRedundantGroups =
|
||||
checkNotNull(playlistTracker.getRedundantGroups(HlsRedundantGroup.VIDEO_RENDITION));
|
||||
this.audioRenditionRedundantGroups =
|
||||
checkNotNull(playlistTracker.getRedundantGroups(HlsRedundantGroup.AUDIO_RENDITION));
|
||||
this.subtitleRenditionRedundantGroups =
|
||||
checkNotNull(playlistTracker.getRedundantGroups(HlsRedundantGroup.SUBTITLE_RENDITION));
|
||||
checkState(!variantRedundantGroups.isEmpty());
|
||||
availablePathwayIds =
|
||||
new HashSet<>(checkNotNull(variantRedundantGroups.get(0)).getAllPathwayIds());
|
||||
excludedPathwayIds = new HashSet<>();
|
||||
handler = clock.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null);
|
||||
currentPathwayId = variantRedundantGroups.get(0).getCurrentPathwayId();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @throws IllegalStateException If the {@code initialPathwayId} is not declared in the redundant
|
||||
* group.
|
||||
*/
|
||||
@Override
|
||||
public void start(
|
||||
Uri initialSteeringManifestUri,
|
||||
@Nullable String initialPathwayId,
|
||||
MediaSourceEventListener.EventDispatcher eventDispatcher) {
|
||||
if (initialPathwayId != null) {
|
||||
checkState(availablePathwayIds.contains(initialPathwayId));
|
||||
currentPathwayId = initialPathwayId;
|
||||
}
|
||||
isActive = true;
|
||||
notifyOnCurrentPathwayUpdated(
|
||||
/* previousPathwayId= */ null, /* previousPathwayExcludeDurationMs= */ C.TIME_UNSET);
|
||||
steeringManifestTracker.start(
|
||||
initialSteeringManifestUri, new SteeringManifestTrackerCallback(), eventDispatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean excludeCurrentPathway(long excludeDurationMs) {
|
||||
if (isActive && currentPathwayPriority != null) {
|
||||
String previousPathwayId = currentPathwayId;
|
||||
performPathwayEvaluationAndUpdate(
|
||||
/* previousPathwayIdExcludeDurationMs= */ excludeDurationMs);
|
||||
if (!currentPathwayId.equals(previousPathwayId)) {
|
||||
excludedPathwayIds.add(previousPathwayId);
|
||||
handler.postDelayed(() -> expireExclusion(previousPathwayId), excludeDurationMs);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
stopInternal();
|
||||
}
|
||||
|
||||
private void expireExclusion(String pathwayId) {
|
||||
checkState(isActive);
|
||||
excludedPathwayIds.remove(pathwayId);
|
||||
if (currentPathwayPriority != null) {
|
||||
performPathwayEvaluationAndUpdate(/* previousPathwayIdExcludeDurationMs= */ C.TIME_UNSET);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopInternal() {
|
||||
steeringManifestTracker.stop();
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
@RequiresNonNull("currentPathwayPriority")
|
||||
private void performPathwayEvaluationAndUpdate(long previousPathwayIdExcludeDurationMs) {
|
||||
String previousPathwayId = currentPathwayId;
|
||||
for (String pathwayId : currentPathwayPriority) {
|
||||
if (previousPathwayIdExcludeDurationMs != C.TIME_UNSET
|
||||
&& pathwayId.equals(previousPathwayId)) {
|
||||
continue;
|
||||
}
|
||||
if (availablePathwayIds.contains(pathwayId) && !excludedPathwayIds.contains(pathwayId)) {
|
||||
currentPathwayId = pathwayId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!currentPathwayId.equals(previousPathwayId)) {
|
||||
notifyOnCurrentPathwayUpdated(previousPathwayId, previousPathwayIdExcludeDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
private void performPathwayClones(ImmutableList<SteeringManifest.PathwayClone> pathwayClones) {
|
||||
for (SteeringManifest.PathwayClone pathwayClone : pathwayClones) {
|
||||
if (!availablePathwayIds.contains(pathwayClone.baseId)
|
||||
|| availablePathwayIds.contains(pathwayClone.id)) {
|
||||
// We should ignore a PathwayClone if its baseId is unknown to the tracker, or the new
|
||||
// pathwayId of the clone matches any existing pathwayId.
|
||||
continue;
|
||||
}
|
||||
ImmutableList.Builder<Uri> newPlaylistUrls = new ImmutableList.Builder<>();
|
||||
ImmutableList.Builder<Uri> basePlaylistUrls = new ImmutableList.Builder<>();
|
||||
performPathwayClonesForVariants(
|
||||
pathwayClone, variantRedundantGroups, newPlaylistUrls, basePlaylistUrls);
|
||||
performPathwayClonesForRenditions(
|
||||
pathwayClone, videoRenditionRedundantGroups, newPlaylistUrls, basePlaylistUrls);
|
||||
performPathwayClonesForRenditions(
|
||||
pathwayClone, audioRenditionRedundantGroups, newPlaylistUrls, basePlaylistUrls);
|
||||
performPathwayClonesForRenditions(
|
||||
pathwayClone, subtitleRenditionRedundantGroups, newPlaylistUrls, basePlaylistUrls);
|
||||
availablePathwayIds.add(pathwayClone.id);
|
||||
if (callback != null) {
|
||||
callback.onNewPathwayAvailable(
|
||||
pathwayClone.id,
|
||||
pathwayClone.baseId,
|
||||
newPlaylistUrls.build(),
|
||||
basePlaylistUrls.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void performPathwayClonesForVariants(
|
||||
SteeringManifest.PathwayClone pathwayClone,
|
||||
List<HlsRedundantGroup> variantRedundantGroups,
|
||||
ImmutableList.Builder<Uri> newPlaylistUrls,
|
||||
ImmutableList.Builder<Uri> basePlaylistUrls) {
|
||||
ImmutableMap<String, Uri> perVariantUris = pathwayClone.uriReplacement.perVariantUris;
|
||||
for (HlsRedundantGroup variantRedundantGroup : variantRedundantGroups) {
|
||||
String basePathwayId = pathwayClone.baseId;
|
||||
Uri basePlaylistUrl = checkNotNull(variantRedundantGroup.getPlaylistUrl(basePathwayId));
|
||||
@Nullable
|
||||
Uri newPlaylistUrl =
|
||||
perVariantUris.containsKey(variantRedundantGroup.groupKey.stableId)
|
||||
? checkNotNull(perVariantUris.get(variantRedundantGroup.groupKey.stableId))
|
||||
: getUriBuilder(basePlaylistUrl, pathwayClone.uriReplacement).build();
|
||||
if (newPlaylistUrl != null) {
|
||||
newPlaylistUrls.add(newPlaylistUrl);
|
||||
basePlaylistUrls.add(basePlaylistUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void performPathwayClonesForRenditions(
|
||||
SteeringManifest.PathwayClone pathwayClone,
|
||||
List<HlsRedundantGroup> renditionRedundantGroups,
|
||||
ImmutableList.Builder<Uri> newPlaylistUrls,
|
||||
ImmutableList.Builder<Uri> basePlaylistUrls) {
|
||||
ImmutableMap<String, Uri> perRenditionUris = pathwayClone.uriReplacement.perRenditionUris;
|
||||
for (HlsRedundantGroup renditionRedundantGroup : renditionRedundantGroups) {
|
||||
String basePathwayId = pathwayClone.baseId;
|
||||
Uri basePlaylistUrl = checkNotNull(renditionRedundantGroup.getPlaylistUrl(basePathwayId));
|
||||
@Nullable
|
||||
Uri newPlaylistUrl =
|
||||
perRenditionUris.containsKey(renditionRedundantGroup.groupKey.stableId)
|
||||
? checkNotNull(perRenditionUris.get(renditionRedundantGroup.groupKey.stableId))
|
||||
: getUriBuilder(basePlaylistUrl, pathwayClone.uriReplacement).build();
|
||||
if (newPlaylistUrl != null) {
|
||||
newPlaylistUrls.add(newPlaylistUrl);
|
||||
basePlaylistUrls.add(basePlaylistUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Uri.Builder getUriBuilder(Uri url, SteeringManifest.UriReplacement uriReplacement) {
|
||||
Uri.Builder newUrlBuilder = url.buildUpon().clearQuery();
|
||||
if (uriReplacement.host != null) {
|
||||
newUrlBuilder.authority(uriReplacement.host);
|
||||
}
|
||||
// Combine existing and new query parameters, giving precedence to new ones.
|
||||
Map<String, String> combinedParams = new TreeMap<>();
|
||||
// Add existing parameters.
|
||||
for (String existingParamName : url.getQueryParameterNames()) {
|
||||
combinedParams.put(existingParamName, checkNotNull(url.getQueryParameter(existingParamName)));
|
||||
}
|
||||
// Add new parameters, overwriting existing ones if keys clash.
|
||||
combinedParams.putAll(uriReplacement.params);
|
||||
// Append all parameters from the sorted map.
|
||||
for (Map.Entry<String, String> param : combinedParams.entrySet()) {
|
||||
newUrlBuilder.appendQueryParameter(param.getKey(), param.getValue());
|
||||
}
|
||||
return newUrlBuilder;
|
||||
}
|
||||
|
||||
private void notifyOnCurrentPathwayUpdated(
|
||||
@Nullable String previousPathwayId, long previousPathwayExcludeDurationMs) {
|
||||
if (callback != null) {
|
||||
callback.onCurrentPathwayUpdated(
|
||||
currentPathwayId, previousPathwayId, previousPathwayExcludeDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
private class SteeringManifestTrackerCallback implements SteeringManifestTracker.Callback {
|
||||
|
||||
@Override
|
||||
public ImmutableMap<String, String> getSteeringQueryParameters() {
|
||||
checkState(isActive);
|
||||
return ImmutableMap.of(
|
||||
PATHWAY_PARAM,
|
||||
currentPathwayId,
|
||||
THROUGHPUT_PARAM,
|
||||
String.valueOf(bandwidthMeter.getBitrateEstimate()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSteeringManifestUpdated(SteeringManifest steeringManifest) {
|
||||
checkState(isActive);
|
||||
performPathwayClones(steeringManifest.pathwayClones);
|
||||
currentPathwayPriority = steeringManifest.pathwayPriority;
|
||||
performPathwayEvaluationAndUpdate(/* previousPathwayIdExcludeDurationMs= */ C.TIME_UNSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSteeringManifestLoadError(IOException error, boolean canceled) {
|
||||
checkState(isActive);
|
||||
if (canceled && currentPathwayPriority == null) {
|
||||
stopInternal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
|
||||
/** Creates {@link DataSource}s for HLS playlists, encryption and media chunks. */
|
||||
@UnstableApi
|
||||
public interface HlsDataSourceFactory {
|
||||
|
||||
/**
|
||||
* Creates a {@link DataSource} for the given data type.
|
||||
*
|
||||
* @param dataType The {@link C.DataType} for which the {@link DataSource} will be used.
|
||||
* @return A {@link DataSource} for the given data type.
|
||||
*/
|
||||
DataSource createDataSource(@C.DataType int dataType);
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.ExperimentalApi;
|
||||
import androidx.media3.common.util.TimestampAdjuster;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||
import androidx.media3.extractor.Extractor;
|
||||
import androidx.media3.extractor.ExtractorInput;
|
||||
import androidx.media3.extractor.PositionHolder;
|
||||
import androidx.media3.extractor.mp4.FragmentedMp4Extractor;
|
||||
import androidx.media3.extractor.text.SubtitleParser;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Factory for HLS media chunk extractors. */
|
||||
@UnstableApi
|
||||
public interface HlsExtractorFactory {
|
||||
|
||||
/**
|
||||
* @deprecated {@code HlsExtractorFactory} instances are mutable, so sharing one in a static field
|
||||
* is not safe. Construct a new instance of {@link DefaultHlsExtractorFactory} for each usage
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated HlsExtractorFactory DEFAULT = new DefaultHlsExtractorFactory();
|
||||
|
||||
/**
|
||||
* Creates an {@link Extractor} for extracting HLS media chunks.
|
||||
*
|
||||
* @param uri The URI of the media chunk.
|
||||
* @param format A {@link Format} associated with the chunk to extract.
|
||||
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
|
||||
* information is available in the multivariant playlist.
|
||||
* @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
|
||||
* @param responseHeaders The HTTP response headers associated with the media segment or
|
||||
* initialization section to extract.
|
||||
* @param sniffingExtractorInput The first extractor input that will be passed to the returned
|
||||
* extractor's {@link Extractor#read(ExtractorInput, PositionHolder)}. Must only be used to
|
||||
* call {@link Extractor#sniff(ExtractorInput)}.
|
||||
* @param playerId The {@link PlayerId} of the player using this extractors factory.
|
||||
* @return An {@link HlsMediaChunkExtractor}.
|
||||
* @throws IOException If an I/O error is encountered while sniffing.
|
||||
*/
|
||||
HlsMediaChunkExtractor createExtractor(
|
||||
Uri uri,
|
||||
Format format,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
TimestampAdjuster timestampAdjuster,
|
||||
Map<String, List<String>> responseHeaders,
|
||||
ExtractorInput sniffingExtractorInput,
|
||||
PlayerId playerId)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Sets the {@link SubtitleParser.Factory} to use for parsing subtitles during extraction. The
|
||||
* default factory value is implementation dependent.
|
||||
*
|
||||
* @param subtitleParserFactory The {@link SubtitleParser.Factory} for parsing subtitles during
|
||||
* extraction.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
default HlsExtractorFactory setSubtitleParserFactory(
|
||||
SubtitleParser.Factory subtitleParserFactory) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether subtitles should be parsed as part of extraction (before being added to the sample
|
||||
* queue) or as part of rendering (when being taken from the sample queue). Defaults to {@code
|
||||
* false} (i.e. subtitles will be parsed as part of rendering).
|
||||
*
|
||||
* <p>This method is experimental and will be renamed or removed in a future release.
|
||||
*
|
||||
* @param parseSubtitlesDuringExtraction Whether to parse subtitles during extraction or
|
||||
* rendering.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@ExperimentalApi // TODO: b/289983417 - Remove legacy subtitle decoding paths.
|
||||
default HlsExtractorFactory experimentalParseSubtitlesDuringExtraction(
|
||||
boolean parseSubtitlesDuringExtraction) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the set of video codecs for which within GOP sample dependency information should be
|
||||
* parsed as part of extraction. Defaults to H.264 and H.265.
|
||||
*
|
||||
* <p>Having access to additional sample dependency information can speed up seeking. See {@link
|
||||
* FragmentedMp4Extractor#FLAG_READ_WITHIN_GOP_SAMPLE_DEPENDENCIES}.
|
||||
*
|
||||
* <p>This method is experimental and will be renamed or removed in a future release.
|
||||
*
|
||||
* @param codecsToParseWithinGopSampleDependencies The set of codecs for which to parse within GOP
|
||||
* sample dependency information.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@ExperimentalApi // TODO: b/470365670 - Remove method once config is enabled by default.
|
||||
default HlsExtractorFactory experimentalSetCodecsToParseWithinGopSampleDependencies(
|
||||
@C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output {@link Format} of emitted {@linkplain C#TRACK_TYPE_TEXT text samples} which
|
||||
* were originally in {@code sourceFormat}.
|
||||
*
|
||||
* <p>In many cases, where an {@link Extractor} emits samples from the source without mutation,
|
||||
* this method simply returns {@code sourceFormat}. In other cases, such as an {@link Extractor}
|
||||
* that transcodes subtitles from the {@code sourceFormat} to {@link
|
||||
* MimeTypes#APPLICATION_MEDIA3_CUES}, the format is updated to indicate the transcoding that is
|
||||
* taking place.
|
||||
*
|
||||
* <p>Non-text source formats are always returned without mutation.
|
||||
*
|
||||
* @param sourceFormat The original text-based format.
|
||||
* @return The {@link Format} that will be associated with a {@linkplain C#TRACK_TYPE_TEXT text
|
||||
* track}.
|
||||
*/
|
||||
default Format getOutputTextFormat(Format sourceFormat) {
|
||||
return sourceFormat;
|
||||
}
|
||||
}
|
||||
+2694
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist;
|
||||
|
||||
/** Holds a multivariant playlist along with a snapshot of one of its media playlists. */
|
||||
@UnstableApi
|
||||
public final class HlsManifest {
|
||||
|
||||
/** The multivariant playlist of an HLS stream. */
|
||||
public final HlsMultivariantPlaylist multivariantPlaylist;
|
||||
|
||||
/** A snapshot of a media playlist referred to by {@link #multivariantPlaylist}. */
|
||||
public final HlsMediaPlaylist mediaPlaylist;
|
||||
|
||||
/**
|
||||
* @param multivariantPlaylist The multivariant playlist.
|
||||
* @param mediaPlaylist The media playlist.
|
||||
*/
|
||||
/* package */ HlsManifest(
|
||||
HlsMultivariantPlaylist multivariantPlaylist, HlsMediaPlaylist mediaPlaylist) {
|
||||
this.multivariantPlaylist = multivariantPlaylist;
|
||||
this.mediaPlaylist = mediaPlaylist;
|
||||
}
|
||||
}
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static androidx.media3.datasource.DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.DrmInitData;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.ParserException;
|
||||
import androidx.media3.common.util.ParsableByteArray;
|
||||
import androidx.media3.common.util.TimestampAdjuster;
|
||||
import androidx.media3.common.util.UriUtil;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DataSourceUtil;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
|
||||
import androidx.media3.exoplayer.source.chunk.MediaChunk;
|
||||
import androidx.media3.exoplayer.upstream.CmcdData;
|
||||
import androidx.media3.extractor.DefaultExtractorInput;
|
||||
import androidx.media3.extractor.ExtractorInput;
|
||||
import androidx.media3.extractor.metadata.id3.Id3Decoder;
|
||||
import androidx.media3.extractor.metadata.id3.PrivFrame;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/** An HLS {@link MediaChunk}. */
|
||||
/* package */ final class HlsMediaChunk extends MediaChunk {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param extractorFactory An {@link HlsExtractorFactory} for creating {@link
|
||||
* HlsMediaChunkExtractor}s.
|
||||
* @param dataSource The {@link DataSource} for loading the data.
|
||||
* @param format The chunk {@link Format}.
|
||||
* @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in microseconds.
|
||||
* @param mediaPlaylist The {@link HlsMediaPlaylist} from which this chunk was obtained.
|
||||
* @param segmentBaseHolder The segment holder.
|
||||
* @param playlistUrl The url of the playlist from which this chunk was obtained.
|
||||
* @param steeredPathwayId The ID of the steered pathway from which data is being loaded, or
|
||||
* {@code null} if not applicable.
|
||||
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
|
||||
* information is available in the multivariant playlist.
|
||||
* @param trackSelectionReason See {@link #trackSelectionReason}.
|
||||
* @param trackSelectionData See {@link #trackSelectionData}.
|
||||
* @param isPrimaryTimestampSource Whether this chunk is providing the timestamp source.
|
||||
* @param timestampAdjusterProvider A provider of {@link TimestampAdjuster}s.
|
||||
* @param timestampAdjusterInitializationTimeoutMs The timeout for waiting for the timestamp
|
||||
* adjuster to be initialized, in milliseconds.
|
||||
* @param previousChunk The previous chunk in the output, or null.
|
||||
* @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise.
|
||||
* @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
|
||||
* otherwise.
|
||||
* @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples.
|
||||
* @param isIndependent Whether the chunk starts with a keyframe.
|
||||
* @param playerId The {@link PlayerId} of the player.
|
||||
* @param cmcdDataFactory The {@link CmcdData.Factory} for generating {@link CmcdData}.
|
||||
*/
|
||||
public static HlsMediaChunk createInstance(
|
||||
HlsExtractorFactory extractorFactory,
|
||||
DataSource dataSource,
|
||||
Format format,
|
||||
long startOfPlaylistInPeriodUs,
|
||||
HlsMediaPlaylist mediaPlaylist,
|
||||
HlsChunkSource.SegmentBaseHolder segmentBaseHolder,
|
||||
Uri playlistUrl,
|
||||
@Nullable String steeredPathwayId,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
@C.SelectionReason int trackSelectionReason,
|
||||
@Nullable Object trackSelectionData,
|
||||
boolean isPrimaryTimestampSource,
|
||||
TimestampAdjusterProvider timestampAdjusterProvider,
|
||||
long timestampAdjusterInitializationTimeoutMs,
|
||||
@Nullable HlsMediaChunk previousChunk,
|
||||
@Nullable byte[] mediaSegmentKey,
|
||||
@Nullable byte[] initSegmentKey,
|
||||
boolean shouldSpliceIn,
|
||||
boolean isIndependent,
|
||||
PlayerId playerId,
|
||||
@Nullable CmcdData.Factory cmcdDataFactory) {
|
||||
// Media segment.
|
||||
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
|
||||
DataSpec dataSpec =
|
||||
new DataSpec.Builder()
|
||||
.setUri(UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url))
|
||||
.setPosition(mediaSegment.byteRangeOffset)
|
||||
.setLength(mediaSegment.byteRangeLength)
|
||||
.setFlags(segmentBaseHolder.isPreload ? FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED : 0)
|
||||
.build();
|
||||
if (cmcdDataFactory != null) {
|
||||
CmcdData cmcdData = cmcdDataFactory.createCmcdData();
|
||||
dataSpec = cmcdData.addToDataSpec(dataSpec);
|
||||
}
|
||||
|
||||
boolean mediaSegmentEncrypted = mediaSegmentKey != null;
|
||||
@Nullable
|
||||
byte[] mediaSegmentIv =
|
||||
mediaSegmentEncrypted
|
||||
? getEncryptionIvArray(checkNotNull(mediaSegment.encryptionIV))
|
||||
: null;
|
||||
DataSource mediaDataSource = buildDataSource(dataSource, mediaSegmentKey, mediaSegmentIv);
|
||||
|
||||
// Init segment.
|
||||
HlsMediaPlaylist.Segment initSegment = mediaSegment.initializationSegment;
|
||||
DataSpec initDataSpec = null;
|
||||
boolean initSegmentEncrypted = false;
|
||||
@Nullable DataSource initDataSource = null;
|
||||
if (initSegment != null) {
|
||||
initSegmentEncrypted = initSegmentKey != null;
|
||||
@Nullable
|
||||
byte[] initSegmentIv =
|
||||
initSegmentEncrypted
|
||||
? getEncryptionIvArray(checkNotNull(initSegment.encryptionIV))
|
||||
: null;
|
||||
Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
|
||||
initDataSpec =
|
||||
new DataSpec.Builder()
|
||||
.setUri(initSegmentUri)
|
||||
.setPosition(initSegment.byteRangeOffset)
|
||||
.setLength(initSegment.byteRangeLength)
|
||||
.build();
|
||||
if (cmcdDataFactory != null) {
|
||||
CmcdData cmcdData =
|
||||
cmcdDataFactory.setObjectType(CmcdData.OBJECT_TYPE_INIT_SEGMENT).createCmcdData();
|
||||
initDataSpec = cmcdData.addToDataSpec(initDataSpec);
|
||||
}
|
||||
|
||||
initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv);
|
||||
}
|
||||
|
||||
long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + mediaSegment.relativeStartTimeUs;
|
||||
long segmentEndTimeInPeriodUs = segmentStartTimeInPeriodUs + mediaSegment.durationUs;
|
||||
int discontinuitySequenceNumber =
|
||||
mediaPlaylist.discontinuitySequence + mediaSegment.relativeDiscontinuitySequence;
|
||||
|
||||
@Nullable HlsMediaChunkExtractor previousExtractor = null;
|
||||
Id3Decoder id3Decoder;
|
||||
ParsableByteArray scratchId3Data;
|
||||
|
||||
if (previousChunk != null) {
|
||||
boolean isSameInitData =
|
||||
initDataSpec == previousChunk.initDataSpec
|
||||
|| (initDataSpec != null
|
||||
&& previousChunk.initDataSpec != null
|
||||
&& initDataSpec.uri.equals(previousChunk.initDataSpec.uri)
|
||||
&& initDataSpec.position == previousChunk.initDataSpec.position);
|
||||
boolean isFollowingChunk =
|
||||
playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted;
|
||||
id3Decoder = previousChunk.id3Decoder;
|
||||
scratchId3Data = previousChunk.scratchId3Data;
|
||||
previousExtractor =
|
||||
isSameInitData
|
||||
&& isFollowingChunk
|
||||
&& !previousChunk.extractorInvalidated
|
||||
&& previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber
|
||||
? previousChunk.extractor
|
||||
: null;
|
||||
} else {
|
||||
id3Decoder = new Id3Decoder();
|
||||
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
|
||||
}
|
||||
return new HlsMediaChunk(
|
||||
extractorFactory,
|
||||
mediaDataSource,
|
||||
dataSpec,
|
||||
format,
|
||||
mediaSegmentEncrypted,
|
||||
initDataSource,
|
||||
initDataSpec,
|
||||
initSegmentEncrypted,
|
||||
playlistUrl,
|
||||
muxedCaptionFormats,
|
||||
trackSelectionReason,
|
||||
trackSelectionData,
|
||||
segmentStartTimeInPeriodUs,
|
||||
segmentEndTimeInPeriodUs,
|
||||
segmentBaseHolder.mediaSequence,
|
||||
segmentBaseHolder.partIndex,
|
||||
/* isPublished= */ !segmentBaseHolder.isPreload,
|
||||
discontinuitySequenceNumber,
|
||||
mediaSegment.hasGapTag,
|
||||
isPrimaryTimestampSource,
|
||||
/* timestampAdjuster= */ timestampAdjusterProvider.getAdjuster(discontinuitySequenceNumber),
|
||||
timestampAdjusterInitializationTimeoutMs,
|
||||
mediaSegment.drmInitData,
|
||||
previousExtractor,
|
||||
id3Decoder,
|
||||
scratchId3Data,
|
||||
shouldSpliceIn,
|
||||
isIndependent,
|
||||
playerId,
|
||||
steeredPathwayId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether samples of a new HLS media chunk should be spliced into existing samples.
|
||||
*
|
||||
* @param previousChunk The previous existing media chunk, or null if the new chunk is the first
|
||||
* in the queue.
|
||||
* @param bufferedPositionUs The position in the sample stream in microseconds since the start of
|
||||
* the period up to which data is already buffered.
|
||||
* @param playlistUrl The URL of the playlist from which the new chunk will be obtained.
|
||||
* @param isIndependent Whether the new chunk is independent (i.e, starts with a keyframe).
|
||||
* @param segmentBaseHolder The {@link HlsChunkSource.SegmentBaseHolder} with information about
|
||||
* the new chunk.
|
||||
* @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in microseconds.
|
||||
* @return Whether samples of the new chunk should be spliced into existing samples.
|
||||
*/
|
||||
public static boolean shouldSpliceIn(
|
||||
@Nullable HlsMediaChunk previousChunk,
|
||||
long bufferedPositionUs,
|
||||
Uri playlistUrl,
|
||||
boolean isIndependent,
|
||||
HlsChunkSource.SegmentBaseHolder segmentBaseHolder,
|
||||
long startOfPlaylistInPeriodUs) {
|
||||
if (previousChunk == null) {
|
||||
// First chunk doesn't require splicing.
|
||||
return false;
|
||||
}
|
||||
if (playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted) {
|
||||
// Continuing with the next chunk in the same playlist after fully loading the previous chunk
|
||||
// (i.e. the load wasn't cancelled or failed) is always possible.
|
||||
return false;
|
||||
}
|
||||
// Changing playlists or continuing after a chunk cancellation/failure requires independent,
|
||||
// non-overlapping segments to avoid the splice.
|
||||
long segmentStartTimeInPeriodUs =
|
||||
startOfPlaylistInPeriodUs + segmentBaseHolder.segmentBase.relativeStartTimeUs;
|
||||
return !isIndependent || segmentStartTimeInPeriodUs < bufferedPositionUs;
|
||||
}
|
||||
|
||||
public static final String PRIV_TIMESTAMP_FRAME_OWNER =
|
||||
"com.apple.streaming.transportStreamTimestamp";
|
||||
|
||||
private static final AtomicInteger uidSource = new AtomicInteger();
|
||||
|
||||
/** A unique identifier for the chunk. */
|
||||
public final int uid;
|
||||
|
||||
/** The discontinuity sequence number of the chunk. */
|
||||
public final int discontinuitySequenceNumber;
|
||||
|
||||
/** The url of the playlist from which this chunk was obtained. */
|
||||
public final Uri playlistUrl;
|
||||
|
||||
/** Whether the chunk is independent, meaning it starts with a keyframe. */
|
||||
public final boolean isIndependent;
|
||||
|
||||
/** The part index or {@link C#INDEX_UNSET} if the chunk is a full segment */
|
||||
public final int partIndex;
|
||||
|
||||
@Nullable private final DataSource initDataSource;
|
||||
@Nullable private final DataSpec initDataSpec;
|
||||
@Nullable private final HlsMediaChunkExtractor previousExtractor;
|
||||
|
||||
private final boolean isPrimaryTimestampSource;
|
||||
private final boolean hasGapTag;
|
||||
private final TimestampAdjuster timestampAdjuster;
|
||||
private final HlsExtractorFactory extractorFactory;
|
||||
@Nullable private final List<Format> muxedCaptionFormats;
|
||||
@Nullable private final DrmInitData drmInitData;
|
||||
private final Id3Decoder id3Decoder;
|
||||
private final ParsableByteArray scratchId3Data;
|
||||
private final boolean mediaSegmentEncrypted;
|
||||
private final boolean initSegmentEncrypted;
|
||||
private final PlayerId playerId;
|
||||
private final long timestampAdjusterInitializationTimeoutMs;
|
||||
|
||||
private @MonotonicNonNull HlsMediaChunkExtractor extractor;
|
||||
private @MonotonicNonNull HlsSampleStreamWrapper output;
|
||||
// nextLoadPosition refers to the init segment if initDataLoadRequired is true.
|
||||
// Otherwise, nextLoadPosition refers to the media segment.
|
||||
private int nextLoadPosition;
|
||||
private boolean initDataLoadRequired;
|
||||
private volatile boolean loadCanceled;
|
||||
private boolean loadCompleted;
|
||||
private ImmutableList<Integer> sampleQueueFirstSampleIndices;
|
||||
private boolean extractorInvalidated;
|
||||
private long publishedDurationUs;
|
||||
private boolean shouldSpliceIn;
|
||||
|
||||
private HlsMediaChunk(
|
||||
HlsExtractorFactory extractorFactory,
|
||||
DataSource mediaDataSource,
|
||||
DataSpec dataSpec,
|
||||
Format format,
|
||||
boolean mediaSegmentEncrypted,
|
||||
@Nullable DataSource initDataSource,
|
||||
@Nullable DataSpec initDataSpec,
|
||||
boolean initSegmentEncrypted,
|
||||
Uri playlistUrl,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
@C.SelectionReason int trackSelectionReason,
|
||||
@Nullable Object trackSelectionData,
|
||||
long startTimeUs,
|
||||
long endTimeUs,
|
||||
long chunkMediaSequence,
|
||||
int partIndex,
|
||||
boolean isPublished,
|
||||
int discontinuitySequenceNumber,
|
||||
boolean hasGapTag,
|
||||
boolean isPrimaryTimestampSource,
|
||||
TimestampAdjuster timestampAdjuster,
|
||||
long timestampAdjusterInitializationTimeoutMs,
|
||||
@Nullable DrmInitData drmInitData,
|
||||
@Nullable HlsMediaChunkExtractor previousExtractor,
|
||||
Id3Decoder id3Decoder,
|
||||
ParsableByteArray scratchId3Data,
|
||||
boolean shouldSpliceIn,
|
||||
boolean isIndependent,
|
||||
PlayerId playerId,
|
||||
@Nullable String steeredPathwayId) {
|
||||
super(
|
||||
mediaDataSource,
|
||||
dataSpec,
|
||||
format,
|
||||
trackSelectionReason,
|
||||
trackSelectionData,
|
||||
startTimeUs,
|
||||
endTimeUs,
|
||||
chunkMediaSequence,
|
||||
steeredPathwayId);
|
||||
this.mediaSegmentEncrypted = mediaSegmentEncrypted;
|
||||
this.partIndex = partIndex;
|
||||
this.publishedDurationUs = isPublished ? endTimeUs - startTimeUs : C.TIME_UNSET;
|
||||
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
||||
this.initDataSpec = initDataSpec;
|
||||
this.initDataSource = initDataSource;
|
||||
this.initDataLoadRequired = initDataSpec != null;
|
||||
this.initSegmentEncrypted = initSegmentEncrypted;
|
||||
this.playlistUrl = playlistUrl;
|
||||
this.isPrimaryTimestampSource = isPrimaryTimestampSource;
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
|
||||
this.hasGapTag = hasGapTag;
|
||||
this.extractorFactory = extractorFactory;
|
||||
this.muxedCaptionFormats = muxedCaptionFormats;
|
||||
this.drmInitData = drmInitData;
|
||||
this.previousExtractor = previousExtractor;
|
||||
this.id3Decoder = id3Decoder;
|
||||
this.scratchId3Data = scratchId3Data;
|
||||
this.shouldSpliceIn = shouldSpliceIn;
|
||||
this.isIndependent = isIndependent;
|
||||
this.playerId = playerId;
|
||||
sampleQueueFirstSampleIndices = ImmutableList.of();
|
||||
uid = uidSource.getAndIncrement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the chunk for loading.
|
||||
*
|
||||
* @param output The {@link HlsSampleStreamWrapper} that will receive the loaded samples.
|
||||
* @param sampleQueueWriteIndices The current write indices in the existing sample queues of the
|
||||
* output.
|
||||
*/
|
||||
public void init(HlsSampleStreamWrapper output, ImmutableList<Integer> sampleQueueWriteIndices) {
|
||||
this.output = output;
|
||||
this.sampleQueueFirstSampleIndices = sampleQueueWriteIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first sample index of this chunk in the specified sample queue in the output.
|
||||
*
|
||||
* <p>Must not be used if {@link #shouldSpliceIn} is true.
|
||||
*
|
||||
* @param sampleQueueIndex The index of the sample queue in the output.
|
||||
* @return The first sample index of this chunk in the specified sample queue.
|
||||
*/
|
||||
public int getFirstSampleIndex(int sampleQueueIndex) {
|
||||
checkState(!shouldSpliceIn);
|
||||
if (sampleQueueIndex >= sampleQueueFirstSampleIndices.size()) {
|
||||
// The sample queue was created by this chunk or a later chunk.
|
||||
return 0;
|
||||
}
|
||||
return sampleQueueFirstSampleIndices.get(sampleQueueIndex);
|
||||
}
|
||||
|
||||
/** Prevents the extractor from being reused by a following media chunk. */
|
||||
public void invalidateExtractor() {
|
||||
extractorInvalidated = true;
|
||||
}
|
||||
|
||||
/** Returns whether samples for this chunk should be spliced into existing samples. */
|
||||
@SuppressWarnings("UngroupedOverloads") // Ungrouped static method with same name
|
||||
public boolean shouldSpliceIn() {
|
||||
return shouldSpliceIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the {@linkplain #shouldSpliceIn() flag} that indicates if this chunk should be spliced
|
||||
* into existing samples.
|
||||
*/
|
||||
public void clearShouldSpliceIn() {
|
||||
shouldSpliceIn = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoadCompleted() {
|
||||
return loadCompleted;
|
||||
}
|
||||
|
||||
// Loadable implementation
|
||||
|
||||
@Override
|
||||
public void cancelLoad() {
|
||||
loadCanceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() throws IOException {
|
||||
// output == null means init() hasn't been called.
|
||||
checkNotNull(output);
|
||||
if (extractor == null && previousExtractor != null && previousExtractor.isReusable()) {
|
||||
extractor = previousExtractor;
|
||||
initDataLoadRequired = false;
|
||||
}
|
||||
maybeLoadInitData();
|
||||
if (!loadCanceled) {
|
||||
if (!hasGapTag) {
|
||||
loadMedia();
|
||||
}
|
||||
loadCompleted = !loadCanceled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the chunk is a published chunk as opposed to a preload hint that may change when the
|
||||
* playlist updates.
|
||||
*/
|
||||
public boolean isPublished() {
|
||||
return publishedDurationUs != C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the end time of a segment or part if it's fully published, or {@link C#TIME_UNSET} if
|
||||
* it's an unpublished preload hint.
|
||||
*
|
||||
* <p>Note that this value can differ from {@link #endTimeUs} for preload parts that have been
|
||||
* loaded before the duration was known.
|
||||
*/
|
||||
public long getPublishedEndTimeUs() {
|
||||
return publishedDurationUs != C.TIME_UNSET ? startTimeUs + publishedDurationUs : C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the publish flag of the media chunk to indicate that it is not based on a part that is a
|
||||
* preload hint in the playlist.
|
||||
*
|
||||
* @param publishedDurationUs The final published duration of the part in microseconds.
|
||||
*/
|
||||
public void publish(long publishedDurationUs) {
|
||||
this.publishedDurationUs = publishedDurationUs;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
@RequiresNonNull("output")
|
||||
private void maybeLoadInitData() throws IOException {
|
||||
if (!initDataLoadRequired) {
|
||||
return;
|
||||
}
|
||||
// initDataLoadRequired => initDataSource != null && initDataSpec != null
|
||||
checkNotNull(initDataSource);
|
||||
checkNotNull(initDataSpec);
|
||||
feedDataToExtractor(
|
||||
initDataSource,
|
||||
initDataSpec,
|
||||
initSegmentEncrypted,
|
||||
/* initializeTimestampAdjuster= */ false);
|
||||
nextLoadPosition = 0;
|
||||
initDataLoadRequired = false;
|
||||
}
|
||||
|
||||
@RequiresNonNull("output")
|
||||
private void loadMedia() throws IOException {
|
||||
feedDataToExtractor(
|
||||
dataSource, dataSpec, mediaSegmentEncrypted, /* initializeTimestampAdjuster= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to feed the given {@code dataSpec} to {@code this.extractor}. Whenever the operation
|
||||
* concludes (because of a thrown exception or because the operation finishes), the number of fed
|
||||
* bytes is written to {@code nextLoadPosition}.
|
||||
*/
|
||||
@RequiresNonNull("output")
|
||||
private void feedDataToExtractor(
|
||||
DataSource dataSource,
|
||||
DataSpec dataSpec,
|
||||
boolean dataIsEncrypted,
|
||||
boolean initializeTimestampAdjuster)
|
||||
throws IOException {
|
||||
// If we previously fed part of this chunk to the extractor, we need to skip it this time. For
|
||||
// encrypted content we need to skip the data by reading it through the source, so as to ensure
|
||||
// correct decryption of the remainder of the chunk. For clear content, we can request the
|
||||
// remainder of the chunk directly.
|
||||
DataSpec loadDataSpec;
|
||||
boolean skipLoadedBytes;
|
||||
if (dataIsEncrypted) {
|
||||
loadDataSpec = dataSpec;
|
||||
skipLoadedBytes = nextLoadPosition != 0;
|
||||
} else {
|
||||
loadDataSpec = dataSpec.subrange(nextLoadPosition);
|
||||
skipLoadedBytes = false;
|
||||
}
|
||||
try {
|
||||
ExtractorInput input =
|
||||
prepareExtraction(dataSource, loadDataSpec, initializeTimestampAdjuster);
|
||||
if (skipLoadedBytes) {
|
||||
input.skipFully(nextLoadPosition);
|
||||
}
|
||||
try {
|
||||
while (!loadCanceled && extractor.read(input)) {}
|
||||
} catch (EOFException e) {
|
||||
if ((trackFormat.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
|
||||
// See onTruncatedSegmentParsed's javadoc for more info on why we are swallowing the EOF
|
||||
// exception for trick play tracks.
|
||||
extractor.onTruncatedSegmentParsed();
|
||||
} else if (dataSpec.length != C.LENGTH_UNSET
|
||||
&& input.getPosition() >= dataSpec.position + dataSpec.length) {
|
||||
// The chunk is fully loaded, but an EOFException was thrown. This can happen if the
|
||||
// chunk is corrupted.
|
||||
throw ParserException.createForMalformedContainer(/* message= */ null, e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
nextLoadPosition = (int) (input.getPosition() - dataSpec.position);
|
||||
}
|
||||
} finally {
|
||||
DataSourceUtil.closeQuietly(dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresNonNull("output")
|
||||
@EnsuresNonNull("extractor")
|
||||
private DefaultExtractorInput prepareExtraction(
|
||||
DataSource dataSource, DataSpec dataSpec, boolean initializeTimestampAdjuster)
|
||||
throws IOException {
|
||||
long bytesToRead = dataSource.open(dataSpec);
|
||||
if (initializeTimestampAdjuster) {
|
||||
try {
|
||||
timestampAdjuster.sharedInitializeOrWait(
|
||||
isPrimaryTimestampSource, startTimeUs, timestampAdjusterInitializationTimeoutMs);
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException();
|
||||
} catch (TimeoutException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
DefaultExtractorInput extractorInput =
|
||||
new DefaultExtractorInput(dataSource, dataSpec.position, bytesToRead);
|
||||
|
||||
if (extractor == null) {
|
||||
long id3Timestamp = peekId3PrivTimestamp(extractorInput);
|
||||
extractorInput.resetPeekPosition();
|
||||
|
||||
extractor =
|
||||
previousExtractor != null
|
||||
? previousExtractor.recreate()
|
||||
: extractorFactory.createExtractor(
|
||||
dataSpec.uri,
|
||||
trackFormat,
|
||||
muxedCaptionFormats,
|
||||
timestampAdjuster,
|
||||
dataSource.getResponseHeaders(),
|
||||
extractorInput,
|
||||
playerId);
|
||||
if (extractor.isPackedAudioExtractor()) {
|
||||
output.setSampleOffsetUs(
|
||||
id3Timestamp != C.TIME_UNSET
|
||||
? timestampAdjuster.adjustTsTimestamp(id3Timestamp)
|
||||
: startTimeUs);
|
||||
} else {
|
||||
// In case the container format changes mid-stream to non-packed-audio, we need to reset
|
||||
// the timestamp offset.
|
||||
output.setSampleOffsetUs(/* sampleOffsetUs= */ 0L);
|
||||
}
|
||||
output.onNewExtractor();
|
||||
extractor.init(output);
|
||||
}
|
||||
output.setDrmInitData(drmInitData);
|
||||
return extractorInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek the presentation timestamp of the first sample in the chunk from an ID3 PRIV as defined in
|
||||
* the HLS spec, version 20, Section 3.4. Returns {@link C#TIME_UNSET} if the frame is not found.
|
||||
* This method only modifies the peek position.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} to obtain the PRIV frame from.
|
||||
* @return The parsed, adjusted timestamp in microseconds
|
||||
* @throws IOException If an error occurred peeking from the input.
|
||||
*/
|
||||
private long peekId3PrivTimestamp(ExtractorInput input) throws IOException {
|
||||
input.resetPeekPosition();
|
||||
try {
|
||||
scratchId3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
|
||||
input.peekFully(scratchId3Data.getData(), 0, Id3Decoder.ID3_HEADER_LENGTH);
|
||||
} catch (EOFException e) {
|
||||
// The input isn't long enough for there to be any ID3 data.
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
int id = scratchId3Data.readUnsignedInt24();
|
||||
if (id != Id3Decoder.ID3_TAG) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
scratchId3Data.skipBytes(3); // version(2), flags(1).
|
||||
int id3Size = scratchId3Data.readSynchSafeInt();
|
||||
int requiredCapacity = id3Size + Id3Decoder.ID3_HEADER_LENGTH;
|
||||
if (requiredCapacity > scratchId3Data.capacity()) {
|
||||
byte[] data = scratchId3Data.getData();
|
||||
scratchId3Data.reset(requiredCapacity);
|
||||
System.arraycopy(data, 0, scratchId3Data.getData(), 0, Id3Decoder.ID3_HEADER_LENGTH);
|
||||
}
|
||||
input.peekFully(scratchId3Data.getData(), Id3Decoder.ID3_HEADER_LENGTH, id3Size);
|
||||
Metadata metadata = id3Decoder.decode(scratchId3Data.getData(), id3Size);
|
||||
if (metadata == null) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
@Nullable
|
||||
PrivFrame privFrame =
|
||||
metadata.getFirstMatchingEntry(
|
||||
PrivFrame.class, frame -> frame.owner.equals(HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER));
|
||||
if (privFrame == null) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
System.arraycopy(privFrame.privateData, 0, scratchId3Data.getData(), 0, 8 /* timestamp size */);
|
||||
scratchId3Data.setPosition(0);
|
||||
scratchId3Data.setLimit(8);
|
||||
// The top 31 bits should be zeros, but explicitly zero them to wrap in the case that the
|
||||
// streaming provider forgot. See: https://github.com/google/ExoPlayer/pull/3495.
|
||||
return scratchId3Data.readLong() & 0x1FFFFFFFFL;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private static byte[] getEncryptionIvArray(String ivString) {
|
||||
String trimmedIv;
|
||||
if (Ascii.toLowerCase(ivString).startsWith("0x")) {
|
||||
trimmedIv = ivString.substring(2);
|
||||
} else {
|
||||
trimmedIv = ivString;
|
||||
}
|
||||
|
||||
byte[] ivData = new BigInteger(trimmedIv, /* radix= */ 16).toByteArray();
|
||||
byte[] ivDataWithPadding = new byte[16];
|
||||
int offset = ivData.length > 16 ? ivData.length - 16 : 0;
|
||||
System.arraycopy(
|
||||
ivData,
|
||||
offset,
|
||||
ivDataWithPadding,
|
||||
ivDataWithPadding.length - ivData.length + offset,
|
||||
ivData.length - offset);
|
||||
return ivDataWithPadding;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original
|
||||
* in order to decrypt the loaded data. Else returns the original.
|
||||
*
|
||||
* <p>{@code fullSegmentEncryptionKey} & {@code encryptionIv} can either both be null, or neither.
|
||||
*/
|
||||
private static DataSource buildDataSource(
|
||||
DataSource dataSource,
|
||||
@Nullable byte[] fullSegmentEncryptionKey,
|
||||
@Nullable byte[] encryptionIv) {
|
||||
if (fullSegmentEncryptionKey != null) {
|
||||
checkNotNull(encryptionIv);
|
||||
return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv);
|
||||
}
|
||||
return dataSource;
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.extractor.ExtractorInput;
|
||||
import androidx.media3.extractor.ExtractorOutput;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Extracts samples and track {@link Format Formats} from {@link HlsMediaChunk HlsMediaChunks}. */
|
||||
@UnstableApi
|
||||
public interface HlsMediaChunkExtractor {
|
||||
|
||||
/**
|
||||
* Initializes the extractor with an {@link ExtractorOutput}. Called at most once.
|
||||
*
|
||||
* @param extractorOutput An {@link ExtractorOutput} to receive extracted data.
|
||||
*/
|
||||
void init(ExtractorOutput extractorOutput);
|
||||
|
||||
/**
|
||||
* Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link
|
||||
* #init(ExtractorOutput)}.
|
||||
*
|
||||
* <p>A single call to this method will block until some progress has been made, but will not
|
||||
* block for longer than this. Hence each call will consume only a small amount of input data.
|
||||
*
|
||||
* <p>When this method throws an {@link IOException}, extraction may continue by providing an
|
||||
* {@link ExtractorInput} with an unchanged {@link ExtractorInput#getPosition() read position} to
|
||||
* a subsequent call to this method.
|
||||
*
|
||||
* @param extractorInput The input to read from.
|
||||
* @return Whether there is any data left to extract. Returns false if the end of input has been
|
||||
* reached.
|
||||
* @throws IOException If an error occurred reading from or parsing the input.
|
||||
*/
|
||||
boolean read(ExtractorInput extractorInput) throws IOException;
|
||||
|
||||
/** Returns whether this is a packed audio extractor, as defined in RFC 8216, Section 3.4. */
|
||||
boolean isPackedAudioExtractor();
|
||||
|
||||
/** Returns whether this instance can be used for extracting multiple continuous segments. */
|
||||
boolean isReusable();
|
||||
|
||||
/**
|
||||
* Returns a new instance for extracting the same type of media as this one. Can only be called on
|
||||
* instances that are not {@link #isReusable() reusable}.
|
||||
*/
|
||||
HlsMediaChunkExtractor recreate();
|
||||
|
||||
/**
|
||||
* Resets the sample parsing state.
|
||||
*
|
||||
* <p>Resetting the parsing state allows support for Fragmented MP4 EXT-X-I-FRAME-STREAM-INF
|
||||
* segments. EXT-X-I-FRAME-STREAM-INF segments are truncated to include only a leading key frame.
|
||||
* After parsing said keyframe, an extractor may reach an unexpected end of file. By resetting its
|
||||
* state, we can continue feeding samples from the following segments to the extractor. See <a
|
||||
* href="https://github.com/google/ExoPlayer/issues/7512">#7512</a> for context.
|
||||
*/
|
||||
void onTruncatedSegmentParsed();
|
||||
}
|
||||
+1051
File diff suppressed because it is too large
Load Diff
+803
@@ -0,0 +1,803 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaLibraryInfo;
|
||||
import androidx.media3.common.StreamKey;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.TransferListener;
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
|
||||
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
|
||||
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory;
|
||||
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistTracker;
|
||||
import androidx.media3.exoplayer.hls.playlist.FilteringHlsPlaylistParserFactory;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParserFactory;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker;
|
||||
import androidx.media3.exoplayer.source.BaseMediaSource;
|
||||
import androidx.media3.exoplayer.source.CompositeSequenceableLoaderFactory;
|
||||
import androidx.media3.exoplayer.source.DefaultCompositeSequenceableLoaderFactory;
|
||||
import androidx.media3.exoplayer.source.MediaPeriod;
|
||||
import androidx.media3.exoplayer.source.MediaSource;
|
||||
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
||||
import androidx.media3.exoplayer.source.MediaSourceFactory;
|
||||
import androidx.media3.exoplayer.source.SequenceableLoader;
|
||||
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
|
||||
import androidx.media3.exoplayer.upstream.Allocator;
|
||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
||||
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||
import androidx.media3.exoplayer.util.ReleasableExecutor;
|
||||
import androidx.media3.extractor.Extractor;
|
||||
import androidx.media3.extractor.text.SubtitleParser;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/** An HLS {@link MediaSource}. */
|
||||
@UnstableApi
|
||||
public final class HlsMediaSource extends BaseMediaSource
|
||||
implements HlsPlaylistTracker.PrimaryPlaylistListener {
|
||||
|
||||
static {
|
||||
MediaLibraryInfo.registerModule("media3.exoplayer.hls");
|
||||
}
|
||||
|
||||
/**
|
||||
* The types of metadata that can be extracted from HLS streams.
|
||||
*
|
||||
* <p>Allowed values:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #METADATA_TYPE_ID3}
|
||||
* <li>{@link #METADATA_TYPE_EMSG}
|
||||
* </ul>
|
||||
*
|
||||
* <p>See {@link Factory#setMetadataType(int)}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({METADATA_TYPE_ID3, METADATA_TYPE_EMSG})
|
||||
public @interface MetadataType {}
|
||||
|
||||
/** Type for ID3 metadata in HLS streams. */
|
||||
public static final int METADATA_TYPE_ID3 = 1;
|
||||
|
||||
/** Type for EMSG metadata in HLS streams. */
|
||||
public static final int METADATA_TYPE_EMSG = 3;
|
||||
|
||||
/** Factory for {@link HlsMediaSource}s. */
|
||||
@SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility.
|
||||
public static final class Factory implements MediaSourceFactory {
|
||||
|
||||
private final HlsDataSourceFactory hlsDataSourceFactory;
|
||||
|
||||
@Nullable private HlsExtractorFactory extractorFactory;
|
||||
@Nullable private SubtitleParser.Factory subtitleParserFactoryOverride;
|
||||
private boolean parseSubtitlesDuringExtraction;
|
||||
private @C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies;
|
||||
private HlsPlaylistParserFactory playlistParserFactory;
|
||||
private HlsPlaylistTracker.Factory playlistTrackerFactory;
|
||||
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
@Nullable private CmcdConfiguration.Factory cmcdConfigurationFactory;
|
||||
private DrmSessionManagerProvider drmSessionManagerProvider;
|
||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
@Nullable private Supplier<ReleasableExecutor> downloadExecutorSupplier;
|
||||
|
||||
private boolean allowChunklessPreparation;
|
||||
private @MetadataType int metadataType;
|
||||
private boolean useSessionKeys;
|
||||
private long elapsedRealTimeOffsetMs;
|
||||
private long timestampAdjusterInitializationTimeoutMs;
|
||||
|
||||
/**
|
||||
* Creates a new factory for {@link HlsMediaSource}s.
|
||||
*
|
||||
* <p>The factory will use the following default components:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link DefaultDrmSessionManagerProvider}
|
||||
* <li>{@link DefaultHlsPlaylistParserFactory}
|
||||
* <li>{@link DefaultHlsPlaylistTracker#FACTORY}
|
||||
* <li>{@link DefaultHlsExtractorFactory}
|
||||
* <li>{@link DefaultLoadErrorHandlingPolicy}
|
||||
* <li>{@link DefaultCompositeSequenceableLoaderFactory}
|
||||
* </ul>
|
||||
*
|
||||
* @param dataSourceFactory A data source factory that will be wrapped by a {@link
|
||||
* DefaultHlsDataSourceFactory} to create {@link DataSource}s for manifests, segments and
|
||||
* keys.
|
||||
*/
|
||||
public Factory(DataSource.Factory dataSourceFactory) {
|
||||
this(new DefaultHlsDataSourceFactory(dataSourceFactory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new factory for {@link HlsMediaSource}s.
|
||||
*
|
||||
* <p>The factory will use the following default components:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link DefaultDrmSessionManagerProvider}
|
||||
* <li>{@link DefaultHlsPlaylistParserFactory}
|
||||
* <li>{@link DefaultHlsPlaylistTracker#FACTORY}
|
||||
* <li>{@link DefaultHlsExtractorFactory}
|
||||
* <li>{@link DefaultLoadErrorHandlingPolicy}
|
||||
* <li>{@link DefaultCompositeSequenceableLoaderFactory}
|
||||
* </ul>
|
||||
*
|
||||
* @param hlsDataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for
|
||||
* manifests, segments and keys.
|
||||
*/
|
||||
public Factory(HlsDataSourceFactory hlsDataSourceFactory) {
|
||||
this.hlsDataSourceFactory = checkNotNull(hlsDataSourceFactory);
|
||||
drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
|
||||
playlistParserFactory = new DefaultHlsPlaylistParserFactory();
|
||||
playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY;
|
||||
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
|
||||
downloadExecutorSupplier = null;
|
||||
metadataType = METADATA_TYPE_ID3;
|
||||
elapsedRealTimeOffsetMs = C.TIME_UNSET;
|
||||
allowChunklessPreparation = true;
|
||||
codecsToParseWithinGopSampleDependencies = C.VIDEO_CODEC_FLAG_H264 | C.VIDEO_CODEC_FLAG_H265;
|
||||
experimentalParseSubtitlesDuringExtraction(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory for {@link Extractor}s for the segments. The default value is {@link
|
||||
* DefaultHlsExtractorFactory}.
|
||||
*
|
||||
* <p>Any values passed to {@link #setSubtitleParserFactory} or {@link
|
||||
* #experimentalParseSubtitlesDuringExtraction} will be forwarded to the provided {@link
|
||||
* HlsExtractorFactory} instance during {@link #createMediaSource}.
|
||||
*
|
||||
* @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the
|
||||
* segments.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setExtractorFactory(@Nullable HlsExtractorFactory extractorFactory) {
|
||||
this.extractorFactory = extractorFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
|
||||
this.loadErrorHandlingPolicy =
|
||||
checkNotNull(
|
||||
loadErrorHandlingPolicy,
|
||||
"MediaSource.Factory#setLoadErrorHandlingPolicy no longer handles null by"
|
||||
+ " instantiating a new DefaultLoadErrorHandlingPolicy. Explicitly construct and"
|
||||
+ " pass an instance in order to retain the old behavior.");
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public Factory setSubtitleParserFactory(SubtitleParser.Factory subtitleParserFactory) {
|
||||
this.subtitleParserFactoryOverride = subtitleParserFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@CanIgnoreReturnValue
|
||||
public Factory experimentalParseSubtitlesDuringExtraction(
|
||||
boolean parseSubtitlesDuringExtraction) {
|
||||
this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@CanIgnoreReturnValue
|
||||
public Factory experimentalSetCodecsToParseWithinGopSampleDependencies(
|
||||
@C.VideoCodecFlags int codecsToParseWithinGopSampleDependencies) {
|
||||
this.codecsToParseWithinGopSampleDependencies = codecsToParseWithinGopSampleDependencies;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory from which playlist parsers will be obtained.
|
||||
*
|
||||
* @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setPlaylistParserFactory(HlsPlaylistParserFactory playlistParserFactory) {
|
||||
this.playlistParserFactory =
|
||||
checkNotNull(
|
||||
playlistParserFactory,
|
||||
"HlsMediaSource.Factory#setPlaylistParserFactory no longer handles null by"
|
||||
+ " instantiating a new DefaultHlsPlaylistParserFactory. Explicitly"
|
||||
+ " construct and pass an instance in order to retain the old behavior.");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link HlsPlaylistTracker} factory.
|
||||
*
|
||||
* @param playlistTrackerFactory A factory for {@link HlsPlaylistTracker} instances.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setPlaylistTrackerFactory(HlsPlaylistTracker.Factory playlistTrackerFactory) {
|
||||
this.playlistTrackerFactory =
|
||||
checkNotNull(
|
||||
playlistTrackerFactory,
|
||||
"HlsMediaSource.Factory#setPlaylistTrackerFactory no longer handles null by"
|
||||
+ " defaulting to DefaultHlsPlaylistTracker.FACTORY. Explicitly"
|
||||
+ " pass a reference to this instance in order to retain the old behavior.");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory to create composite {@link SequenceableLoader}s for when this media source
|
||||
* loads data from multiple streams (video, audio etc...).
|
||||
*
|
||||
* @param compositeSequenceableLoaderFactory A factory to create composite {@link
|
||||
* SequenceableLoader}s for when this media source loads data from multiple streams (video,
|
||||
* audio etc...).
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setCompositeSequenceableLoaderFactory(
|
||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) {
|
||||
this.compositeSequenceableLoaderFactory =
|
||||
checkNotNull(
|
||||
compositeSequenceableLoaderFactory,
|
||||
"HlsMediaSource.Factory#setCompositeSequenceableLoaderFactory no longer handles null"
|
||||
+ " by instantiating a new DefaultCompositeSequenceableLoaderFactory. Explicitly"
|
||||
+ " construct and pass an instance in order to retain the old behavior.");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads
|
||||
* will be enabled for streams that provide sufficient information in their multivariant
|
||||
* playlist.
|
||||
*
|
||||
* @param allowChunklessPreparation Whether chunkless preparation is allowed.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setAllowChunklessPreparation(boolean allowChunklessPreparation) {
|
||||
this.allowChunklessPreparation = allowChunklessPreparation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of metadata to extract from the HLS source (defaults to {@link
|
||||
* #METADATA_TYPE_ID3}).
|
||||
*
|
||||
* <p>HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is
|
||||
* wrapped in an EMSG box [<a href="https://aomediacodec.github.io/av1-id3/">spec</a>].
|
||||
*
|
||||
* <p>If this is set to {@link #METADATA_TYPE_ID3} then raw ID3 metadata of will be extracted
|
||||
* from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant
|
||||
* stream only) will be unwrapped to expose the inner data. All other in-band metadata will be
|
||||
* dropped.
|
||||
*
|
||||
* <p>If this is set to {@link #METADATA_TYPE_EMSG} then all EMSG data from the fMP4 variant
|
||||
* stream will be extracted. No metadata will be extracted from TS streams, since they don't
|
||||
* support EMSG.
|
||||
*
|
||||
* @param metadataType The type of metadata to extract.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setMetadataType(@MetadataType int metadataType) {
|
||||
this.metadataType = metadataType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to use #EXT-X-SESSION-KEY tags provided in the multivariant playlist. If
|
||||
* enabled, it's assumed that any single session key declared in the multivariant playlist can
|
||||
* be used to obtain all of the keys required for playback. For media where this is not true,
|
||||
* this option should not be enabled.
|
||||
*
|
||||
* @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setUseSessionKeys(boolean useSessionKeys) {
|
||||
this.useSessionKeys = useSessionKeys;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public Factory setCmcdConfigurationFactory(CmcdConfiguration.Factory cmcdConfigurationFactory) {
|
||||
this.cmcdConfigurationFactory = checkNotNull(cmcdConfigurationFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public Factory setDrmSessionManagerProvider(
|
||||
DrmSessionManagerProvider drmSessionManagerProvider) {
|
||||
this.drmSessionManagerProvider =
|
||||
checkNotNull(
|
||||
drmSessionManagerProvider,
|
||||
"MediaSource.Factory#setDrmSessionManagerProvider no longer handles null by"
|
||||
+ " instantiating a new DefaultDrmSessionManagerProvider. Explicitly construct"
|
||||
+ " and pass an instance in order to retain the old behavior.");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeout for the loading thread to wait for the timestamp adjuster to initialize, in
|
||||
* milliseconds.The default value is zero, which is interpreted as an infinite timeout.
|
||||
*
|
||||
* @param timestampAdjusterInitializationTimeoutMs The timeout in milliseconds. A timeout of
|
||||
* zero is interpreted as an infinite timeout.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setTimestampAdjusterInitializationTimeoutMs(
|
||||
long timestampAdjusterInitializationTimeoutMs) {
|
||||
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix
|
||||
* epoch. By default, is it set to {@link C#TIME_UNSET}.
|
||||
*
|
||||
* @param elapsedRealTimeOffsetMs The offset between {@link SystemClock#elapsedRealtime()} and
|
||||
* the time since the Unix epoch, in milliseconds.
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@VisibleForTesting
|
||||
/* package */ Factory setElapsedRealTimeOffsetMs(long elapsedRealTimeOffsetMs) {
|
||||
this.elapsedRealTimeOffsetMs = elapsedRealTimeOffsetMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public Factory setDownloadExecutor(Supplier<ReleasableExecutor> downloadExecutor) {
|
||||
this.downloadExecutorSupplier = downloadExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link HlsMediaSource} using the current parameters.
|
||||
*
|
||||
* @param mediaItem The {@link MediaItem}.
|
||||
* @return The new {@link HlsMediaSource}.
|
||||
* @throws NullPointerException if {@link MediaItem#localConfiguration} is {@code null}.
|
||||
*/
|
||||
@Override
|
||||
public HlsMediaSource createMediaSource(MediaItem mediaItem) {
|
||||
checkNotNull(mediaItem.localConfiguration);
|
||||
if (extractorFactory == null) {
|
||||
extractorFactory = new DefaultHlsExtractorFactory();
|
||||
}
|
||||
if (subtitleParserFactoryOverride != null) {
|
||||
extractorFactory.setSubtitleParserFactory(subtitleParserFactoryOverride);
|
||||
}
|
||||
extractorFactory.experimentalParseSubtitlesDuringExtraction(parseSubtitlesDuringExtraction);
|
||||
extractorFactory.experimentalSetCodecsToParseWithinGopSampleDependencies(
|
||||
codecsToParseWithinGopSampleDependencies);
|
||||
HlsExtractorFactory extractorFactory = this.extractorFactory;
|
||||
HlsPlaylistParserFactory playlistParserFactory = this.playlistParserFactory;
|
||||
List<StreamKey> streamKeys = mediaItem.localConfiguration.streamKeys;
|
||||
if (!streamKeys.isEmpty()) {
|
||||
playlistParserFactory =
|
||||
new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);
|
||||
}
|
||||
@Nullable
|
||||
CmcdConfiguration cmcdConfiguration =
|
||||
cmcdConfigurationFactory == null
|
||||
? null
|
||||
: cmcdConfigurationFactory.createCmcdConfiguration(mediaItem);
|
||||
|
||||
return new HlsMediaSource(
|
||||
mediaItem,
|
||||
hlsDataSourceFactory,
|
||||
extractorFactory,
|
||||
compositeSequenceableLoaderFactory,
|
||||
cmcdConfiguration,
|
||||
drmSessionManagerProvider.get(mediaItem),
|
||||
loadErrorHandlingPolicy,
|
||||
playlistTrackerFactory.createTracker(
|
||||
hlsDataSourceFactory,
|
||||
loadErrorHandlingPolicy,
|
||||
playlistParserFactory,
|
||||
cmcdConfiguration,
|
||||
downloadExecutorSupplier),
|
||||
elapsedRealTimeOffsetMs,
|
||||
allowChunklessPreparation,
|
||||
metadataType,
|
||||
useSessionKeys,
|
||||
timestampAdjusterInitializationTimeoutMs,
|
||||
downloadExecutorSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @C.ContentType int[] getSupportedTypes() {
|
||||
return new int[] {C.CONTENT_TYPE_HLS};
|
||||
}
|
||||
}
|
||||
|
||||
private final HlsExtractorFactory extractorFactory;
|
||||
private final HlsDataSourceFactory dataSourceFactory;
|
||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
@Nullable private final CmcdConfiguration cmcdConfiguration;
|
||||
private final DrmSessionManager drmSessionManager;
|
||||
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private final boolean allowChunklessPreparation;
|
||||
private final @MetadataType int metadataType;
|
||||
private final boolean useSessionKeys;
|
||||
private final HlsPlaylistTracker playlistTracker;
|
||||
private final long elapsedRealTimeOffsetMs;
|
||||
private final long timestampAdjusterInitializationTimeoutMs;
|
||||
@Nullable private final Supplier<ReleasableExecutor> downloadExecutorSupplier;
|
||||
|
||||
private MediaItem.LiveConfiguration liveConfiguration;
|
||||
@Nullable private TransferListener mediaTransferListener;
|
||||
|
||||
@GuardedBy("this")
|
||||
private MediaItem mediaItem;
|
||||
|
||||
private HlsMediaSource(
|
||||
MediaItem mediaItem,
|
||||
HlsDataSourceFactory dataSourceFactory,
|
||||
HlsExtractorFactory extractorFactory,
|
||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
||||
@Nullable CmcdConfiguration cmcdConfiguration,
|
||||
DrmSessionManager drmSessionManager,
|
||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||
HlsPlaylistTracker playlistTracker,
|
||||
long elapsedRealTimeOffsetMs,
|
||||
boolean allowChunklessPreparation,
|
||||
@MetadataType int metadataType,
|
||||
boolean useSessionKeys,
|
||||
long timestampAdjusterInitializationTimeoutMs,
|
||||
@Nullable Supplier<ReleasableExecutor> downloadExecutorSupplier) {
|
||||
this.mediaItem = mediaItem;
|
||||
this.liveConfiguration = mediaItem.liveConfiguration;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.extractorFactory = extractorFactory;
|
||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||
this.cmcdConfiguration = cmcdConfiguration;
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||
this.playlistTracker = playlistTracker;
|
||||
this.elapsedRealTimeOffsetMs = elapsedRealTimeOffsetMs;
|
||||
this.allowChunklessPreparation = allowChunklessPreparation;
|
||||
this.metadataType = metadataType;
|
||||
this.useSessionKeys = useSessionKeys;
|
||||
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
|
||||
this.downloadExecutorSupplier = downloadExecutorSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized MediaItem getMediaItem() {
|
||||
return mediaItem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||
MediaItem existingMediaItem = getMediaItem();
|
||||
MediaItem.LocalConfiguration existingConfiguration =
|
||||
checkNotNull(existingMediaItem.localConfiguration);
|
||||
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
|
||||
return newConfiguration != null
|
||||
&& newConfiguration.uri.equals(existingConfiguration.uri)
|
||||
&& newConfiguration.streamKeys.equals(existingConfiguration.streamKeys)
|
||||
&& Objects.equals(newConfiguration.drmConfiguration, existingConfiguration.drmConfiguration)
|
||||
&& existingMediaItem.liveConfiguration.equals(mediaItem.liveConfiguration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateMediaItem(MediaItem mediaItem) {
|
||||
this.mediaItem = mediaItem;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||
this.mediaTransferListener = mediaTransferListener;
|
||||
drmSessionManager.setPlayer(
|
||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId());
|
||||
drmSessionManager.prepare();
|
||||
MediaSourceEventListener.EventDispatcher eventDispatcher =
|
||||
createEventDispatcher(/* mediaPeriodId= */ null);
|
||||
playlistTracker.start(
|
||||
checkNotNull(getMediaItem().localConfiguration).uri,
|
||||
eventDispatcher,
|
||||
/* primaryPlaylistListener= */ this,
|
||||
getBandwidthMeter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
playlistTracker.maybeThrowPrimaryPlaylistRefreshError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher = createEventDispatcher(id);
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher = createDrmEventDispatcher(id);
|
||||
return new HlsMediaPeriod(
|
||||
extractorFactory,
|
||||
playlistTracker,
|
||||
dataSourceFactory,
|
||||
mediaTransferListener,
|
||||
cmcdConfiguration,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
loadErrorHandlingPolicy,
|
||||
mediaSourceEventDispatcher,
|
||||
allocator,
|
||||
compositeSequenceableLoaderFactory,
|
||||
allowChunklessPreparation,
|
||||
metadataType,
|
||||
useSessionKeys,
|
||||
getPlayerId(),
|
||||
timestampAdjusterInitializationTimeoutMs,
|
||||
downloadExecutorSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
((HlsMediaPeriod) mediaPeriod).release();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void releaseSourceInternal() {
|
||||
playlistTracker.stop();
|
||||
drmSessionManager.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist) {
|
||||
long windowStartTimeMs =
|
||||
mediaPlaylist.hasProgramDateTime ? Util.usToMs(mediaPlaylist.startTimeUs) : C.TIME_UNSET;
|
||||
// For playlist types EVENT and VOD we know segments are never removed, so the presentation
|
||||
// started at the same time as the window. Otherwise, we don't know the presentation start time.
|
||||
long presentationStartTimeMs =
|
||||
mediaPlaylist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
|
||||
|| mediaPlaylist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
|
||||
? windowStartTimeMs
|
||||
: C.TIME_UNSET;
|
||||
// The multivariant playlist is non-null because the first playlist has been fetched by now.
|
||||
HlsManifest manifest =
|
||||
new HlsManifest(checkNotNull(playlistTracker.getMultivariantPlaylist()), mediaPlaylist);
|
||||
SinglePeriodTimeline timeline =
|
||||
playlistTracker.isLive()
|
||||
? createTimelineForLive(
|
||||
mediaPlaylist, presentationStartTimeMs, windowStartTimeMs, manifest)
|
||||
: createTimelineForOnDemand(
|
||||
mediaPlaylist, presentationStartTimeMs, windowStartTimeMs, manifest);
|
||||
refreshSourceInfo(timeline);
|
||||
}
|
||||
|
||||
private SinglePeriodTimeline createTimelineForLive(
|
||||
HlsMediaPlaylist playlist,
|
||||
long presentationStartTimeMs,
|
||||
long windowStartTimeMs,
|
||||
HlsManifest manifest) {
|
||||
long offsetFromInitialStartTimeUs =
|
||||
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||
long periodDurationUs =
|
||||
playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
|
||||
long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist);
|
||||
long targetLiveOffsetUs;
|
||||
if (liveConfiguration.targetOffsetMs != C.TIME_UNSET) {
|
||||
// Media item has a defined target offset.
|
||||
targetLiveOffsetUs = Util.msToUs(liveConfiguration.targetOffsetMs);
|
||||
} else {
|
||||
// Decide target offset from playlist.
|
||||
targetLiveOffsetUs = getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs);
|
||||
}
|
||||
// Ensure target live offset is within the live window and greater than the live edge offset.
|
||||
targetLiveOffsetUs =
|
||||
Util.constrainValue(
|
||||
targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs);
|
||||
updateLiveConfiguration(playlist, targetLiveOffsetUs);
|
||||
long windowDefaultStartPositionUs =
|
||||
getLiveWindowDefaultStartPositionUs(playlist, liveEdgeOffsetUs);
|
||||
boolean suppressPositionProjection =
|
||||
playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
|
||||
&& playlist.hasPositiveStartOffset;
|
||||
return new SinglePeriodTimeline(
|
||||
presentationStartTimeMs,
|
||||
windowStartTimeMs,
|
||||
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
|
||||
periodDurationUs,
|
||||
/* windowDurationUs= */ playlist.durationUs,
|
||||
/* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,
|
||||
windowDefaultStartPositionUs,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ !playlist.hasEndTag,
|
||||
suppressPositionProjection,
|
||||
manifest,
|
||||
getMediaItem(),
|
||||
liveConfiguration);
|
||||
}
|
||||
|
||||
private SinglePeriodTimeline createTimelineForOnDemand(
|
||||
HlsMediaPlaylist playlist,
|
||||
long presentationStartTimeMs,
|
||||
long windowStartTimeMs,
|
||||
HlsManifest manifest) {
|
||||
long windowDefaultStartPositionUs;
|
||||
if (playlist.startOffsetUs == C.TIME_UNSET || playlist.segments.isEmpty()) {
|
||||
windowDefaultStartPositionUs = 0;
|
||||
} else {
|
||||
if (playlist.preciseStart || playlist.startOffsetUs == playlist.durationUs) {
|
||||
windowDefaultStartPositionUs = playlist.startOffsetUs;
|
||||
} else {
|
||||
windowDefaultStartPositionUs =
|
||||
findClosestPrecedingSegment(playlist.segments, playlist.startOffsetUs)
|
||||
.relativeStartTimeUs;
|
||||
}
|
||||
}
|
||||
return new SinglePeriodTimeline(
|
||||
presentationStartTimeMs,
|
||||
windowStartTimeMs,
|
||||
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
|
||||
/* periodDurationUs= */ playlist.durationUs,
|
||||
/* windowDurationUs= */ playlist.durationUs,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
windowDefaultStartPositionUs,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ false,
|
||||
/* suppressPositionProjection= */ true,
|
||||
manifest,
|
||||
getMediaItem(),
|
||||
/* liveConfiguration= */ null);
|
||||
}
|
||||
|
||||
private long getLiveEdgeOffsetUs(HlsMediaPlaylist playlist) {
|
||||
return playlist.hasProgramDateTime
|
||||
? Util.msToUs(Util.getNowUnixTimeMs(elapsedRealTimeOffsetMs)) - playlist.getEndTimeUs()
|
||||
: 0;
|
||||
}
|
||||
|
||||
private long getLiveWindowDefaultStartPositionUs(
|
||||
HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
|
||||
long startPositionUs =
|
||||
playlist.startOffsetUs != C.TIME_UNSET
|
||||
? playlist.startOffsetUs
|
||||
: playlist.durationUs
|
||||
+ liveEdgeOffsetUs
|
||||
- Util.msToUs(liveConfiguration.targetOffsetMs);
|
||||
if (playlist.preciseStart) {
|
||||
return startPositionUs;
|
||||
}
|
||||
@Nullable
|
||||
HlsMediaPlaylist.Part part =
|
||||
findClosestPrecedingIndependentPart(playlist.trailingParts, startPositionUs);
|
||||
if (part != null) {
|
||||
return part.relativeStartTimeUs;
|
||||
}
|
||||
if (playlist.segments.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
HlsMediaPlaylist.Segment segment =
|
||||
findClosestPrecedingSegment(playlist.segments, startPositionUs);
|
||||
part = findClosestPrecedingIndependentPart(segment.parts, startPositionUs);
|
||||
if (part != null) {
|
||||
return part.relativeStartTimeUs;
|
||||
}
|
||||
return segment.relativeStartTimeUs;
|
||||
}
|
||||
|
||||
private void updateLiveConfiguration(HlsMediaPlaylist playlist, long targetLiveOffsetUs) {
|
||||
MediaItem.LiveConfiguration mediaItemLiveConfiguration = getMediaItem().liveConfiguration;
|
||||
boolean disableSpeedAdjustment =
|
||||
mediaItemLiveConfiguration.minPlaybackSpeed == C.RATE_UNSET
|
||||
&& mediaItemLiveConfiguration.maxPlaybackSpeed == C.RATE_UNSET
|
||||
&& playlist.serverControl.holdBackUs == C.TIME_UNSET
|
||||
&& playlist.serverControl.partHoldBackUs == C.TIME_UNSET;
|
||||
liveConfiguration =
|
||||
liveConfiguration
|
||||
.buildUpon()
|
||||
.setTargetOffsetMs(Util.usToMs(targetLiveOffsetUs))
|
||||
.setMinPlaybackSpeed(disableSpeedAdjustment ? 1f : liveConfiguration.minPlaybackSpeed)
|
||||
.setMaxPlaybackSpeed(disableSpeedAdjustment ? 1f : liveConfiguration.maxPlaybackSpeed)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the target live offset, in microseconds, for a live playlist.
|
||||
*
|
||||
* <p>The target offset is derived by checking the following in this order:
|
||||
*
|
||||
* <ol>
|
||||
* <li>The playlist defines a start offset.
|
||||
* <li>The playlist defines a part hold back in server control and has part duration.
|
||||
* <li>The playlist defines a hold back in server control.
|
||||
* <li>Fallback to {@code 3 x target duration}.
|
||||
* </ol>
|
||||
*
|
||||
* @param playlist The playlist.
|
||||
* @param liveEdgeOffsetUs The current live edge offset.
|
||||
* @return The selected target live offset, in microseconds.
|
||||
*/
|
||||
private static long getTargetLiveOffsetUs(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
|
||||
HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl;
|
||||
long targetOffsetUs;
|
||||
if (playlist.startOffsetUs != C.TIME_UNSET) {
|
||||
targetOffsetUs = playlist.durationUs - playlist.startOffsetUs;
|
||||
} else if (serverControl.partHoldBackUs != C.TIME_UNSET
|
||||
&& playlist.partTargetDurationUs != C.TIME_UNSET) {
|
||||
// Select part hold back only if the playlist has a part target duration.
|
||||
targetOffsetUs = serverControl.partHoldBackUs;
|
||||
} else if (serverControl.holdBackUs != C.TIME_UNSET) {
|
||||
targetOffsetUs = serverControl.holdBackUs;
|
||||
} else {
|
||||
// Fallback, see RFC 8216, Section 4.4.3.8.
|
||||
targetOffsetUs = 3 * playlist.targetDurationUs;
|
||||
}
|
||||
return targetOffsetUs + liveEdgeOffsetUs;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static HlsMediaPlaylist.Part findClosestPrecedingIndependentPart(
|
||||
List<HlsMediaPlaylist.Part> parts, long positionUs) {
|
||||
@Nullable HlsMediaPlaylist.Part closestPart = null;
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
HlsMediaPlaylist.Part part = parts.get(i);
|
||||
if (part.relativeStartTimeUs <= positionUs && part.isIndependent) {
|
||||
closestPart = part;
|
||||
} else if (part.relativeStartTimeUs > positionUs) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return closestPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the segment that contains {@code positionUs}, or the last segment if the position is
|
||||
* beyond the segments list.
|
||||
*/
|
||||
private static HlsMediaPlaylist.Segment findClosestPrecedingSegment(
|
||||
List<HlsMediaPlaylist.Segment> segments, long positionUs) {
|
||||
int segmentIndex =
|
||||
Util.binarySearchFloor(
|
||||
segments, positionUs, /* inclusive= */ true, /* stayInBounds= */ true);
|
||||
return segments.get(segmentIndex);
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.decoder.DecoderInputBuffer;
|
||||
import androidx.media3.exoplayer.FormatHolder;
|
||||
import androidx.media3.exoplayer.source.SampleStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/** {@link SampleStream} for a particular sample queue in HLS. */
|
||||
/* package */ final class HlsSampleStream implements SampleStream {
|
||||
|
||||
private final int trackGroupIndex;
|
||||
private final HlsSampleStreamWrapper sampleStreamWrapper;
|
||||
private int sampleQueueIndex;
|
||||
|
||||
public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) {
|
||||
this.sampleStreamWrapper = sampleStreamWrapper;
|
||||
this.trackGroupIndex = trackGroupIndex;
|
||||
sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;
|
||||
}
|
||||
|
||||
public void bindSampleQueue() {
|
||||
checkArgument(sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING);
|
||||
sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
|
||||
}
|
||||
|
||||
public void unbindSampleQueue() {
|
||||
if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
|
||||
sampleStreamWrapper.unbindSampleQueue(trackGroupIndex);
|
||||
sampleQueueIndex = HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
// SampleStream implementation.
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
|
||||
|| (hasValidSampleQueueIndex() && sampleStreamWrapper.isReady(sampleQueueIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowError() throws IOException {
|
||||
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) {
|
||||
throw new SampleQueueMappingException(
|
||||
sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);
|
||||
} else if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) {
|
||||
sampleStreamWrapper.maybeThrowError();
|
||||
} else if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) {
|
||||
sampleStreamWrapper.maybeThrowError(sampleQueueIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readData(
|
||||
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
|
||||
if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) {
|
||||
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
}
|
||||
return hasValidSampleQueueIndex()
|
||||
? sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, readFlags)
|
||||
: C.RESULT_NOTHING_READ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int skipData(long positionUs) {
|
||||
return hasValidSampleQueueIndex()
|
||||
? sampleStreamWrapper.skipData(sampleQueueIndex, positionUs)
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private boolean hasValidSampleQueueIndex() {
|
||||
return sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING
|
||||
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL
|
||||
&& sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL;
|
||||
}
|
||||
}
|
||||
+2056
File diff suppressed because it is too large
Load Diff
+180
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Holds metadata associated to an HLS media track. */
|
||||
@UnstableApi
|
||||
public final class HlsTrackMetadataEntry implements Metadata.Entry {
|
||||
|
||||
/** Holds attributes defined in an EXT-X-STREAM-INF tag. */
|
||||
public static final class VariantInfo {
|
||||
|
||||
/**
|
||||
* The average bitrate as declared by the AVERAGE-BANDWIDTH attribute of the EXT-X-STREAM-INF
|
||||
* tag, or {@link Format#NO_VALUE} if the attribute is not declared.
|
||||
*/
|
||||
public final int averageBitrate;
|
||||
|
||||
/** The peak bitrate as declared by the BANDWIDTH attribute of the EXT-X-STREAM-INF tag. */
|
||||
public final int peakBitrate;
|
||||
|
||||
/**
|
||||
* The VIDEO value as defined in the EXT-X-STREAM-INF tag, or null if the VIDEO attribute is not
|
||||
* present.
|
||||
*/
|
||||
@Nullable public final String videoGroupId;
|
||||
|
||||
/**
|
||||
* The AUDIO value as defined in the EXT-X-STREAM-INF tag, or null if the AUDIO attribute is not
|
||||
* present.
|
||||
*/
|
||||
@Nullable public final String audioGroupId;
|
||||
|
||||
/**
|
||||
* The SUBTITLES value as defined in the EXT-X-STREAM-INF tag, or null if the SUBTITLES
|
||||
* attribute is not present.
|
||||
*/
|
||||
@Nullable public final String subtitleGroupId;
|
||||
|
||||
/**
|
||||
* The CLOSED-CAPTIONS value as defined in the EXT-X-STREAM-INF tag, or null if the
|
||||
* CLOSED-CAPTIONS attribute is not present.
|
||||
*/
|
||||
@Nullable public final String captionGroupId;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param averageBitrate See {@link #averageBitrate}.
|
||||
* @param peakBitrate See {@link #peakBitrate}.
|
||||
* @param videoGroupId See {@link #videoGroupId}.
|
||||
* @param audioGroupId See {@link #audioGroupId}.
|
||||
* @param subtitleGroupId See {@link #subtitleGroupId}.
|
||||
* @param captionGroupId See {@link #captionGroupId}.
|
||||
*/
|
||||
public VariantInfo(
|
||||
int averageBitrate,
|
||||
int peakBitrate,
|
||||
@Nullable String videoGroupId,
|
||||
@Nullable String audioGroupId,
|
||||
@Nullable String subtitleGroupId,
|
||||
@Nullable String captionGroupId) {
|
||||
this.averageBitrate = averageBitrate;
|
||||
this.peakBitrate = peakBitrate;
|
||||
this.videoGroupId = videoGroupId;
|
||||
this.audioGroupId = audioGroupId;
|
||||
this.subtitleGroupId = subtitleGroupId;
|
||||
this.captionGroupId = captionGroupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
return false;
|
||||
}
|
||||
VariantInfo that = (VariantInfo) other;
|
||||
return averageBitrate == that.averageBitrate
|
||||
&& peakBitrate == that.peakBitrate
|
||||
&& TextUtils.equals(videoGroupId, that.videoGroupId)
|
||||
&& TextUtils.equals(audioGroupId, that.audioGroupId)
|
||||
&& TextUtils.equals(subtitleGroupId, that.subtitleGroupId)
|
||||
&& TextUtils.equals(captionGroupId, that.captionGroupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = averageBitrate;
|
||||
result = 31 * result + peakBitrate;
|
||||
result = 31 * result + (videoGroupId != null ? videoGroupId.hashCode() : 0);
|
||||
result = 31 * result + (audioGroupId != null ? audioGroupId.hashCode() : 0);
|
||||
result = 31 * result + (subtitleGroupId != null ? subtitleGroupId.hashCode() : 0);
|
||||
result = 31 * result + (captionGroupId != null ? captionGroupId.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The GROUP-ID value of this track, if the track is derived from an EXT-X-MEDIA tag. Null if the
|
||||
* track is not derived from an EXT-X-MEDIA TAG.
|
||||
*/
|
||||
@Nullable public final String groupId;
|
||||
|
||||
/**
|
||||
* The NAME value of this track, if the track is derived from an EXT-X-MEDIA tag. Null if the
|
||||
* track is not derived from an EXT-X-MEDIA TAG.
|
||||
*/
|
||||
@Nullable public final String name;
|
||||
|
||||
/**
|
||||
* The EXT-X-STREAM-INF tags attributes associated with this track. This field is non-applicable
|
||||
* (and therefore empty) if this track is derived from an EXT-X-MEDIA tag.
|
||||
*/
|
||||
public final List<VariantInfo> variantInfos;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param groupId See {@link #groupId}.
|
||||
* @param name See {@link #name}.
|
||||
* @param variantInfos See {@link #variantInfos}.
|
||||
*/
|
||||
public HlsTrackMetadataEntry(
|
||||
@Nullable String groupId, @Nullable String name, List<VariantInfo> variantInfos) {
|
||||
this.groupId = groupId;
|
||||
this.name = name;
|
||||
this.variantInfos = Collections.unmodifiableList(new ArrayList<>(variantInfos));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HlsTrackMetadataEntry" + (groupId != null ? (" [" + groupId + ", " + name + "]") : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HlsTrackMetadataEntry that = (HlsTrackMetadataEntry) other;
|
||||
return TextUtils.equals(groupId, that.groupId)
|
||||
&& TextUtils.equals(name, that.name)
|
||||
&& variantInfos.equals(that.variantInfos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = groupId != null ? groupId.hashCode() : 0;
|
||||
result = 31 * result + (name != null ? name.hashCode() : 0);
|
||||
result = 31 * result + variantInfos.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static android.media.MediaParser.PARAMETER_TS_IGNORE_AAC_STREAM;
|
||||
import static android.media.MediaParser.PARAMETER_TS_IGNORE_AVC_STREAM;
|
||||
import static android.media.MediaParser.PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM;
|
||||
import static android.media.MediaParser.PARAMETER_TS_MODE;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE;
|
||||
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_EXPOSE_CAPTION_FORMATS;
|
||||
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_IGNORE_TIMESTAMP_OFFSET;
|
||||
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_IN_BAND_CRYPTO_INFO;
|
||||
import static androidx.media3.exoplayer.source.mediaparser.MediaParserUtil.PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaParser;
|
||||
import android.media.MediaParser.OutputConsumer;
|
||||
import android.media.MediaParser.SeekPoint;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.media3.common.FileTypes;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||
import androidx.media3.exoplayer.source.mediaparser.InputReaderAdapterV30;
|
||||
import androidx.media3.exoplayer.source.mediaparser.MediaParserUtil;
|
||||
import androidx.media3.exoplayer.source.mediaparser.OutputConsumerAdapterV30;
|
||||
import androidx.media3.extractor.ExtractorInput;
|
||||
import androidx.media3.extractor.ExtractorOutput;
|
||||
import androidx.media3.extractor.text.SubtitleParser;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.IOException;
|
||||
|
||||
/** {@link HlsMediaChunkExtractor} implemented on top of the platform's {@link MediaParser}. */
|
||||
@RequiresApi(30)
|
||||
@UnstableApi
|
||||
public final class MediaParserHlsMediaChunkExtractor implements HlsMediaChunkExtractor {
|
||||
|
||||
/**
|
||||
* {@link HlsExtractorFactory} implementation that produces {@link
|
||||
* MediaParserHlsMediaChunkExtractor} for all container formats except WebVTT, for which a {@link
|
||||
* BundledHlsMediaChunkExtractor} is returned.
|
||||
*/
|
||||
public static final HlsExtractorFactory FACTORY =
|
||||
(uri,
|
||||
format,
|
||||
muxedCaptionFormats,
|
||||
timestampAdjuster,
|
||||
responseHeaders,
|
||||
sniffingExtractorInput,
|
||||
playerId) -> {
|
||||
if (FileTypes.inferFileTypeFromMimeType(format.sampleMimeType) == FileTypes.WEBVTT) {
|
||||
// The segment contains WebVTT. MediaParser does not support WebVTT parsing, so we use the
|
||||
// bundled extractor.
|
||||
return new BundledHlsMediaChunkExtractor(
|
||||
new WebvttExtractor(
|
||||
format.language,
|
||||
timestampAdjuster,
|
||||
SubtitleParser.Factory.UNSUPPORTED,
|
||||
/* parseSubtitlesDuringExtraction= */ false),
|
||||
format,
|
||||
timestampAdjuster);
|
||||
}
|
||||
|
||||
boolean overrideInBandCaptionDeclarations = muxedCaptionFormats != null;
|
||||
ImmutableList.Builder<MediaFormat> muxedCaptionMediaFormatsBuilder =
|
||||
ImmutableList.builder();
|
||||
if (muxedCaptionFormats != null) {
|
||||
// The manifest contains captions declarations. We use those to determine which captions
|
||||
// will be exposed by MediaParser.
|
||||
for (int i = 0; i < muxedCaptionFormats.size(); i++) {
|
||||
muxedCaptionMediaFormatsBuilder.add(
|
||||
MediaParserUtil.toCaptionsMediaFormat(muxedCaptionFormats.get(i)));
|
||||
}
|
||||
} else {
|
||||
// The manifest does not declare any captions in the stream. Imitate the default HLS
|
||||
// extractor factory and declare a 608 track by default.
|
||||
muxedCaptionMediaFormatsBuilder.add(
|
||||
MediaParserUtil.toCaptionsMediaFormat(
|
||||
new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608).build()));
|
||||
}
|
||||
|
||||
ImmutableList<MediaFormat> muxedCaptionMediaFormats =
|
||||
muxedCaptionMediaFormatsBuilder.build();
|
||||
|
||||
// TODO: Factor out code for optimizing the sniffing order across both factories.
|
||||
OutputConsumerAdapterV30 outputConsumerAdapter = new OutputConsumerAdapterV30();
|
||||
outputConsumerAdapter.setMuxedCaptionFormats(
|
||||
muxedCaptionFormats != null ? muxedCaptionFormats : ImmutableList.of());
|
||||
outputConsumerAdapter.setTimestampAdjuster(timestampAdjuster);
|
||||
MediaParser mediaParser =
|
||||
createMediaParserInstance(
|
||||
outputConsumerAdapter,
|
||||
format,
|
||||
overrideInBandCaptionDeclarations,
|
||||
muxedCaptionMediaFormats,
|
||||
playerId,
|
||||
MediaParser.PARSER_NAME_FMP4,
|
||||
MediaParser.PARSER_NAME_AC3,
|
||||
MediaParser.PARSER_NAME_AC4,
|
||||
MediaParser.PARSER_NAME_ADTS,
|
||||
MediaParser.PARSER_NAME_MP3,
|
||||
MediaParser.PARSER_NAME_TS);
|
||||
|
||||
PeekingInputReader peekingInputReader = new PeekingInputReader(sniffingExtractorInput);
|
||||
// The chunk extractor constructor requires an instance with a known parser name, so we
|
||||
// advance once for MediaParser to sniff the content.
|
||||
mediaParser.advance(peekingInputReader);
|
||||
outputConsumerAdapter.setSelectedParserName(mediaParser.getParserName());
|
||||
|
||||
return new MediaParserHlsMediaChunkExtractor(
|
||||
mediaParser,
|
||||
outputConsumerAdapter,
|
||||
format,
|
||||
overrideInBandCaptionDeclarations,
|
||||
muxedCaptionMediaFormats,
|
||||
/* leadingBytesToSkip= */ peekingInputReader.totalPeekedBytes,
|
||||
playerId);
|
||||
};
|
||||
|
||||
private final OutputConsumerAdapterV30 outputConsumerAdapter;
|
||||
private final InputReaderAdapterV30 inputReaderAdapter;
|
||||
private final MediaParser mediaParser;
|
||||
private final Format format;
|
||||
private final boolean overrideInBandCaptionDeclarations;
|
||||
private final ImmutableList<MediaFormat> muxedCaptionMediaFormats;
|
||||
private final PlayerId playerId;
|
||||
|
||||
private int pendingSkipBytes;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param mediaParser The {@link MediaParser} instance to use for extraction of segments. The
|
||||
* provided instance must have completed sniffing, or must have been created by name.
|
||||
* @param outputConsumerAdapter The {@link OutputConsumerAdapterV30} with which {@code
|
||||
* mediaParser} was created.
|
||||
* @param format The {@link Format} associated with the segment.
|
||||
* @param overrideInBandCaptionDeclarations Whether to ignore any in-band caption track
|
||||
* declarations in favor of using the {@code muxedCaptionMediaFormats} instead. If false,
|
||||
* caption declarations found in the extracted media will be used, causing {@code
|
||||
* muxedCaptionMediaFormats} to be ignored instead.
|
||||
* @param muxedCaptionMediaFormats The list of in-band caption {@link MediaFormat MediaFormats}
|
||||
* that {@link MediaParser} should expose.
|
||||
* @param leadingBytesToSkip The number of bytes to skip from the start of the input before
|
||||
* starting extraction.
|
||||
* @param playerId The {@link PlayerId} of the player using this chunk extractor.
|
||||
*/
|
||||
public MediaParserHlsMediaChunkExtractor(
|
||||
MediaParser mediaParser,
|
||||
OutputConsumerAdapterV30 outputConsumerAdapter,
|
||||
Format format,
|
||||
boolean overrideInBandCaptionDeclarations,
|
||||
ImmutableList<MediaFormat> muxedCaptionMediaFormats,
|
||||
int leadingBytesToSkip,
|
||||
PlayerId playerId) {
|
||||
this.mediaParser = mediaParser;
|
||||
this.outputConsumerAdapter = outputConsumerAdapter;
|
||||
this.overrideInBandCaptionDeclarations = overrideInBandCaptionDeclarations;
|
||||
this.muxedCaptionMediaFormats = muxedCaptionMediaFormats;
|
||||
this.format = format;
|
||||
this.playerId = playerId;
|
||||
pendingSkipBytes = leadingBytesToSkip;
|
||||
inputReaderAdapter = new InputReaderAdapterV30();
|
||||
}
|
||||
|
||||
// ChunkExtractor implementation.
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput extractorOutput) {
|
||||
outputConsumerAdapter.setExtractorOutput(extractorOutput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean read(ExtractorInput extractorInput) throws IOException {
|
||||
extractorInput.skipFully(pendingSkipBytes);
|
||||
pendingSkipBytes = 0;
|
||||
inputReaderAdapter.setDataReader(extractorInput, extractorInput.getLength());
|
||||
return mediaParser.advance(inputReaderAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPackedAudioExtractor() {
|
||||
String parserName = mediaParser.getParserName();
|
||||
return MediaParser.PARSER_NAME_AC3.equals(parserName)
|
||||
|| MediaParser.PARSER_NAME_AC4.equals(parserName)
|
||||
|| MediaParser.PARSER_NAME_ADTS.equals(parserName)
|
||||
|| MediaParser.PARSER_NAME_MP3.equals(parserName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReusable() {
|
||||
String parserName = mediaParser.getParserName();
|
||||
return MediaParser.PARSER_NAME_FMP4.equals(parserName)
|
||||
|| MediaParser.PARSER_NAME_TS.equals(parserName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HlsMediaChunkExtractor recreate() {
|
||||
checkState(!isReusable());
|
||||
return new MediaParserHlsMediaChunkExtractor(
|
||||
createMediaParserInstance(
|
||||
outputConsumerAdapter,
|
||||
format,
|
||||
overrideInBandCaptionDeclarations,
|
||||
muxedCaptionMediaFormats,
|
||||
playerId,
|
||||
mediaParser.getParserName()),
|
||||
outputConsumerAdapter,
|
||||
format,
|
||||
overrideInBandCaptionDeclarations,
|
||||
muxedCaptionMediaFormats,
|
||||
/* leadingBytesToSkip= */ 0,
|
||||
playerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTruncatedSegmentParsed() {
|
||||
mediaParser.seek(SeekPoint.START);
|
||||
}
|
||||
|
||||
// Allow constants that are not part of the public MediaParser API.
|
||||
@SuppressLint({"WrongConstant"})
|
||||
private static MediaParser createMediaParserInstance(
|
||||
OutputConsumer outputConsumer,
|
||||
Format format,
|
||||
boolean overrideInBandCaptionDeclarations,
|
||||
ImmutableList<MediaFormat> muxedCaptionMediaFormats,
|
||||
PlayerId playerId,
|
||||
String... parserNames) {
|
||||
MediaParser mediaParser =
|
||||
parserNames.length == 1
|
||||
? MediaParser.createByName(parserNames[0], outputConsumer)
|
||||
: MediaParser.create(outputConsumer, parserNames);
|
||||
mediaParser.setParameter(PARAMETER_EXPOSE_CAPTION_FORMATS, muxedCaptionMediaFormats);
|
||||
mediaParser.setParameter(
|
||||
PARAMETER_OVERRIDE_IN_BAND_CAPTION_DECLARATIONS, overrideInBandCaptionDeclarations);
|
||||
mediaParser.setParameter(PARAMETER_IN_BAND_CRYPTO_INFO, true);
|
||||
mediaParser.setParameter(PARAMETER_EAGERLY_EXPOSE_TRACK_TYPE, true);
|
||||
mediaParser.setParameter(PARAMETER_IGNORE_TIMESTAMP_OFFSET, true);
|
||||
mediaParser.setParameter(PARAMETER_TS_IGNORE_SPLICE_INFO_STREAM, true);
|
||||
mediaParser.setParameter(PARAMETER_TS_MODE, "hls");
|
||||
@Nullable String codecs = format.codecs;
|
||||
if (!TextUtils.isEmpty(codecs)) {
|
||||
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
|
||||
// exist. If we know from the codec attribute that they don't exist, then we can
|
||||
// explicitly ignore them even if they're declared.
|
||||
if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
|
||||
mediaParser.setParameter(PARAMETER_TS_IGNORE_AAC_STREAM, true);
|
||||
}
|
||||
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
|
||||
mediaParser.setParameter(PARAMETER_TS_IGNORE_AVC_STREAM, true);
|
||||
}
|
||||
}
|
||||
if (SDK_INT >= 31) {
|
||||
MediaParserUtil.setLogSessionIdOnMediaParser(mediaParser, playerId);
|
||||
}
|
||||
return mediaParser;
|
||||
}
|
||||
|
||||
private static final class PeekingInputReader implements MediaParser.SeekableInputReader {
|
||||
|
||||
private final ExtractorInput extractorInput;
|
||||
private int totalPeekedBytes;
|
||||
|
||||
private PeekingInputReader(ExtractorInput extractorInput) {
|
||||
this.extractorInput = extractorInput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int readLength) throws IOException {
|
||||
int peekedBytes = extractorInput.peek(buffer, offset, readLength);
|
||||
totalPeekedBytes += peekedBytes;
|
||||
return peekedBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition() {
|
||||
return extractorInput.getPeekPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLength() {
|
||||
return extractorInput.getLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekToPosition(long position) {
|
||||
// Seeking is not allowed when sniffing the content.
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.TrackGroup;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.source.SampleQueue;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Thrown when it is not possible to map a {@link TrackGroup} to a {@link SampleQueue}. */
|
||||
@UnstableApi
|
||||
public final class SampleQueueMappingException extends IOException {
|
||||
|
||||
/**
|
||||
* @param mimeType The MIME type of the track group whose mapping failed.
|
||||
*/
|
||||
public SampleQueueMappingException(@Nullable String mimeType) {
|
||||
super("Unable to bind a sample queue to TrackGroup with MIME type " + mimeType + ".");
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static androidx.media3.common.util.TimestampAdjuster.MODE_SHARED;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.TimestampAdjuster;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/** Provides {@link TimestampAdjuster} instances for use during HLS playbacks. */
|
||||
@UnstableApi
|
||||
public final class TimestampAdjusterProvider {
|
||||
|
||||
// TODO: Prevent this array from growing indefinitely large by removing adjusters that are no
|
||||
// longer required.
|
||||
private final SparseArray<TimestampAdjuster> timestampAdjusters;
|
||||
|
||||
public TimestampAdjusterProvider() {
|
||||
timestampAdjusters = new SparseArray<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link TimestampAdjuster} suitable for adjusting the pts timestamps contained in a
|
||||
* chunk with a given discontinuity sequence.
|
||||
*
|
||||
* @param discontinuitySequence The chunk's discontinuity sequence.
|
||||
* @return A {@link TimestampAdjuster}.
|
||||
*/
|
||||
public TimestampAdjuster getAdjuster(int discontinuitySequence) {
|
||||
@Nullable TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence);
|
||||
if (adjuster == null) {
|
||||
adjuster = new TimestampAdjuster(MODE_SHARED);
|
||||
timestampAdjusters.put(discontinuitySequence, adjuster);
|
||||
}
|
||||
return adjuster;
|
||||
}
|
||||
|
||||
/** Resets the provider. */
|
||||
public void reset() {
|
||||
timestampAdjusters.clear();
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.source.SampleQueue;
|
||||
import androidx.media3.exoplayer.source.chunk.MediaChunk;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Thrown when an attempt is made to write a sample to a {@link SampleQueue} whose timestamp is
|
||||
* inconsistent with the chunk from which it originates.
|
||||
*/
|
||||
/* package */ final class UnexpectedSampleTimestampException extends IOException {
|
||||
|
||||
/** The {@link MediaChunk} that contained the rejected sample. */
|
||||
public final MediaChunk mediaChunk;
|
||||
|
||||
/**
|
||||
* The timestamp of the last sample that was loaded from {@link #mediaChunk} and successfully
|
||||
* written to the {@link SampleQueue}, in microseconds. {@link C#TIME_UNSET} if the first sample
|
||||
* in the chunk was rejected.
|
||||
*/
|
||||
public final long lastAcceptedSampleTimeUs;
|
||||
|
||||
/** The timestamp of the rejected sample, in microseconds. */
|
||||
public final long rejectedSampleTimeUs;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
*
|
||||
* @param mediaChunk The {@link MediaChunk} with the unexpected sample timestamp.
|
||||
* @param lastAcceptedSampleTimeUs The timestamp of the last sample that was loaded from the chunk
|
||||
* and successfully written to the {@link SampleQueue}, in microseconds. {@link C#TIME_UNSET}
|
||||
* if the first sample in the chunk was rejected.
|
||||
* @param rejectedSampleTimeUs The timestamp of the rejected sample, in microseconds.
|
||||
*/
|
||||
public UnexpectedSampleTimestampException(
|
||||
MediaChunk mediaChunk, long lastAcceptedSampleTimeUs, long rejectedSampleTimeUs) {
|
||||
super(
|
||||
"Unexpected sample timestamp: "
|
||||
+ Util.usToMs(rejectedSampleTimeUs)
|
||||
+ " in chunk ["
|
||||
+ mediaChunk.startTimeUs
|
||||
+ ", "
|
||||
+ mediaChunk.endTimeUs
|
||||
+ "]");
|
||||
this.mediaChunk = mediaChunk;
|
||||
this.lastAcceptedSampleTimeUs = lastAcceptedSampleTimeUs;
|
||||
this.rejectedSampleTimeUs = rejectedSampleTimeUs;
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.ParserException;
|
||||
import androidx.media3.common.util.ParsableByteArray;
|
||||
import androidx.media3.common.util.TimestampAdjuster;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.extractor.Extractor;
|
||||
import androidx.media3.extractor.ExtractorInput;
|
||||
import androidx.media3.extractor.ExtractorOutput;
|
||||
import androidx.media3.extractor.PositionHolder;
|
||||
import androidx.media3.extractor.SeekMap;
|
||||
import androidx.media3.extractor.TrackOutput;
|
||||
import androidx.media3.extractor.text.SubtitleParser;
|
||||
import androidx.media3.extractor.text.SubtitleTranscodingExtractorOutput;
|
||||
import androidx.media3.extractor.text.webvtt.WebvttParserUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* A special purpose extractor for WebVTT content in HLS.
|
||||
*
|
||||
* <p>This extractor passes through non-empty WebVTT files untouched, however derives the correct
|
||||
* sample timestamp for each by sniffing the X-TIMESTAMP-MAP header along with the start timestamp
|
||||
* of the first cue header. Empty WebVTT files are not passed through, since it's not possible to
|
||||
* derive a sample timestamp in this case.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class WebvttExtractor implements Extractor {
|
||||
|
||||
private static final Pattern LOCAL_TIMESTAMP = Pattern.compile("LOCAL:([^,]+)");
|
||||
private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(-?\\d+)");
|
||||
private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */;
|
||||
private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH;
|
||||
|
||||
@Nullable private final String language;
|
||||
private final TimestampAdjuster timestampAdjuster;
|
||||
private final ParsableByteArray sampleDataWrapper;
|
||||
private final SubtitleParser.Factory subtitleParserFactory;
|
||||
private final boolean parseSubtitlesDuringExtraction;
|
||||
|
||||
private @MonotonicNonNull ExtractorOutput output;
|
||||
|
||||
private byte[] sampleData;
|
||||
private int sampleSize;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #WebvttExtractor(String, TimestampAdjuster, SubtitleParser.Factory,
|
||||
* boolean)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) {
|
||||
this(
|
||||
language,
|
||||
timestampAdjuster,
|
||||
SubtitleParser.Factory.UNSUPPORTED,
|
||||
/* parseSubtitlesDuringExtraction= */ false);
|
||||
}
|
||||
|
||||
public WebvttExtractor(
|
||||
@Nullable String language,
|
||||
TimestampAdjuster timestampAdjuster,
|
||||
SubtitleParser.Factory subtitleParserFactory,
|
||||
boolean parseSubtitlesDuringExtraction) {
|
||||
this.language = language;
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
this.sampleDataWrapper = new ParsableByteArray();
|
||||
sampleData = new byte[1024];
|
||||
this.subtitleParserFactory = subtitleParserFactory;
|
||||
this.parseSubtitlesDuringExtraction = parseSubtitlesDuringExtraction;
|
||||
}
|
||||
|
||||
// Extractor implementation.
|
||||
|
||||
@Override
|
||||
public boolean sniff(ExtractorInput input) throws IOException {
|
||||
// Check whether there is a header without BOM.
|
||||
input.peekFully(
|
||||
sampleData, /* offset= */ 0, /* length= */ HEADER_MIN_LENGTH, /* allowEndOfInput= */ false);
|
||||
sampleDataWrapper.reset(sampleData, HEADER_MIN_LENGTH);
|
||||
if (WebvttParserUtil.isWebvttHeaderLine(sampleDataWrapper)) {
|
||||
return true;
|
||||
}
|
||||
// The header did not match, try including the BOM.
|
||||
input.peekFully(
|
||||
sampleData,
|
||||
/* offset= */ HEADER_MIN_LENGTH,
|
||||
HEADER_MAX_LENGTH - HEADER_MIN_LENGTH,
|
||||
/* allowEndOfInput= */ false);
|
||||
sampleDataWrapper.reset(sampleData, HEADER_MAX_LENGTH);
|
||||
return WebvttParserUtil.isWebvttHeaderLine(sampleDataWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
this.output =
|
||||
parseSubtitlesDuringExtraction
|
||||
? new SubtitleTranscodingExtractorOutput(output, subtitleParserFactory)
|
||||
: output;
|
||||
this.output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(long position, long timeUs) {
|
||||
// This extractor is only used for the HLS use case, which should not call this method.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
|
||||
// output == null suggests init() hasn't been called
|
||||
checkNotNull(output);
|
||||
int currentFileSize = (int) input.getLength();
|
||||
|
||||
// Increase the size of sampleData if necessary.
|
||||
if (sampleSize == sampleData.length) {
|
||||
sampleData =
|
||||
Arrays.copyOf(
|
||||
sampleData,
|
||||
(currentFileSize != C.LENGTH_UNSET ? currentFileSize : sampleData.length) * 3 / 2);
|
||||
}
|
||||
|
||||
// Consume to the input.
|
||||
int bytesRead = input.read(sampleData, sampleSize, sampleData.length - sampleSize);
|
||||
if (bytesRead != C.RESULT_END_OF_INPUT) {
|
||||
sampleSize += bytesRead;
|
||||
if (currentFileSize == C.LENGTH_UNSET || sampleSize != currentFileSize) {
|
||||
return Extractor.RESULT_CONTINUE;
|
||||
}
|
||||
}
|
||||
|
||||
// We've reached the end of the input, which corresponds to the end of the current file.
|
||||
processSample();
|
||||
return Extractor.RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
@RequiresNonNull("output")
|
||||
private void processSample() throws ParserException {
|
||||
ParsableByteArray webvttData = new ParsableByteArray(sampleData);
|
||||
|
||||
// Validate the first line of the header.
|
||||
WebvttParserUtil.validateWebvttHeaderLine(webvttData);
|
||||
|
||||
// Defaults to use if the header doesn't contain an X-TIMESTAMP-MAP header.
|
||||
long vttTimestampUs = 0;
|
||||
long tsTimestampUs = 0;
|
||||
|
||||
// Parse the remainder of the header looking for X-TIMESTAMP-MAP.
|
||||
for (String line = webvttData.readLine();
|
||||
!TextUtils.isEmpty(line);
|
||||
line = webvttData.readLine()) {
|
||||
if (line.startsWith("X-TIMESTAMP-MAP")) {
|
||||
Matcher localTimestampMatcher = LOCAL_TIMESTAMP.matcher(line);
|
||||
if (!localTimestampMatcher.find()) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"X-TIMESTAMP-MAP doesn't contain local timestamp: " + line, /* cause= */ null);
|
||||
}
|
||||
Matcher mediaTimestampMatcher = MEDIA_TIMESTAMP.matcher(line);
|
||||
if (!mediaTimestampMatcher.find()) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"X-TIMESTAMP-MAP doesn't contain media timestamp: " + line, /* cause= */ null);
|
||||
}
|
||||
vttTimestampUs =
|
||||
WebvttParserUtil.parseTimestampUs(checkNotNull(localTimestampMatcher.group(1)));
|
||||
tsTimestampUs =
|
||||
TimestampAdjuster.ptsToUs(Long.parseLong(checkNotNull(mediaTimestampMatcher.group(1))));
|
||||
}
|
||||
}
|
||||
|
||||
// Find the first cue header and parse the start time.
|
||||
Matcher cueHeaderMatcher = WebvttParserUtil.findNextCueHeader(webvttData);
|
||||
if (cueHeaderMatcher == null) {
|
||||
// No cues found. Don't output a sample, but still output a corresponding track.
|
||||
buildTrackOutput(0);
|
||||
return;
|
||||
}
|
||||
|
||||
long firstCueTimeUs =
|
||||
WebvttParserUtil.parseTimestampUs(checkNotNull(cueHeaderMatcher.group(1)));
|
||||
long sampleTimeUs =
|
||||
timestampAdjuster.adjustTsTimestamp(
|
||||
TimestampAdjuster.usToWrappedPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs));
|
||||
long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs;
|
||||
// Output the track.
|
||||
TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs);
|
||||
// Output the sample.
|
||||
sampleDataWrapper.reset(sampleData, sampleSize);
|
||||
trackOutput.sampleData(sampleDataWrapper, sampleSize);
|
||||
trackOutput.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
|
||||
}
|
||||
|
||||
@RequiresNonNull("output")
|
||||
private TrackOutput buildTrackOutput(long subsampleOffsetUs) {
|
||||
TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT);
|
||||
trackOutput.format(
|
||||
new Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.TEXT_VTT)
|
||||
.setLanguage(language)
|
||||
.setSubsampleOffsetUs(subsampleOffsetUs)
|
||||
.build());
|
||||
output.endTracks();
|
||||
return trackOutput;
|
||||
}
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.offline;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.UriUtil;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.datasource.cache.CacheDataSource;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsPlaylist;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
|
||||
import androidx.media3.exoplayer.offline.SegmentDownloader;
|
||||
import androidx.media3.exoplayer.upstream.ParsingLoadable.Parser;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* A downloader for HLS streams.
|
||||
*
|
||||
* <p>Example usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
|
||||
* CacheDataSource.Factory cacheDataSourceFactory =
|
||||
* new CacheDataSource.Factory()
|
||||
* .setCache(cache)
|
||||
* .setUpstreamDataSourceFactory(new DefaultHttpDataSource.Factory());
|
||||
* // Create a downloader for the first variant in a multivariant playlist.
|
||||
* HlsDownloader hlsDownloader =
|
||||
* new HlsDownloader.Factory(cacheDataSourceFactory)
|
||||
* .create(new MediaItem.Builder()
|
||||
* .setUri(playlistUri)
|
||||
* .setStreamKeys(
|
||||
* ImmutableList.of(
|
||||
* new StreamKey(HlsMultivariantPlaylist.GROUP_INDEX_VARIANT, 0)))
|
||||
* .build());
|
||||
* // Perform the download.
|
||||
* hlsDownloader.download(progressListener);
|
||||
* // Use the downloaded data for playback.
|
||||
* HlsMediaSource mediaSource =
|
||||
* new HlsMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem);
|
||||
* }</pre>
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
|
||||
|
||||
/** A factory for {@linkplain HlsDownloader HLS downloaders}. */
|
||||
public static final class Factory extends BaseFactory<HlsPlaylist> {
|
||||
|
||||
/**
|
||||
* Creates a factory for {@link HlsDownloader}.
|
||||
*
|
||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
||||
* download will be written.
|
||||
*/
|
||||
public Factory(CacheDataSource.Factory cacheDataSourceFactory) {
|
||||
super(cacheDataSourceFactory, new HlsPlaylistParser());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a parser for HLS playlists.
|
||||
*
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setManifestParser(HlsPlaylistParser manifestParser) {
|
||||
this.manifestParser = manifestParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Executor} used to make requests for the media being downloaded. Providing an
|
||||
* {@link Executor} that uses multiple threads will speed up the download by allowing parts of
|
||||
* it to be executed in parallel.
|
||||
*
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setExecutor(Executor executor) {
|
||||
super.setExecutor(executor);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum difference of the start time of two segments, up to which the segments (of
|
||||
* the same URI) should be merged into a single download segment, in milliseconds.
|
||||
*
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setMaxMergedSegmentStartTimeDiffMs(long maxMergedSegmentStartTimeDiffMs) {
|
||||
super.setMaxMergedSegmentStartTimeDiffMs(maxMergedSegmentStartTimeDiffMs);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the start position in microseconds that the download should start from.
|
||||
*
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setStartPositionUs(long startPositionUs) {
|
||||
super.setStartPositionUs(startPositionUs);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration in microseconds from the {@code startPositionUs} to be downloaded, or
|
||||
* {@link C#TIME_UNSET} if the media should be downloaded to the end.
|
||||
*
|
||||
* @return This factory, for convenience.
|
||||
*/
|
||||
@Override
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setDurationUs(long durationUs) {
|
||||
super.setDurationUs(durationUs);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Creates {@linkplain HlsDownloader HLS downloaders}. */
|
||||
@Override
|
||||
public HlsDownloader create(MediaItem mediaItem) {
|
||||
return new HlsDownloader(
|
||||
mediaItem,
|
||||
manifestParser,
|
||||
cacheDataSourceFactory,
|
||||
executor,
|
||||
maxMergedSegmentStartTimeDiffMs,
|
||||
startPositionUs,
|
||||
durationUs);
|
||||
}
|
||||
}
|
||||
|
||||
private final Parser<HlsPlaylist> manifestParser;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link HlsDownloader.Factory#create(MediaItem)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public HlsDownloader(MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory) {
|
||||
this(mediaItem, cacheDataSourceFactory, Runnable::run);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link HlsDownloader.Factory#create(MediaItem)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public HlsDownloader(
|
||||
MediaItem mediaItem, CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
|
||||
this(
|
||||
mediaItem,
|
||||
new HlsPlaylistParser(),
|
||||
cacheDataSourceFactory,
|
||||
executor,
|
||||
DEFAULT_MAX_MERGED_SEGMENT_START_TIME_DIFF_MS,
|
||||
/* startPositionUs= */ 0,
|
||||
/* durationUs= */ C.TIME_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param mediaItem The {@link MediaItem} to be downloaded.
|
||||
* @param manifestParser A parser for HLS playlists.
|
||||
* @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
|
||||
* download will be written.
|
||||
* @param executor An {@link Executor} used to make requests for the media being downloaded.
|
||||
* Providing an {@link Executor} that uses multiple threads will speed up the download by
|
||||
* allowing parts of it to be executed in parallel.
|
||||
* @param maxMergedSegmentStartTimeDiffMs The maximum difference of the start time of two
|
||||
* segments, up to which the segments (of the same URI) should be merged into a single
|
||||
* download segment, in milliseconds.
|
||||
* @param startPositionUs The start position in microseconds that the download should start from.
|
||||
* @param durationUs The duration in microseconds from the {@code startPositionUs} to be
|
||||
* downloaded, or {@link C#TIME_UNSET} if the media should be downloaded to the end.
|
||||
*/
|
||||
private HlsDownloader(
|
||||
MediaItem mediaItem,
|
||||
Parser<HlsPlaylist> manifestParser,
|
||||
CacheDataSource.Factory cacheDataSourceFactory,
|
||||
Executor executor,
|
||||
long maxMergedSegmentStartTimeDiffMs,
|
||||
long startPositionUs,
|
||||
long durationUs) {
|
||||
super(
|
||||
mediaItem,
|
||||
manifestParser,
|
||||
cacheDataSourceFactory,
|
||||
executor,
|
||||
maxMergedSegmentStartTimeDiffMs,
|
||||
startPositionUs,
|
||||
durationUs);
|
||||
this.manifestParser = manifestParser;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Segment> getSegments(DataSource dataSource, HlsPlaylist manifest, boolean removing)
|
||||
throws IOException, InterruptedException {
|
||||
ArrayList<DataSpec> mediaPlaylistDataSpecs = new ArrayList<>();
|
||||
HlsMultivariantPlaylist multivariantPlaylist = HlsMultivariantPlaylist.EMPTY;
|
||||
if (manifest instanceof HlsMultivariantPlaylist) {
|
||||
multivariantPlaylist = (HlsMultivariantPlaylist) manifest;
|
||||
addMediaPlaylistDataSpecs(multivariantPlaylist.mediaPlaylistUrls, mediaPlaylistDataSpecs);
|
||||
} else {
|
||||
mediaPlaylistDataSpecs.add(
|
||||
SegmentDownloader.getCompressibleDataSpec(Uri.parse(manifest.baseUri)));
|
||||
}
|
||||
|
||||
Parser<HlsPlaylist> mediaPlaylistParser =
|
||||
!multivariantPlaylist.variableDefinitions.isEmpty()
|
||||
? new HlsPlaylistParser(multivariantPlaylist, /* previousMediaPlaylist= */ null)
|
||||
: manifestParser;
|
||||
|
||||
ArrayList<Segment> segments = new ArrayList<>();
|
||||
HashSet<Uri> seenEncryptionKeyUris = new HashSet<>();
|
||||
for (DataSpec mediaPlaylistDataSpec : mediaPlaylistDataSpecs) {
|
||||
segments.add(new Segment(/* startTimeUs= */ 0, mediaPlaylistDataSpec));
|
||||
HlsMediaPlaylist mediaPlaylist;
|
||||
try {
|
||||
mediaPlaylist =
|
||||
(HlsMediaPlaylist)
|
||||
getManifest(dataSource, mediaPlaylistParser, mediaPlaylistDataSpec, removing);
|
||||
} catch (IOException e) {
|
||||
if (!removing) {
|
||||
throw e;
|
||||
}
|
||||
// Generating an incomplete segment list is allowed. Advance to the next media playlist.
|
||||
continue;
|
||||
}
|
||||
@Nullable HlsMediaPlaylist.Segment lastInitSegment = null;
|
||||
List<HlsMediaPlaylist.Segment> hlsSegments = mediaPlaylist.segments;
|
||||
long startPositionUs = removing ? 0 : this.startPositionUs;
|
||||
long durationUs = removing ? C.TIME_UNSET : this.durationUs;
|
||||
for (int i = 0; i < hlsSegments.size(); i++) {
|
||||
HlsMediaPlaylist.Segment segment = hlsSegments.get(i);
|
||||
long segmentStartTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
|
||||
if (segmentStartTimeUs + segment.durationUs <= startPositionUs) {
|
||||
// The current segment is before the start position.
|
||||
continue;
|
||||
}
|
||||
if (durationUs != C.TIME_UNSET && segmentStartTimeUs >= startPositionUs + durationUs) {
|
||||
// The current segment is after the end position.
|
||||
break;
|
||||
}
|
||||
HlsMediaPlaylist.Segment initSegment = segment.initializationSegment;
|
||||
if (initSegment != null && initSegment != lastInitSegment) {
|
||||
lastInitSegment = initSegment;
|
||||
addSegment(mediaPlaylist, initSegment, seenEncryptionKeyUris, segments);
|
||||
}
|
||||
addSegment(mediaPlaylist, segment, seenEncryptionKeyUris, segments);
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private void addMediaPlaylistDataSpecs(List<Uri> mediaPlaylistUrls, List<DataSpec> out) {
|
||||
for (int i = 0; i < mediaPlaylistUrls.size(); i++) {
|
||||
out.add(SegmentDownloader.getCompressibleDataSpec(mediaPlaylistUrls.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
private void addSegment(
|
||||
HlsMediaPlaylist mediaPlaylist,
|
||||
HlsMediaPlaylist.Segment segment,
|
||||
HashSet<Uri> seenEncryptionKeyUris,
|
||||
ArrayList<Segment> out) {
|
||||
String baseUri = mediaPlaylist.baseUri;
|
||||
long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
|
||||
if (segment.fullSegmentEncryptionKeyUri != null) {
|
||||
Uri keyUri = UriUtil.resolveToUri(baseUri, segment.fullSegmentEncryptionKeyUri);
|
||||
if (seenEncryptionKeyUris.add(keyUri)) {
|
||||
out.add(new Segment(startTimeUs, SegmentDownloader.getCompressibleDataSpec(keyUri)));
|
||||
}
|
||||
}
|
||||
Uri segmentUri = UriUtil.resolveToUri(baseUri, segment.url);
|
||||
DataSpec dataSpec = new DataSpec(segmentUri, segment.byteRangeOffset, segment.byteRangeLength);
|
||||
out.add(new Segment(startTimeUs, dataSpec));
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.exoplayer.hls.offline;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.exoplayer.hls;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
||||
|
||||
/** Default implementation for {@link HlsPlaylistParserFactory}. */
|
||||
@UnstableApi
|
||||
public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
|
||||
|
||||
@Override
|
||||
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
|
||||
return new HlsPlaylistParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
|
||||
HlsMultivariantPlaylist multivariantPlaylist,
|
||||
@Nullable HlsMediaPlaylist previousMediaPlaylist) {
|
||||
return new HlsPlaylistParser(multivariantPlaylist, previousMediaPlaylist);
|
||||
}
|
||||
}
|
||||
+1404
File diff suppressed because it is too large
Load Diff
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.StreamKey;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.offline.FilteringManifestParser;
|
||||
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link HlsPlaylistParserFactory} that includes only the streams identified by the given stream
|
||||
* keys.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
|
||||
|
||||
private final HlsPlaylistParserFactory hlsPlaylistParserFactory;
|
||||
private final List<StreamKey> streamKeys;
|
||||
|
||||
/**
|
||||
* @param hlsPlaylistParserFactory A factory for the parsers of the playlists which will be
|
||||
* filtered.
|
||||
* @param streamKeys The stream keys. If null or empty then filtering will not occur.
|
||||
*/
|
||||
public FilteringHlsPlaylistParserFactory(
|
||||
HlsPlaylistParserFactory hlsPlaylistParserFactory, List<StreamKey> streamKeys) {
|
||||
this.hlsPlaylistParserFactory = hlsPlaylistParserFactory;
|
||||
this.streamKeys = streamKeys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
|
||||
return new FilteringManifestParser<>(
|
||||
hlsPlaylistParserFactory.createPlaylistParser(), streamKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
|
||||
HlsMultivariantPlaylist multivariantPlaylist,
|
||||
@Nullable HlsMediaPlaylist previousMediaPlaylist) {
|
||||
return new FilteringManifestParser<>(
|
||||
hlsPlaylistParserFactory.createPlaylistParser(multivariantPlaylist, previousMediaPlaylist),
|
||||
streamKeys);
|
||||
}
|
||||
}
|
||||
+1622
File diff suppressed because it is too large
Load Diff
+395
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.DrmInitData;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.StreamKey;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Represents an HLS multivariant playlist. */
|
||||
@UnstableApi
|
||||
public final class HlsMultivariantPlaylist extends HlsPlaylist {
|
||||
|
||||
/** Represents an empty multivariant playlist, from which no attributes can be inherited. */
|
||||
public static final HlsMultivariantPlaylist EMPTY =
|
||||
new HlsMultivariantPlaylist(
|
||||
/* baseUri= */ "",
|
||||
/* tags= */ Collections.emptyList(),
|
||||
/* variants= */ Collections.emptyList(),
|
||||
/* videos= */ Collections.emptyList(),
|
||||
/* audios= */ Collections.emptyList(),
|
||||
/* subtitles= */ Collections.emptyList(),
|
||||
/* closedCaptions= */ Collections.emptyList(),
|
||||
/* muxedAudioFormat= */ null,
|
||||
/* muxedCaptionFormats= */ Collections.emptyList(),
|
||||
/* hasIndependentSegments= */ false,
|
||||
/* variableDefinitions= */ Collections.emptyMap(),
|
||||
/* sessionKeyDrmInitData= */ Collections.emptyList(),
|
||||
/* contentSteeringInfo= */ null);
|
||||
|
||||
// These constants must not be changed because they are persisted in offline stream keys.
|
||||
public static final int GROUP_INDEX_VARIANT = 0;
|
||||
public static final int GROUP_INDEX_AUDIO = 1;
|
||||
public static final int GROUP_INDEX_SUBTITLE = 2;
|
||||
|
||||
/** A variant (i.e. an #EXT-X-STREAM-INF tag) in a multivariant playlist. */
|
||||
public static final class Variant {
|
||||
|
||||
/** The variant's url. */
|
||||
public final Uri url;
|
||||
|
||||
/** Format information associated with this variant. */
|
||||
public final Format format;
|
||||
|
||||
/** The video rendition group referenced by this variant, or {@code null}. */
|
||||
@Nullable public final String videoGroupId;
|
||||
|
||||
/** The audio rendition group referenced by this variant, or {@code null}. */
|
||||
@Nullable public final String audioGroupId;
|
||||
|
||||
/** The subtitle rendition group referenced by this variant, or {@code null}. */
|
||||
@Nullable public final String subtitleGroupId;
|
||||
|
||||
/** The caption rendition group referenced by this variant, or {@code null}. */
|
||||
@Nullable public final String captionGroupId;
|
||||
|
||||
/** The identifier of the pathway that this variant belongs, or {@code null}. */
|
||||
@Nullable public final String pathwayId;
|
||||
|
||||
/** The stable identifier for this variant, or {@code null}. */
|
||||
@Nullable public final String stableVariantId;
|
||||
|
||||
/**
|
||||
* @param url See {@link #url}.
|
||||
* @param format See {@link #format}.
|
||||
* @param videoGroupId See {@link #videoGroupId}.
|
||||
* @param audioGroupId See {@link #audioGroupId}.
|
||||
* @param subtitleGroupId See {@link #subtitleGroupId}.
|
||||
* @param captionGroupId See {@link #captionGroupId}.
|
||||
* @param pathwayId See {@link #pathwayId}.
|
||||
* @param stableVariantId See {@link #stableVariantId}.
|
||||
*/
|
||||
public Variant(
|
||||
Uri url,
|
||||
Format format,
|
||||
@Nullable String videoGroupId,
|
||||
@Nullable String audioGroupId,
|
||||
@Nullable String subtitleGroupId,
|
||||
@Nullable String captionGroupId,
|
||||
@Nullable String pathwayId,
|
||||
@Nullable String stableVariantId) {
|
||||
this.url = url;
|
||||
this.format = format;
|
||||
this.videoGroupId = videoGroupId;
|
||||
this.audioGroupId = audioGroupId;
|
||||
this.subtitleGroupId = subtitleGroupId;
|
||||
this.captionGroupId = captionGroupId;
|
||||
this.pathwayId = pathwayId;
|
||||
this.stableVariantId = stableVariantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a variant for a given media playlist url.
|
||||
*
|
||||
* @param url The media playlist url.
|
||||
* @return The variant instance.
|
||||
*/
|
||||
public static Variant createMediaPlaylistVariantUrl(Uri url) {
|
||||
Format format =
|
||||
new Format.Builder().setId("0").setContainerMimeType(MimeTypes.APPLICATION_M3U8).build();
|
||||
return new Variant(
|
||||
url,
|
||||
format,
|
||||
/* videoGroupId= */ null,
|
||||
/* audioGroupId= */ null,
|
||||
/* subtitleGroupId= */ null,
|
||||
/* captionGroupId= */ null,
|
||||
/* pathwayId= */ null,
|
||||
/* stableVariantId= */ null);
|
||||
}
|
||||
|
||||
/** Returns a copy of this instance with the given {@link Format}. */
|
||||
public Variant copyWithFormat(Format format) {
|
||||
return new Variant(
|
||||
url,
|
||||
format,
|
||||
videoGroupId,
|
||||
audioGroupId,
|
||||
subtitleGroupId,
|
||||
captionGroupId,
|
||||
pathwayId,
|
||||
stableVariantId);
|
||||
}
|
||||
}
|
||||
|
||||
/** A rendition (i.e. an #EXT-X-MEDIA tag) in a multivariant playlist. */
|
||||
public static final class Rendition {
|
||||
|
||||
/** The rendition's url, or null if the tag does not have a URI attribute. */
|
||||
@Nullable public final Uri url;
|
||||
|
||||
/** Format information associated with this rendition. */
|
||||
public final Format format;
|
||||
|
||||
/** The group to which this rendition belongs. */
|
||||
public final String groupId;
|
||||
|
||||
/** The name of the rendition. */
|
||||
public final String name;
|
||||
|
||||
/** The stable identifier for this rendition, or {@code null}. * */
|
||||
@Nullable public final String stableRenditionId;
|
||||
|
||||
/**
|
||||
* @param url See {@link #url}.
|
||||
* @param format See {@link #format}.
|
||||
* @param groupId See {@link #groupId}.
|
||||
* @param name See {@link #name}.
|
||||
* @param stableRenditionId See {@link #stableRenditionId}.
|
||||
*/
|
||||
public Rendition(
|
||||
@Nullable Uri url,
|
||||
Format format,
|
||||
String groupId,
|
||||
String name,
|
||||
@Nullable String stableRenditionId) {
|
||||
this.url = url;
|
||||
this.format = format;
|
||||
this.groupId = groupId;
|
||||
this.name = name;
|
||||
this.stableRenditionId = stableRenditionId;
|
||||
}
|
||||
}
|
||||
|
||||
/** Content Steering information derived from an #EXT-X-CONTENT-STEERING tag. */
|
||||
public static final class ContentSteeringInfo {
|
||||
|
||||
/** A URI to a Steering Manifest. */
|
||||
public final Uri serverUri;
|
||||
|
||||
/**
|
||||
* The ID of a pathway that must be used until the initial Steering Manifest has been obtained,
|
||||
* or {@code null} which suggests that any available pathway can be used until the initial
|
||||
* Steering Manifest has been obtained.
|
||||
*/
|
||||
@Nullable public final String pathwayId;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param serverUri See {@link #serverUri}.
|
||||
* @param pathwayId See {@link #pathwayId}.
|
||||
*/
|
||||
public ContentSteeringInfo(Uri serverUri, @Nullable String pathwayId) {
|
||||
this.serverUri = serverUri;
|
||||
this.pathwayId = pathwayId;
|
||||
}
|
||||
}
|
||||
|
||||
/** All of the media playlist URLs referenced by the playlist. */
|
||||
public final List<Uri> mediaPlaylistUrls;
|
||||
|
||||
/** The variants declared by the playlist. */
|
||||
public final List<Variant> variants;
|
||||
|
||||
/** The video renditions declared by the playlist. */
|
||||
public final List<Rendition> videos;
|
||||
|
||||
/** The audio renditions declared by the playlist. */
|
||||
public final List<Rendition> audios;
|
||||
|
||||
/** The subtitle renditions declared by the playlist. */
|
||||
public final List<Rendition> subtitles;
|
||||
|
||||
/** The closed caption renditions declared by the playlist. */
|
||||
public final List<Rendition> closedCaptions;
|
||||
|
||||
/**
|
||||
* The format of the audio muxed in the variants. May be null if the playlist does not declare any
|
||||
* muxed audio.
|
||||
*/
|
||||
@Nullable public final Format muxedAudioFormat;
|
||||
|
||||
/**
|
||||
* The format of the closed captions declared by the playlist. May be empty if the playlist
|
||||
* explicitly declares no captions are available, or null if the playlist does not declare any
|
||||
* captions information.
|
||||
*/
|
||||
@Nullable public final List<Format> muxedCaptionFormats;
|
||||
|
||||
/** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */
|
||||
public final Map<String, String> variableDefinitions;
|
||||
|
||||
/** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */
|
||||
public final List<DrmInitData> sessionKeyDrmInitData;
|
||||
|
||||
/** Content Steering information derived from #EXT-X-CONTENT-STEERING tag. */
|
||||
@Nullable public final ContentSteeringInfo contentSteeringInfo;
|
||||
|
||||
/**
|
||||
* @param baseUri See {@link #baseUri}.
|
||||
* @param tags See {@link #tags}.
|
||||
* @param variants See {@link #variants}.
|
||||
* @param videos See {@link #videos}.
|
||||
* @param audios See {@link #audios}.
|
||||
* @param subtitles See {@link #subtitles}.
|
||||
* @param closedCaptions See {@link #closedCaptions}.
|
||||
* @param muxedAudioFormat See {@link #muxedAudioFormat}.
|
||||
* @param muxedCaptionFormats See {@link #muxedCaptionFormats}.
|
||||
* @param hasIndependentSegments See {@link #hasIndependentSegments}.
|
||||
* @param variableDefinitions See {@link #variableDefinitions}.
|
||||
* @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}.
|
||||
* @param contentSteeringInfo See {@link #contentSteeringInfo}.
|
||||
*/
|
||||
public HlsMultivariantPlaylist(
|
||||
String baseUri,
|
||||
List<String> tags,
|
||||
List<Variant> variants,
|
||||
List<Rendition> videos,
|
||||
List<Rendition> audios,
|
||||
List<Rendition> subtitles,
|
||||
List<Rendition> closedCaptions,
|
||||
@Nullable Format muxedAudioFormat,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
boolean hasIndependentSegments,
|
||||
Map<String, String> variableDefinitions,
|
||||
List<DrmInitData> sessionKeyDrmInitData,
|
||||
@Nullable ContentSteeringInfo contentSteeringInfo) {
|
||||
super(baseUri, tags, hasIndependentSegments);
|
||||
this.mediaPlaylistUrls =
|
||||
Collections.unmodifiableList(
|
||||
getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions));
|
||||
this.variants = Collections.unmodifiableList(variants);
|
||||
this.videos = Collections.unmodifiableList(videos);
|
||||
this.audios = Collections.unmodifiableList(audios);
|
||||
this.subtitles = Collections.unmodifiableList(subtitles);
|
||||
this.closedCaptions = Collections.unmodifiableList(closedCaptions);
|
||||
this.muxedAudioFormat = muxedAudioFormat;
|
||||
this.muxedCaptionFormats =
|
||||
muxedCaptionFormats != null ? Collections.unmodifiableList(muxedCaptionFormats) : null;
|
||||
this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions);
|
||||
this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData);
|
||||
this.contentSteeringInfo = contentSteeringInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HlsMultivariantPlaylist copy(List<StreamKey> streamKeys) {
|
||||
return new HlsMultivariantPlaylist(
|
||||
baseUri,
|
||||
tags,
|
||||
copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys),
|
||||
// TODO: Allow stream keys to specify video renditions to be retained.
|
||||
/* videos= */ Collections.emptyList(),
|
||||
copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys),
|
||||
copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys),
|
||||
// TODO: Update to retain all closed captions.
|
||||
/* closedCaptions= */ Collections.emptyList(),
|
||||
muxedAudioFormat,
|
||||
muxedCaptionFormats,
|
||||
hasIndependentSegments,
|
||||
variableDefinitions,
|
||||
sessionKeyDrmInitData,
|
||||
contentSteeringInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a playlist with a single variant.
|
||||
*
|
||||
* @param variantUrl The url of the single variant.
|
||||
* @return A multivariant playlist with a single variant for the provided url.
|
||||
*/
|
||||
public static HlsMultivariantPlaylist createSingleVariantMultivariantPlaylist(String variantUrl) {
|
||||
List<Variant> variant =
|
||||
Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl)));
|
||||
return new HlsMultivariantPlaylist(
|
||||
/* baseUri= */ "",
|
||||
/* tags= */ Collections.emptyList(),
|
||||
variant,
|
||||
/* videos= */ Collections.emptyList(),
|
||||
/* audios= */ Collections.emptyList(),
|
||||
/* subtitles= */ Collections.emptyList(),
|
||||
/* closedCaptions= */ Collections.emptyList(),
|
||||
/* muxedAudioFormat= */ null,
|
||||
/* muxedCaptionFormats= */ null,
|
||||
/* hasIndependentSegments= */ false,
|
||||
/* variableDefinitions= */ Collections.emptyMap(),
|
||||
/* sessionKeyDrmInitData= */ Collections.emptyList(),
|
||||
/* contentSteeringInfo= */ null);
|
||||
}
|
||||
|
||||
private static List<Uri> getMediaPlaylistUrls(
|
||||
List<Variant> variants,
|
||||
List<Rendition> videos,
|
||||
List<Rendition> audios,
|
||||
List<Rendition> subtitles,
|
||||
List<Rendition> closedCaptions) {
|
||||
ArrayList<Uri> mediaPlaylistUrls = new ArrayList<>();
|
||||
for (int i = 0; i < variants.size(); i++) {
|
||||
Uri uri = variants.get(i).url;
|
||||
if (!mediaPlaylistUrls.contains(uri)) {
|
||||
mediaPlaylistUrls.add(uri);
|
||||
}
|
||||
}
|
||||
addMediaPlaylistUrls(videos, mediaPlaylistUrls);
|
||||
addMediaPlaylistUrls(audios, mediaPlaylistUrls);
|
||||
addMediaPlaylistUrls(subtitles, mediaPlaylistUrls);
|
||||
addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls);
|
||||
return mediaPlaylistUrls;
|
||||
}
|
||||
|
||||
private static void addMediaPlaylistUrls(List<Rendition> renditions, List<Uri> out) {
|
||||
for (int i = 0; i < renditions.size(); i++) {
|
||||
Uri uri = renditions.get(i).url;
|
||||
if (uri != null && !out.contains(uri)) {
|
||||
out.add(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> List<T> copyStreams(
|
||||
List<T> streams, int groupIndex, List<StreamKey> streamKeys) {
|
||||
List<T> copiedStreams = new ArrayList<>(streamKeys.size());
|
||||
// TODO:
|
||||
// 1. When variants with the same URL are not de-duplicated, duplicates must not increment
|
||||
// trackIndex so as to avoid breaking stream keys that have been persisted for offline. All
|
||||
// duplicates should be copied if the first variant is copied, or discarded otherwise.
|
||||
// 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to
|
||||
// avoid breaking stream keys that have been persisted for offline. All renditions with null
|
||||
// URLs should be copied. They may become unreachable if all variants that reference them are
|
||||
// removed, but this is OK.
|
||||
// 3. Renditions with URLs matching copied variants should always themselves be copied, even if
|
||||
// the corresponding stream key is omitted. Else we're throwing away information for no gain.
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
T stream = streams.get(i);
|
||||
for (int j = 0; j < streamKeys.size(); j++) {
|
||||
StreamKey streamKey = streamKeys.get(j);
|
||||
if (streamKey.groupIndex == groupIndex && streamKey.streamIndex == i) {
|
||||
copiedStreams.add(stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return copiedStreams;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.offline.FilterableManifest;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Represents an HLS playlist. */
|
||||
@UnstableApi
|
||||
public abstract class HlsPlaylist implements FilterableManifest<HlsPlaylist> {
|
||||
|
||||
/** The base uri. Used to resolve relative paths. */
|
||||
public final String baseUri;
|
||||
|
||||
/** The list of tags in the playlist. */
|
||||
public final List<String> tags;
|
||||
|
||||
/**
|
||||
* Whether the media is formed of independent segments, as defined by the
|
||||
* #EXT-X-INDEPENDENT-SEGMENTS tag.
|
||||
*/
|
||||
public final boolean hasIndependentSegments;
|
||||
|
||||
/**
|
||||
* @param baseUri See {@link #baseUri}.
|
||||
* @param tags See {@link #tags}.
|
||||
* @param hasIndependentSegments See {@link #hasIndependentSegments}.
|
||||
*/
|
||||
protected HlsPlaylist(String baseUri, List<String> tags, boolean hasIndependentSegments) {
|
||||
this.baseUri = baseUri;
|
||||
this.tags = Collections.unmodifiableList(tags);
|
||||
this.hasIndependentSegments = hasIndependentSegments;
|
||||
}
|
||||
}
|
||||
+1909
File diff suppressed because it is too large
Load Diff
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.upstream.ParsingLoadable;
|
||||
|
||||
/** Factory for {@link HlsPlaylist} parsers. */
|
||||
@UnstableApi
|
||||
public interface HlsPlaylistParserFactory {
|
||||
|
||||
/**
|
||||
* Returns a stand-alone playlist parser. Playlists parsed by the returned parser do not inherit
|
||||
* any attributes from other playlists.
|
||||
*/
|
||||
ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser();
|
||||
|
||||
/**
|
||||
* Returns a playlist parser for playlists that were referenced by the given {@link
|
||||
* HlsMultivariantPlaylist}. Returned {@link HlsMediaPlaylist} instances may inherit attributes
|
||||
* from {@code multivariantPlaylist}.
|
||||
*
|
||||
* @param multivariantPlaylist The multivariant playlist that referenced any parsed media
|
||||
* playlists.
|
||||
* @param previousMediaPlaylist The previous media playlist or null if there is no previous media
|
||||
* playlist.
|
||||
* @return A parser for HLS playlists.
|
||||
*/
|
||||
ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
|
||||
HlsMultivariantPlaylist multivariantPlaylist,
|
||||
@Nullable HlsMediaPlaylist previousMediaPlaylist);
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.SystemClock;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.hls.HlsDataSourceFactory;
|
||||
import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher;
|
||||
import androidx.media3.exoplayer.upstream.BandwidthMeter;
|
||||
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||
import androidx.media3.exoplayer.upstream.contentsteering.ContentSteeringTracker;
|
||||
import androidx.media3.exoplayer.util.ReleasableExecutor;
|
||||
import com.google.common.base.Supplier;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tracks playlists associated to an HLS stream and provides snapshots.
|
||||
*
|
||||
* <p>The playlist tracker is responsible for exposing the seeking window, which is defined by the
|
||||
* segments that one of the playlists exposes. This playlist is called primary and needs to be
|
||||
* periodically refreshed in the case of live streams. Note that the primary playlist is one of the
|
||||
* media playlists while the multivariant playlist is an optional kind of playlist defined by the
|
||||
* HLS specification (RFC 8216).
|
||||
*
|
||||
* <p>Playlist loads might encounter errors. The tracker may choose to exclude them to ensure a
|
||||
* primary playlist is always available.
|
||||
*/
|
||||
@UnstableApi
|
||||
public interface HlsPlaylistTracker {
|
||||
|
||||
/** Factory for {@link HlsPlaylistTracker} instances. */
|
||||
interface Factory {
|
||||
|
||||
/**
|
||||
* Creates a new tracker instance.
|
||||
*
|
||||
* @param dataSourceFactory The {@link HlsDataSourceFactory} to use for playlist loading.
|
||||
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for playlist load errors.
|
||||
* @param playlistParserFactory The {@link HlsPlaylistParserFactory} for playlist parsing.
|
||||
* @param cmcdConfiguration The {@link CmcdConfiguration} to use for playlist loading.
|
||||
* @param downloadExecutorSupplier A supplier for a {@link ReleasableExecutor} that is used for
|
||||
* loading the playlist.
|
||||
*/
|
||||
HlsPlaylistTracker createTracker(
|
||||
HlsDataSourceFactory dataSourceFactory,
|
||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||
HlsPlaylistParserFactory playlistParserFactory,
|
||||
@Nullable CmcdConfiguration cmcdConfiguration,
|
||||
@Nullable Supplier<ReleasableExecutor> downloadExecutorSupplier);
|
||||
}
|
||||
|
||||
/** Listener for primary playlist changes. */
|
||||
interface PrimaryPlaylistListener {
|
||||
|
||||
/**
|
||||
* Called when the primary playlist changes.
|
||||
*
|
||||
* @param mediaPlaylist The primary playlist new snapshot.
|
||||
*/
|
||||
void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
|
||||
}
|
||||
|
||||
/** Called on playlist loading events. */
|
||||
interface PlaylistEventListener {
|
||||
|
||||
/** Called a playlist changes. */
|
||||
void onPlaylistChanged();
|
||||
|
||||
/**
|
||||
* Called if an error is encountered while loading a playlist.
|
||||
*
|
||||
* @param url The loaded url that caused the error.
|
||||
* @param loadErrorInfo The load error info.
|
||||
* @param forceRetry Whether retry should be forced without considering exclusion.
|
||||
* @return Whether the playlist will be excluded from future loads.
|
||||
*/
|
||||
boolean onPlaylistError(
|
||||
Uri url, LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo, boolean forceRetry);
|
||||
}
|
||||
|
||||
/** Thrown when a playlist is considered to be stuck due to a server side error. */
|
||||
final class PlaylistStuckException extends IOException {
|
||||
|
||||
/** The url of the stuck playlist. */
|
||||
public final Uri url;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param url See {@link #url}.
|
||||
*/
|
||||
public PlaylistStuckException(Uri url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown when the media sequence of a new snapshot indicates the server has reset. */
|
||||
final class PlaylistResetException extends IOException {
|
||||
|
||||
/** The url of the reset playlist. */
|
||||
public final Uri url;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param url See {@link #url}.
|
||||
*/
|
||||
public PlaylistResetException(Uri url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the playlist tracker.
|
||||
*
|
||||
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()}
|
||||
* call.
|
||||
*
|
||||
* @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a
|
||||
* multivariant playlist.
|
||||
* @param eventDispatcher A dispatcher to notify of events.
|
||||
* @param primaryPlaylistListener A callback for the primary playlist change events.
|
||||
* @param bandwidthMeter A {@link BandwidthMeter}.
|
||||
*/
|
||||
void start(
|
||||
Uri initialPlaylistUri,
|
||||
EventDispatcher eventDispatcher,
|
||||
PrimaryPlaylistListener primaryPlaylistListener,
|
||||
BandwidthMeter bandwidthMeter);
|
||||
|
||||
/**
|
||||
* Stops the playlist tracker and releases any acquired resources.
|
||||
*
|
||||
* <p>Must be called once per {@link #start} call.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Registers a listener to receive events from the playlist tracker.
|
||||
*
|
||||
* @param listener The listener.
|
||||
*/
|
||||
void addListener(PlaylistEventListener listener);
|
||||
|
||||
/**
|
||||
* Unregisters a listener.
|
||||
*
|
||||
* @param listener The listener to unregister.
|
||||
*/
|
||||
void removeListener(PlaylistEventListener listener);
|
||||
|
||||
/**
|
||||
* Returns the multivariant playlist.
|
||||
*
|
||||
* <p>If the uri passed to {@link #start} points to a media playlist, an {@link
|
||||
* HlsMultivariantPlaylist} with a single variant for said media playlist is returned.
|
||||
*
|
||||
* @return The multivariant playlist. Null if the initial playlist has yet to be loaded.
|
||||
*/
|
||||
@Nullable
|
||||
HlsMultivariantPlaylist getMultivariantPlaylist();
|
||||
|
||||
/**
|
||||
* Returns the {@linkplain ContentSteeringTracker content steering tracker}.
|
||||
*
|
||||
* @return The content steering tracker. Null if the initial playlist has yet to be loaded, or no
|
||||
* information for content steering is declared in the initial playlist.
|
||||
*/
|
||||
@Nullable
|
||||
ContentSteeringTracker getContentSteeringTracker();
|
||||
|
||||
/**
|
||||
* Returns the {@link HlsRedundantGroup} list corresponding to the {@code type}.
|
||||
*
|
||||
* @param type The type of the requested {@link HlsRedundantGroup}.
|
||||
* @return The list of requested {@link HlsRedundantGroup}. Null if the initial playlist has yet
|
||||
* to be loaded.
|
||||
*/
|
||||
@Nullable
|
||||
List<HlsRedundantGroup> getRedundantGroups(@HlsRedundantGroup.Type int type);
|
||||
|
||||
/**
|
||||
* Returns the most recent snapshot available of the playlist referenced by the provided {@link
|
||||
* Uri}.
|
||||
*
|
||||
* @param url The {@link Uri} corresponding to the requested media playlist.
|
||||
* @param isForPlayback Whether the caller might use the snapshot to request media segments for
|
||||
* playback. If true, the primary playlist may be updated to the one requested.
|
||||
* @return The most recent snapshot of the playlist referenced by the provided {@link Uri}. May be
|
||||
* null if no snapshot has been loaded yet.
|
||||
*/
|
||||
@Nullable
|
||||
HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback);
|
||||
|
||||
/**
|
||||
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
|
||||
* media playlist has been loaded.
|
||||
*/
|
||||
long getInitialStartTimeUs();
|
||||
|
||||
/**
|
||||
* Returns whether the snapshot of the playlist referenced by the provided {@link Uri} is valid,
|
||||
* meaning all the segments referenced by the playlist are expected to be available. If the
|
||||
* playlist is not valid then some of the segments may no longer be available.
|
||||
*
|
||||
* @param url The {@link Uri}.
|
||||
* @return Whether the snapshot of the playlist referenced by the provided {@link Uri} is valid.
|
||||
*/
|
||||
boolean isSnapshotValid(Uri url);
|
||||
|
||||
/**
|
||||
* If the tracker is having trouble refreshing the multivariant playlist or the primary playlist,
|
||||
* this method throws the underlying error. Otherwise, does nothing.
|
||||
*
|
||||
* @throws IOException The underlying error.
|
||||
*/
|
||||
void maybeThrowPrimaryPlaylistRefreshError() throws IOException;
|
||||
|
||||
/**
|
||||
* If the playlist is having trouble refreshing the playlist referenced by the given {@link Uri},
|
||||
* this method throws the underlying error.
|
||||
*
|
||||
* @param url The {@link Uri}.
|
||||
* @throws IOException The underlying error.
|
||||
*/
|
||||
void maybeThrowPlaylistRefreshError(Uri url) throws IOException;
|
||||
|
||||
/**
|
||||
* Excludes the given media playlist for the given duration, in milliseconds.
|
||||
*
|
||||
* @param playlistUrl The URL of the media playlist.
|
||||
* @param exclusionDurationMs The duration for which to exclude the playlist.
|
||||
* @return Whether exclusion was successful.
|
||||
*/
|
||||
boolean excludeMediaPlaylist(Uri playlistUrl, long exclusionDurationMs);
|
||||
|
||||
/**
|
||||
* Requests a playlist refresh and removes it from the exclusion list.
|
||||
*
|
||||
* <p>The playlist tracker may choose to delay the playlist refresh. The request is discarded if a
|
||||
* refresh was already pending.
|
||||
*
|
||||
* @param url The {@link Uri} of the playlist to be refreshed.
|
||||
*/
|
||||
void refreshPlaylist(Uri url);
|
||||
|
||||
/**
|
||||
* Returns whether the tracked playlists describe a live stream.
|
||||
*
|
||||
* @return True if the content is live. False otherwise.
|
||||
*/
|
||||
boolean isLive();
|
||||
|
||||
/**
|
||||
* Deactivate the playlist for playback.
|
||||
*
|
||||
* <p>The default implementation is a no-op.
|
||||
*
|
||||
* @param url The {@link Uri} of the playlist to deactivate for playback.
|
||||
*/
|
||||
default void deactivatePlaylistForPlayback(Uri url) {}
|
||||
|
||||
/**
|
||||
* Returns whether the {@code playlistUrl} is excluded at the given {@code nowMs} time.
|
||||
*
|
||||
* @param playlistUrl The URL of the media playlist.
|
||||
* @param nowMs The current value of {@link SystemClock#elapsedRealtime()}.
|
||||
*/
|
||||
boolean isExcluded(Uri playlistUrl, long nowMs);
|
||||
|
||||
/**
|
||||
* Returns whether all playlist URLs in the {@link HlsRedundantGroup} are {@linkplain
|
||||
* #isExcluded(Uri, long) excluded} at the given {@code nowMs} time.
|
||||
*
|
||||
* @param redundantGroup The {@link HlsRedundantGroup}.
|
||||
* @param nowMs The current value of {@link SystemClock#elapsedRealtime()}.
|
||||
*/
|
||||
boolean isExcluded(HlsRedundantGroup redundantGroup, long nowMs);
|
||||
|
||||
/**
|
||||
* Returns the {@link HlsRedundantGroup} where the {@code playlistUrl} belongs.
|
||||
*
|
||||
* @param playlistUrl The URL of the media playlist.
|
||||
*/
|
||||
@Nullable
|
||||
HlsRedundantGroup getRedundantGroup(Uri playlistUrl);
|
||||
}
|
||||
+588
@@ -0,0 +1,588 @@
|
||||
/*
|
||||
* Copyright 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.ParserException;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Rendition;
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Variant;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Represents a group of {@linkplain Variant variants} or {@linkplain Rendition renditions} that are
|
||||
* identical streams but from different locations (different playlist urls), and indicated by a
|
||||
* shared {@link GroupKey}.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class HlsRedundantGroup {
|
||||
|
||||
/**
|
||||
* Represents the type of a {@link HlsRedundantGroup}. One of {@link #VARIANT}, {@link
|
||||
* #VIDEO_RENDITION}, {@link #AUDIO_RENDITION} and {@link #SUBTITLE_RENDITION}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef(value = {VARIANT, VIDEO_RENDITION, AUDIO_RENDITION, SUBTITLE_RENDITION})
|
||||
public @interface Type {}
|
||||
|
||||
/** The {@link HlsRedundantGroup} groups {@linkplain Variant variants}. */
|
||||
public static final @Type int VARIANT = 0;
|
||||
|
||||
/** The {@link HlsRedundantGroup} groups {@linkplain Rendition video renditions}. */
|
||||
public static final @Type int VIDEO_RENDITION = 1;
|
||||
|
||||
/** The {@link HlsRedundantGroup} groups {@linkplain Rendition audio renditions}. */
|
||||
public static final @Type int AUDIO_RENDITION = 2;
|
||||
|
||||
/** The {@link HlsRedundantGroup} groups {@linkplain Rendition subtitle renditions}. */
|
||||
public static final @Type int SUBTITLE_RENDITION = 3;
|
||||
|
||||
/**
|
||||
* A factory for creating lists of {@linkplain HlsRedundantGroup redundant groups} from a {@link
|
||||
* HlsMultivariantPlaylist}.
|
||||
*/
|
||||
public static class Factory {
|
||||
|
||||
private final HlsMultivariantPlaylist multivariantPlaylist;
|
||||
private final boolean contentSteeringEnabled;
|
||||
private final Map<String, String> videoGroupIdToPathwayId;
|
||||
private final Map<String, String> audioGroupIdToPathwayId;
|
||||
private final Map<String, String> subtitleGroupIdToPathwayId;
|
||||
private @MonotonicNonNull ImmutableList<HlsRedundantGroup> variantRedundantGroupList;
|
||||
private @MonotonicNonNull ImmutableList<HlsRedundantGroup> videoRenditionRedundantGroupList;
|
||||
private @MonotonicNonNull ImmutableList<HlsRedundantGroup> audioRenditionRedundantGroupList;
|
||||
private @MonotonicNonNull ImmutableList<HlsRedundantGroup> subtitleRenditionRedundantGroupList;
|
||||
|
||||
/**
|
||||
* Creates a new factory.
|
||||
*
|
||||
* @param multivariantPlaylist The {@link HlsMultivariantPlaylist} for which to create
|
||||
* {@linkplain HlsRedundantGroup redundant groups}.
|
||||
*/
|
||||
public Factory(HlsMultivariantPlaylist multivariantPlaylist) {
|
||||
this.multivariantPlaylist = multivariantPlaylist;
|
||||
this.contentSteeringEnabled = multivariantPlaylist.contentSteeringInfo != null;
|
||||
videoGroupIdToPathwayId = new HashMap<>();
|
||||
audioGroupIdToPathwayId = new HashMap<>();
|
||||
subtitleGroupIdToPathwayId = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of {@linkplain HlsRedundantGroup redundant groups} for {@linkplain Variant
|
||||
* variants}.
|
||||
*
|
||||
* <p>If a {@link Variant} has a non-null {@linkplain Variant#pathwayId pathway ID}, then its
|
||||
* {@link Variant#url} will be associated with that pathway ID when added to the {@link
|
||||
* HlsRedundantGroup}. Otherwise, a generated pathway ID will be used. The first is '.', and the
|
||||
* subsequent ones for the same group are '..', '...', according to the order they are listed in
|
||||
* the {@link HlsMultivariantPlaylist}.
|
||||
*
|
||||
* @return A list of {@linkplain HlsRedundantGroup redundant groups}.
|
||||
* @throws ParserException If two variants grouped into one {@link HlsRedundantGroup} have the
|
||||
* same {@link Variant#pathwayId} but different {@linkplain Variant#url urls}.
|
||||
* @throws ParserException If content steering enabled and a {@linkplain Variant#videoGroupId
|
||||
* video group ID}, {@link Variant#audioGroupId audio group ID} or {@link
|
||||
* Variant#subtitleGroupId subtitle group ID} is associated with more than one pathway.
|
||||
* @throws ParserException If content steering is enabled and the set of available pathway IDs
|
||||
* is inconsistent among variant redundant groups.
|
||||
*/
|
||||
public ImmutableList<HlsRedundantGroup> createVariantRedundantGroupList()
|
||||
throws ParserException {
|
||||
ensureVariantRedundantGroupListCreated();
|
||||
return variantRedundantGroupList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of {@linkplain HlsRedundantGroup redundant groups} for {@linkplain Rendition
|
||||
* renditions} with given {@code type}.
|
||||
*
|
||||
* <p>If content steering is enabled, then a pathway Id will be determined by the {@linkplain
|
||||
* Rendition#groupId group ID} and the {@linkplain Variant#pathwayId pathway ID} of the
|
||||
* {@linkplain Variant} that links to the rendition. Otherwise, a generated pathway ID will be
|
||||
* used. The first is '.', and the subsequent ones for the same group are '..', '...', according
|
||||
* to the order they are listed in the {@link HlsMultivariantPlaylist}.
|
||||
*
|
||||
* @param type The type of {@linkplain Rendition renditions} for which to create {@linkplain
|
||||
* HlsRedundantGroup redundant groups}.
|
||||
* @return A list of {@linkplain HlsRedundantGroup redundant groups}.
|
||||
* @throws ParserException If two renditions grouped into one {@link HlsRedundantGroup} have the
|
||||
* same pathway ID but different {@linkplain Rendition#url urls}.
|
||||
* @throws ParserException If content steering enabled and a {@linkplain Rendition#groupId group
|
||||
* ID} is associated with more than one pathway.
|
||||
* @throws ParserException If content steering is enabled and the set of available pathway IDs
|
||||
* of a rendition redundant group is inconsistent with the variant redundant groups.
|
||||
* @throws IllegalArgumentException If the {@code type} is not one of {@link #VIDEO_RENDITION},
|
||||
* {@link #AUDIO_RENDITION} or {@link #SUBTITLE_RENDITION}.
|
||||
*/
|
||||
public ImmutableList<HlsRedundantGroup> createRenditionRedundantGroupList(@Type int type)
|
||||
throws ParserException {
|
||||
ensureVariantRedundantGroupListCreated();
|
||||
switch (type) {
|
||||
case VIDEO_RENDITION:
|
||||
if (videoRenditionRedundantGroupList == null) {
|
||||
videoRenditionRedundantGroupList =
|
||||
createAndValidateRenditionRedundantGroupList(
|
||||
multivariantPlaylist.videos, videoGroupIdToPathwayId);
|
||||
}
|
||||
return videoRenditionRedundantGroupList;
|
||||
case AUDIO_RENDITION:
|
||||
if (audioRenditionRedundantGroupList == null) {
|
||||
audioRenditionRedundantGroupList =
|
||||
createAndValidateRenditionRedundantGroupList(
|
||||
multivariantPlaylist.audios, audioGroupIdToPathwayId);
|
||||
}
|
||||
return audioRenditionRedundantGroupList;
|
||||
case SUBTITLE_RENDITION:
|
||||
if (subtitleRenditionRedundantGroupList == null) {
|
||||
subtitleRenditionRedundantGroupList =
|
||||
createAndValidateRenditionRedundantGroupList(
|
||||
multivariantPlaylist.subtitles, subtitleGroupIdToPathwayId);
|
||||
}
|
||||
return subtitleRenditionRedundantGroupList;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid type for creating rendition redundant group list");
|
||||
}
|
||||
}
|
||||
|
||||
@EnsuresNonNull("variantRedundantGroupList")
|
||||
private void ensureVariantRedundantGroupListCreated() throws ParserException {
|
||||
if (variantRedundantGroupList == null) {
|
||||
ArrayList<HlsRedundantGroup> redundantGroupList = new ArrayList<>();
|
||||
HashMap<GroupKey, Integer> redundantGroupIndices = new HashMap<>();
|
||||
HashMap<GroupKey, Integer> generatedPathwayIdCounts = new HashMap<>();
|
||||
for (int i = 0; i < multivariantPlaylist.variants.size(); i++) {
|
||||
Variant variant = multivariantPlaylist.variants.get(i);
|
||||
GroupKey groupKey = new GroupKey(variant.format, variant.stableVariantId);
|
||||
String pathwayId =
|
||||
propagateRedundantGroupList(
|
||||
variant.url,
|
||||
variant.pathwayId,
|
||||
/* indexInMultivariantPlaylist= */ i,
|
||||
redundantGroupList,
|
||||
groupKey,
|
||||
redundantGroupIndices,
|
||||
generatedPathwayIdCounts);
|
||||
if (contentSteeringEnabled) {
|
||||
if (variant.videoGroupId != null) {
|
||||
updateGroupIdToPathwayIdMapping(
|
||||
videoGroupIdToPathwayId, variant.videoGroupId, pathwayId);
|
||||
}
|
||||
if (variant.audioGroupId != null) {
|
||||
updateGroupIdToPathwayIdMapping(
|
||||
audioGroupIdToPathwayId, variant.audioGroupId, pathwayId);
|
||||
}
|
||||
if (variant.subtitleGroupId != null) {
|
||||
updateGroupIdToPathwayIdMapping(
|
||||
subtitleGroupIdToPathwayId, variant.subtitleGroupId, pathwayId);
|
||||
}
|
||||
}
|
||||
}
|
||||
variantRedundantGroupList = ImmutableList.copyOf(redundantGroupList);
|
||||
validateVariantRedundantGroupList();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresNonNull("variantRedundantGroupList")
|
||||
private ImmutableList<HlsRedundantGroup> createAndValidateRenditionRedundantGroupList(
|
||||
List<Rendition> renditions, Map<String, String> groupIdToPathwayId) throws ParserException {
|
||||
ArrayList<HlsRedundantGroup> redundantGroupList = new ArrayList<>();
|
||||
HashMap<GroupKey, Integer> redundantGroupIndices = new HashMap<>();
|
||||
HashMap<GroupKey, Integer> generatedPathwayIdCounts = new HashMap<>();
|
||||
for (int i = 0; i < renditions.size(); i++) {
|
||||
Rendition rendition = renditions.get(i);
|
||||
if (rendition.url == null) {
|
||||
continue;
|
||||
}
|
||||
GroupKey groupKey =
|
||||
new GroupKey(rendition.format, rendition.stableRenditionId, rendition.name);
|
||||
@Nullable String knownPathwayId = null;
|
||||
if (contentSteeringEnabled) {
|
||||
knownPathwayId = groupIdToPathwayId.get(rendition.groupId);
|
||||
if (knownPathwayId == null) {
|
||||
// When Content Steering is enabled, we skip the rendition if we don't know its pathway
|
||||
// ID. This also implies that there is no variant referring to this rendition with the
|
||||
// rendition.groupId.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
propagateRedundantGroupList(
|
||||
rendition.url,
|
||||
knownPathwayId,
|
||||
/* indexInMultivariantPlaylist= */ i,
|
||||
redundantGroupList,
|
||||
groupKey,
|
||||
redundantGroupIndices,
|
||||
generatedPathwayIdCounts);
|
||||
}
|
||||
validateRenditionRedundantGroupList(redundantGroupList);
|
||||
return ImmutableList.copyOf(redundantGroupList);
|
||||
}
|
||||
|
||||
private static void updateGroupIdToPathwayIdMapping(
|
||||
Map<String, String> groupIdToPathwayId, String groupId, String pathwayId)
|
||||
throws ParserException {
|
||||
if (groupIdToPathwayId.containsKey(groupId)
|
||||
&& !pathwayId.equals(groupIdToPathwayId.get(groupId))) {
|
||||
throw ParserException.createForMalformedManifest(
|
||||
String.format(
|
||||
"The group ID %s is associated with more than one pathway from" + " variants",
|
||||
groupId),
|
||||
/* cause= */ null);
|
||||
}
|
||||
groupIdToPathwayId.put(groupId, pathwayId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagates a list of {@link HlsRedundantGroup} with a {@link Uri playlist url}.
|
||||
*
|
||||
* <p>This method either creates a new {@link HlsRedundantGroup} for the provided {@link Uri}
|
||||
* and adds it to the passed {@code redundantGroupList}, or adds the provided {@link Uri} into
|
||||
* an existing {@link HlsRedundantGroup} in the {@code redundantGroupList}.
|
||||
*
|
||||
* @param url The {@link Uri} to be added either into a new {@link HlsRedundantGroup} or an
|
||||
* existing {@link HlsRedundantGroup}.
|
||||
* @param knownPathwayId The known pathway ID of the {@code url}, which will be added with the
|
||||
* {@code url} to an {@link HlsRedundantGroup}. If {@code null} is passed, a generated
|
||||
* pathway ID will be used. The first is '.', and the subsequent ones for the same group are
|
||||
* '..', '...', according to the order of the corresponding {@link Variant} or {@link
|
||||
* Rendition} in the {@link HlsMultivariantPlaylist}.
|
||||
* @param indexInMultivariantPlaylist The index of the {@code playlistUrl} in either {@link
|
||||
* HlsMultivariantPlaylist#variants}, {@link HlsMultivariantPlaylist#videos}, {@link
|
||||
* HlsMultivariantPlaylist#audios} or {@link HlsMultivariantPlaylist#subtitles}. May be
|
||||
* {@link C#INDEX_UNSET} if the {@code playlistUrl} is not declared in the multivariant
|
||||
* playlist.
|
||||
* @param redundantGroupList The list of {@link HlsRedundantGroup} to be propagated.
|
||||
* @param groupKey The {@link GroupKey} to look up the existing {@link HlsRedundantGroup}.
|
||||
* @param redundantGroupIndices A map to look up an existing {@link HlsRedundantGroup} for a
|
||||
* {@link GroupKey}. The keys are the {@link GroupKey group keys} of the {@link
|
||||
* HlsRedundantGroup redundant groups}, and the values are the indices of the corresponding
|
||||
* {@link HlsRedundantGroup redundant groups} in the {@code redundantGroupList}.
|
||||
* @param generatedPathwayIdCounts A map to look up the count of the generated pathway ID for an
|
||||
* {@link HlsRedundantGroup} of a {@link GroupKey}. The keys are the {@link GroupKey group
|
||||
* keys} of the {@link HlsRedundantGroup redundant groups}, and the values are the counts of
|
||||
* the generated pathway IDs of the {@link HlsRedundantGroup redundant groups}.
|
||||
* @return The pathway ID of the given {@code url}, which is either the {@code knownPathwayId}
|
||||
* if provided, or a newly generated pathway ID.
|
||||
* @throws ParserException If two different urls grouped into one {@link HlsRedundantGroup} have
|
||||
* the same pathway ID to associate with.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
private String propagateRedundantGroupList(
|
||||
Uri url,
|
||||
@Nullable String knownPathwayId,
|
||||
int indexInMultivariantPlaylist,
|
||||
List<HlsRedundantGroup> redundantGroupList,
|
||||
GroupKey groupKey,
|
||||
Map<GroupKey, Integer> redundantGroupIndices,
|
||||
Map<GroupKey, Integer> generatedPathwayIdCounts)
|
||||
throws ParserException {
|
||||
@Nullable String pathwayId = knownPathwayId;
|
||||
@Nullable Integer redundantGroupIndex = redundantGroupIndices.get(groupKey);
|
||||
if (redundantGroupIndex == null) {
|
||||
if (pathwayId == null) {
|
||||
// The default pathwayId is ".".
|
||||
pathwayId = ".";
|
||||
generatedPathwayIdCounts.put(groupKey, 1);
|
||||
} else {
|
||||
generatedPathwayIdCounts.put(groupKey, 0);
|
||||
}
|
||||
HlsRedundantGroup newRedundantGroup =
|
||||
new HlsRedundantGroup(groupKey, pathwayId, url, indexInMultivariantPlaylist);
|
||||
redundantGroupIndices.put(groupKey, redundantGroupList.size());
|
||||
redundantGroupList.add(newRedundantGroup);
|
||||
} else {
|
||||
if (pathwayId == null) {
|
||||
// In the absence of a pathwayId, a generated pathway ID is used. The first is '.', and
|
||||
// the subsequent ones for the same group are '..', '...' and so on.
|
||||
int generatedPathwayIdCount = checkNotNull(generatedPathwayIdCounts.get(groupKey));
|
||||
if (contentSteeringEnabled && generatedPathwayIdCount >= 1) {
|
||||
throw ParserException.createForMalformedManifest(
|
||||
"At most one playlist URL within an HlsRedundantGroup can have an undefined pathway"
|
||||
+ " when Content Steering is enabled",
|
||||
/* cause= */ null);
|
||||
}
|
||||
pathwayId = Strings.repeat(".", ++generatedPathwayIdCount);
|
||||
generatedPathwayIdCounts.put(groupKey, generatedPathwayIdCount);
|
||||
}
|
||||
HlsRedundantGroup redundantGroup = redundantGroupList.get(redundantGroupIndex);
|
||||
@Nullable Uri existingUrl = redundantGroup.getPlaylistUrl(pathwayId);
|
||||
if (existingUrl != null && !url.equals(existingUrl)) {
|
||||
throw ParserException.createForMalformedManifest(
|
||||
String.format(
|
||||
"Different playlist URLs are found for pathway ID %s within the"
|
||||
+ " HlsRedundantGroup",
|
||||
pathwayId),
|
||||
/* cause= */ null);
|
||||
}
|
||||
redundantGroup.put(pathwayId, url, indexInMultivariantPlaylist);
|
||||
}
|
||||
return pathwayId;
|
||||
}
|
||||
|
||||
@RequiresNonNull("variantRedundantGroupList")
|
||||
private void validateVariantRedundantGroupList() throws ParserException {
|
||||
if (!contentSteeringEnabled) {
|
||||
return;
|
||||
}
|
||||
Set<String> pathwayIdSet = checkNotNull(variantRedundantGroupList.get(0)).getAllPathwayIds();
|
||||
for (int i = 1; i < variantRedundantGroupList.size(); i++) {
|
||||
HlsRedundantGroup redundantGroup = variantRedundantGroupList.get(i);
|
||||
if (!redundantGroup.getAllPathwayIds().equals(pathwayIdSet)) {
|
||||
throw ParserException.createForMalformedManifest(
|
||||
"The set of available pathway IDs is inconsistent among variant redundant groups",
|
||||
/* cause= */ null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresNonNull("variantRedundantGroupList")
|
||||
private void validateRenditionRedundantGroupList(
|
||||
List<HlsRedundantGroup> renditionRedundantGroupList) throws ParserException {
|
||||
if (!contentSteeringEnabled) {
|
||||
return;
|
||||
}
|
||||
Set<String> pathwayIdSet = checkNotNull(variantRedundantGroupList.get(0)).getAllPathwayIds();
|
||||
for (int i = 0; i < renditionRedundantGroupList.size(); i++) {
|
||||
HlsRedundantGroup redundantGroup = renditionRedundantGroupList.get(i);
|
||||
if (!redundantGroup.getAllPathwayIds().equals(pathwayIdSet)) {
|
||||
throw ParserException.createForMalformedManifest(
|
||||
"The set of available pathway IDs of a rendition redundant group is inconsistent with"
|
||||
+ " variant redundant groups",
|
||||
/* cause= */ null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a key that groups {@linkplain Variant variants} or {@linkplain Rendition renditions}
|
||||
* into one {@link HlsRedundantGroup}.
|
||||
*/
|
||||
public static class GroupKey {
|
||||
|
||||
/**
|
||||
* The {@link Format} shared by the {@linkplain Variant grouped variants} or the {@linkplain
|
||||
* Rendition grouped renditions}.
|
||||
*/
|
||||
public final Format format;
|
||||
|
||||
/**
|
||||
* The stable identifier shared by variants or renditions, or {@code null}.
|
||||
*
|
||||
* <p>If the {@link GroupKey} groups {@linkplain Variant variants}, then this field is the
|
||||
* {@link Variant#stableVariantId} of the variants; If the {@link GroupKey} groups {@linkplain
|
||||
* Rendition renditions}, then this field is the {@link Rendition#stableRenditionId} of the
|
||||
* renditions.
|
||||
*/
|
||||
@Nullable public final String stableId;
|
||||
|
||||
/**
|
||||
* The {@linkplain Rendition#name name} of the {@linkplain Rendition grouped renditions}. This
|
||||
* can be {@code null} if unknown or the {@link GroupKey} groups {@linkplain Variant variants}.
|
||||
*/
|
||||
@Nullable public final String name;
|
||||
|
||||
/**
|
||||
* Creates a {@link GroupKey}.
|
||||
*
|
||||
* @param format See {@link #format}.
|
||||
* @param stableId See {@link #stableId}.
|
||||
*/
|
||||
public GroupKey(Format format, @Nullable String stableId) {
|
||||
this(format, stableId, /* name= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link GroupKey}.
|
||||
*
|
||||
* @param format See {@link #format}.
|
||||
* @param stableId See {@link #stableId}.
|
||||
* @param name See {@link #name}.
|
||||
*/
|
||||
public GroupKey(Format format, @Nullable String stableId, @Nullable String name) {
|
||||
// Normalize the format to ensure only fields affecting identity are part of the key.
|
||||
this.format = format.buildUpon().setId(null).setMetadata(null).build();
|
||||
this.stableId = stableId;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof GroupKey)) {
|
||||
return false;
|
||||
}
|
||||
GroupKey groupKey = (GroupKey) other;
|
||||
return Objects.equals(format, groupKey.format)
|
||||
&& Objects.equals(stableId, groupKey.stableId)
|
||||
&& Objects.equals(name, groupKey.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(format, stableId, name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link GroupKey} that the {@linkplain Variant grouped variants} or {@linkplain Rendition
|
||||
* grouped renditions} share.
|
||||
*/
|
||||
public final GroupKey groupKey;
|
||||
|
||||
private final HashMap<String, Uri> pathwayIdToPlaylistUrl;
|
||||
private final List<Integer> indicesInMultivariantPlaylist;
|
||||
private String currentPathwayId;
|
||||
|
||||
/**
|
||||
* Creates a {@link HlsRedundantGroup}.
|
||||
*
|
||||
* @param groupKey See {@link #groupKey}.
|
||||
* @param pathwayId The ID of the default current pathway.
|
||||
* @param playlistUrl The playlist url that is associated with the default current pathway.
|
||||
*/
|
||||
public HlsRedundantGroup(GroupKey groupKey, String pathwayId, Uri playlistUrl) {
|
||||
this(groupKey, pathwayId, playlistUrl, C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link HlsRedundantGroup}.
|
||||
*
|
||||
* @param groupKey See {@link #groupKey}.
|
||||
* @param pathwayId The ID of the default current pathway.
|
||||
* @param playlistUrl The playlist url that is associated with the default current pathway.
|
||||
* @param indexInMultivariantPlaylist The index of the {@code playlistUrl} in either {@link
|
||||
* HlsMultivariantPlaylist#variants}, {@link HlsMultivariantPlaylist#videos}, {@link
|
||||
* HlsMultivariantPlaylist#audios} or {@link HlsMultivariantPlaylist#subtitles}. May be {@link
|
||||
* C#INDEX_UNSET} if the {@code playlistUrl} is not declared in the multivariant playlist.
|
||||
*/
|
||||
public HlsRedundantGroup(
|
||||
GroupKey groupKey, String pathwayId, Uri playlistUrl, int indexInMultivariantPlaylist) {
|
||||
this.groupKey = groupKey;
|
||||
this.pathwayIdToPlaylistUrl = new HashMap<>();
|
||||
this.pathwayIdToPlaylistUrl.put(pathwayId, playlistUrl);
|
||||
this.currentPathwayId = pathwayId;
|
||||
this.indicesInMultivariantPlaylist = new ArrayList<>();
|
||||
if (indexInMultivariantPlaylist != C.INDEX_UNSET) {
|
||||
indicesInMultivariantPlaylist.add(indexInMultivariantPlaylist);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a {@code playlistUrl} and its associated {@code pathwayId} to the {@link
|
||||
* HlsRedundantGroup}.
|
||||
*/
|
||||
public void put(String pathwayId, Uri playlistUrl) {
|
||||
put(pathwayId, playlistUrl, C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a {@code playlistUrl} and its associated {@code pathwayId} to the {@link
|
||||
* HlsRedundantGroup}.
|
||||
*
|
||||
* @param pathwayId The ID of the pathway to add.
|
||||
* @param playlistUrl The playlist url to add.
|
||||
* @param indexInMultivariantPlaylist The index of the {@code playlistUrl} in either {@link
|
||||
* HlsMultivariantPlaylist#variants}, {@link HlsMultivariantPlaylist#videos}, {@link
|
||||
* HlsMultivariantPlaylist#audios} or {@link HlsMultivariantPlaylist#subtitles}. May be {@link
|
||||
* C#INDEX_UNSET} if the {@code playlistUrl} is not declared in the multivariant playlist.
|
||||
*/
|
||||
public void put(String pathwayId, Uri playlistUrl, int indexInMultivariantPlaylist) {
|
||||
pathwayIdToPlaylistUrl.put(pathwayId, playlistUrl);
|
||||
if (indexInMultivariantPlaylist != C.INDEX_UNSET) {
|
||||
indicesInMultivariantPlaylist.add(indexInMultivariantPlaylist);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the size of the {@link HlsRedundantGroup}. */
|
||||
public int size() {
|
||||
return pathwayIdToPlaylistUrl.size();
|
||||
}
|
||||
|
||||
/** Sets the {@code pathwayId} that is currently chosen for playback. */
|
||||
public void setCurrentPathwayId(String pathwayId) {
|
||||
checkState(pathwayIdToPlaylistUrl.containsKey(pathwayId));
|
||||
currentPathwayId = pathwayId;
|
||||
}
|
||||
|
||||
/** Returns the pathway ID that is currently chosen for playback. */
|
||||
public String getCurrentPathwayId() {
|
||||
return currentPathwayId;
|
||||
}
|
||||
|
||||
/** Returns all pathway IDs that belong to this {@link HlsRedundantGroup}. */
|
||||
public ImmutableSet<String> getAllPathwayIds() {
|
||||
return ImmutableSet.copyOf(pathwayIdToPlaylistUrl.keySet());
|
||||
}
|
||||
|
||||
/** Returns the playlist url that is associated with the currently chosen pathway ID. */
|
||||
public Uri getCurrentPlaylistUrl() {
|
||||
return checkNotNull(pathwayIdToPlaylistUrl.get(currentPathwayId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the playlist url associated with the given {@code pathwayId}, or {@code null} if the
|
||||
* given {@code pathwayId} doesn't exist in this {@link HlsRedundantGroup}.
|
||||
*/
|
||||
@Nullable
|
||||
public Uri getPlaylistUrl(String pathwayId) {
|
||||
return pathwayIdToPlaylistUrl.get(pathwayId);
|
||||
}
|
||||
|
||||
/** Returns all playlist urls that belong to this {@link HlsRedundantGroup}. */
|
||||
public ImmutableSet<Uri> getAllPlaylistUrls() {
|
||||
return ImmutableSet.copyOf(pathwayIdToPlaylistUrl.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the indices of the {@linkplain Variant variants} or {@linkplain Rendition renditions}
|
||||
* belonging to this {@link HlsRedundantGroup} in either {@link HlsMultivariantPlaylist#variants},
|
||||
* {@link HlsMultivariantPlaylist#videos}, {@link HlsMultivariantPlaylist#audios} or {@link
|
||||
* HlsMultivariantPlaylist#subtitles}.
|
||||
*/
|
||||
public ImmutableList<Integer> getIndicesInMultivariantPlaylist() {
|
||||
return ImmutableList.copyOf(indicesInMultivariantPlaylist);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.exoplayer.hls.playlist;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
||||
Reference in New Issue
Block a user