Merge pull request #7784 from google/dev-v2-r2.11.8

r2.11.8
This commit is contained in:
Oliver Woodman 2020-08-24 18:17:48 +01:00 committed by GitHub
commit e822196336
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 234 additions and 90 deletions

View File

@ -1,5 +1,31 @@
# Release notes # # Release notes #
### 2.11.8 (2020-08-25) ###
* Fix distorted playback of floating point audio when samples exceed the
`[-1, 1]` nominal range.
* MP4:
* Add support for `piff` and `isml` brands
([#7584](https://github.com/google/ExoPlayer/issues/7584)).
* Fix playback of very short MP4 files.
* FMP4:
* Fix `saiz` and `senc` sample count checks, resolving a "length
mismatch" `ParserException` when playing certain protected FMP4 streams
([#7592](https://github.com/google/ExoPlayer/issues/7592)).
* Fix handling of `traf` boxes containing multiple `sbgp` or `sgpd`
boxes.
* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than
failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)).
* Better infer the content type of `.ism` and `.isml` streaming URLs.
* Workaround an issue on Broadcom based devices where playbacks would not
transition to `STATE_ENDED` when using video tunneling mode
([#7647](https://github.com/google/ExoPlayer/issues/7647)).
* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the
media load timeout
([#7170](https://github.com/google/ExoPlayer/issues/7170)).
* Demo app: Fix playback of ClearKey protected content on API level 26 and
earlier ([#7735](https://github.com/google/ExoPlayer/issues/7735)).
### 2.11.7 (2020-06-29) ### ### 2.11.7 (2020-06-29) ###
* IMA extension: Fix the way postroll "content complete" notifications are * IMA extension: Fix the way postroll "content complete" notifications are

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.11.7' releaseVersion = '2.11.8'
releaseVersionCode = 2011007 releaseVersionCode = 2011008
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.demo;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.media.MediaDrm;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Pair; import android.util.Pair;
@ -47,6 +46,7 @@ import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.drm.MediaDrmCallback;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
@ -485,7 +485,7 @@ public class PlayerActivity extends AppCompatActivity
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
} else if (Util.SDK_INT < 18) { } else if (Util.SDK_INT < 18) {
errorStringId = R.string.error_drm_unsupported_before_api_18; errorStringId = R.string.error_drm_unsupported_before_api_18;
} else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) { } else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) {
errorStringId = R.string.error_drm_unsupported_scheme; errorStringId = R.string.error_drm_unsupported_scheme;
} else { } else {
MediaDrmCallback mediaDrmCallback = MediaDrmCallback mediaDrmCallback =

View File

@ -32,7 +32,7 @@ android {
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.19.0' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.19.4'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'

View File

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.11.7"; public static final String VERSION = "2.11.8";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.7"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.8";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2011007; public static final int VERSION_INT = 2011008;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
@ -115,9 +116,13 @@ import java.nio.ByteBuffer;
// 32 bit floating point -> 16 bit resampling. Floating point values are in the range // 32 bit floating point -> 16 bit resampling. Floating point values are in the range
// [-1.0, 1.0], so need to be scaled by Short.MAX_VALUE. // [-1.0, 1.0], so need to be scaled by Short.MAX_VALUE.
for (int i = position; i < limit; i += 4) { for (int i = position; i < limit; i += 4) {
short value = (short) (inputBuffer.getFloat(i) * Short.MAX_VALUE); // Clamp to avoid integer overflow if the floating point values exceed their nominal range
buffer.put((byte) (value & 0xFF)); // [Internal ref: b/161204847].
buffer.put((byte) ((value >> 8) & 0xFF)); float floatValue =
Util.constrainValue(inputBuffer.getFloat(i), /* min= */ -1, /* max= */ 1);
short shortValue = (short) (floatValue * Short.MAX_VALUE);
buffer.put((byte) (shortValue & 0xFF));
buffer.put((byte) ((shortValue >> 8) & 0xFF));
} }
break; break;
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:

View File

@ -75,6 +75,15 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
private final MediaDrm mediaDrm; private final MediaDrm mediaDrm;
private int referenceCount; private int referenceCount;
/**
* Returns whether the DRM scheme with the given UUID is supported on this device.
*
* @see MediaDrm#isCryptoSchemeSupported(UUID)
*/
public static boolean isCryptoSchemeSupported(UUID uuid) {
return MediaDrm.isCryptoSchemeSupported(adjustUuid(uuid));
}
/** /**
* Creates an instance with an initial reference count of 1. {@link #release()} must be called on * Creates an instance with an initial reference count of 1. {@link #release()} must be called on
* the instance when it's no longer required. * the instance when it's no longer required.

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.flv;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList; import java.util.ArrayList;
@ -65,11 +64,11 @@ import java.util.Map;
} }
@Override @Override
protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { protected boolean parsePayload(ParsableByteArray data, long timeUs) {
int nameType = readAmfType(data); int nameType = readAmfType(data);
if (nameType != AMF_TYPE_STRING) { if (nameType != AMF_TYPE_STRING) {
// Should never happen. // Ignore segments with unexpected name type.
throw new ParserException(); return false;
} }
String name = readAmfString(data); String name = readAmfString(data);
if (!NAME_METADATA.equals(name)) { if (!NAME_METADATA.equals(name)) {

View File

@ -735,12 +735,7 @@ public class FragmentedMp4Extractor implements Extractor {
parseSenc(senc.data, fragment); parseSenc(senc.data, fragment);
} }
LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); parseSampleGroups(traf, encryptionBox != null ? encryptionBox.schemeType : null, fragment);
LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd);
if (sbgp != null && sgpd != null) {
parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null,
fragment);
}
int leafChildrenSize = traf.leafChildren.size(); int leafChildrenSize = traf.leafChildren.size();
for (int i = 0; i < leafChildrenSize; i++) { for (int i = 0; i < leafChildrenSize; i++) {
@ -798,8 +793,12 @@ public class FragmentedMp4Extractor implements Extractor {
int defaultSampleInfoSize = saiz.readUnsignedByte(); int defaultSampleInfoSize = saiz.readUnsignedByte();
int sampleCount = saiz.readUnsignedIntToInt(); int sampleCount = saiz.readUnsignedIntToInt();
if (sampleCount != out.sampleCount) { if (sampleCount > out.sampleCount) {
throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); throw new ParserException(
"Saiz sample count "
+ sampleCount
+ " is greater than fragment sample count"
+ out.sampleCount);
} }
int totalSize = 0; int totalSize = 0;
@ -815,7 +814,10 @@ public class FragmentedMp4Extractor implements Extractor {
totalSize += defaultSampleInfoSize * sampleCount; totalSize += defaultSampleInfoSize * sampleCount;
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
} }
out.initEncryptionData(totalSize); Arrays.fill(out.sampleHasSubsampleEncryptionTable, sampleCount, out.sampleCount, false);
if (totalSize > 0) {
out.initEncryptionData(totalSize);
}
} }
/** /**
@ -990,8 +992,10 @@ public class FragmentedMp4Extractor implements Extractor {
checkNonNegative(sampleDurationsPresent ? trun.readInt() : defaultSampleValues.duration); checkNonNegative(sampleDurationsPresent ? trun.readInt() : defaultSampleValues.duration);
int sampleSize = int sampleSize =
checkNonNegative(sampleSizesPresent ? trun.readInt() : defaultSampleValues.size); checkNonNegative(sampleSizesPresent ? trun.readInt() : defaultSampleValues.size);
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags int sampleFlags =
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; sampleFlagsPresent
? trun.readInt()
: (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags : defaultSampleValues.flags;
if (sampleCompositionTimeOffsetsPresent) { if (sampleCompositionTimeOffsetsPresent) {
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
// version 0 trun boxes, however a significant number of streams violate the spec and use // version 0 trun boxes, however a significant number of streams violate the spec and use
@ -1055,8 +1059,16 @@ public class FragmentedMp4Extractor implements Extractor {
boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0; boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
int sampleCount = senc.readUnsignedIntToInt(); int sampleCount = senc.readUnsignedIntToInt();
if (sampleCount != out.sampleCount) { if (sampleCount == 0) {
throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); // Samples are unencrypted.
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, out.sampleCount, false);
return;
} else if (sampleCount != out.sampleCount) {
throw new ParserException(
"Senc sample count "
+ sampleCount
+ " is different from fragment sample count"
+ out.sampleCount);
} }
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
@ -1064,28 +1076,43 @@ public class FragmentedMp4Extractor implements Extractor {
out.fillEncryptionData(senc); out.fillEncryptionData(senc);
} }
private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, private static void parseSampleGroups(
TrackFragment out) throws ParserException { ContainerAtom traf, @Nullable String schemeType, TrackFragment out) throws ParserException {
sbgp.setPosition(Atom.HEADER_SIZE); // Find sbgp and sgpd boxes with grouping_type == seig.
int sbgpFullAtom = sbgp.readInt(); @Nullable ParsableByteArray sbgp = null;
if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { @Nullable ParsableByteArray sgpd = null;
// Only seig grouping type is supported. for (int i = 0; i < traf.leafChildren.size(); i++) {
LeafAtom leafAtom = traf.leafChildren.get(i);
ParsableByteArray leafAtomData = leafAtom.data;
if (leafAtom.type == Atom.TYPE_sbgp) {
leafAtomData.setPosition(Atom.FULL_HEADER_SIZE);
if (leafAtomData.readInt() == SAMPLE_GROUP_TYPE_seig) {
sbgp = leafAtomData;
}
} else if (leafAtom.type == Atom.TYPE_sgpd) {
leafAtomData.setPosition(Atom.FULL_HEADER_SIZE);
if (leafAtomData.readInt() == SAMPLE_GROUP_TYPE_seig) {
sgpd = leafAtomData;
}
}
}
if (sbgp == null || sgpd == null) {
return; return;
} }
if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) {
sbgp.skipBytes(4); // default_length. sbgp.setPosition(Atom.HEADER_SIZE);
int sbgpVersion = Atom.parseFullAtomVersion(sbgp.readInt());
sbgp.skipBytes(4); // grouping_type == seig.
if (sbgpVersion == 1) {
sbgp.skipBytes(4); // grouping_type_parameter.
} }
if (sbgp.readInt() != 1) { // entry_count. if (sbgp.readInt() != 1) { // entry_count.
throw new ParserException("Entry count in sbgp != 1 (unsupported)."); throw new ParserException("Entry count in sbgp != 1 (unsupported).");
} }
sgpd.setPosition(Atom.HEADER_SIZE); sgpd.setPosition(Atom.HEADER_SIZE);
int sgpdFullAtom = sgpd.readInt(); int sgpdVersion = Atom.parseFullAtomVersion(sgpd.readInt());
if (sgpd.readInt() != SAMPLE_GROUP_TYPE_seig) { sgpd.skipBytes(4); // grouping_type == seig.
// Only seig grouping type is supported.
return;
}
int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom);
if (sgpdVersion == 1) { if (sgpdVersion == 1) {
if (sgpd.readUnsignedInt() == 0) { if (sgpd.readUnsignedInt() == 0) {
throw new ParserException("Variable length description in sgpd found (unsupported)"); throw new ParserException("Variable length description in sgpd found (unsupported)");
@ -1096,6 +1123,7 @@ public class FragmentedMp4Extractor implements Extractor {
if (sgpd.readUnsignedInt() != 1) { // entry_count. if (sgpd.readUnsignedInt() != 1) { // entry_count.
throw new ParserException("Entry count in sgpd != 1 (unsupported)."); throw new ParserException("Entry count in sgpd != 1 (unsupported).");
} }
// CencSampleEncryptionInformationGroupEntry // CencSampleEncryptionInformationGroupEntry
sgpd.skipBytes(1); // reserved = 0. sgpd.skipBytes(1); // reserved = 0.
int patternByte = sgpd.readUnsignedByte(); int patternByte = sgpd.readUnsignedByte();

View File

@ -57,6 +57,8 @@ import java.io.IOException;
0x71742020, // qt[space][space], Apple QuickTime 0x71742020, // qt[space][space], Apple QuickTime
0x4d534e56, // MSNV, Sony PSP 0x4d534e56, // MSNV, Sony PSP
0x64627931, // dby1, Dolby Vision 0x64627931, // dby1, Dolby Vision
0x69736d6c, // isml
0x70696666, // piff
}; };
/** /**
@ -101,7 +103,12 @@ import java.io.IOException;
// Read an atom header. // Read an atom header.
int headerSize = Atom.HEADER_SIZE; int headerSize = Atom.HEADER_SIZE;
buffer.reset(headerSize); buffer.reset(headerSize);
input.peekFully(buffer.data, 0, headerSize); boolean success =
input.peekFully(buffer.data, 0, headerSize, /* allowEndOfInput= */ true);
if (!success) {
// We've reached the end of the file.
break;
}
long atomSize = buffer.readUnsignedInt(); long atomSize = buffer.readUnsignedInt();
int atomType = buffer.readInt(); int atomType = buffer.readInt();
if (atomSize == Atom.DEFINES_LARGE_SIZE) { if (atomSize == Atom.DEFINES_LARGE_SIZE) {

View File

@ -657,6 +657,10 @@ public final class TsExtractor implements Extractor {
int descriptorTag = data.readUnsignedByte(); int descriptorTag = data.readUnsignedByte();
int descriptorLength = data.readUnsignedByte(); int descriptorLength = data.readUnsignedByte();
int positionOfNextDescriptor = data.getPosition() + descriptorLength; int positionOfNextDescriptor = data.getPosition() + descriptorLength;
if (positionOfNextDescriptor > descriptorsEndPosition) {
// Descriptor claims to extend past the end position. Skip it.
break;
}
if (descriptorTag == TS_PMT_DESC_REGISTRATION) { // registration_descriptor if (descriptorTag == TS_PMT_DESC_REGISTRATION) { // registration_descriptor
long formatIdentifier = data.readUnsignedInt(); long formatIdentifier = data.readUnsignedInt();
if (formatIdentifier == AC3_FORMAT_IDENTIFIER) { if (formatIdentifier == AC3_FORMAT_IDENTIFIER) {

View File

@ -1937,6 +1937,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
String name = codecInfo.name; String name = codecInfo.name;
return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name)) return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name))
|| (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name)) || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name))
|| (Util.SDK_INT <= 29
&& ("OMX.broadcom.video_decoder.tunnel".equals(name)
|| "OMX.broadcom.video_decoder.tunnel.secure".equals(name)))
|| ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
} }

View File

@ -83,7 +83,7 @@ public final class MediaFormatUtil {
* *
* @param format The {@link MediaFormat} being configured. * @param format The {@link MediaFormat} being configured.
* @param key The key to set. * @param key The key to set.
* @param value The {@link byte[]} that will be wrapped to obtain the value. * @param value The byte array that will be wrapped to obtain the value.
*/ */
public static void maybeSetByteBuffer(MediaFormat format, String key, @Nullable byte[] value) { public static void maybeSetByteBuffer(MediaFormat format, String key, @Nullable byte[] value) {
if (value != null) { if (value != null) {

View File

@ -40,7 +40,7 @@ public final class ConditionVariable {
} }
/** /**
* Creates an instance. * Creates an instance, which starts closed.
* *
* @param clock The {@link Clock} whose {@link Clock#elapsedRealtime()} method is used to * @param clock The {@link Clock} whose {@link Clock#elapsedRealtime()} method is used to
* determine when {@link #block(long)} should time out. * determine when {@link #block(long)} should time out.

View File

@ -135,6 +135,11 @@ public final class Util {
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
// https://docs.microsoft.com/en-us/azure/media-services/previous/media-services-deliver-content-overview#URLs.
private static final Pattern ISM_URL_PATTERN = Pattern.compile(".*\\.isml?(?:/(manifest(.*))?)?");
private static final String ISM_HLS_FORMAT_EXTENSION = "format=m3u8-aapl";
private static final String ISM_DASH_FORMAT_EXTENSION = "format=mpd-time-csf";
// Replacement map of ISO language codes used for normalization. // Replacement map of ISO language codes used for normalization.
@Nullable private static HashMap<String, String> languageTagReplacementMap; @Nullable private static HashMap<String, String> languageTagReplacementMap;
@ -1599,11 +1604,41 @@ public final class Util {
return C.TYPE_DASH; return C.TYPE_DASH;
} else if (fileName.endsWith(".m3u8")) { } else if (fileName.endsWith(".m3u8")) {
return C.TYPE_HLS; return C.TYPE_HLS;
} else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) {
return C.TYPE_SS;
} else {
return C.TYPE_OTHER;
} }
Matcher ismMatcher = ISM_URL_PATTERN.matcher(fileName);
if (ismMatcher.matches()) {
@Nullable String extensions = ismMatcher.group(2);
if (extensions != null) {
if (extensions.contains(ISM_DASH_FORMAT_EXTENSION)) {
return C.TYPE_DASH;
} else if (extensions.contains(ISM_HLS_FORMAT_EXTENSION)) {
return C.TYPE_HLS;
}
}
return C.TYPE_SS;
}
return C.TYPE_OTHER;
}
/**
* If the provided URI is an ISM Presentation URI, returns the URI with "Manifest" appended to its
* path (i.e., the corresponding default manifest URI). Else returns the provided URI without
* modification. See [MS-SSTR] v20180912, section 2.2.1.
*
* @param uri The original URI.
* @return The fixed URI.
*/
public static Uri fixSmoothStreamingIsmManifestUri(Uri uri) {
@Nullable String path = toLowerInvariant(uri.getPath());
if (path == null) {
return uri;
}
Matcher ismMatcher = ISM_URL_PATTERN.matcher(path);
if (ismMatcher.matches() && ismMatcher.group(1) == null) {
// Add missing "Manifest" suffix.
return Uri.withAppendedPath(uri, "Manifest");
}
return uri;
} }
/** /**

View File

@ -24,6 +24,7 @@ import static com.google.android.exoplayer2.util.Util.parseXsDuration;
import static com.google.android.exoplayer2.util.Util.unescapeFileName; import static com.google.android.exoplayer2.util.Util.unescapeFileName;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
@ -77,15 +78,77 @@ public class UtilTest {
} }
@Test @Test
public void testInferContentType() { public void inferContentType_handlesHlsIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl,quality=hd)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl)"))
.isEqualTo(C.TYPE_HLS);
}
@Test
public void inferContentType_handlesHlsIsmV3Uris() {
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl-v3)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl-v3,quality=hd)"))
.isEqualTo(C.TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl-v3)"))
.isEqualTo(C.TYPE_HLS);
}
@Test
public void inferContentType_handlesDashIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(format=mpd-time-csf)"))
.isEqualTo(C.TYPE_DASH);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(format=mpd-time-csf,quality=hd)"))
.isEqualTo(C.TYPE_DASH);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(quality=hd,format=mpd-time-csf)"))
.isEqualTo(C.TYPE_DASH);
}
@Test
public void inferContentType_handlesSmoothStreamingIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType("http://a.b/c.isml")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.ism/")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.ism/Manifest")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType("http://a.b/c.ism/Manifest")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType("http://a.b/c.isml/manifest")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(filter=x)")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType("http://a.b/c.isml/manifest(filter=x)")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest_hd")).isEqualTo(C.TYPE_SS);
}
@Test
public void inferContentType_handlesOtherIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism/video.mp4")).isEqualTo(C.TYPE_OTHER);
assertThat(Util.inferContentType("http://a.b/c.ism/prefix-manifest")).isEqualTo(C.TYPE_OTHER); assertThat(Util.inferContentType("http://a.b/c.ism/prefix-manifest")).isEqualTo(C.TYPE_OTHER);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest-suffix")).isEqualTo(C.TYPE_OTHER); }
@Test
public void fixSmoothStreamingIsmManifestUri_addsManifestSuffix() {
assertThat(Util.fixSmoothStreamingIsmManifestUri(Uri.parse("http://a.b/c.ism")))
.isEqualTo(Uri.parse("http://a.b/c.ism/Manifest"));
assertThat(Util.fixSmoothStreamingIsmManifestUri(Uri.parse("http://a.b/c.isml")))
.isEqualTo(Uri.parse("http://a.b/c.isml/Manifest"));
assertThat(Util.fixSmoothStreamingIsmManifestUri(Uri.parse("http://a.b/c.ism/")))
.isEqualTo(Uri.parse("http://a.b/c.ism/Manifest"));
assertThat(Util.fixSmoothStreamingIsmManifestUri(Uri.parse("http://a.b/c.isml/")))
.isEqualTo(Uri.parse("http://a.b/c.isml/Manifest"));
}
@Test
public void fixSmoothStreamingIsmManifestUri_doesNotAlterManifestUri() {
assertThat(Util.fixSmoothStreamingIsmManifestUri(Uri.parse("http://a.b/c.ism/Manifest")))
.isEqualTo(Uri.parse("http://a.b/c.ism/Manifest"));
assertThat(Util.fixSmoothStreamingIsmManifestUri(Uri.parse("http://a.b/c.isml/Manifest")))
.isEqualTo(Uri.parse("http://a.b/c.isml/Manifest"));
assertThat(
Util.fixSmoothStreamingIsmManifestUri(Uri.parse("http://a.b/c.ism/Manifest(filter=x)")))
.isEqualTo(Uri.parse("http://a.b/c.ism/Manifest(filter=x)"));
assertThat(Util.fixSmoothStreamingIsmManifestUri(Uri.parse("http://a.b/c.ism/Manifest_hd")))
.isEqualTo(Uri.parse("http://a.b/c.ism/Manifest_hd"));
} }
@Test @Test

View File

@ -39,7 +39,6 @@ import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
@ -50,6 +49,7 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -531,7 +531,7 @@ public final class SsMediaSource extends BaseMediaSource
@Nullable Object tag) { @Nullable Object tag) {
Assertions.checkState(manifest == null || !manifest.isLive); Assertions.checkState(manifest == null || !manifest.isLive);
this.manifest = manifest; this.manifest = manifest;
this.manifestUri = manifestUri == null ? null : SsUtil.fixManifestUri(manifestUri); this.manifestUri = manifestUri == null ? null : Util.fixSmoothStreamingIsmManifestUri(manifestUri);
this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestDataSourceFactory = manifestDataSourceFactory;
this.manifestParser = manifestParser; this.manifestParser = manifestParser;
this.chunkSourceFactory = chunkSourceFactory; this.chunkSourceFactory = chunkSourceFactory;

View File

@ -1,35 +0,0 @@
/*
* 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 com.google.android.exoplayer2.source.smoothstreaming.manifest;
import android.net.Uri;
import com.google.android.exoplayer2.util.Util;
/** SmoothStreaming related utility methods. */
public final class SsUtil {
/** Returns a fixed SmoothStreaming client manifest {@link Uri}. */
public static Uri fixManifestUri(Uri manifestUri) {
String lastPathSegment = manifestUri.getLastPathSegment();
if (lastPathSegment != null
&& Util.toLowerInvariant(lastPathSegment).matches("manifest(\\(.+\\))?")) {
return manifestUri;
}
return Uri.withAppendedPath(manifestUri, "Manifest");
}
private SsUtil() {}
}

View File

@ -23,10 +23,10 @@ import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -64,7 +64,7 @@ public final class SsDownloader extends SegmentDownloader<SsManifest> {
*/ */
public SsDownloader( public SsDownloader(
Uri manifestUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) { Uri manifestUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {
super(SsUtil.fixManifestUri(manifestUri), streamKeys, constructorHelper); super(Util.fixSmoothStreamingIsmManifestUri(manifestUri), streamKeys, constructorHelper);
} }
@Override @Override