diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d99656b5a9..16f276c6ae 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,31 @@ # 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) ### * IMA extension: Fix the way postroll "content complete" notifications are diff --git a/constants.gradle b/constants.gradle index 9d38d82369..0fceb92692 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.11.7' - releaseVersionCode = 2011007 + releaseVersion = '2.11.8' + releaseVersionCode = 2011008 minSdkVersion = 16 appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 0454472abf..d5da379263 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.demo; import android.content.Intent; import android.content.pm.PackageManager; -import android.media.MediaDrm; import android.net.Uri; import android.os.Bundle; 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.HttpMediaDrmCallback; 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.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.offline.DownloadHelper; @@ -485,7 +485,7 @@ public class PlayerActivity extends AppCompatActivity drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); } else if (Util.SDK_INT < 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; } else { MediaDrmCallback mediaDrmCallback = diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index b83caf62ee..1f36a29dd1 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -32,7 +32,7 @@ android { } 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 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 35b6199cd3..bc1f0032c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** 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. - 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}. */ // 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. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // 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} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 883f5bcb92..a4d2a1b67a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Util; 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 // [-1.0, 1.0], so need to be scaled by Short.MAX_VALUE. for (int i = position; i < limit; i += 4) { - short value = (short) (inputBuffer.getFloat(i) * Short.MAX_VALUE); - buffer.put((byte) (value & 0xFF)); - buffer.put((byte) ((value >> 8) & 0xFF)); + // Clamp to avoid integer overflow if the floating point values exceed their nominal range + // [Internal ref: b/161204847]. + 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; case C.ENCODING_PCM_16BIT: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 56d1aeea4b..f4f84c92dc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -75,6 +75,15 @@ public final class FrameworkMediaDrm implements ExoMediaDrm out.sampleCount) { + throw new ParserException( + "Saiz sample count " + + sampleCount + + " is greater than fragment sample count" + + out.sampleCount); } int totalSize = 0; @@ -815,7 +814,10 @@ public class FragmentedMp4Extractor implements Extractor { totalSize += defaultSampleInfoSize * sampleCount; 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); int sampleSize = checkNonNegative(sampleSizesPresent ? trun.readInt() : defaultSampleValues.size); - int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags - : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; + int sampleFlags = + sampleFlagsPresent + ? trun.readInt() + : (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags : defaultSampleValues.flags; if (sampleCompositionTimeOffsetsPresent) { // 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 @@ -1055,8 +1059,16 @@ public class FragmentedMp4Extractor implements Extractor { boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0; int sampleCount = senc.readUnsignedIntToInt(); - if (sampleCount != out.sampleCount) { - throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); + if (sampleCount == 0) { + // 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); @@ -1064,28 +1076,43 @@ public class FragmentedMp4Extractor implements Extractor { out.fillEncryptionData(senc); } - private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, - TrackFragment out) throws ParserException { - sbgp.setPosition(Atom.HEADER_SIZE); - int sbgpFullAtom = sbgp.readInt(); - if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { - // Only seig grouping type is supported. + private static void parseSampleGroups( + ContainerAtom traf, @Nullable String schemeType, TrackFragment out) throws ParserException { + // Find sbgp and sgpd boxes with grouping_type == seig. + @Nullable ParsableByteArray sbgp = null; + @Nullable ParsableByteArray sgpd = null; + 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; } - 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. throw new ParserException("Entry count in sbgp != 1 (unsupported)."); } sgpd.setPosition(Atom.HEADER_SIZE); - int sgpdFullAtom = sgpd.readInt(); - if (sgpd.readInt() != SAMPLE_GROUP_TYPE_seig) { - // Only seig grouping type is supported. - return; - } - int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom); + int sgpdVersion = Atom.parseFullAtomVersion(sgpd.readInt()); + sgpd.skipBytes(4); // grouping_type == seig. if (sgpdVersion == 1) { if (sgpd.readUnsignedInt() == 0) { 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. throw new ParserException("Entry count in sgpd != 1 (unsupported)."); } + // CencSampleEncryptionInformationGroupEntry sgpd.skipBytes(1); // reserved = 0. int patternByte = sgpd.readUnsignedByte(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index dac74bfe2b..1e1c5450be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -57,6 +57,8 @@ import java.io.IOException; 0x71742020, // qt[space][space], Apple QuickTime 0x4d534e56, // MSNV, Sony PSP 0x64627931, // dby1, Dolby Vision + 0x69736d6c, // isml + 0x70696666, // piff }; /** @@ -101,7 +103,12 @@ import java.io.IOException; // Read an atom header. int headerSize = Atom.HEADER_SIZE; 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(); int atomType = buffer.readInt(); if (atomSize == Atom.DEFINES_LARGE_SIZE) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2bd5b12551..91643c4fca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -657,6 +657,10 @@ public final class TsExtractor implements Extractor { int descriptorTag = data.readUnsignedByte(); int descriptorLength = data.readUnsignedByte(); 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 long formatIdentifier = data.readUnsignedInt(); if (formatIdentifier == AC3_FORMAT_IDENTIFIER) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index e1026ed196..70c102ebeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -1937,6 +1937,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { String name = codecInfo.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 <= 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); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java index 118445835b..0ed58db266 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java @@ -83,7 +83,7 @@ public final class MediaFormatUtil { * * @param format The {@link MediaFormat} being configured. * @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) { if (value != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index 69782ab1e8..7372aa4545 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -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 * determine when {@link #block(long)} should time out. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index a7a46b163d..b1bddc13b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -135,6 +135,11 @@ public final class Util { + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); 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. @Nullable private static HashMap languageTagReplacementMap; @@ -1599,11 +1604,41 @@ public final class Util { return C.TYPE_DASH; } else if (fileName.endsWith(".m3u8")) { 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; } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 07334872e1..71cf194ec0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -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.common.truth.Truth.assertThat; +import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; @@ -77,15 +78,77 @@ public class UtilTest { } @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.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.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_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/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 diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 89dd8039ef..02df8f28ff 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -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.StreamElement; 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.DataSource; 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.TransferListener; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -531,7 +531,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable Object tag) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; - this.manifestUri = manifestUri == null ? null : SsUtil.fixManifestUri(manifestUri); + this.manifestUri = manifestUri == null ? null : Util.fixSmoothStreamingIsmManifestUri(manifestUri); this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java deleted file mode 100644 index b54b2abc74..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java +++ /dev/null @@ -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() {} -} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 1331fe4617..8808954135 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -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.StreamElement; 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.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -64,7 +64,7 @@ public final class SsDownloader extends SegmentDownloader { */ public SsDownloader( Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { - super(SsUtil.fixManifestUri(manifestUri), streamKeys, constructorHelper); + super(Util.fixSmoothStreamingIsmManifestUri(manifestUri), streamKeys, constructorHelper); } @Override