From f1dd1894fa4ed9842e650cf2998a4e156a5aeef8 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 6 Jul 2020 10:46:02 +0100 Subject: [PATCH 01/20] Add compatible brands to MP4 sniffer Issue: #7584 PiperOrigin-RevId: 319744023 --- .../com/google/android/exoplayer2/extractor/mp4/Sniffer.java | 2 ++ 1 file changed, 2 insertions(+) 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 dac74bfe2b3..40e516aefdb 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 @@ 0x71742020, // qt[space][space], Apple QuickTime 0x4d534e56, // MSNV, Sony PSP 0x64627931, // dby1, Dolby Vision + 0x69736d6c, // isml + 0x70696666, // piff }; /** From 4633a63546dfdc418cf75d3cbc1b0fae4b7a7383 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 6 Jul 2020 13:57:30 +0100 Subject: [PATCH 02/20] Upgrade IMA to 3.19.2 PiperOrigin-RevId: 319764381 --- extensions/ima/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index b83caf62ee8..3c81a58ce98 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.2' implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' From 3b7669ff724bd74b156cf6be8c7cc1a3b7037a0e Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 10 Jul 2020 08:19:49 +0100 Subject: [PATCH 03/20] Fix saiz and senc sample count checks for FMP4 Issue: #7592 PiperOrigin-RevId: 320556981 --- RELEASENOTES.md | 8 ++++++ .../extractor/mp4/FragmentedMp4Extractor.java | 25 +++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d99656b5a9d..29c50c1b299 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,13 @@ # Release notes # +### 2.11.8 (2020-08-25) ### + +* MP4: Add support for `piff` and `isml` brands + ([#7584](https://github.com/google/ExoPlayer/issues/7584)). +* 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)). + ### 2.11.7 (2020-06-29) ### * IMA extension: Fix the way postroll "content complete" notifications are diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index c0d1581c39d..f75c40d4486 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -798,8 +798,12 @@ private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArra int defaultSampleInfoSize = saiz.readUnsignedByte(); int sampleCount = saiz.readUnsignedIntToInt(); - if (sampleCount != out.sampleCount) { - throw new ParserException("Length mismatch: " + sampleCount + ", " + out.sampleCount); + if (sampleCount > out.sampleCount) { + throw new ParserException( + "Saiz sample count " + + sampleCount + + " is greater than fragment sample count" + + out.sampleCount); } int totalSize = 0; @@ -815,7 +819,10 @@ private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArra 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); + } } /** @@ -1055,8 +1062,16 @@ private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment 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); From c0cd73f5fd983c2f5bea9ff090d23aef3e09d965 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 10 Jul 2020 08:23:39 +0100 Subject: [PATCH 04/20] Upgrade IMA SDK to 3.19.4 This brings in a fix for the IMA SDK ignoring the media load timeout. Issue: #7170 PiperOrigin-RevId: 320557386 --- RELEASENOTES.md | 3 +++ extensions/ima/build.gradle | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 29c50c1b299..bfdf17ea794 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,9 @@ * 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)). +* 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)). ### 2.11.7 (2020-06-29) ### diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 3c81a58ce98..1f36a29dd1f 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.2' + 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' From c5edc1c2f56e593b007a367447bed64f3c4b6b0f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Jul 2020 13:14:57 +0100 Subject: [PATCH 05/20] Clip float point PCM to its allowed range before resampling PiperOrigin-RevId: 321340777 --- .../exoplayer2/audio/ResamplingAudioProcessor.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 883f5bcb924..00d9bb4d1d1 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 @@ 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 @@ public void queueInput(ByteBuffer inputBuffer) { // 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 allowed 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: From 53d12747e56ae2c742c647595565b003ec4b71c3 Mon Sep 17 00:00:00 2001 From: krocard Date: Wed, 15 Jul 2020 13:57:25 +0100 Subject: [PATCH 06/20] Name [-1,1] the "nominal" range of float samples Float values are allowed to be > 0dbfs, it is just not nominal as it will might distort the signal when played without attenuation. This is also consistent with [AudioTrack.write(FloatBuffer)](https://developer.android.com/reference/android/media/AudioTrack#write(float[],%20int,%20int,%20int)) that explicitly allows it up to 3dbfs. PiperOrigin-RevId: 321345077 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/audio/ResamplingAudioProcessor.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bfdf17ea794..445f7e157de 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### 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)). * FMP4: Fix `saiz` and `senc` sample count checks, resolving a "length 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 00d9bb4d1d1..a4d2a1b67ac 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 @@ -116,7 +116,7 @@ public void queueInput(ByteBuffer inputBuffer) { // 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) { - // Clamp to avoid integer overflow if the floating point values exceed their allowed range + // 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); From 3198c51bdbbc50faa0727717b34c04aa569bf47b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Jul 2020 09:04:07 +0100 Subject: [PATCH 07/20] Remove invalid documentation that causes javadoc to crash PiperOrigin-RevId: 322311636 --- .../google/android/exoplayer2/mediacodec/MediaFormatUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 118445835ba..0ed58db266b 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 static void maybeSetFloat(MediaFormat format, String key, float value) { * * @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) { From c010d28b1429313c015076ab696ca7e7143650c0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Jul 2020 17:04:23 +0100 Subject: [PATCH 08/20] FLV: Ignore invalid SCRIPTDATA name type, rather than fail playback Issue: #7675 PiperOrigin-RevId: 323371286 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/flv/ScriptTagPayloadReader.java | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 445f7e157de..b6c2b2ac100 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ * 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)). +* FLV: Ignore SCRIPTDATA segments with invalid name types, rather than failing + playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)). * 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)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 806cc9fad44..b7f94abb2b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -17,7 +17,6 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; @@ -65,11 +64,11 @@ protected boolean parseHeader(ParsableByteArray data) { } @Override - protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) { int nameType = readAmfType(data); if (nameType != AMF_TYPE_STRING) { - // Should never happen. - throw new ParserException(); + // Ignore segments with unexpected name type. + return false; } String name = readAmfString(data); if (!NAME_METADATA.equals(name)) { From 8c8ffe601da8aa0d1cbe44f20253001123d9559e Mon Sep 17 00:00:00 2001 From: krocard Date: Wed, 29 Jul 2020 12:06:05 +0100 Subject: [PATCH 09/20] OMX.broadcom.video_decoder.tunnel.secure needs EOS workaround The passthrough codec does not propagate the EOS back to ExoPlayer. Issue: https://github.com/google/ExoPlayer/issues/7647 PiperOrigin-RevId: 323758941 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b6c2b2ac100..dcae95e28fb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ ([#7592](https://github.com/google/ExoPlayer/issues/7592)). * FLV: Ignore SCRIPTDATA segments with invalid name types, rather than failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)). +* 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)). 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 e1026ed1963..70c102ebeb0 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 @@ private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecIn 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); } From d0875ce7903fd9481a46b1d9d6317e3f74104cd1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 30 Jul 2020 15:59:01 +0100 Subject: [PATCH 10/20] Document that ConditionVariable instances start closed PiperOrigin-RevId: 324002247 --- .../com/google/android/exoplayer2/util/ConditionVariable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 69782ab1e87..7372aa45455 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 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. From 43b80fbb780a0e14b262fae35aebd64fe4e8381b Mon Sep 17 00:00:00 2001 From: kimvde Date: Thu, 6 Aug 2020 13:40:00 +0100 Subject: [PATCH 11/20] FragmentedMp4Extractor: allow both first_sample_flags and sample_flags Having both in the trun box is not allowed (see section section 8.8.8.1 of ISO/IEC 14496-12:2015) but this CL makes the code more robust in case this happens. Before this change, the first sample flag was not read, making subsequent reads incorrect. Issue: #7698 PiperOrigin-RevId: 325212160 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index f75c40d4486..1b910d210ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -997,8 +997,10 @@ private static int parseTrun( 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 From 36efdc74922fb8dc4c016e1f0337316ad44b7ed8 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 6 Aug 2020 12:41:02 +0100 Subject: [PATCH 12/20] Fix MP4 sniffing for very short files The sniffer sniffs boxes at the start of the file to try and determine whether the file is fragmented. However, if the file is extremely short then it's possible that sniffing will try and read beyond the end of the file, resulting i EOFException being thrown. In general it's OK for sniffing to throw EOFException if the file is not of the correct type. The problem in this case is that EOFException can be thrown for an actual MP4 file, due to the sniffer continuing up sniff atoms up to bytesToSearch in case the file is fragmented. PiperOrigin-RevId: 325205389 --- RELEASENOTES.md | 6 ++++-- .../google/android/exoplayer2/extractor/mp4/Sniffer.java | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dcae95e28fb..5b3228c0d6c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,8 +4,10 @@ * 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)). +* 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)). 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 40e516aefdb..1e1c5450bec 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 @@ -103,7 +103,12 @@ private static boolean sniffInternal(ExtractorInput input, boolean fragmented) // 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) { From 62829be1ce970880ac8c3a0b56a0102f94127943 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Aug 2020 11:23:42 +0100 Subject: [PATCH 13/20] FMP4: Correctly handle multiple sbgp and sgpd boxes Find sbgp and sgpd boxes with grouping_type == seig in the case they don't come first. Previoulsy we would only find them if they came first. Issue: Issue: #7716 PiperOrigin-RevId: 325407819 --- RELEASENOTES.md | 9 ++-- .../extractor/mp4/FragmentedMp4Extractor.java | 51 +++++++++++-------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5b3228c0d6c..8a18da74b35 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,9 +8,12 @@ * 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)). +* 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)). * Workaround an issue on Broadcom based devices where playbacks would not diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 1b910d210ee..a1c880ff72c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -735,12 +735,7 @@ private static void parseTraf(ContainerAtom traf, SparseArray track parseSenc(senc.data, fragment); } - LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); - LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); - if (sbgp != null && sgpd != null) { - parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, - fragment); - } + parseSampleGroups(traf, encryptionBox != null ? encryptionBox.schemeType : null, fragment); int leafChildrenSize = traf.leafChildren.size(); for (int i = 0; i < leafChildrenSize; i++) { @@ -1081,28 +1076,43 @@ private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment 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)"); @@ -1113,6 +1123,7 @@ private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, St 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(); From fc2e4ef4fa7c71ed12a3bb109834f2971b7152fd Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Aug 2020 15:26:21 +0100 Subject: [PATCH 14/20] TS EsInfo: Be robust against a invalid descriptor length Issue: Issue: #7722 PiperOrigin-RevId: 325431839 --- .../google/android/exoplayer2/extractor/ts/TsExtractor.java | 4 ++++ 1 file changed, 4 insertions(+) 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 2bd5b125516..91643c4fca8 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 @@ private EsInfo readEsInfo(ParsableByteArray data, int length) { 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) { From 590e81e5aee38ee2e6d51b2fe955f35d017a2349 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 11 Aug 2020 14:53:13 +0100 Subject: [PATCH 15/20] Infer ISM content type from URL specified extension PiperOrigin-RevId: 326012248 --- RELEASENOTES.md | 4 +- .../android/exoplayer2/util/UtilTest.java | 1122 +++++++++++++++++ .../google/android/exoplayer2/util/Util.java | 21 +- .../android/exoplayer2/util/UtilTest.java | 38 +- 4 files changed, 1179 insertions(+), 6 deletions(-) create mode 100644 library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8a18da74b35..05300b47b94 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,8 +14,8 @@ ([#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)). +* FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than + failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)). * 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)). diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java new file mode 100644 index 00000000000..0bf5028282b --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -0,0 +1,1122 @@ +/* + * 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 com.google.android.exoplayer2.util; + +import static com.google.android.exoplayer2.util.Util.binarySearchCeil; +import static com.google.android.exoplayer2.util.Util.binarySearchFloor; +import static com.google.android.exoplayer2.util.Util.escapeFileName; +import static com.google.android.exoplayer2.util.Util.getCodecsOfType; +import static com.google.android.exoplayer2.util.Util.parseXsDateTime; +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.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.StrikethroughSpan; +import android.text.style.UnderlineSpan; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; +import java.util.zip.Deflater; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Unit tests for {@link Util}. */ +@RunWith(AndroidJUnit4.class) +public class UtilTest { + + @Test + public void addWithOverflowDefault_withoutOverFlow_returnsSum() { + long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(15); + + res = Util.addWithOverflowDefault(Long.MAX_VALUE - 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE + 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + } + + @Test + public void addWithOverflowDefault_withOverFlow_returnsOverflowDefault() { + long res = Util.addWithOverflowDefault(Long.MAX_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + + @Test + public void subtrackWithOverflowDefault_withoutUnderflow_returnsSubtract() { + long res = Util.subtractWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(-5); + + res = Util.subtractWithOverflowDefault(Long.MIN_VALUE + 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE - 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + } + + @Test + public void subtrackWithOverflowDefault_withUnderflow_returnsOverflowDefault() { + long res = Util.subtractWithOverflowDefault(Long.MIN_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + + @Test + 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); + } + + @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 arrayBinarySearchFloor_emptyArrayAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void arrayBinarySearchFloor_emptyArrayAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void arrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new int[] {1, 3, 5}, + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void arrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new int[] {1, 3, 5}, + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void arrayBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { + assertThat( + binarySearchFloor( + new int[] {1, 3, 5}, + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + arrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void + arrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void + arrayBinarySearchFloor_targetInArrayAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 1, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void + arrayBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 2, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void + arrayBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + new int[] {1, 1, 1, 1, 1, 3, 5}, + /* value= */ 2, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void longArrayBinarySearchFloor_emptyArrayAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new LongArray(), /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void longArrayBinarySearchFloor_emptyArrayAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new LongArray(), /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void + longArrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + newLongArray(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void longArrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + newLongArray(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void longArrayBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { + assertThat( + binarySearchFloor( + newLongArray(1, 3, 5), + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + longArrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void + longArrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void + longArrayBinarySearchFloor_targetInArrayAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void + longArrayBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 2, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void + longArrayBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + newLongArray(1, 1, 1, 1, 1, 3, 5), + /* value= */ 2, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void listBinarySearchFloor_emptyListAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + new ArrayList<>(), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void listBinarySearchFloor_emptyListAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + new ArrayList<>(), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void listBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void listBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void listBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 3, 5), + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + listBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(-1); + } + + @Test + public void + listBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(0); + } + + @Test + public void + listBinarySearchFloor_targetInListAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 1, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void + listBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 2, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void + listBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchFloor( + Arrays.asList(1, 1, 1, 1, 1, 3, 5), + /* value= */ 2, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(4); + } + + @Test + public void arrayBinarySearchCeil_emptyArrayAndStayInBoundsFalse_returns0() { + assertThat( + binarySearchCeil( + new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void arrayBinarySearchCeil_emptyArrayAndStayInBoundsTrue_returnsMinus1() { + assertThat( + binarySearchCeil( + new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) + .isEqualTo(-1); + } + + @Test + public void arrayBinarySearchCeil_targetSmallerThanValues_returns0() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5}, + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void arrayBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsFalse_returnsLength() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5}, + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(3); + } + + @Test + public void arrayBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsTrue_returnsLastIndex() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5}, + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(2); + } + + @Test + public void + arrayBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsFalse_returnsLength() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 5, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(7); + } + + @Test + public void + arrayBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsTrue_returnsLastIndex() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 5, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(6); + } + + @Test + public void + arrayBinarySearchCeil_targetInArrayAndInclusiveTrue_returnsLastIndexWithValueEqualToTarget() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 5, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(6); + } + + @Test + public void + arrayBinarySearchCeil_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 4, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + arrayBinarySearchCeil_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchCeil( + new int[] {1, 3, 5, 5, 5, 5, 5}, + /* value= */ 4, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void listBinarySearchCeil_emptyListAndStayInBoundsFalse_returns0() { + assertThat( + binarySearchCeil( + new ArrayList<>(), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void listBinarySearchCeil_emptyListAndStayInBoundsTrue_returnsMinus1() { + assertThat( + binarySearchCeil( + new ArrayList<>(), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(-1); + } + + @Test + public void listBinarySearchCeil_targetSmallerThanValues_returns0() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5), + /* value= */ 0, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(0); + } + + @Test + public void listBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsFalse_returnsLength() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5), + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(3); + } + + @Test + public void listBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsTrue_returnsLastIndex() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5), + /* value= */ 6, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(2); + } + + @Test + public void + listBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsFalse_returnsLength() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 5, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(7); + } + + @Test + public void + listBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsTrue_returnsLastIndex() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 5, + /* inclusive= */ false, + /* stayInBounds= */ true)) + .isEqualTo(6); + } + + @Test + public void + listBinarySearchCeil_targetInListAndInclusiveTrue_returnsLastIndexWithValueEqualToTarget() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 5, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(6); + } + + @Test + public void + listBinarySearchCeil_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 4, + /* inclusive= */ false, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void + listBinarySearchCeil_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { + assertThat( + binarySearchCeil( + Arrays.asList(1, 3, 5, 5, 5, 5, 5), + /* value= */ 4, + /* inclusive= */ true, + /* stayInBounds= */ false)) + .isEqualTo(2); + } + + @Test + public void parseXsDuration_returnsParsedDurationInMillis() { + assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); + assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L); + } + + @Test + public void parseXsDateTime_returnsParsedDateTimeInMillis() throws Exception { + assertThat(parseXsDateTime("2014-06-19T23:07:42")).isEqualTo(1403219262000L); + assertThat(parseXsDateTime("2014-08-06T11:00:00Z")).isEqualTo(1407322800000L); + assertThat(parseXsDateTime("2014-08-06T11:00:00,000Z")).isEqualTo(1407322800000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55-08:00")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55-0800")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55.000-0800")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55.000-800")).isEqualTo(1411161535000L); + } + + @Test + public void toUnsignedLong_withPositiveValue_returnsValue() { + int x = 0x05D67F23; + + long result = Util.toUnsignedLong(x); + + assertThat(result).isEqualTo(0x05D67F23L); + } + + @Test + public void toUnsignedLong_withNegativeValue_returnsValue() { + int x = 0xF5D67F23; + + long result = Util.toUnsignedLong(x); + + assertThat(result).isEqualTo(0xF5D67F23L); + } + + @Test + public void toLong_withZeroValue_returnsZero() { + assertThat(Util.toLong(0, 0)).isEqualTo(0); + } + + @Test + public void toLong_withLongValue_returnsValue() { + assertThat(Util.toLong(1, -4)).isEqualTo(0x1FFFFFFFCL); + } + + @Test + public void toLong_withBigValue_returnsValue() { + assertThat(Util.toLong(0x7ABCDEF, 0x12345678)).isEqualTo(0x7ABCDEF_12345678L); + } + + @Test + public void toLong_withMaxValue_returnsValue() { + assertThat(Util.toLong(0x0FFFFFFF, 0xFFFFFFFF)).isEqualTo(0x0FFFFFFF_FFFFFFFFL); + } + + @Test + public void toLong_withBigNegativeValue_returnsValue() { + assertThat(Util.toLong(0xFEDCBA, 0x87654321)).isEqualTo(0xFEDCBA_87654321L); + } + + @Test + public void truncateAscii_shortInput_returnsInput() { + String input = "a short string"; + + assertThat(Util.truncateAscii(input, 100)).isSameInstanceAs(input); + } + + @Test + public void truncateAscii_longInput_truncated() { + String input = "a much longer string"; + + assertThat(Util.truncateAscii(input, 5).toString()).isEqualTo("a muc"); + } + + @Test + public void truncateAscii_preservesStylingSpans() { + SpannableString input = new SpannableString("a short string"); + input.setSpan(new UnderlineSpan(), 0, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + input.setSpan(new StrikethroughSpan(), 4, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + CharSequence result = Util.truncateAscii(input, 7); + + assertThat(result).isInstanceOf(SpannableString.class); + assertThat(result.toString()).isEqualTo("a short"); + // TODO(internal b/161804035): Use SpannedSubject when it's available in a dependency we can use + // from here. + Spanned spannedResult = (Spanned) result; + Object[] spans = spannedResult.getSpans(0, result.length(), Object.class); + assertThat(spans).hasLength(2); + assertThat(spans[0]).isInstanceOf(UnderlineSpan.class); + assertThat(spannedResult.getSpanStart(spans[0])).isEqualTo(0); + assertThat(spannedResult.getSpanEnd(spans[0])).isEqualTo(7); + assertThat(spannedResult.getSpanFlags(spans[0])).isEqualTo(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(spans[1]).isInstanceOf(StrikethroughSpan.class); + assertThat(spannedResult.getSpanStart(spans[1])).isEqualTo(4); + assertThat(spannedResult.getSpanEnd(spans[1])).isEqualTo(7); + assertThat(spannedResult.getSpanFlags(spans[1])).isEqualTo(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void toHexString_returnsHexString() { + byte[] bytes = createByteArray(0x12, 0xFC, 0x06); + + assertThat(Util.toHexString(bytes)).isEqualTo("12fc06"); + } + + @Test + public void getCodecsOfType_withNull_returnsNull() { + assertThat(getCodecsOfType(null, C.TRACK_TYPE_VIDEO)).isNull(); + } + + @Test + public void getCodecsOfType_withInvalidTrackType_returnsNull() { + assertThat(getCodecsOfType("avc1.64001e,vp9.63.1", C.TRACK_TYPE_AUDIO)).isNull(); + } + + @Test + public void getCodecsOfType_withAudioTrack_returnsCodec() { + assertThat(getCodecsOfType(" vp9.63.1, ec-3 ", C.TRACK_TYPE_AUDIO)).isEqualTo("ec-3"); + } + + @Test + public void getCodecsOfType_withVideoTrack_returnsCodec() { + assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) + .isEqualTo("avc1.61e,vp9.63.1"); + assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) + .isEqualTo("avc1.61e,vp9.63.1"); + } + + @Test + public void getCodecsOfType_withInvalidCodec_returnsNull() { + assertThat(getCodecsOfType("invalidCodec1, invalidCodec2 ", C.TRACK_TYPE_AUDIO)).isNull(); + } + + @Test + public void unescapeFileName_invalidFileName_returnsNull() { + assertThat(Util.unescapeFileName("%a")).isNull(); + assertThat(Util.unescapeFileName("%xyz")).isNull(); + } + + @Test + public void escapeUnescapeFileName_returnsEscapedString() { + assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName"); + assertEscapeUnescapeFileName("key:value", "key%3avalue"); + assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25"); + + Random random = new Random(0); + for (int i = 0; i < 1000; i++) { + String string = buildTestString(1000, random); + assertEscapeUnescapeFileName(string); + } + } + + @Test + public void crc32_returnsUpdatedCrc32() { + byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; + int start = 1; + int end = 4; + int initialValue = 0xFFFFFFFF; + + int result = Util.crc32(bytes, start, end, initialValue); + + assertThat(result).isEqualTo(0x67CE9747); + } + + @Test + public void crc8_returnsUpdatedCrc8() { + byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; + int start = 1; + int end = 4; + int initialValue = 0; + + int result = Util.crc8(bytes, start, end, initialValue); + + assertThat(result).isEqualTo(0x4); + } + + @Test + public void getBigEndianInt_fromBigEndian() { + byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C}; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); + + assertThat(Util.getBigEndianInt(byteBuffer, 0)).isEqualTo(0x1F2E3D4C); + } + + @Test + public void getBigEndianInt_fromLittleEndian() { + byte[] bytes = {(byte) 0xC2, (byte) 0xD3, (byte) 0xE4, (byte) 0xF5}; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + assertThat(Util.getBigEndianInt(byteBuffer, 0)).isEqualTo(0xC2D3E4F5); + } + + @Test + public void getBigEndianInt_unaligned() { + byte[] bytes = {9, 8, 7, 6, 5}; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + assertThat(Util.getBigEndianInt(byteBuffer, 1)).isEqualTo(0x08070605); + } + + @Test + public void inflate_withDeflatedData_success() { + byte[] testData = buildTestData(/*arbitrary test data size*/ 256 * 1024); + byte[] compressedData = new byte[testData.length * 2]; + Deflater compresser = new Deflater(9); + compresser.setInput(testData); + compresser.finish(); + int compressedDataLength = compresser.deflate(compressedData); + compresser.end(); + + ParsableByteArray input = new ParsableByteArray(compressedData, compressedDataLength); + ParsableByteArray output = new ParsableByteArray(); + assertThat(Util.inflate(input, output, /* inflater= */ null)).isTrue(); + assertThat(output.limit()).isEqualTo(testData.length); + assertThat(Arrays.copyOf(output.getData(), output.limit())).isEqualTo(testData); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void normalizeLanguageCode_keepsUndefinedTagsUnchanged() { + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); + assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); + assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void normalizeLanguageCode_normalizesCodeToTwoLetterISOAndLowerCase_keepingAllSubtags() { + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + // Regional subtag (South America) + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + // Script subtag (Simplified Taiwanese) + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); + // Non-spec compliant subtags. + assertThat(Util.normalizeLanguageCode("sv-illegalSubtag")).isEqualTo("sv-illegalsubtag"); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void normalizeLanguageCode_iso6392BibliographicalAndTextualCodes_areNormalizedToSameTag() { + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + assertThat(Util.normalizeLanguageCode("alb")).isEqualTo(Util.normalizeLanguageCode("sqi")); + assertThat(Util.normalizeLanguageCode("arm")).isEqualTo(Util.normalizeLanguageCode("hye")); + assertThat(Util.normalizeLanguageCode("baq")).isEqualTo(Util.normalizeLanguageCode("eus")); + assertThat(Util.normalizeLanguageCode("bur")).isEqualTo(Util.normalizeLanguageCode("mya")); + assertThat(Util.normalizeLanguageCode("chi")).isEqualTo(Util.normalizeLanguageCode("zho")); + assertThat(Util.normalizeLanguageCode("cze")).isEqualTo(Util.normalizeLanguageCode("ces")); + assertThat(Util.normalizeLanguageCode("dut")).isEqualTo(Util.normalizeLanguageCode("nld")); + assertThat(Util.normalizeLanguageCode("fre")).isEqualTo(Util.normalizeLanguageCode("fra")); + assertThat(Util.normalizeLanguageCode("geo")).isEqualTo(Util.normalizeLanguageCode("kat")); + assertThat(Util.normalizeLanguageCode("ger")).isEqualTo(Util.normalizeLanguageCode("deu")); + assertThat(Util.normalizeLanguageCode("gre")).isEqualTo(Util.normalizeLanguageCode("ell")); + assertThat(Util.normalizeLanguageCode("ice")).isEqualTo(Util.normalizeLanguageCode("isl")); + assertThat(Util.normalizeLanguageCode("mac")).isEqualTo(Util.normalizeLanguageCode("mkd")); + assertThat(Util.normalizeLanguageCode("mao")).isEqualTo(Util.normalizeLanguageCode("mri")); + assertThat(Util.normalizeLanguageCode("may")).isEqualTo(Util.normalizeLanguageCode("msa")); + assertThat(Util.normalizeLanguageCode("per")).isEqualTo(Util.normalizeLanguageCode("fas")); + assertThat(Util.normalizeLanguageCode("rum")).isEqualTo(Util.normalizeLanguageCode("ron")); + assertThat(Util.normalizeLanguageCode("slo")).isEqualTo(Util.normalizeLanguageCode("slk")); + assertThat(Util.normalizeLanguageCode("scc")).isEqualTo(Util.normalizeLanguageCode("srp")); + assertThat(Util.normalizeLanguageCode("tib")).isEqualTo(Util.normalizeLanguageCode("bod")); + assertThat(Util.normalizeLanguageCode("wel")).isEqualTo(Util.normalizeLanguageCode("cym")); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void + normalizeLanguageCode_deprecatedLanguageTagsAndModernReplacement_areNormalizedToSameTag() { + // See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, "ISO 639:1988" + assertThat(Util.normalizeLanguageCode("in")).isEqualTo(Util.normalizeLanguageCode("id")); + assertThat(Util.normalizeLanguageCode("in")).isEqualTo(Util.normalizeLanguageCode("ind")); + assertThat(Util.normalizeLanguageCode("iw")).isEqualTo(Util.normalizeLanguageCode("he")); + assertThat(Util.normalizeLanguageCode("iw")).isEqualTo(Util.normalizeLanguageCode("heb")); + assertThat(Util.normalizeLanguageCode("ji")).isEqualTo(Util.normalizeLanguageCode("yi")); + assertThat(Util.normalizeLanguageCode("ji")).isEqualTo(Util.normalizeLanguageCode("yid")); + + // Legacy tags + assertThat(Util.normalizeLanguageCode("i-lux")).isEqualTo(Util.normalizeLanguageCode("lb")); + assertThat(Util.normalizeLanguageCode("i-lux")).isEqualTo(Util.normalizeLanguageCode("ltz")); + assertThat(Util.normalizeLanguageCode("i-hak")).isEqualTo(Util.normalizeLanguageCode("hak")); + assertThat(Util.normalizeLanguageCode("i-hak")).isEqualTo(Util.normalizeLanguageCode("zh-hak")); + assertThat(Util.normalizeLanguageCode("i-navajo")).isEqualTo(Util.normalizeLanguageCode("nv")); + assertThat(Util.normalizeLanguageCode("i-navajo")).isEqualTo(Util.normalizeLanguageCode("nav")); + assertThat(Util.normalizeLanguageCode("no-bok")).isEqualTo(Util.normalizeLanguageCode("nb")); + assertThat(Util.normalizeLanguageCode("no-bok")).isEqualTo(Util.normalizeLanguageCode("nob")); + assertThat(Util.normalizeLanguageCode("no-nyn")).isEqualTo(Util.normalizeLanguageCode("nn")); + assertThat(Util.normalizeLanguageCode("no-nyn")).isEqualTo(Util.normalizeLanguageCode("nno")); + assertThat(Util.normalizeLanguageCode("zh-guoyu")).isEqualTo(Util.normalizeLanguageCode("cmn")); + assertThat(Util.normalizeLanguageCode("zh-guoyu")) + .isEqualTo(Util.normalizeLanguageCode("zh-cmn")); + assertThat(Util.normalizeLanguageCode("zh-hakka")).isEqualTo(Util.normalizeLanguageCode("hak")); + assertThat(Util.normalizeLanguageCode("zh-hakka")) + .isEqualTo(Util.normalizeLanguageCode("zh-hak")); + assertThat(Util.normalizeLanguageCode("zh-min-nan")) + .isEqualTo(Util.normalizeLanguageCode("nan")); + assertThat(Util.normalizeLanguageCode("zh-min-nan")) + .isEqualTo(Util.normalizeLanguageCode("zh-nan")); + assertThat(Util.normalizeLanguageCode("zh-xiang")).isEqualTo(Util.normalizeLanguageCode("hsn")); + assertThat(Util.normalizeLanguageCode("zh-xiang")) + .isEqualTo(Util.normalizeLanguageCode("zh-hsn")); + } + + // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved + @Test + @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) + public void normalizeLanguageCode_macrolanguageTags_areFullyMaintained() { + // See https://en.wikipedia.org/wiki/ISO_639_macrolanguage + assertThat(Util.normalizeLanguageCode("zh-cmn")).isEqualTo("zh-cmn"); + assertThat(Util.normalizeLanguageCode("zho-cmn")).isEqualTo("zh-cmn"); + assertThat(Util.normalizeLanguageCode("ar-ayl")).isEqualTo("ar-ayl"); + assertThat(Util.normalizeLanguageCode("ara-ayl")).isEqualTo("ar-ayl"); + + // Special case of short codes that are actually part of a macrolanguage. + assertThat(Util.normalizeLanguageCode("nb")).isEqualTo("no-nob"); + assertThat(Util.normalizeLanguageCode("nn")).isEqualTo("no-nno"); + assertThat(Util.normalizeLanguageCode("nob")).isEqualTo("no-nob"); + assertThat(Util.normalizeLanguageCode("nno")).isEqualTo("no-nno"); + assertThat(Util.normalizeLanguageCode("tw")).isEqualTo("ak-twi"); + assertThat(Util.normalizeLanguageCode("twi")).isEqualTo("ak-twi"); + assertThat(Util.normalizeLanguageCode("bs")).isEqualTo("hbs-bos"); + assertThat(Util.normalizeLanguageCode("bos")).isEqualTo("hbs-bos"); + assertThat(Util.normalizeLanguageCode("hr")).isEqualTo("hbs-hrv"); + assertThat(Util.normalizeLanguageCode("hrv")).isEqualTo("hbs-hrv"); + assertThat(Util.normalizeLanguageCode("sr")).isEqualTo("hbs-srp"); + assertThat(Util.normalizeLanguageCode("srp")).isEqualTo("hbs-srp"); + assertThat(Util.normalizeLanguageCode("id")).isEqualTo("ms-ind"); + assertThat(Util.normalizeLanguageCode("ind")).isEqualTo("ms-ind"); + assertThat(Util.normalizeLanguageCode("cmn")).isEqualTo("zh-cmn"); + assertThat(Util.normalizeLanguageCode("hak")).isEqualTo("zh-hak"); + assertThat(Util.normalizeLanguageCode("nan")).isEqualTo("zh-nan"); + assertThat(Util.normalizeLanguageCode("hsn")).isEqualTo("zh-hsn"); + } + + @Test + public void tableExists_withExistingTable() { + SQLiteDatabase database = getInMemorySQLiteOpenHelper().getWritableDatabase(); + database.execSQL("CREATE TABLE TestTable (ID INTEGER NOT NULL)"); + + assertThat(Util.tableExists(database, "TestTable")).isTrue(); + } + + @Test + public void tableExists_withNonExistingTable() { + SQLiteDatabase database = getInMemorySQLiteOpenHelper().getReadableDatabase(); + + assertThat(Util.tableExists(database, "table")).isFalse(); + } + + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { + assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); + assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); + } + + private static void assertEscapeUnescapeFileName(String fileName) { + String escapedFileName = Util.escapeFileName(fileName); + assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); + } + + private static LongArray newLongArray(long... values) { + LongArray longArray = new LongArray(); + for (long value : values) { + longArray.add(value); + } + return longArray; + } + + /** Returns a {@link SQLiteOpenHelper} that provides an in-memory database. */ + private static SQLiteOpenHelper getInMemorySQLiteOpenHelper() { + return new SQLiteOpenHelper( + /* context= */ null, /* name= */ null, /* factory= */ null, /* version= */ 1) { + @Override + public void onCreate(SQLiteDatabase db) {} + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {} + }; + } + + /** Generates an array of random bytes with the specified length. */ + private static byte[] buildTestData(int length, int seed) { + byte[] source = new byte[length]; + new Random(seed).nextBytes(source); + return source; + } + + /** Equivalent to {@code buildTestData(length, length)}. */ + // TODO(internal b/161804035): Use TestUtils when it's available in a dependency we can use here. + private static byte[] buildTestData(int length) { + return buildTestData(length, length); + } + + /** Generates a random string with the specified maximum length. */ + // TODO(internal b/161804035): Use TestUtils when it's available in a dependency we can use here. + private static String buildTestString(int maximumLength, Random random) { + int length = random.nextInt(maximumLength); + StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append((char) random.nextInt()); + } + return builder.toString(); + } + + /** Converts an array of integers in the range [0, 255] into an equivalent byte array. */ + // TODO(internal b/161804035): Use TestUtils when it's available in a dependency we can use here. + private static byte[] createByteArray(int... bytes) { + byte[] byteArray = new byte[bytes.length]; + for (int i = 0; i < byteArray.length; i++) { + Assertions.checkState(0x00 <= bytes[i] && bytes[i] <= 0xFF); + byteArray[i] = (byte) bytes[i]; + } + return byteArray; + } +} 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 a7a46b163db..58fd42d3d01 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,12 @@ 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(".*\\.ism(?:l)?(?:/(?: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 +1605,20 @@ public static int inferContentType(String fileName) { return C.TYPE_DASH; } else if (fileName.endsWith(".m3u8")) { return C.TYPE_HLS; - } else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) { + } + Matcher ismMatcher = ISM_URL_PATTERN.matcher(fileName); + if (ismMatcher.matches()) { + @Nullable String extensions = ismMatcher.group(1); + 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; - } else { - return C.TYPE_OTHER; } + return C.TYPE_OTHER; } /** 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 07334872e19..e0421a5d657 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 @@ -77,13 +77,49 @@ public void testSubtrackWithOverflowDefault() { } @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); + } + @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); } From 1c7c6fb90d8d18bd47001c196873df8a61225f82 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 11 Aug 2020 16:27:02 +0100 Subject: [PATCH 16/20] Increase flexibility of ISM URL handling PiperOrigin-RevId: 326025335 --- RELEASENOTES.md | 1 + .../android/exoplayer2/util/UtilTest.java | 29 ++++++++++++++- .../google/android/exoplayer2/util/Util.java | 26 ++++++++++++-- .../source/smoothstreaming/SsMediaSource.java | 4 +-- .../smoothstreaming/manifest/SsUtil.java | 35 ------------------- .../smoothstreaming/offline/SsDownloader.java | 4 +-- 6 files changed, 56 insertions(+), 43 deletions(-) delete mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 05300b47b94..d7e09e4864e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,7 @@ boxes. * FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)). +* Better infer content type for `.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)). diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 0bf5028282b..162dcbae9d0 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -26,6 +26,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; import android.text.SpannableString; import android.text.Spanned; import android.text.style.StrikethroughSpan; @@ -127,13 +128,39 @@ public void inferContentType_handlesSmoothStreamingIsmUris() { 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/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 58fd42d3d01..b1bddc13b87 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 @@ -136,8 +136,7 @@ public final class Util { 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(".*\\.ism(?:l)?(?:/(?:manifest(?:\\((.+)\\))?)?)?"); + 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"; @@ -1608,7 +1607,7 @@ public static int inferContentType(String fileName) { } Matcher ismMatcher = ISM_URL_PATTERN.matcher(fileName); if (ismMatcher.matches()) { - @Nullable String extensions = ismMatcher.group(1); + @Nullable String extensions = ismMatcher.group(2); if (extensions != null) { if (extensions.contains(ISM_DASH_FORMAT_EXTENSION)) { return C.TYPE_DASH; @@ -1621,6 +1620,27 @@ public static int inferContentType(String fileName) { 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; + } + /** * Returns the specified millisecond time formatted as a string. * 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 89dd8039ef7..02df8f28ff9 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.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.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 @@ private SsMediaSource( @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 b54b2abc74e..00000000000 --- 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 1331fe46178..88089541356 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.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 From 629fe637f74a778487e3fe51bddf7da076a06c19 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 19 Aug 2020 18:35:35 +0100 Subject: [PATCH 17/20] Bump version to 2.11.8 --- RELEASENOTES.md | 2 +- constants.gradle | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d7e09e4864e..f4c300146cc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,7 +16,7 @@ boxes. * FLV: Ignore `SCRIPTDATA` segments with invalid name types, rather than failing playback ([#7675](https://github.com/google/ExoPlayer/issues/7675)). -* Better infer content type for `.ism` and `.isml` streaming URLs. +* 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)). diff --git a/constants.gradle b/constants.gradle index 9d38d823697..0fceb92692c 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/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 35b6199cd36..bc1f0032c35 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} From 4faee07d577c3f0e50e7eb08895fe9d3f6ce0a97 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 19 Aug 2020 18:38:36 +0100 Subject: [PATCH 18/20] Tweak release note --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f4c300146cc..c5a6c7770b3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,7 +11,7 @@ * 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)). + ([#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 From bcc4f797b22d2a45e50be15b7e1f1d533cb798d7 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 10 Aug 2020 23:26:50 +0100 Subject: [PATCH 19/20] Demo app: Fix DRM support check for ClearKey Issue: Issue: #7735 PiperOrigin-RevId: 325900705 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/demo/PlayerActivity.java | 4 ++-- .../google/android/exoplayer2/drm/FrameworkMediaDrm.java | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c5a6c7770b3..16f276c6aed 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,6 +23,8 @@ * 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) ### 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 0454472abf5..d5da3792634 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 @@ 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.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 @@ private MediaSource createLeafMediaSource(UriSample parameters) { 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/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 56d1aeea4bb..f4f84c92dc8 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 Date: Thu, 20 Aug 2020 00:00:38 +0100 Subject: [PATCH 20/20] Fix tests --- .../android/exoplayer2/util/UtilTest.java | 1149 ----------------- .../android/exoplayer2/util/UtilTest.java | 29 +- 2 files changed, 28 insertions(+), 1150 deletions(-) delete mode 100644 library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java deleted file mode 100644 index 162dcbae9d0..00000000000 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ /dev/null @@ -1,1149 +0,0 @@ -/* - * 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 com.google.android.exoplayer2.util; - -import static com.google.android.exoplayer2.util.Util.binarySearchCeil; -import static com.google.android.exoplayer2.util.Util.binarySearchFloor; -import static com.google.android.exoplayer2.util.Util.escapeFileName; -import static com.google.android.exoplayer2.util.Util.getCodecsOfType; -import static com.google.android.exoplayer2.util.Util.parseXsDateTime; -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.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.style.StrikethroughSpan; -import android.text.style.UnderlineSpan; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Random; -import java.util.zip.Deflater; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; - -/** Unit tests for {@link Util}. */ -@RunWith(AndroidJUnit4.class) -public class UtilTest { - - @Test - public void addWithOverflowDefault_withoutOverFlow_returnsSum() { - long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0); - assertThat(res).isEqualTo(15); - - res = Util.addWithOverflowDefault(Long.MAX_VALUE - 1, 1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(Long.MAX_VALUE); - - res = Util.addWithOverflowDefault(Long.MIN_VALUE + 1, -1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(Long.MIN_VALUE); - } - - @Test - public void addWithOverflowDefault_withOverFlow_returnsOverflowDefault() { - long res = Util.addWithOverflowDefault(Long.MAX_VALUE, 1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(12345); - - res = Util.addWithOverflowDefault(Long.MIN_VALUE, -1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(12345); - } - - @Test - public void subtrackWithOverflowDefault_withoutUnderflow_returnsSubtract() { - long res = Util.subtractWithOverflowDefault(5, 10, /* overflowResult= */ 0); - assertThat(res).isEqualTo(-5); - - res = Util.subtractWithOverflowDefault(Long.MIN_VALUE + 1, 1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(Long.MIN_VALUE); - - res = Util.subtractWithOverflowDefault(Long.MAX_VALUE - 1, -1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(Long.MAX_VALUE); - } - - @Test - public void subtrackWithOverflowDefault_withUnderflow_returnsOverflowDefault() { - long res = Util.subtractWithOverflowDefault(Long.MIN_VALUE, 1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(12345); - - res = Util.subtractWithOverflowDefault(Long.MAX_VALUE, -1, /* overflowResult= */ 12345); - assertThat(res).isEqualTo(12345); - } - - @Test - 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); - } - - @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 - public void arrayBinarySearchFloor_emptyArrayAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void arrayBinarySearchFloor_emptyArrayAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void arrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - new int[] {1, 3, 5}, - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void arrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - new int[] {1, 3, 5}, - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void arrayBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { - assertThat( - binarySearchFloor( - new int[] {1, 3, 5}, - /* value= */ 6, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(2); - } - - @Test - public void - arrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - new int[] {1, 1, 1, 1, 1, 3, 5}, - /* value= */ 1, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void - arrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - new int[] {1, 1, 1, 1, 1, 3, 5}, - /* value= */ 1, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void - arrayBinarySearchFloor_targetInArrayAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { - assertThat( - binarySearchFloor( - new int[] {1, 1, 1, 1, 1, 3, 5}, - /* value= */ 1, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(0); - } - - @Test - public void - arrayBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchFloor( - new int[] {1, 1, 1, 1, 1, 3, 5}, - /* value= */ 2, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(4); - } - - @Test - public void - arrayBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchFloor( - new int[] {1, 1, 1, 1, 1, 3, 5}, - /* value= */ 2, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(4); - } - - @Test - public void longArrayBinarySearchFloor_emptyArrayAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - new LongArray(), /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void longArrayBinarySearchFloor_emptyArrayAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - new LongArray(), /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void - longArrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - newLongArray(1, 3, 5), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void longArrayBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - newLongArray(1, 3, 5), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void longArrayBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { - assertThat( - binarySearchFloor( - newLongArray(1, 3, 5), - /* value= */ 6, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(2); - } - - @Test - public void - longArrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - newLongArray(1, 1, 1, 1, 1, 3, 5), - /* value= */ 1, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void - longArrayBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - newLongArray(1, 1, 1, 1, 1, 3, 5), - /* value= */ 1, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void - longArrayBinarySearchFloor_targetInArrayAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { - assertThat( - binarySearchFloor( - newLongArray(1, 1, 1, 1, 1, 3, 5), - /* value= */ 1, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(0); - } - - @Test - public void - longArrayBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchFloor( - newLongArray(1, 1, 1, 1, 1, 3, 5), - /* value= */ 2, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(4); - } - - @Test - public void - longArrayBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchFloor( - newLongArray(1, 1, 1, 1, 1, 3, 5), - /* value= */ 2, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(4); - } - - @Test - public void listBinarySearchFloor_emptyListAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - new ArrayList<>(), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void listBinarySearchFloor_emptyListAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - new ArrayList<>(), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void listBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - Arrays.asList(1, 3, 5), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void listBinarySearchFloor_targetSmallerThanValuesAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - Arrays.asList(1, 3, 5), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void listBinarySearchFloor_targetBiggerThanValues_returnsLastIndex() { - assertThat( - binarySearchFloor( - Arrays.asList(1, 3, 5), - /* value= */ 6, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(2); - } - - @Test - public void - listBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsFalse_returnsMinus1() { - assertThat( - binarySearchFloor( - Arrays.asList(1, 1, 1, 1, 1, 3, 5), - /* value= */ 1, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(-1); - } - - @Test - public void - listBinarySearchFloor_targetEqualToFirstValueAndInclusiveFalseAndStayInBoundsTrue_returns0() { - assertThat( - binarySearchFloor( - Arrays.asList(1, 1, 1, 1, 1, 3, 5), - /* value= */ 1, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(0); - } - - @Test - public void - listBinarySearchFloor_targetInListAndInclusiveTrue_returnsFirstIndexWithValueEqualToTarget() { - assertThat( - binarySearchFloor( - Arrays.asList(1, 1, 1, 1, 1, 3, 5), - /* value= */ 1, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(0); - } - - @Test - public void - listBinarySearchFloor_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchFloor( - Arrays.asList(1, 1, 1, 1, 1, 3, 5), - /* value= */ 2, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(4); - } - - @Test - public void - listBinarySearchFloor_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchFloor( - Arrays.asList(1, 1, 1, 1, 1, 3, 5), - /* value= */ 2, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(4); - } - - @Test - public void arrayBinarySearchCeil_emptyArrayAndStayInBoundsFalse_returns0() { - assertThat( - binarySearchCeil( - new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ false)) - .isEqualTo(0); - } - - @Test - public void arrayBinarySearchCeil_emptyArrayAndStayInBoundsTrue_returnsMinus1() { - assertThat( - binarySearchCeil( - new int[0], /* value= */ 0, /* inclusive= */ false, /* stayInBounds= */ true)) - .isEqualTo(-1); - } - - @Test - public void arrayBinarySearchCeil_targetSmallerThanValues_returns0() { - assertThat( - binarySearchCeil( - new int[] {1, 3, 5}, - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(0); - } - - @Test - public void arrayBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsFalse_returnsLength() { - assertThat( - binarySearchCeil( - new int[] {1, 3, 5}, - /* value= */ 6, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(3); - } - - @Test - public void arrayBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsTrue_returnsLastIndex() { - assertThat( - binarySearchCeil( - new int[] {1, 3, 5}, - /* value= */ 6, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(2); - } - - @Test - public void - arrayBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsFalse_returnsLength() { - assertThat( - binarySearchCeil( - new int[] {1, 3, 5, 5, 5, 5, 5}, - /* value= */ 5, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(7); - } - - @Test - public void - arrayBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsTrue_returnsLastIndex() { - assertThat( - binarySearchCeil( - new int[] {1, 3, 5, 5, 5, 5, 5}, - /* value= */ 5, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(6); - } - - @Test - public void - arrayBinarySearchCeil_targetInArrayAndInclusiveTrue_returnsLastIndexWithValueEqualToTarget() { - assertThat( - binarySearchCeil( - new int[] {1, 3, 5, 5, 5, 5, 5}, - /* value= */ 5, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(6); - } - - @Test - public void - arrayBinarySearchCeil_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchCeil( - new int[] {1, 3, 5, 5, 5, 5, 5}, - /* value= */ 4, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(2); - } - - @Test - public void - arrayBinarySearchCeil_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchCeil( - new int[] {1, 3, 5, 5, 5, 5, 5}, - /* value= */ 4, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(2); - } - - @Test - public void listBinarySearchCeil_emptyListAndStayInBoundsFalse_returns0() { - assertThat( - binarySearchCeil( - new ArrayList<>(), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(0); - } - - @Test - public void listBinarySearchCeil_emptyListAndStayInBoundsTrue_returnsMinus1() { - assertThat( - binarySearchCeil( - new ArrayList<>(), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(-1); - } - - @Test - public void listBinarySearchCeil_targetSmallerThanValues_returns0() { - assertThat( - binarySearchCeil( - Arrays.asList(1, 3, 5), - /* value= */ 0, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(0); - } - - @Test - public void listBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsFalse_returnsLength() { - assertThat( - binarySearchCeil( - Arrays.asList(1, 3, 5), - /* value= */ 6, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(3); - } - - @Test - public void listBinarySearchCeil_targetBiggerThanValuesAndStayInBoundsTrue_returnsLastIndex() { - assertThat( - binarySearchCeil( - Arrays.asList(1, 3, 5), - /* value= */ 6, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(2); - } - - @Test - public void - listBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsFalse_returnsLength() { - assertThat( - binarySearchCeil( - Arrays.asList(1, 3, 5, 5, 5, 5, 5), - /* value= */ 5, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(7); - } - - @Test - public void - listBinarySearchCeil_targetEqualToLastValueAndInclusiveFalseAndStayInBoundsTrue_returnsLastIndex() { - assertThat( - binarySearchCeil( - Arrays.asList(1, 3, 5, 5, 5, 5, 5), - /* value= */ 5, - /* inclusive= */ false, - /* stayInBounds= */ true)) - .isEqualTo(6); - } - - @Test - public void - listBinarySearchCeil_targetInListAndInclusiveTrue_returnsLastIndexWithValueEqualToTarget() { - assertThat( - binarySearchCeil( - Arrays.asList(1, 3, 5, 5, 5, 5, 5), - /* value= */ 5, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(6); - } - - @Test - public void - listBinarySearchCeil_targetBetweenValuesAndInclusiveFalse_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchCeil( - Arrays.asList(1, 3, 5, 5, 5, 5, 5), - /* value= */ 4, - /* inclusive= */ false, - /* stayInBounds= */ false)) - .isEqualTo(2); - } - - @Test - public void - listBinarySearchCeil_targetBetweenValuesAndInclusiveTrue_returnsIndexWhereTargetShouldBeInserted() { - assertThat( - binarySearchCeil( - Arrays.asList(1, 3, 5, 5, 5, 5, 5), - /* value= */ 4, - /* inclusive= */ true, - /* stayInBounds= */ false)) - .isEqualTo(2); - } - - @Test - public void parseXsDuration_returnsParsedDurationInMillis() { - assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); - assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L); - } - - @Test - public void parseXsDateTime_returnsParsedDateTimeInMillis() throws Exception { - assertThat(parseXsDateTime("2014-06-19T23:07:42")).isEqualTo(1403219262000L); - assertThat(parseXsDateTime("2014-08-06T11:00:00Z")).isEqualTo(1407322800000L); - assertThat(parseXsDateTime("2014-08-06T11:00:00,000Z")).isEqualTo(1407322800000L); - assertThat(parseXsDateTime("2014-09-19T13:18:55-08:00")).isEqualTo(1411161535000L); - assertThat(parseXsDateTime("2014-09-19T13:18:55-0800")).isEqualTo(1411161535000L); - assertThat(parseXsDateTime("2014-09-19T13:18:55.000-0800")).isEqualTo(1411161535000L); - assertThat(parseXsDateTime("2014-09-19T13:18:55.000-800")).isEqualTo(1411161535000L); - } - - @Test - public void toUnsignedLong_withPositiveValue_returnsValue() { - int x = 0x05D67F23; - - long result = Util.toUnsignedLong(x); - - assertThat(result).isEqualTo(0x05D67F23L); - } - - @Test - public void toUnsignedLong_withNegativeValue_returnsValue() { - int x = 0xF5D67F23; - - long result = Util.toUnsignedLong(x); - - assertThat(result).isEqualTo(0xF5D67F23L); - } - - @Test - public void toLong_withZeroValue_returnsZero() { - assertThat(Util.toLong(0, 0)).isEqualTo(0); - } - - @Test - public void toLong_withLongValue_returnsValue() { - assertThat(Util.toLong(1, -4)).isEqualTo(0x1FFFFFFFCL); - } - - @Test - public void toLong_withBigValue_returnsValue() { - assertThat(Util.toLong(0x7ABCDEF, 0x12345678)).isEqualTo(0x7ABCDEF_12345678L); - } - - @Test - public void toLong_withMaxValue_returnsValue() { - assertThat(Util.toLong(0x0FFFFFFF, 0xFFFFFFFF)).isEqualTo(0x0FFFFFFF_FFFFFFFFL); - } - - @Test - public void toLong_withBigNegativeValue_returnsValue() { - assertThat(Util.toLong(0xFEDCBA, 0x87654321)).isEqualTo(0xFEDCBA_87654321L); - } - - @Test - public void truncateAscii_shortInput_returnsInput() { - String input = "a short string"; - - assertThat(Util.truncateAscii(input, 100)).isSameInstanceAs(input); - } - - @Test - public void truncateAscii_longInput_truncated() { - String input = "a much longer string"; - - assertThat(Util.truncateAscii(input, 5).toString()).isEqualTo("a muc"); - } - - @Test - public void truncateAscii_preservesStylingSpans() { - SpannableString input = new SpannableString("a short string"); - input.setSpan(new UnderlineSpan(), 0, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - input.setSpan(new StrikethroughSpan(), 4, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - - CharSequence result = Util.truncateAscii(input, 7); - - assertThat(result).isInstanceOf(SpannableString.class); - assertThat(result.toString()).isEqualTo("a short"); - // TODO(internal b/161804035): Use SpannedSubject when it's available in a dependency we can use - // from here. - Spanned spannedResult = (Spanned) result; - Object[] spans = spannedResult.getSpans(0, result.length(), Object.class); - assertThat(spans).hasLength(2); - assertThat(spans[0]).isInstanceOf(UnderlineSpan.class); - assertThat(spannedResult.getSpanStart(spans[0])).isEqualTo(0); - assertThat(spannedResult.getSpanEnd(spans[0])).isEqualTo(7); - assertThat(spannedResult.getSpanFlags(spans[0])).isEqualTo(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - assertThat(spans[1]).isInstanceOf(StrikethroughSpan.class); - assertThat(spannedResult.getSpanStart(spans[1])).isEqualTo(4); - assertThat(spannedResult.getSpanEnd(spans[1])).isEqualTo(7); - assertThat(spannedResult.getSpanFlags(spans[1])).isEqualTo(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - } - - @Test - public void toHexString_returnsHexString() { - byte[] bytes = createByteArray(0x12, 0xFC, 0x06); - - assertThat(Util.toHexString(bytes)).isEqualTo("12fc06"); - } - - @Test - public void getCodecsOfType_withNull_returnsNull() { - assertThat(getCodecsOfType(null, C.TRACK_TYPE_VIDEO)).isNull(); - } - - @Test - public void getCodecsOfType_withInvalidTrackType_returnsNull() { - assertThat(getCodecsOfType("avc1.64001e,vp9.63.1", C.TRACK_TYPE_AUDIO)).isNull(); - } - - @Test - public void getCodecsOfType_withAudioTrack_returnsCodec() { - assertThat(getCodecsOfType(" vp9.63.1, ec-3 ", C.TRACK_TYPE_AUDIO)).isEqualTo("ec-3"); - } - - @Test - public void getCodecsOfType_withVideoTrack_returnsCodec() { - assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) - .isEqualTo("avc1.61e,vp9.63.1"); - assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO)) - .isEqualTo("avc1.61e,vp9.63.1"); - } - - @Test - public void getCodecsOfType_withInvalidCodec_returnsNull() { - assertThat(getCodecsOfType("invalidCodec1, invalidCodec2 ", C.TRACK_TYPE_AUDIO)).isNull(); - } - - @Test - public void unescapeFileName_invalidFileName_returnsNull() { - assertThat(Util.unescapeFileName("%a")).isNull(); - assertThat(Util.unescapeFileName("%xyz")).isNull(); - } - - @Test - public void escapeUnescapeFileName_returnsEscapedString() { - assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName"); - assertEscapeUnescapeFileName("key:value", "key%3avalue"); - assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25"); - - Random random = new Random(0); - for (int i = 0; i < 1000; i++) { - String string = buildTestString(1000, random); - assertEscapeUnescapeFileName(string); - } - } - - @Test - public void crc32_returnsUpdatedCrc32() { - byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; - int start = 1; - int end = 4; - int initialValue = 0xFFFFFFFF; - - int result = Util.crc32(bytes, start, end, initialValue); - - assertThat(result).isEqualTo(0x67CE9747); - } - - @Test - public void crc8_returnsUpdatedCrc8() { - byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F}; - int start = 1; - int end = 4; - int initialValue = 0; - - int result = Util.crc8(bytes, start, end, initialValue); - - assertThat(result).isEqualTo(0x4); - } - - @Test - public void getBigEndianInt_fromBigEndian() { - byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C}; - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); - - assertThat(Util.getBigEndianInt(byteBuffer, 0)).isEqualTo(0x1F2E3D4C); - } - - @Test - public void getBigEndianInt_fromLittleEndian() { - byte[] bytes = {(byte) 0xC2, (byte) 0xD3, (byte) 0xE4, (byte) 0xF5}; - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); - - assertThat(Util.getBigEndianInt(byteBuffer, 0)).isEqualTo(0xC2D3E4F5); - } - - @Test - public void getBigEndianInt_unaligned() { - byte[] bytes = {9, 8, 7, 6, 5}; - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); - - assertThat(Util.getBigEndianInt(byteBuffer, 1)).isEqualTo(0x08070605); - } - - @Test - public void inflate_withDeflatedData_success() { - byte[] testData = buildTestData(/*arbitrary test data size*/ 256 * 1024); - byte[] compressedData = new byte[testData.length * 2]; - Deflater compresser = new Deflater(9); - compresser.setInput(testData); - compresser.finish(); - int compressedDataLength = compresser.deflate(compressedData); - compresser.end(); - - ParsableByteArray input = new ParsableByteArray(compressedData, compressedDataLength); - ParsableByteArray output = new ParsableByteArray(); - assertThat(Util.inflate(input, output, /* inflater= */ null)).isTrue(); - assertThat(output.limit()).isEqualTo(testData.length); - assertThat(Arrays.copyOf(output.getData(), output.limit())).isEqualTo(testData); - } - - // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved - @Test - @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) - public void normalizeLanguageCode_keepsUndefinedTagsUnchanged() { - assertThat(Util.normalizeLanguageCode(null)).isNull(); - assertThat(Util.normalizeLanguageCode("")).isEmpty(); - assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); - assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); - } - - // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved - @Test - @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) - public void normalizeLanguageCode_normalizesCodeToTwoLetterISOAndLowerCase_keepingAllSubtags() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); - assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); - assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); - // Regional subtag (South America) - assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); - // Script subtag (Simplified Taiwanese) - assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); - assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); - // Non-spec compliant subtags. - assertThat(Util.normalizeLanguageCode("sv-illegalSubtag")).isEqualTo("sv-illegalsubtag"); - } - - // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved - @Test - @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) - public void normalizeLanguageCode_iso6392BibliographicalAndTextualCodes_areNormalizedToSameTag() { - // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. - assertThat(Util.normalizeLanguageCode("alb")).isEqualTo(Util.normalizeLanguageCode("sqi")); - assertThat(Util.normalizeLanguageCode("arm")).isEqualTo(Util.normalizeLanguageCode("hye")); - assertThat(Util.normalizeLanguageCode("baq")).isEqualTo(Util.normalizeLanguageCode("eus")); - assertThat(Util.normalizeLanguageCode("bur")).isEqualTo(Util.normalizeLanguageCode("mya")); - assertThat(Util.normalizeLanguageCode("chi")).isEqualTo(Util.normalizeLanguageCode("zho")); - assertThat(Util.normalizeLanguageCode("cze")).isEqualTo(Util.normalizeLanguageCode("ces")); - assertThat(Util.normalizeLanguageCode("dut")).isEqualTo(Util.normalizeLanguageCode("nld")); - assertThat(Util.normalizeLanguageCode("fre")).isEqualTo(Util.normalizeLanguageCode("fra")); - assertThat(Util.normalizeLanguageCode("geo")).isEqualTo(Util.normalizeLanguageCode("kat")); - assertThat(Util.normalizeLanguageCode("ger")).isEqualTo(Util.normalizeLanguageCode("deu")); - assertThat(Util.normalizeLanguageCode("gre")).isEqualTo(Util.normalizeLanguageCode("ell")); - assertThat(Util.normalizeLanguageCode("ice")).isEqualTo(Util.normalizeLanguageCode("isl")); - assertThat(Util.normalizeLanguageCode("mac")).isEqualTo(Util.normalizeLanguageCode("mkd")); - assertThat(Util.normalizeLanguageCode("mao")).isEqualTo(Util.normalizeLanguageCode("mri")); - assertThat(Util.normalizeLanguageCode("may")).isEqualTo(Util.normalizeLanguageCode("msa")); - assertThat(Util.normalizeLanguageCode("per")).isEqualTo(Util.normalizeLanguageCode("fas")); - assertThat(Util.normalizeLanguageCode("rum")).isEqualTo(Util.normalizeLanguageCode("ron")); - assertThat(Util.normalizeLanguageCode("slo")).isEqualTo(Util.normalizeLanguageCode("slk")); - assertThat(Util.normalizeLanguageCode("scc")).isEqualTo(Util.normalizeLanguageCode("srp")); - assertThat(Util.normalizeLanguageCode("tib")).isEqualTo(Util.normalizeLanguageCode("bod")); - assertThat(Util.normalizeLanguageCode("wel")).isEqualTo(Util.normalizeLanguageCode("cym")); - } - - // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved - @Test - @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) - public void - normalizeLanguageCode_deprecatedLanguageTagsAndModernReplacement_areNormalizedToSameTag() { - // See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, "ISO 639:1988" - assertThat(Util.normalizeLanguageCode("in")).isEqualTo(Util.normalizeLanguageCode("id")); - assertThat(Util.normalizeLanguageCode("in")).isEqualTo(Util.normalizeLanguageCode("ind")); - assertThat(Util.normalizeLanguageCode("iw")).isEqualTo(Util.normalizeLanguageCode("he")); - assertThat(Util.normalizeLanguageCode("iw")).isEqualTo(Util.normalizeLanguageCode("heb")); - assertThat(Util.normalizeLanguageCode("ji")).isEqualTo(Util.normalizeLanguageCode("yi")); - assertThat(Util.normalizeLanguageCode("ji")).isEqualTo(Util.normalizeLanguageCode("yid")); - - // Legacy tags - assertThat(Util.normalizeLanguageCode("i-lux")).isEqualTo(Util.normalizeLanguageCode("lb")); - assertThat(Util.normalizeLanguageCode("i-lux")).isEqualTo(Util.normalizeLanguageCode("ltz")); - assertThat(Util.normalizeLanguageCode("i-hak")).isEqualTo(Util.normalizeLanguageCode("hak")); - assertThat(Util.normalizeLanguageCode("i-hak")).isEqualTo(Util.normalizeLanguageCode("zh-hak")); - assertThat(Util.normalizeLanguageCode("i-navajo")).isEqualTo(Util.normalizeLanguageCode("nv")); - assertThat(Util.normalizeLanguageCode("i-navajo")).isEqualTo(Util.normalizeLanguageCode("nav")); - assertThat(Util.normalizeLanguageCode("no-bok")).isEqualTo(Util.normalizeLanguageCode("nb")); - assertThat(Util.normalizeLanguageCode("no-bok")).isEqualTo(Util.normalizeLanguageCode("nob")); - assertThat(Util.normalizeLanguageCode("no-nyn")).isEqualTo(Util.normalizeLanguageCode("nn")); - assertThat(Util.normalizeLanguageCode("no-nyn")).isEqualTo(Util.normalizeLanguageCode("nno")); - assertThat(Util.normalizeLanguageCode("zh-guoyu")).isEqualTo(Util.normalizeLanguageCode("cmn")); - assertThat(Util.normalizeLanguageCode("zh-guoyu")) - .isEqualTo(Util.normalizeLanguageCode("zh-cmn")); - assertThat(Util.normalizeLanguageCode("zh-hakka")).isEqualTo(Util.normalizeLanguageCode("hak")); - assertThat(Util.normalizeLanguageCode("zh-hakka")) - .isEqualTo(Util.normalizeLanguageCode("zh-hak")); - assertThat(Util.normalizeLanguageCode("zh-min-nan")) - .isEqualTo(Util.normalizeLanguageCode("nan")); - assertThat(Util.normalizeLanguageCode("zh-min-nan")) - .isEqualTo(Util.normalizeLanguageCode("zh-nan")); - assertThat(Util.normalizeLanguageCode("zh-xiang")).isEqualTo(Util.normalizeLanguageCode("hsn")); - assertThat(Util.normalizeLanguageCode("zh-xiang")) - .isEqualTo(Util.normalizeLanguageCode("zh-hsn")); - } - - // TODO: Revert to @Config(sdk = Config.ALL_SDKS) once b/143232359 is resolved - @Test - @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.TARGET_SDK) - public void normalizeLanguageCode_macrolanguageTags_areFullyMaintained() { - // See https://en.wikipedia.org/wiki/ISO_639_macrolanguage - assertThat(Util.normalizeLanguageCode("zh-cmn")).isEqualTo("zh-cmn"); - assertThat(Util.normalizeLanguageCode("zho-cmn")).isEqualTo("zh-cmn"); - assertThat(Util.normalizeLanguageCode("ar-ayl")).isEqualTo("ar-ayl"); - assertThat(Util.normalizeLanguageCode("ara-ayl")).isEqualTo("ar-ayl"); - - // Special case of short codes that are actually part of a macrolanguage. - assertThat(Util.normalizeLanguageCode("nb")).isEqualTo("no-nob"); - assertThat(Util.normalizeLanguageCode("nn")).isEqualTo("no-nno"); - assertThat(Util.normalizeLanguageCode("nob")).isEqualTo("no-nob"); - assertThat(Util.normalizeLanguageCode("nno")).isEqualTo("no-nno"); - assertThat(Util.normalizeLanguageCode("tw")).isEqualTo("ak-twi"); - assertThat(Util.normalizeLanguageCode("twi")).isEqualTo("ak-twi"); - assertThat(Util.normalizeLanguageCode("bs")).isEqualTo("hbs-bos"); - assertThat(Util.normalizeLanguageCode("bos")).isEqualTo("hbs-bos"); - assertThat(Util.normalizeLanguageCode("hr")).isEqualTo("hbs-hrv"); - assertThat(Util.normalizeLanguageCode("hrv")).isEqualTo("hbs-hrv"); - assertThat(Util.normalizeLanguageCode("sr")).isEqualTo("hbs-srp"); - assertThat(Util.normalizeLanguageCode("srp")).isEqualTo("hbs-srp"); - assertThat(Util.normalizeLanguageCode("id")).isEqualTo("ms-ind"); - assertThat(Util.normalizeLanguageCode("ind")).isEqualTo("ms-ind"); - assertThat(Util.normalizeLanguageCode("cmn")).isEqualTo("zh-cmn"); - assertThat(Util.normalizeLanguageCode("hak")).isEqualTo("zh-hak"); - assertThat(Util.normalizeLanguageCode("nan")).isEqualTo("zh-nan"); - assertThat(Util.normalizeLanguageCode("hsn")).isEqualTo("zh-hsn"); - } - - @Test - public void tableExists_withExistingTable() { - SQLiteDatabase database = getInMemorySQLiteOpenHelper().getWritableDatabase(); - database.execSQL("CREATE TABLE TestTable (ID INTEGER NOT NULL)"); - - assertThat(Util.tableExists(database, "TestTable")).isTrue(); - } - - @Test - public void tableExists_withNonExistingTable() { - SQLiteDatabase database = getInMemorySQLiteOpenHelper().getReadableDatabase(); - - assertThat(Util.tableExists(database, "table")).isFalse(); - } - - private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { - assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); - assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); - } - - private static void assertEscapeUnescapeFileName(String fileName) { - String escapedFileName = Util.escapeFileName(fileName); - assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); - } - - private static LongArray newLongArray(long... values) { - LongArray longArray = new LongArray(); - for (long value : values) { - longArray.add(value); - } - return longArray; - } - - /** Returns a {@link SQLiteOpenHelper} that provides an in-memory database. */ - private static SQLiteOpenHelper getInMemorySQLiteOpenHelper() { - return new SQLiteOpenHelper( - /* context= */ null, /* name= */ null, /* factory= */ null, /* version= */ 1) { - @Override - public void onCreate(SQLiteDatabase db) {} - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {} - }; - } - - /** Generates an array of random bytes with the specified length. */ - private static byte[] buildTestData(int length, int seed) { - byte[] source = new byte[length]; - new Random(seed).nextBytes(source); - return source; - } - - /** Equivalent to {@code buildTestData(length, length)}. */ - // TODO(internal b/161804035): Use TestUtils when it's available in a dependency we can use here. - private static byte[] buildTestData(int length) { - return buildTestData(length, length); - } - - /** Generates a random string with the specified maximum length. */ - // TODO(internal b/161804035): Use TestUtils when it's available in a dependency we can use here. - private static String buildTestString(int maximumLength, Random random) { - int length = random.nextInt(maximumLength); - StringBuilder builder = new StringBuilder(length); - for (int i = 0; i < length; i++) { - builder.append((char) random.nextInt()); - } - return builder.toString(); - } - - /** Converts an array of integers in the range [0, 255] into an equivalent byte array. */ - // TODO(internal b/161804035): Use TestUtils when it's available in a dependency we can use here. - private static byte[] createByteArray(int... bytes) { - byte[] byteArray = new byte[bytes.length]; - for (int i = 0; i < byteArray.length; i++) { - Assertions.checkState(0x00 <= bytes[i] && bytes[i] <= 0xFF); - byteArray[i] = (byte) bytes[i]; - } - return byteArray; - } -} 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 e0421a5d657..71cf194ec0b 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.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; @@ -115,13 +116,39 @@ public void inferContentType_handlesSmoothStreamingIsmUris() { 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