From 6a5cd6889258df6b9a08b770e2bf351a5bb30e18 Mon Sep 17 00:00:00 2001 From: Ian Bird Date: Fri, 9 Oct 2015 12:33:01 +0100 Subject: [PATCH 01/22] Make MediaCodecUtil.getMediaCodecInfo public --- .../com/google/android/exoplayer/MediaCodecUtil.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index a2bd8f7466..4a7b362a58 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -94,8 +94,15 @@ public final class MediaCodecUtil { /** * Returns the name of the best decoder and its capabilities for the given mimeType. + * + * @param mimeType The mime type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. + * + * @return The name of the best decoder and its capabilities for the given mimeType, or null if + * no decoder exists. */ - private static synchronized Pair getMediaCodecInfo( + public static synchronized Pair getMediaCodecInfo( String mimeType, boolean secure) throws DecoderQueryException { CodecKey key = new CodecKey(mimeType, secure); if (codecs.containsKey(key)) { From ee8a00b68ab1d032d700957efdecd0bcaf16d785 Mon Sep 17 00:00:00 2001 From: Ian Bird Date: Fri, 9 Oct 2015 14:12:48 +0100 Subject: [PATCH 02/22] Add support for TrueHD audio codec in WebMExtractor --- .../android/exoplayer/extractor/webm/WebmExtractor.java | 5 +++++ .../java/com/google/android/exoplayer/util/MimeTypes.java | 1 + 2 files changed, 6 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java index 771fe1e90b..0aa2f6fa18 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java @@ -77,6 +77,7 @@ public final class WebmExtractor implements Extractor { private static final String CODEC_ID_DTS = "A_DTS"; private static final String CODEC_ID_DTS_EXPRESS = "A_DTS/EXPRESS"; private static final String CODEC_ID_DTS_LOSSLESS = "A_DTS/LOSSLESS"; + private static final String CODEC_ID_TRUEHD = "A_TRUEHD"; private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; private static final int VORBIS_MAX_INPUT_SIZE = 8192; @@ -1041,6 +1042,7 @@ public final class WebmExtractor implements Extractor { || CODEC_ID_DTS.equals(codecId) || CODEC_ID_DTS_EXPRESS.equals(codecId) || CODEC_ID_DTS_LOSSLESS.equals(codecId) + || CODEC_ID_TRUEHD.equals(codecId) || CODEC_ID_SUBRIP.equals(codecId); } @@ -1201,6 +1203,9 @@ public final class WebmExtractor implements Extractor { case CODEC_ID_DTS_LOSSLESS: mimeType = MimeTypes.AUDIO_DTS_HD; break; + case CODEC_ID_TRUEHD: + mimeType = MimeTypes.AUDIO_TRUEHD; + break; case CODEC_ID_SUBRIP: mimeType = MimeTypes.APPLICATION_SUBRIP; break; diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index afb62a5bf5..214e4cd862 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -49,6 +49,7 @@ public final class MimeTypes { public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis"; public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus"; + public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From e933e2d49f31c1ecc5144d203e72e719c9200195 Mon Sep 17 00:00:00 2001 From: Ian Bird Date: Fri, 9 Oct 2015 14:38:39 +0100 Subject: [PATCH 03/22] Add support for MPEG2 video codec in WebMExtractor --- .../android/exoplayer/extractor/webm/WebmExtractor.java | 5 +++++ .../java/com/google/android/exoplayer/util/MimeTypes.java | 1 + 2 files changed, 6 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java index 771fe1e90b..3b189dc878 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java @@ -64,6 +64,7 @@ public final class WebmExtractor implements Extractor { private static final String DOC_TYPE_MATROSKA = "matroska"; private static final String CODEC_ID_VP8 = "V_VP8"; private static final String CODEC_ID_VP9 = "V_VP9"; + private static final String CODEC_ID_MPEG2 = "V_MPEG2"; private static final String CODEC_ID_MPEG4_SP = "V_MPEG4/ISO/SP"; private static final String CODEC_ID_MPEG4_ASP = "V_MPEG4/ISO/ASP"; private static final String CODEC_ID_MPEG4_AP = "V_MPEG4/ISO/AP"; @@ -1028,6 +1029,7 @@ public final class WebmExtractor implements Extractor { private static boolean isCodecSupported(String codecId) { return CODEC_ID_VP8.equals(codecId) || CODEC_ID_VP9.equals(codecId) + || CODEC_ID_MPEG2.equals(codecId) || CODEC_ID_MPEG4_SP.equals(codecId) || CODEC_ID_MPEG4_ASP.equals(codecId) || CODEC_ID_MPEG4_AP.equals(codecId) @@ -1147,6 +1149,9 @@ public final class WebmExtractor implements Extractor { case CODEC_ID_VP9: mimeType = MimeTypes.VIDEO_VP9; break; + case CODEC_ID_MPEG2: + mimeType = MimeTypes.VIDEO_MPEG2; + break; case CODEC_ID_MPEG4_SP: case CODEC_ID_MPEG4_ASP: case CODEC_ID_MPEG4_AP: diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index afb62a5bf5..dd66b28390 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -34,6 +34,7 @@ public final class MimeTypes { public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; + public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; From 6bf817f107ded1bc749d78dd599b6fd8e41d684e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:12:43 +0100 Subject: [PATCH 04/22] Workaround EOS propagation for all devices with RK decoder. As per the end of the related issue, it's likely that all devices running the affected API levels + decoder are affected by the same issue. Issue #464 --- .../google/android/exoplayer/MediaCodecTrackRenderer.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 9970758c90..8952c20da3 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -922,11 +922,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer * propagation incorrectly on the host device. False otherwise. */ private static boolean codecNeedsEosPropagationWorkaround(String name) { - return Util.SDK_INT <= 17 - && "OMX.rk.video_decoder.avc".equals(name) - && ("ht7s3".equals(Util.DEVICE) // Tesco HUDL - || "rk30sdk".equals(Util.DEVICE) // Rockchip rk30 - || "rk31sdk".equals(Util.DEVICE)); // Rockchip rk31 + return Util.SDK_INT <= 17 && "OMX.rk.video_decoder.avc".equals(name); } /** From bcb4ea4f703e667e910d8b4016ddab67d11af2eb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:15:13 +0100 Subject: [PATCH 05/22] Allow launching of ExoPlayer demo app via adb shell. For example: adb shell am start -a com.google.android.exoplayer.demo.action.VIEW -d http://... --- demo/src/main/AndroidManifest.xml | 15 +++++- .../exoplayer/demo/PlayerActivity.java | 51 ++++++++++++++++--- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 1575a1e6b7..230fd31a93 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -41,9 +41,20 @@ + android:theme="@style/PlayerTheme"> + + + + + + + + + + diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 8f4d3e16cc..f9932579f6 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -78,13 +78,19 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, DemoPlayer.Listener, DemoPlayer.CaptionListener, DemoPlayer.Id3MetadataListener, AudioCapabilitiesReceiver.Listener { + // For use within demo app code. + public static final String CONTENT_ID_EXTRA = "content_id"; + public static final String CONTENT_TYPE_EXTRA = "content_type"; public static final int TYPE_DASH = 0; public static final int TYPE_SS = 1; public static final int TYPE_HLS = 2; public static final int TYPE_OTHER = 3; - public static final String CONTENT_TYPE_EXTRA = "content_type"; - public static final String CONTENT_ID_EXTRA = "content_id"; + // For use when launching the demo app using adb. + private static final String CONTENT_EXT_EXTRA = "type"; + private static final String EXT_DASH = ".mpd"; + private static final String EXT_SS = ".ism"; + private static final String EXT_HLS = ".m3u8"; private static final String TAG = "PlayerActivity"; private static final int MENU_GROUP_TRACKS = 1; @@ -129,11 +135,6 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Intent intent = getIntent(); - contentUri = intent.getData(); - contentType = intent.getIntExtra(CONTENT_TYPE_EXTRA, -1); - contentId = intent.getStringExtra(CONTENT_ID_EXTRA); - setContentView(R.layout.player_activity); View root = findViewById(R.id.root); root.setOnTouchListener(new OnTouchListener() { @@ -185,9 +186,21 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, audioCapabilitiesReceiver.register(); } + @Override + public void onNewIntent(Intent intent) { + releasePlayer(); + playerPosition = 0; + setIntent(intent); + } + @Override public void onResume() { super.onResume(); + Intent intent = getIntent(); + contentUri = intent.getData(); + contentType = intent.getIntExtra(CONTENT_TYPE_EXTRA, + inferContentType(contentUri, intent.getStringExtra(CONTENT_EXT_EXTRA))); + contentId = intent.getStringExtra(CONTENT_ID_EXTRA); configureSubtitleView(); if (player == null) { preparePlayer(true); @@ -597,4 +610,28 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); } + /** + * Makes a best guess to infer the type from a media {@link Uri} and an optional overriding file + * extension. + * + * @param uri The {@link Uri} of the media. + * @param fileExtension An overriding file extension. + * @return The inferred type. + */ + private static int inferContentType(Uri uri, String fileExtension) { + String lastPathSegment = !TextUtils.isEmpty(fileExtension) ? "." + fileExtension + : uri.getLastPathSegment(); + if (lastPathSegment == null) { + return TYPE_OTHER; + } else if (lastPathSegment.endsWith(EXT_DASH)) { + return TYPE_DASH; + } else if (lastPathSegment.endsWith(EXT_SS)) { + return TYPE_SS; + } else if (lastPathSegment.endsWith(EXT_HLS)) { + return TYPE_HLS; + } else { + return TYPE_OTHER; + } + } + } From ab489d35e96f6e09a4115d479f82d0b152c6d402 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:16:15 +0100 Subject: [PATCH 06/22] Don't pass keyboard escape key to media controller. This makes it possible to exit the player when using e.g. an Android TV with a keyboard. --- .../java/com/google/android/exoplayer/demo/PlayerActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index f9932579f6..dbcdd2cacc 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -151,7 +151,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, root.setOnKeyListener(new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE + || keyCode == KeyEvent.KEYCODE_MENU) { return false; } return mediaController.dispatchKeyEvent(event); From 3682141ee1c43d967c53406165a874a4dfad33c7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:18:12 +0100 Subject: [PATCH 07/22] webm_extractor: Re-sync to next level 1 element on invalid data Try re-sync'ing to the next level 1 element when invalid data is found. This corrects the behavior for test case 4 in the mkv test suite. Partially Fixes Issue #631 --- .../extractor/webm/DefaultEbmlReaderTest.java | 5 ++ .../extractor/webm/VarintReaderTest.java | 19 +++-- .../java/com/google/android/exoplayer/C.java | 5 ++ .../extractor/webm/DefaultEbmlReader.java | 42 +++++++++- .../extractor/webm/EbmlReaderOutput.java | 8 ++ .../extractor/webm/VarintReader.java | 81 +++++++++++++------ .../extractor/webm/WebmExtractor.java | 11 ++- 7 files changed, 137 insertions(+), 34 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/DefaultEbmlReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/DefaultEbmlReaderTest.java index 7225247290..1365772660 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/DefaultEbmlReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/DefaultEbmlReaderTest.java @@ -195,6 +195,11 @@ public class DefaultEbmlReaderTest extends TestCase { } } + @Override + public boolean isLevel1Element(int id) { + return false; + } + @Override public void startMasterElement(int id, long contentPosition, long contentSize) { events.add(formatEvent(id, "start contentPosition=" + contentPosition diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/VarintReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/VarintReaderTest.java index ccd40b16dc..ba498526e7 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/VarintReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/VarintReaderTest.java @@ -95,17 +95,26 @@ public class VarintReaderTest extends TestCase { int bytesRead = input.read(new byte[1], 0, 1); assertEquals(1, bytesRead); // End of input allowed. - long result = reader.readUnsignedVarint(input, true, false); - assertEquals(-1, result); + long result = reader.readUnsignedVarint(input, true, false, 8); + assertEquals(C.RESULT_END_OF_INPUT, result); // End of input not allowed. try { - reader.readUnsignedVarint(input, false, false); + reader.readUnsignedVarint(input, false, false, 8); fail(); } catch (EOFException e) { // Expected. } } + public void testReadVarintExceedsMaximumAllowedLength() throws IOException, InterruptedException { + VarintReader reader = new VarintReader(); + DataSource dataSource = buildDataSource(DATA_8_BYTE_0); + dataSource.open(new DataSpec(Uri.parse(TEST_URI))); + ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED); + long result = reader.readUnsignedVarint(input, false, true, 4); + assertEquals(C.RESULT_MAX_LENGTH_EXCEEDED, result); + } + public void testReadVarint() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); testReadVarint(reader, true, DATA_1_BYTE_0, 1, 0); @@ -183,7 +192,7 @@ public class VarintReaderTest extends TestCase { DataSource dataSource = buildDataSource(data); dataSource.open(new DataSpec(Uri.parse(TEST_URI))); ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED); - long result = reader.readUnsignedVarint(input, false, removeMask); + long result = reader.readUnsignedVarint(input, false, removeMask, 8); assertEquals(expectedLength, input.getPosition()); assertEquals(expectedValue, result); } @@ -198,7 +207,7 @@ public class VarintReaderTest extends TestCase { dataSource.open(new DataSpec(Uri.parse(TEST_URI), position, C.LENGTH_UNBOUNDED, null)); input = new DefaultExtractorInput(dataSource, position, C.LENGTH_UNBOUNDED); try { - result = reader.readUnsignedVarint(input, false, removeMask); + result = reader.readUnsignedVarint(input, false, removeMask, 8); position = input.getPosition(); } catch (IOException e) { // Expected. We'll try again from the position that the input was advanced to. diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index 145e151099..81c52bb2f5 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -90,6 +90,11 @@ public final class C { */ public static final int RESULT_END_OF_INPUT = -1; + /** + * A return value for methods where the length of parsed data exceeds the maximum length allowed. + */ + public static final int RESULT_MAX_LENGTH_EXCEEDED = -2; + private C() {} } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/DefaultEbmlReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/DefaultEbmlReader.java index 83435f0e77..71326412e6 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/DefaultEbmlReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/DefaultEbmlReader.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.util.Assertions; +import java.io.EOFException; import java.io.IOException; import java.nio.charset.Charset; import java.util.Stack; @@ -32,6 +33,9 @@ import java.util.Stack; private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1; private static final int ELEMENT_STATE_READ_CONTENT = 2; + private static final int MAX_ID_BYTES = 4; + private static final int MAX_LENGTH_BYTES = 8; + private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8; private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4; private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8; @@ -68,8 +72,11 @@ import java.util.Stack; } if (elementState == ELEMENT_STATE_READ_ID) { - long result = varintReader.readUnsignedVarint(input, true, false); - if (result == -1) { + long result = varintReader.readUnsignedVarint(input, true, false, MAX_ID_BYTES); + if (result == C.RESULT_MAX_LENGTH_EXCEEDED) { + result = maybeResyncToNextLevel1Element(input); + } + if (result == C.RESULT_END_OF_INPUT) { return false; } // Element IDs are at most 4 bytes, so we can cast to integers. @@ -78,7 +85,7 @@ import java.util.Stack; } if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) { - elementContentSize = varintReader.readUnsignedVarint(input, false, true); + elementContentSize = varintReader.readUnsignedVarint(input, false, true, MAX_LENGTH_BYTES); elementState = ELEMENT_STATE_READ_CONTENT; } @@ -127,6 +134,35 @@ import java.util.Stack; } } + /** + * Does a byte by byte search to try and find the next level 1 element. This method is called if + * some invalid data is encountered in the parser. + * + * @param input The {@link ExtractorInput} from which data has to be read. + * @return id of the next level 1 element that has been found. + * @throws EOFException If the end of input was encountered when searching for the next level 1 + * element. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private long maybeResyncToNextLevel1Element(ExtractorInput input) throws EOFException, + IOException, InterruptedException { + while (true) { + input.resetPeekPosition(); + input.peekFully(scratch, 0, MAX_ID_BYTES); + int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]); + if (varintLength != -1 && varintLength <= MAX_ID_BYTES) { + int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false); + if (output.isLevel1Element(potentialId)) { + input.skipFully(varintLength); + input.resetPeekPosition(); + return potentialId; + } + } + input.skipFully(1); + } + } + /** * Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}. * diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/EbmlReaderOutput.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/EbmlReaderOutput.java index d72f886f9b..91c58cedbc 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/EbmlReaderOutput.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/EbmlReaderOutput.java @@ -36,6 +36,14 @@ import java.io.IOException; */ int getElementType(int id); + /** + * Checks if the given id is that of a level 1 element. + * + * @param id The element ID. + * @return True the given id is that of a level 1 element. false otherwise. + */ + boolean isLevel1Element(int id); + /** * Called when the start of a master element is encountered. *

diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/VarintReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/VarintReader.java index 03cdb2debb..d775b066c9 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/VarintReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/VarintReader.java @@ -1,5 +1,6 @@ package com.google.android.exoplayer.extractor.webm; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.extractor.ExtractorInput; import java.io.EOFException; @@ -19,8 +20,8 @@ import java.io.IOException; * *

{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes. */ - private static final int[] VARINT_LENGTH_MASKS = new int[] { - 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 + private static final long[] VARINT_LENGTH_MASKS = new long[] { + 0x80L, 0x40L, 0x20L, 0x10L, 0x08L, 0x04L, 0x02L, 0x01L }; private final byte[] scratch; @@ -53,48 +54,41 @@ import java.io.IOException; * * @param input The {@link ExtractorInput} from which the integer should be read. * @param allowEndOfInput True if encountering the end of the input having read no data is - * allowed, and should result in {@code -1} being returned. False if it should be - * considered an error, causing an {@link EOFException} to be thrown. - * @param removeLengthMask Removes the variable-length integer length mask from the value - * @return The read value, or -1 if {@code allowEndOfStream} is true and the end of the input was - * encountered. + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @param removeLengthMask Removes the variable-length integer length mask from the value. + * @param maximumAllowedLength Maximum allowed length of the variable integer to be read. + * @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true + * and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the + * length of the varint exceeded maximumAllowedLength. * @throws IOException If an error occurs reading from the input. * @throws InterruptedException If the thread is interrupted. */ public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput, - boolean removeLengthMask) throws IOException, InterruptedException { + boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException { if (state == STATE_BEGIN_READING) { // Read the first byte to establish the length. if (!input.readFully(scratch, 0, 1, allowEndOfInput)) { - return -1; + return C.RESULT_END_OF_INPUT; } int firstByte = scratch[0] & 0xFF; - length = -1; - for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) { - if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) { - length = i + 1; - break; - } - } + length = parseUnsignedVarintLength(firstByte); if (length == -1) { throw new IllegalStateException("No valid varint length mask found"); } state = STATE_READ_CONTENTS; } + if (length > maximumAllowedLength) { + state = STATE_BEGIN_READING; + return C.RESULT_MAX_LENGTH_EXCEEDED; + } + // Read the remaining bytes. input.readFully(scratch, 1, length - 1); - // Parse the value. - if (removeLengthMask) { - scratch[0] &= ~VARINT_LENGTH_MASKS[length - 1]; - } - long varint = 0; - for (int i = 0; i < length; i++) { - varint = (varint << 8) | (scratch[i] & 0xFF); - } state = STATE_BEGIN_READING; - return varint; + return assembleVarint(scratch, length, removeLengthMask); } /** @@ -104,4 +98,41 @@ import java.io.IOException; return length; } + /** + * Parses and the length of the varint given the first byte. + * + * @param firstByte First byte of the varint. + * @return Length of the varint beginning with the given byte if it was valid, -1 otherwise. + */ + public static int parseUnsignedVarintLength(int firstByte) { + int varIntLength = -1; + for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) { + if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) { + varIntLength = i + 1; + break; + } + } + return varIntLength; + } + + /** + * Assemble a varint from the given byte array. + * + * @param varintBytes Bytes that make up the varint. + * @param varintLength Length of the varint to assemble. + * @param removeLengthMask Removes the variable-length integer length mask from the value. + * @return Parsed and assembled varint. + */ + public static long assembleVarint(byte[] varintBytes, int varintLength, + boolean removeLengthMask) { + long varint = varintBytes[0] & 0xFFL; + if (removeLengthMask) { + varint &= ~VARINT_LENGTH_MASKS[varintLength - 1]; + } + for (int i = 1; i < varintLength; i++) { + varint = (varint << 8) | (varintBytes[i] & 0xFFL); + } + return varint; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java index ee9b3738a3..dae9cc22ca 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java @@ -349,6 +349,10 @@ public final class WebmExtractor implements Extractor { } } + /* package */ boolean isLevel1Element(int id) { + return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS; + } + /* package */ void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException { switch (id) { @@ -641,7 +645,7 @@ public final class WebmExtractor implements Extractor { // differ only in the way flags are specified. if (blockState == BLOCK_STATE_START) { - blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true); + blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true, 8); blockTrackNumberLength = varintReader.getLastLength(); blockDurationUs = UNKNOWN; blockState = BLOCK_STATE_HEADER; @@ -1073,6 +1077,11 @@ public final class WebmExtractor implements Extractor { return WebmExtractor.this.getElementType(id); } + @Override + public boolean isLevel1Element(int id) { + return WebmExtractor.this.isLevel1Element(id); + } + @Override public void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException { From a5ebb49a1a1b2bda285f72d90b92f8fb99de17be Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:20:15 +0100 Subject: [PATCH 08/22] Set the maximum input size based on the sample table for MP4s. --- .../exoplayer/MediaCodecTrackRenderer.java | 2 +- .../google/android/exoplayer/MediaFormat.java | 6 ++++ .../exoplayer/extractor/mp4/AtomParsers.java | 8 +++-- .../exoplayer/extractor/mp4/Mp4Extractor.java | 5 ++- .../extractor/mp4/TrackSampleTable.java | 36 ++++++++++++++----- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 8952c20da3..f8a2518f88 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -643,7 +643,7 @@ public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer adaptiveReconfigurationBytes); codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0); } else { - codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0); + codec.queueInputBuffer(inputIndex, 0, bufferSize, presentationTimeUs, 0); } inputIndex = -1; codecHasQueuedBuffers = true; diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index be21897c55..f97fbdcdec 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -205,6 +205,12 @@ public final class MediaFormat { this.maxHeight = maxHeight; } + public MediaFormat copyWithMaxInputSize(int maxInputSize) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight); + } + public MediaFormat copyWithMaxVideoDimensions(int maxWidth, int maxHeight) { return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index 7028cf5159..f0c10fcd3e 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java @@ -106,10 +106,11 @@ import java.util.List; long[] offsets = new long[sampleCount]; int[] sizes = new int[sampleCount]; + int maximumSize = 0; long[] timestamps = new long[sampleCount]; int[] flags = new int[sampleCount]; if (sampleCount == 0) { - return new TrackSampleTable(offsets, sizes, timestamps, flags); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } // Prepare to read chunk offsets. @@ -172,6 +173,9 @@ import java.util.List; for (int i = 0; i < sampleCount; i++) { offsets[i] = offsetBytes; sizes[i] = fixedSampleSize == 0 ? stsz.readUnsignedIntToInt() : fixedSampleSize; + if (sizes[i] > maximumSize) { + maximumSize = sizes[i]; + } timestamps[i] = timestampTimeUnits + timestampOffset; // All samples are synchronization samples if the stss is not present. @@ -244,7 +248,7 @@ import java.util.List; Assertions.checkArgument(remainingSamplesInChunk == 0); Assertions.checkArgument(remainingTimestampDeltaChanges == 0); Assertions.checkArgument(remainingTimestampOffsetChanges == 0); - return new TrackSampleTable(offsets, sizes, timestamps, flags); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java index b5db6e4cc5..3c7611b02c 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java @@ -262,7 +262,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { } Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i)); - mp4Track.trackOutput.format(track.mediaFormat); + // Each sample has up to three bytes of overhead for the start code that replaces its length. + // Allow ten source samples per output sample, like the platform extractor. + int maxInputSize = trackSampleTable.maximumSize + 3 * 10; + mp4Track.trackOutput.format(track.mediaFormat.copyWithMaxInputSize(maxInputSize)); tracks.add(mp4Track); long firstSampleOffset = trackSampleTable.offsets[0]; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackSampleTable.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackSampleTable.java index 1848185e97..6717b3c54a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackSampleTable.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackSampleTable.java @@ -19,31 +19,49 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; -/** Sample table for a track in an MP4 file. */ +/** + * Sample table for a track in an MP4 file. + */ /* package */ final class TrackSampleTable { - /** Sample index when no sample is available. */ + /** + * Sample index when no sample is available. + */ public static final int NO_SAMPLE = -1; - /** Number of samples. */ + /** + * Number of samples. + */ public final int sampleCount; - /** Sample offsets in bytes. */ + /** + * Sample offsets in bytes. + */ public final long[] offsets; - /** Sample sizes in bytes. */ + /** + * Sample sizes in bytes. + */ public final int[] sizes; - /** Sample timestamps in microseconds. */ + /** + * Maximum sample size in {@link #sizes}. + */ + public final int maximumSize; + /** + * Sample timestamps in microseconds. + */ public final long[] timestampsUs; - /** Sample flags. */ + /** + * Sample flags. + */ public final int[] flags; - TrackSampleTable( - long[] offsets, int[] sizes, long[] timestampsUs, int[] flags) { + TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs, int[] flags) { Assertions.checkArgument(sizes.length == timestampsUs.length); Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length); this.offsets = offsets; this.sizes = sizes; + this.maximumSize = maximumSize; this.timestampsUs = timestampsUs; this.flags = flags; sampleCount = offsets.length; From a764b359e80112f02bb3964a3936b62275ffd44d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:21:16 +0100 Subject: [PATCH 09/22] Add support for DTS passthrough on supporting devices before API 23. NVIDIA Shield before API 23 supports DTS passthrough, so this change inlines the constant value. --- .../java/com/google/android/exoplayer/C.java | 5 + .../android/exoplayer/audio/AudioTrack.java | 106 +++++++++++------- 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index 81c52bb2f5..f3c5a11e89 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -68,6 +68,11 @@ public final class C { @SuppressWarnings("InlinedApi") public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; + // TODO: Switch these constants to use AudioFormat fields when the target API version is >= 23. + // The inlined values here are for NVIDIA Shield devices which support DTS on earlier versions. + public static final int ENCODING_DTS = 7; + public static final int ENCODING_DTS_HD = 8; + /** * @see MediaExtractor#SAMPLE_FLAG_SYNC */ diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 461a3cfdd7..44159befd0 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -202,7 +202,7 @@ public final class AudioTrack { private int temporaryBufferSize; /** - * Bitrate measured in kilobits per second, if {@link #isPassthrough()} returns true. + * Bitrate measured in kilobits per second, if using passthrough. */ private int passthroughBitrate; @@ -359,7 +359,7 @@ public final class AudioTrack { } } - audioTrackUtil.reconfigure(audioTrack, isPassthrough()); + audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); setAudioTrackVolume(); return sessionId; @@ -472,8 +472,7 @@ public final class AudioTrack { return RESULT_BUFFER_CONSUMED; } - // Workarounds for issues with AC-3 passthrough AudioTracks on API versions 21/22: - if (Util.SDK_INT <= 22 && isPassthrough()) { + if (needsPassthroughWorkarounds()) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its // buffer empties. See [Internal: b/18899620]. if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) { @@ -491,8 +490,14 @@ public final class AudioTrack { int result = 0; if (temporaryBufferSize == 0) { - if (isPassthrough() && passthroughBitrate == UNKNOWN_BITRATE) { - passthroughBitrate = Ac3Util.getBitrate(size, sampleRate); + if (passthroughBitrate == UNKNOWN_BITRATE) { + if (isAc3Passthrough()) { + passthroughBitrate = Ac3Util.getBitrate(size, sampleRate); + } else if (isDtsPassthrough()) { + int unscaledBitrate = size * 8 * sampleRate; + int divisor = 1000 * 512; + passthroughBitrate = (unscaledBitrate + divisor / 2) / divisor; + } } // This is the first time we've seen this {@code buffer}. @@ -583,7 +588,7 @@ public final class AudioTrack { public boolean hasPendingData() { return isInitialized() && (bytesToFrames(submittedBytes) > audioTrackUtil.getPlaybackHeadPosition() - || audioTrackUtil.overrideHasPendingData()); + || overrideHasPendingData()); } /** Sets the playback volume. */ @@ -709,10 +714,13 @@ public final class AudioTrack { } } - // Don't sample the timestamp and latency if this is a passthrough AudioTrack, as the returned - // values cause audio/video synchronization to be incorrect. - if (!isPassthrough() - && systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { + if (needsPassthroughWorkarounds()) { + // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on + // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353]. + return; + } + + if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { audioTimestampSet = audioTrackUtil.updateTimestamp(); if (audioTimestampSet) { // Perform sanity checks on the timestamp. @@ -818,17 +826,50 @@ public final class AudioTrack { } private boolean isPassthrough() { + return isAc3Passthrough() || isDtsPassthrough(); + } + + private boolean isAc3Passthrough() { return encoding == C.ENCODING_AC3 || encoding == C.ENCODING_E_AC3; } + private boolean isDtsPassthrough() { + return encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD; + } + + /** + * Returns whether to work around problems with passthrough audio tracks. + * See [Internal: b/18899620, b/19187573, b/21145353]. + */ + private boolean needsPassthroughWorkarounds() { + return Util.SDK_INT < 23 && isAc3Passthrough(); + } + + /** + * Returns whether the audio track should behave as though it has pending data. This is to work + * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we + * empty their buffers when paused. In this case, they should still behave as if they have + * pending data, otherwise writing will never resume. + */ + private boolean overrideHasPendingData() { + return needsPassthroughWorkarounds() + && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED + && audioTrack.getPlaybackHeadPosition() == 0; + } + private static int getEncodingForMimeType(String mimeType) { - if (MimeTypes.AUDIO_AC3.equals(mimeType)) { - return C.ENCODING_AC3; + switch (mimeType) { + case MimeTypes.AUDIO_AC3: + return C.ENCODING_AC3; + case MimeTypes.AUDIO_EC3: + return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_DTS: + return C.ENCODING_DTS; + case MimeTypes.AUDIO_DTS_HD: + return C.ENCODING_DTS_HD; + default: + return AudioFormat.ENCODING_INVALID; } - if (MimeTypes.AUDIO_EC3.equals(mimeType)) { - return C.ENCODING_E_AC3; - } - return AudioFormat.ENCODING_INVALID; } /** @@ -837,7 +878,7 @@ public final class AudioTrack { private static class AudioTrackUtil { protected android.media.AudioTrack audioTrack; - private boolean isPassthrough; + private boolean needsPassthroughWorkaround; private int sampleRate; private long lastRawPlaybackHeadPosition; private long rawPlaybackHeadWrapCount; @@ -851,11 +892,13 @@ public final class AudioTrack { * Reconfigures the audio track utility helper to use the specified {@code audioTrack}. * * @param audioTrack The audio track to wrap. - * @param isPassthrough Whether the audio track is used for passthrough (e.g. AC-3) playback. + * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough + * audio tracks on platform API version 21/22. */ - public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) { + public void reconfigure(android.media.AudioTrack audioTrack, + boolean needsPassthroughWorkaround) { this.audioTrack = audioTrack; - this.isPassthrough = isPassthrough; + this.needsPassthroughWorkaround = needsPassthroughWorkaround; stopTimestampUs = -1; lastRawPlaybackHeadPosition = 0; rawPlaybackHeadWrapCount = 0; @@ -865,20 +908,6 @@ public final class AudioTrack { } } - /** - * Returns whether the audio track should behave as though it has pending data. This is to work - * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we - * empty their buffers when paused. In this case, they should still behave as if they have - * pending data, otherwise writing will never resume. - * - * @see #handleBuffer - */ - public boolean overrideHasPendingData() { - return Util.SDK_INT <= 22 && isPassthrough - && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED - && audioTrack.getPlaybackHeadPosition() == 0; - } - /** * Stops the audio track in a way that ensures media written to it is played out in full, and * that {@link #getPlaybackHeadPosition()} and {@link #getPlaybackHeadPositionUs()} continue to @@ -929,7 +958,7 @@ public final class AudioTrack { } long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); - if (Util.SDK_INT <= 22 && isPassthrough) { + if (needsPassthroughWorkaround) { // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 // where the playback head position jumps back to zero on paused passthrough/direct audio // tracks. See [Internal: b/19187573]. @@ -1009,8 +1038,9 @@ public final class AudioTrack { } @Override - public void reconfigure(android.media.AudioTrack audioTrack, boolean isPassthrough) { - super.reconfigure(audioTrack, isPassthrough); + public void reconfigure(android.media.AudioTrack audioTrack, + boolean needsPassthroughWorkaround) { + super.reconfigure(audioTrack, needsPassthroughWorkaround); rawTimestampFramePositionWrapCount = 0; lastRawTimestampFramePosition = 0; lastTimestampFramePosition = 0; From 952bd4e73cf261be9baa6b251d59cfff3d455783 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:24:48 +0100 Subject: [PATCH 10/22] Don't calculate a maximum input size for H.264 on Sony 4k TV. Issue: #800 --- .../android/exoplayer/MediaCodecVideoTrackRenderer.java | 5 +++++ .../main/java/com/google/android/exoplayer/util/Util.java | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index 6cf315bdac..e3a68e050c 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -560,6 +560,11 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { // Already set. The source of the format may know better, so do nothing. return; } + if ("BRAVIA 4K 2015".equals(Util.MODEL)) { + // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video + // maximum input size, so use the default value. + return; + } int maxHeight = format.getInteger(android.media.MediaFormat.KEY_HEIGHT); if (codecIsAdaptive && format.containsKey(android.media.MediaFormat.KEY_MAX_HEIGHT)) { maxHeight = Math.max(maxHeight, format.getInteger(android.media.MediaFormat.KEY_MAX_HEIGHT)); diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index cfbd282c64..8bd876a73f 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -75,6 +75,12 @@ public final class Util { */ public static final String MANUFACTURER = android.os.Build.MANUFACTURER; + /** + * Like {@link android.os.Build#MODEL}, but in a place where it can be conveniently overridden for + * local testing. + */ + public static final String MODEL = android.os.Build.MODEL; + private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" From 79055066813123c939c29e5a5e223a5ff043b91e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:25:58 +0100 Subject: [PATCH 11/22] On Sony Bravia devices check for 4k panel. Documentation: https://developer.sony.com/develop/tvs/android-tv/design-guide/ On API 23 we should also check Display.Mode (where supported). Issue: #800 --- .../chunk/VideoFormatSelectorUtil.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java b/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java index 5dcf6878fe..f9a4aae6b7 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java @@ -55,11 +55,9 @@ public final class VideoFormatSelectorUtil { public static int[] selectVideoFormatsForDefaultDisplay(Context context, List formatWrappers, String[] allowedContainerMimeTypes, boolean filterHdFormats) throws DecoderQueryException { - WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - Display display = windowManager.getDefaultDisplay(); - Point displaySize = getDisplaySize(display); + Point viewportSize = getViewportSize(context); return selectVideoFormats(formatWrappers, allowedContainerMimeTypes, filterHdFormats, true, - displaySize.x, displaySize.y); + viewportSize.x, viewportSize.y); } /** @@ -184,6 +182,19 @@ public final class VideoFormatSelectorUtil { } } + private static Point getViewportSize(Context context) { + // Before API 23 the platform Display object does not provide a way to identify Android TVs that + // can show 4k resolution in a SurfaceView, so check for supported devices here. + // See also https://developer.sony.com/develop/tvs/android-tv/design-guide/. + if (Util.MODEL != null && Util.MODEL.startsWith("BRAVIA") + && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) { + return new Point(3840, 2160); + } + + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + return getDisplaySize(windowManager.getDefaultDisplay()); + } + private static Point getDisplaySize(Display display) { Point displaySize = new Point(); if (Util.SDK_INT >= 17) { From c4235d0e8dfff9c7cb930645cec19997390dbb1c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:26:58 +0100 Subject: [PATCH 12/22] Ignore tfdt boxes for SmoothStreaming playbacks. Issue #838 --- .../extractor/mp4/FragmentedMp4Extractor.java | 15 ++++++++++++--- .../SmoothStreamingChunkSource.java | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java index 291b167971..0df0f40e08 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java @@ -53,6 +53,11 @@ public final class FragmentedMp4Extractor implements Extractor { */ public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; + /** + * Flag to ignore any tfdt boxes in the stream. + */ + public static final int WORKAROUND_IGNORE_TFDT_BOX = 2; + private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; @@ -329,7 +334,12 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseTraf(Track track, DefaultSampleValues extendsDefaults, ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) { LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); - long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); + long decodeTime; + if (tfdtAtom == null || (workaroundFlags & WORKAROUND_IGNORE_TFDT_BOX) != 0) { + decodeTime = 0; + } else { + decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); + } LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data); @@ -475,8 +485,7 @@ public final class FragmentedMp4Extractor implements Extractor { long timescale = track.timescale; long cumulativeTime = decodeTime; boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_vide - && ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) - == WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); + && (workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0; for (int i = 0; i < sampleCount; i++) { // Use trun values if present, otherwise tfhd, otherwise trex. int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 6ccb297804..5ab9a4aec8 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -427,7 +427,8 @@ public class SmoothStreamingChunkSource implements ChunkSource, // Build the extractor. FragmentedMp4Extractor mp4Extractor = new FragmentedMp4Extractor( - FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); + FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME + | FragmentedMp4Extractor.WORKAROUND_IGNORE_TFDT_BOX); Track mp4Track = new Track(trackIndex, mp4TrackType, element.timescale, durationUs, mediaFormat, trackEncryptionBoxes, mp4TrackType == Track.TYPE_vide ? 4 : -1); mp4Extractor.setTrack(mp4Track); From 9b4e9723e5f459ac86e621ae01a70929b4aba16e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:28:14 +0100 Subject: [PATCH 13/22] Don't use SEC VP8 decoder on Galaxy S3s. The only Samsung devices with names starting "d2" that we're aware of are Galaxy S3 variants, and also one Samsung Galaxy Pocket Neo d2aio SAMSUNG-SGH-I747Z. This change speculatively includes that device too because its name is very similar to SAMSUNG-SGH-I747 which is known to be affected. Issue: #548 --- .../java/com/google/android/exoplayer/MediaCodecUtil.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index 4a7b362a58..01c814560d 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -209,8 +209,10 @@ public final class MediaCodecUtil { return false; } - // Work around an issue where the VP8 decoder on Samsung Galaxy S4 Mini does not render video. - if (Util.SDK_INT <= 19 && Util.DEVICE != null && Util.DEVICE.startsWith("serrano") + // Work around an issue where the VP8 decoder on Samsung Galaxy S3/S4 Mini does not render + // video. + if (Util.SDK_INT <= 19 && Util.DEVICE != null + && (Util.DEVICE.startsWith("d2") || Util.DEVICE.startsWith("serrano")) && "samsung".equals(Util.MANUFACTURER) && name.equals("OMX.SEC.vp8.dec")) { return false; } From bcb9f8282df7b05a66329e5115511c67f1651d2d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:32:10 +0100 Subject: [PATCH 14/22] Enable SmoothFrameTimeHelper by default. Context: - Currently, playback is significantly more juddery with it disabled, particularly on AndroidTV. - We should be able to do the "best" job of this internally, so injection doesn't buy anything useful. If someone has a better implementation for adjusting the frame release, they should improve the core library. --- .../demo/player/DashRendererBuilder.java | 4 +- .../demo/player/ExtractorRendererBuilder.java | 4 +- .../demo/player/HlsRendererBuilder.java | 4 +- .../SmoothStreamingRendererBuilder.java | 6 +- .../MediaCodecVideoTrackRenderer.java | 123 ++++-------------- ....java => VideoFrameReleaseTimeHelper.java} | 100 +++++++++----- .../playbacktests/gts/H264DashTest.java | 2 +- 7 files changed, 102 insertions(+), 141 deletions(-) rename library/src/main/java/com/google/android/exoplayer/{SmoothFrameReleaseTimeHelper.java => VideoFrameReleaseTimeHelper.java} (62%) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java index 219a62fd1e..3d2f24dd02 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java @@ -219,8 +219,8 @@ public class DashRendererBuilder implements RendererBuilder { ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); - TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, + TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, drmSessionManager, true, mainHandler, player, 50); // Build the audio renderer. diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java index 9a9718f883..faf39ab907 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java @@ -61,8 +61,8 @@ public class ExtractorRendererBuilder implements RendererBuilder { DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, - null, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, player.getMainHandler(), + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, + sampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, player.getMainHandler(), player, 50); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, null, true, player.getMainHandler(), player, AudioCapabilities.getCapabilities(context)); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index 16441c2277..51475c9341 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -148,8 +148,8 @@ public class HlsRendererBuilder implements RendererBuilder { variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE); HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); - MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, - MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, mainHandler, player, 50); + MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, + sampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, mainHandler, player, 50); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, null, true, player.getMainHandler(), player, AudioCapabilities.getCapabilities(context)); MetadataTrackRenderer> id3Renderer = new MetadataTrackRenderer<>( diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java index c8f20b4e2f..7e4211807d 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java @@ -163,9 +163,9 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder { ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, DemoPlayer.TYPE_VIDEO); - TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, - drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, - mainHandler, player, 50); + TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, drmSessionManager, true, mainHandler, + player, 50); // Build the audio renderer. DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index e3a68e050c..caacf82d75 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer.util.Util; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.Context; import android.media.MediaCodec; import android.media.MediaCrypto; import android.os.Handler; @@ -86,34 +87,6 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } - /** - * An interface for fine-grained adjustment of frame release times. - */ - public interface FrameReleaseTimeHelper { - - /** - * Enables the helper. - */ - void enable(); - - /** - * Disables the helper. - */ - void disable(); - - /** - * Called to make a fine-grained adjustment to a frame release time. - * - * @param framePresentationTimeUs The frame's media presentation time, in microseconds. - * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in - * the same time base as {@link System#nanoTime()}. - * @return An adjusted release time for the frame, in nanoseconds and in the same time base as - * {@link System#nanoTime()}. - */ - public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs); - - } - // TODO: Use MediaFormat constants if these get exposed through the API. See [Internal: b/14127601]. private static final String KEY_CROP_LEFT = "crop-left"; private static final String KEY_CROP_RIGHT = "crop-right"; @@ -127,7 +100,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { */ public static final int MSG_SET_SURFACE = 1; - private final FrameReleaseTimeHelper frameReleaseTimeHelper; + private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventListener eventListener; private final long allowedJoiningTimeUs; private final int videoScalingMode; @@ -152,64 +125,30 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { private float lastReportedPixelWidthHeightRatio; /** + * @param context A context. * @param source The upstream source from which the renderer obtains samples. * @param videoScalingMode The scaling mode to pass to * {@link MediaCodec#setVideoScalingMode(int)}. */ - public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode) { - this(source, null, true, videoScalingMode); - } - - /** - * @param source The upstream source from which the renderer obtains samples. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * begin in parallel with key acquisision. This parameter specifies whether the renderer is - * permitted to play clear regions of encrypted media files before {@code drmSessionManager} - * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param videoScalingMode The scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)}. - */ - public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, int videoScalingMode) { - this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode, 0); + public MediaCodecVideoTrackRenderer(Context context, SampleSource source, int videoScalingMode) { + this(context, source, videoScalingMode, 0); } /** + * @param context A context. * @param source The upstream source from which the renderer obtains samples. * @param videoScalingMode The scaling mode to pass to * {@link MediaCodec#setVideoScalingMode(int)}. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. */ - public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode, + public MediaCodecVideoTrackRenderer(Context context, SampleSource source, int videoScalingMode, long allowedJoiningTimeMs) { - this(source, null, true, videoScalingMode, allowedJoiningTimeMs); - } - - /** - * @param source The upstream source from which the renderer obtains samples. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * begin in parallel with key acquisision. This parameter specifies whether the renderer is - * permitted to play clear regions of encrypted media files before {@code drmSessionManager} - * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param videoScalingMode The scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)}. - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - */ - public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs) { - this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode, - allowedJoiningTimeMs, null, null, null, -1); + this(context, source, videoScalingMode, allowedJoiningTimeMs, null, null, -1); } /** + * @param context A context. * @param source The upstream source from which the renderer obtains samples. * @param videoScalingMode The scaling mode to pass to * {@link MediaCodec#setVideoScalingMode(int)}. @@ -221,15 +160,20 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between * invocations of {@link EventListener#onDroppedFrames(int, long)}. */ - public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode, + public MediaCodecVideoTrackRenderer(Context context, SampleSource source, int videoScalingMode, long allowedJoiningTimeMs, Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { - this(source, null, true, videoScalingMode, allowedJoiningTimeMs, null, eventHandler, + this(context, source, videoScalingMode, allowedJoiningTimeMs, null, false, eventHandler, eventListener, maxDroppedFrameCountToNotify); } /** + * @param context A context. * @param source The upstream source from which the renderer obtains samples. + * @param videoScalingMode The scaling mode to pass to + * {@link MediaCodec#setVideoScalingMode(int)}. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. @@ -237,26 +181,20 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { * begin in parallel with key acquisision. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param videoScalingMode The scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)}. - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param frameReleaseTimeHelper An optional helper to make fine-grained adjustments to frame - * release times. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between * invocations of {@link EventListener#onDroppedFrames(int, long)}. */ - public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs, - FrameReleaseTimeHelper frameReleaseTimeHelper, Handler eventHandler, - EventListener eventListener, int maxDroppedFrameCountToNotify) { + public MediaCodecVideoTrackRenderer(Context context, SampleSource source, int videoScalingMode, + long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener, + int maxDroppedFrameCountToNotify) { super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener); + this.frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); this.videoScalingMode = videoScalingMode; this.allowedJoiningTimeUs = allowedJoiningTimeMs * 1000; - this.frameReleaseTimeHelper = frameReleaseTimeHelper; this.eventListener = eventListener; this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify; joiningDeadlineUs = -1; @@ -285,9 +223,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { if (joining && allowedJoiningTimeUs > 0) { joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs; } - if (frameReleaseTimeHelper != null) { - frameReleaseTimeHelper.enable(); - } + frameReleaseTimeHelper.enable(); } @Override @@ -340,9 +276,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { lastReportedWidth = -1; lastReportedHeight = -1; lastReportedPixelWidthHeightRatio = -1; - if (frameReleaseTimeHelper != null) { - frameReleaseTimeHelper.disable(); - } + frameReleaseTimeHelper.disable(); super.onDisabled(); } @@ -468,14 +402,9 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000); // Apply a timestamp adjustment, if there is one. - long adjustedReleaseTimeNs; - if (frameReleaseTimeHelper != null) { - adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime( - bufferInfo.presentationTimeUs, unadjustedFrameReleaseTimeNs); - earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - } else { - adjustedReleaseTimeNs = unadjustedFrameReleaseTimeNs; - } + long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime( + bufferInfo.presentationTimeUs, unadjustedFrameReleaseTimeNs); + earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; if (earlyUs < -30000) { // We're more than 30ms late rendering the frame. diff --git a/library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java b/library/src/main/java/com/google/android/exoplayer/VideoFrameReleaseTimeHelper.java similarity index 62% rename from library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java rename to library/src/main/java/com/google/android/exoplayer/VideoFrameReleaseTimeHelper.java index ce93136734..7aeff788c9 100644 --- a/library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java +++ b/library/src/main/java/com/google/android/exoplayer/VideoFrameReleaseTimeHelper.java @@ -15,17 +15,17 @@ */ package com.google.android.exoplayer; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer.FrameReleaseTimeHelper; - import android.annotation.TargetApi; +import android.content.Context; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; +import android.view.WindowManager; /** * Makes a best effort to adjust frame release timestamps for a smoother visual result. */ @TargetApi(16) -public final class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelper, FrameCallback { +public final class VideoFrameReleaseTimeHelper implements FrameCallback { private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; private static final long MAX_ALLOWED_DRIFT_NS = 20000000; @@ -33,32 +33,45 @@ public final class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelpe private static final long VSYNC_OFFSET_PERCENTAGE = 80; private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6; - private final boolean usePrimaryDisplayVsync; + private final boolean useDefaultDisplayVsync; private final long vsyncDurationNs; private final long vsyncOffsetNs; private Choreographer choreographer; private long sampledVsyncTimeNs; - private long lastUnadjustedFrameTimeUs; + private long lastFramePresentationTimeUs; private long adjustedLastFrameTimeNs; private long pendingAdjustedFrameTimeNs; private boolean haveSync; - private long syncReleaseTimeNs; - private long syncFrameTimeNs; - private int frameCount; + private long syncUnadjustedReleaseTimeNs; + private long syncFramePresentationTimeNs; + private long frameCount; /** - * @param primaryDisplayRefreshRate The refresh rate of the default display. - * @param usePrimaryDisplayVsync Whether to snap to the primary display vsync. May not be - * suitable when rendering to secondary displays. + * Constructs an instance that smoothes frame release but does not snap release to the default + * display's vsync signal. */ - public SmoothFrameReleaseTimeHelper( - float primaryDisplayRefreshRate, boolean usePrimaryDisplayVsync) { - this.usePrimaryDisplayVsync = usePrimaryDisplayVsync; - if (usePrimaryDisplayVsync) { - vsyncDurationNs = (long) (1000000000d / primaryDisplayRefreshRate); + public VideoFrameReleaseTimeHelper() { + this(-1, false); + } + + /** + * Constructs an instance that smoothes frame release and snaps release to the default display's + * vsync signal. + * + * @param context A context from which information about the default display can be retrieved. + */ + public VideoFrameReleaseTimeHelper(Context context) { + this(getDefaultDisplayRefreshRate(context), true); + } + + private VideoFrameReleaseTimeHelper(float defaultDisplayRefreshRate, + boolean useDefaultDisplayVsync) { + this.useDefaultDisplayVsync = useDefaultDisplayVsync; + if (useDefaultDisplayVsync) { + vsyncDurationNs = (long) (1000000000d / defaultDisplayRefreshRate); vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; } else { vsyncDurationNs = -1; @@ -66,19 +79,23 @@ public final class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelpe } } - @Override + /** + * Enables the helper. + */ public void enable() { haveSync = false; - if (usePrimaryDisplayVsync) { + if (useDefaultDisplayVsync) { sampledVsyncTimeNs = 0; choreographer = Choreographer.getInstance(); choreographer.postFrameCallback(this); } } - @Override + /** + * Disables the helper. + */ public void disable() { - if (usePrimaryDisplayVsync) { + if (useDefaultDisplayVsync) { choreographer.removeFrameCallback(this); choreographer = null; } @@ -90,17 +107,25 @@ public final class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelpe choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS); } - @Override - public long adjustReleaseTime(long unadjustedFrameTimeUs, long unadjustedReleaseTimeNs) { - long unadjustedFrameTimeNs = unadjustedFrameTimeUs * 1000; + /** + * Called to make a fine-grained adjustment to a frame release time. + * + * @param framePresentationTimeUs The frame's media presentation time, in microseconds. + * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in + * the same time base as {@link System#nanoTime()}. + * @return An adjusted release time for the frame, in nanoseconds and in the same time base as + * {@link System#nanoTime()}. + */ + public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs) { + long framePresentationTimeNs = framePresentationTimeUs * 1000; // Until we know better, the adjustment will be a no-op. - long adjustedFrameTimeNs = unadjustedFrameTimeNs; + long adjustedFrameTimeNs = framePresentationTimeNs; long adjustedReleaseTimeNs = unadjustedReleaseTimeNs; if (haveSync) { // See if we've advanced to the next frame. - if (unadjustedFrameTimeUs != lastUnadjustedFrameTimeUs) { + if (framePresentationTimeUs != lastFramePresentationTimeUs) { frameCount++; adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs; } @@ -109,20 +134,22 @@ public final class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelpe // Calculate the average frame time across all the frames we've seen since the last sync. // This will typically give us a frame rate at a finer granularity than the frame times // themselves (which often only have millisecond granularity). - long averageFrameTimeNs = (unadjustedFrameTimeNs - syncFrameTimeNs) / frameCount; + long averageFrameDurationNs = (framePresentationTimeNs - syncFramePresentationTimeNs) + / frameCount; // Project the adjusted frame time forward using the average. - long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameTimeNs; + long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameDurationNs; if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) { haveSync = false; } else { adjustedFrameTimeNs = candidateAdjustedFrameTimeNs; - adjustedReleaseTimeNs = syncReleaseTimeNs + adjustedFrameTimeNs - syncFrameTimeNs; + adjustedReleaseTimeNs = syncUnadjustedReleaseTimeNs + adjustedFrameTimeNs + - syncFramePresentationTimeNs; } } else { // We're synced but haven't waited the required number of frames to apply an adjustment. // Check drift anyway. - if (isDriftTooLarge(unadjustedFrameTimeNs, unadjustedReleaseTimeNs)) { + if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) { haveSync = false; } } @@ -130,14 +157,14 @@ public final class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelpe // If we need to sync, do so now. if (!haveSync) { - syncFrameTimeNs = unadjustedFrameTimeNs; - syncReleaseTimeNs = unadjustedReleaseTimeNs; + syncFramePresentationTimeNs = framePresentationTimeNs; + syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs; frameCount = 0; haveSync = true; onSynced(); } - lastUnadjustedFrameTimeUs = unadjustedFrameTimeUs; + lastFramePresentationTimeUs = framePresentationTimeUs; pendingAdjustedFrameTimeNs = adjustedFrameTimeNs; if (sampledVsyncTimeNs == 0) { @@ -155,8 +182,8 @@ public final class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelpe } private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) { - long elapsedFrameTimeNs = frameTimeNs - syncFrameTimeNs; - long elapsedReleaseTimeNs = releaseTimeNs - syncReleaseTimeNs; + long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs; + long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs; return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS; } @@ -177,4 +204,9 @@ public final class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelpe return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; } + private static float getDefaultDisplayRefreshRate(Context context) { + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + return manager.getDefaultDisplay().getRefreshRate(); + } + } diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/H264DashTest.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/H264DashTest.java index 3303abaa6e..78e1445385 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/H264DashTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/gts/H264DashTest.java @@ -217,7 +217,7 @@ public final class H264DashTest extends ActivityInstrumentationTestCase2 Date: Mon, 12 Oct 2015 12:32:49 +0100 Subject: [PATCH 15/22] Add missing Eclipse files for playback tests. --- playbacktests/src/main/.classpath | 10 ++++++ playbacktests/src/main/.project | 53 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 playbacktests/src/main/.classpath create mode 100644 playbacktests/src/main/.project diff --git a/playbacktests/src/main/.classpath b/playbacktests/src/main/.classpath new file mode 100644 index 0000000000..3ae82311ba --- /dev/null +++ b/playbacktests/src/main/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/playbacktests/src/main/.project b/playbacktests/src/main/.project new file mode 100644 index 0000000000..7bce46ef78 --- /dev/null +++ b/playbacktests/src/main/.project @@ -0,0 +1,53 @@ + + + ExoPlayerPlaybackTests + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + + + 1363908154650 + + 22 + + org.eclipse.ui.ide.multiFilter + 1.0-name-matches-false-false-BUILD + + + + 1363908154652 + + 10 + + org.eclipse.ui.ide.multiFilter + 1.0-name-matches-true-false-build + + + + From 414ad053141cc697cb2d932d277d345e9a51121b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 12:34:17 +0100 Subject: [PATCH 16/22] Fix package for vp9opus demo. --- demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml | 8 ++++---- .../demo/{webm => vp9opus}/DashRendererBuilder.java | 2 +- .../demo/{webm => vp9opus}/FilePickerActivity.java | 2 +- .../demo/{webm => vp9opus}/SampleChooserActivity.java | 2 +- .../exoplayer/demo/{webm => vp9opus}/VideoPlayer.java | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/{webm => vp9opus}/DashRendererBuilder.java (99%) rename demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/{webm => vp9opus}/FilePickerActivity.java (98%) rename demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/{webm => vp9opus}/SampleChooserActivity.java (98%) rename demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/{webm => vp9opus}/VideoPlayer.java (99%) diff --git a/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml b/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml index 72070a4162..be4dd9c0cf 100644 --- a/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml +++ b/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ @@ -35,7 +35,7 @@ android:allowBackup="false" android:icon="@drawable/ic_launcher"> - @@ -44,12 +44,12 @@ - - diff --git a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/DashRendererBuilder.java similarity index 99% rename from demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java rename to demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/DashRendererBuilder.java index f199091cb2..9bef54606e 100644 --- a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java +++ b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/DashRendererBuilder.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.demo.webm; +package com.google.android.exoplayer.demo.vp9opus; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.LoadControl; diff --git a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/FilePickerActivity.java b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/FilePickerActivity.java similarity index 98% rename from demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/FilePickerActivity.java rename to demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/FilePickerActivity.java index c7e5817d19..61125b9eeb 100644 --- a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/FilePickerActivity.java +++ b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/FilePickerActivity.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.demo.webm; +package com.google.android.exoplayer.demo.vp9opus; import android.app.Activity; import android.app.ListActivity; diff --git a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/SampleChooserActivity.java b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/SampleChooserActivity.java similarity index 98% rename from demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/SampleChooserActivity.java rename to demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/SampleChooserActivity.java index 7f1fa98e5f..c03c815277 100644 --- a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/SampleChooserActivity.java +++ b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/SampleChooserActivity.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.demo.webm; +package com.google.android.exoplayer.demo.vp9opus; import android.app.Activity; import android.content.Context; diff --git a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/VideoPlayer.java b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/VideoPlayer.java similarity index 99% rename from demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/VideoPlayer.java rename to demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/VideoPlayer.java index 1de59c9131..b5adada15d 100644 --- a/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/webm/VideoPlayer.java +++ b/demo_misc/vp9_opus_sw/src/main/java/com/google/android/exoplayer/demo/vp9opus/VideoPlayer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.demo.webm; +package com.google.android.exoplayer.demo.vp9opus; import com.google.android.exoplayer.AspectRatioFrameLayout; import com.google.android.exoplayer.ExoPlaybackException; From 20e05a31b2a71e8cfe22cde4bcf0fe5ea073bb0b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 17:57:10 +0100 Subject: [PATCH 17/22] Do TTML color parsing directly in Exoplayer. - Added TtmlColorParser to workaround JellyBean issues with named colors. - Support rgb and rgba expressions as well. --- .../text/ttml/TtmlColorParserTest.java | 113 ++++++++++++++ .../exoplayer/text/ttml/TtmlParserTest.java | 63 +++++--- .../exoplayer/text/ttml/TtmlColorParser.java | 138 ++++++++++++++++++ .../exoplayer/text/ttml/TtmlParser.java | 5 +- 4 files changed, 294 insertions(+), 25 deletions(-) create mode 100644 library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlColorParserTest.java create mode 100644 library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlColorParser.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlColorParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlColorParserTest.java new file mode 100644 index 0000000000..922b054011 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlColorParserTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2014 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.exoplayer.text.ttml; + +import android.graphics.Color; +import android.test.InstrumentationTestCase; + +/** + * Unit test for TtmlColorParser. + */ +public class TtmlColorParserTest extends InstrumentationTestCase { + + public void testHexCodeParsing() { + assertEquals(Color.WHITE, TtmlColorParser.parseColor("#ffffff")); + assertEquals(Color.WHITE, TtmlColorParser.parseColor("#ffffffff")); + assertEquals(Color.parseColor("#00ffffff"), TtmlColorParser.parseColor("#00ffffff")); + assertEquals(Color.parseColor("#12341234"), TtmlColorParser.parseColor("#12341234")); + } + + public void testColorNameParsing() { + assertEquals(TtmlColorParser.TRANSPARENT, TtmlColorParser.parseColor("transparent")); + assertEquals(TtmlColorParser.BLACK, TtmlColorParser.parseColor("black")); + assertEquals(TtmlColorParser.GRAY, TtmlColorParser.parseColor("gray")); + assertEquals(TtmlColorParser.SILVER, TtmlColorParser.parseColor("silver")); + assertEquals(TtmlColorParser.WHITE, TtmlColorParser.parseColor("white")); + assertEquals(TtmlColorParser.MAROON, TtmlColorParser.parseColor("maroon")); + assertEquals(TtmlColorParser.RED, TtmlColorParser.parseColor("red")); + assertEquals(TtmlColorParser.PURPLE, TtmlColorParser.parseColor("purple")); + assertEquals(TtmlColorParser.FUCHSIA, TtmlColorParser.parseColor("fuchsia")); + assertEquals(TtmlColorParser.MAGENTA, TtmlColorParser.parseColor("magenta")); + assertEquals(TtmlColorParser.GREEN, TtmlColorParser.parseColor("green")); + assertEquals(TtmlColorParser.LIME, TtmlColorParser.parseColor("lime")); + assertEquals(TtmlColorParser.OLIVE, TtmlColorParser.parseColor("olive")); + assertEquals(TtmlColorParser.YELLOW, TtmlColorParser.parseColor("yellow")); + assertEquals(TtmlColorParser.NAVY, TtmlColorParser.parseColor("navy")); + assertEquals(TtmlColorParser.BLUE, TtmlColorParser.parseColor("blue")); + assertEquals(TtmlColorParser.TEAL, TtmlColorParser.parseColor("teal")); + assertEquals(TtmlColorParser.AQUA, TtmlColorParser.parseColor("aqua")); + assertEquals(TtmlColorParser.CYAN, TtmlColorParser.parseColor("cyan")); + } + + public void testParseUnknownColor() { + try { + TtmlColorParser.parseColor("colorOfAnElectron"); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testParseNull() { + try { + TtmlColorParser.parseColor(null); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testParseEmpty() { + try { + TtmlColorParser.parseColor(""); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testRgbColorParsing() { + assertEquals(Color.WHITE, TtmlColorParser.parseColor("rgb(255,255,255)")); + // spaces do not matter + assertEquals(Color.WHITE, TtmlColorParser.parseColor(" rgb ( 255, 255, 255)")); + } + + public void testRgbColorParsing_rgbValuesOutOfBounds() { + int outOfBounds = TtmlColorParser.parseColor("rgb(999, 999, 999)"); + int color = Color.rgb(999, 999, 999); + // behave like framework Color behaves + assertEquals(color, outOfBounds); + } + + public void testRgbColorParsing_rgbValuesNegative() { + try { + TtmlColorParser.parseColor("rgb(-4, 55, 209)"); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testRgbaColorParsing() { + assertEquals(Color.WHITE, TtmlColorParser.parseColor("rgba(255,255,255,0)")); + assertEquals(Color.argb(0, 255, 255, 255), TtmlColorParser.parseColor("rgba(255,255,255,255)")); + assertEquals(Color.BLACK, TtmlColorParser.parseColor("rgba(0, 0, 0, 0)")); + assertEquals(Color.argb(0, 0, 0, 0), TtmlColorParser.parseColor("rgba(0, 0, 0, 255)")); + assertEquals(Color.RED, TtmlColorParser.parseColor("rgba(255, 0, 0, 0)")); + assertEquals(Color.argb(0, 255, 0, 0), TtmlColorParser.parseColor("rgba(255, 0, 0, 255)")); + assertEquals(Color.argb(205, 255, 0, 0), TtmlColorParser.parseColor("rgba(255, 0, 0, 50)")); + } +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java index 9d5d953977..4133c3ce3d 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/text/ttml/TtmlParserTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer.text.ttml; import com.google.android.exoplayer.text.Cue; -import android.graphics.Color; import android.test.InstrumentationTestCase; import android.text.Layout; import android.text.SpannableStringBuilder; @@ -67,8 +66,8 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlNode body = queryChildrenForTag(root, TtmlNode.TAG_BODY, 0); TtmlNode firstDiv = queryChildrenForTag(body, TtmlNode.TAG_DIV, 0); TtmlStyle firstPStyle = queryChildrenForTag(firstDiv, TtmlNode.TAG_P, 0).style; - assertEquals(Color.parseColor("yellow"), firstPStyle.getColor()); - assertEquals(Color.parseColor("blue"), firstPStyle.getBackgroundColor()); + assertEquals(TtmlColorParser.parseColor("yellow"), firstPStyle.getColor()); + assertEquals(TtmlColorParser.parseColor("blue"), firstPStyle.getBackgroundColor()); assertEquals("serif", firstPStyle.getFontFamily()); assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, firstPStyle.getStyle()); assertTrue(firstPStyle.isUnderline()); @@ -78,24 +77,43 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); assertEquals(4, subtitle.getEventTimeCount()); assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, - Color.CYAN, Color.parseColor("lime"), false, true, null); + TtmlColorParser.CYAN, TtmlColorParser.parseColor("lime"), false, true, null); + } + + /** + * regression test for devices on JellyBean where some named colors are not correctly defined + * on framework level. Tests that lime resolves to #FF00FF00 not + * #00FF00. + * + * See: https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/ + * graphics/java/android/graphics/Color.java#L414 + * https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/ + * graphics/java/android/graphics/Color.java#L414 + * + * @throws IOException thrown if reading subtitle file fails. + */ + public void testLime() throws IOException { + TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); + assertEquals(4, subtitle.getEventTimeCount()); + assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, + TtmlColorParser.CYAN, TtmlColorParser.LIME, false, true, null); } public void testInheritGlobalStyle() throws IOException { TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE); assertEquals(2, subtitle.getEventTimeCount()); assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, - Color.BLUE, Color.YELLOW, true, false, null); + TtmlColorParser.BLUE, TtmlColorParser.YELLOW, true, false, null); } public void testInheritGlobalStyleOverriddenByInlineAttributes() throws IOException { TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE); assertEquals(4, subtitle.getEventTimeCount()); - assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, Color.BLUE, - Color.YELLOW, true, false, null); - assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, Color.RED, - Color.YELLOW, true, false, null); + assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, TtmlColorParser.BLUE, + TtmlColorParser.YELLOW, true, false, null); + assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, TtmlColorParser.RED, + TtmlColorParser.YELLOW, true, false, null); } public void testInheritGlobalAndParent() throws IOException { @@ -103,9 +121,10 @@ public final class TtmlParserTest extends InstrumentationTestCase { assertEquals(4, subtitle.getEventTimeCount()); assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_NORMAL, - Color.RED, Color.parseColor("lime"), false, true, Layout.Alignment.ALIGN_CENTER); + TtmlColorParser.RED, TtmlColorParser.parseColor("lime"), false, true, + Layout.Alignment.ALIGN_CENTER); assertSpans(subtitle, 20, "text 2", "serif", TtmlStyle.STYLE_BOLD_ITALIC, - Color.BLUE, Color.YELLOW, true, true, Layout.Alignment.ALIGN_CENTER); + TtmlColorParser.BLUE, TtmlColorParser.YELLOW, true, true, Layout.Alignment.ALIGN_CENTER); } public void testInheritMultipleStyles() throws IOException { @@ -113,7 +132,7 @@ public final class TtmlParserTest extends InstrumentationTestCase { assertEquals(12, subtitle.getEventTimeCount()); assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC, - Color.BLUE, Color.YELLOW, false, true, null); + TtmlColorParser.BLUE, TtmlColorParser.YELLOW, false, true, null); } public void testInheritMultipleStylesWithoutLocalAttributes() throws IOException { @@ -121,7 +140,7 @@ public final class TtmlParserTest extends InstrumentationTestCase { assertEquals(12, subtitle.getEventTimeCount()); assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC, - Color.BLUE, Color.BLACK, false, true, null); + TtmlColorParser.BLUE, TtmlColorParser.BLACK, false, true, null); } @@ -130,7 +149,7 @@ public final class TtmlParserTest extends InstrumentationTestCase { assertEquals(12, subtitle.getEventTimeCount()); assertSpans(subtitle, 30, "text 2.5", "sansSerifInline", TtmlStyle.STYLE_ITALIC, - Color.RED, Color.YELLOW, true, true, null); + TtmlColorParser.RED, TtmlColorParser.YELLOW, true, true, null); } public void testEmptyStyleAttribute() throws IOException { @@ -175,16 +194,16 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlStyle style = globalStyles.get("s2"); assertEquals("serif", style.getFontFamily()); - assertEquals(Color.RED, style.getBackgroundColor()); - assertEquals(Color.BLACK, style.getColor()); + assertEquals(TtmlColorParser.RED, style.getBackgroundColor()); + assertEquals(TtmlColorParser.BLACK, style.getColor()); assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); assertTrue(style.isLinethrough()); style = globalStyles.get("s3"); // only difference: color must be RED - assertEquals(Color.RED, style.getColor()); + assertEquals(TtmlColorParser.RED, style.getColor()); assertEquals("serif", style.getFontFamily()); - assertEquals(Color.RED, style.getBackgroundColor()); + assertEquals(TtmlColorParser.RED, style.getBackgroundColor()); assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); assertTrue(style.isLinethrough()); } @@ -224,8 +243,8 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style; assertNotNull(style); - assertEquals(Color.BLACK, style.getBackgroundColor()); - assertEquals(Color.YELLOW, style.getColor()); + assertEquals(TtmlColorParser.BLACK, style.getBackgroundColor()); + assertEquals(TtmlColorParser.YELLOW, style.getColor()); assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle()); assertEquals("sansSerif", style.getFontFamily()); assertFalse(style.isUnderline()); @@ -243,8 +262,8 @@ public final class TtmlParserTest extends InstrumentationTestCase { TtmlStyle style = queryChildrenForTag(div, TtmlNode.TAG_P, 0).style; assertNotNull(style); - assertEquals(Color.BLACK, style.getBackgroundColor()); - assertEquals(Color.YELLOW, style.getColor()); + assertEquals(TtmlColorParser.BLACK, style.getBackgroundColor()); + assertEquals(TtmlColorParser.YELLOW, style.getColor()); assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle()); assertEquals("sansSerif", style.getFontFamily()); assertFalse(style.isUnderline()); diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlColorParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlColorParser.java new file mode 100644 index 0000000000..4a519dae7e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlColorParser.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2014 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.exoplayer.text.ttml; + +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.Util; + +import android.text.TextUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser to parse ttml color value expression + * (http://www.w3.org/TR/ttml1/#style-value-color) + */ +/*package*/ final class TtmlColorParser { + + private static final String RGB = "rgb"; + private static final String RGBA = "rgba"; + + private static final Pattern RGB_PATTERN = Pattern.compile( + "^rgb\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$"); + + private static final Pattern RGBA_PATTERN = Pattern.compile( + "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$"); + + + static final int TRANSPARENT = 0x00000000; + static final int BLACK = 0xFF000000; + static final int SILVER = 0xFFC0C0C0; + static final int GRAY = 0xFF808080; + static final int WHITE = 0xFFFFFFFF; + static final int MAROON = 0xFF800000; + static final int RED = 0xFFFF0000; + static final int PURPLE = 0xFF800080; + static final int FUCHSIA = 0xFFFF00FF; + static final int MAGENTA = FUCHSIA; + static final int GREEN = 0xFF008000; + static final int LIME = 0xFF00FF00; + static final int OLIVE = 0xFF808000; + static final int YELLOW = 0xFFFFFF00; + static final int NAVY = 0xFF000080; + static final int BLUE = 0xFF0000FF; + static final int TEAL = 0xFF008080; + static final int AQUA = 0x00FFFFFF; + static final int CYAN = 0xFF00FFFF; + + private static final Map COLOR_NAME_MAP; + static { + COLOR_NAME_MAP = new HashMap<>(); + COLOR_NAME_MAP.put("transparent", TRANSPARENT); + COLOR_NAME_MAP.put("black", BLACK); + COLOR_NAME_MAP.put("silver", SILVER); + COLOR_NAME_MAP.put("gray", GRAY); + COLOR_NAME_MAP.put("white", WHITE); + COLOR_NAME_MAP.put("maroon", MAROON); + COLOR_NAME_MAP.put("red", RED); + COLOR_NAME_MAP.put("purple", PURPLE); + COLOR_NAME_MAP.put("fuchsia", FUCHSIA); + COLOR_NAME_MAP.put("magenta", MAGENTA); + COLOR_NAME_MAP.put("green", GREEN); + COLOR_NAME_MAP.put("lime", LIME); + COLOR_NAME_MAP.put("olive", OLIVE); + COLOR_NAME_MAP.put("yellow", YELLOW); + COLOR_NAME_MAP.put("navy", NAVY); + COLOR_NAME_MAP.put("blue", BLUE); + COLOR_NAME_MAP.put("teal", TEAL); + COLOR_NAME_MAP.put("aqua", AQUA); + COLOR_NAME_MAP.put("cyan", CYAN); + } + + public static int parseColor(String colorExpression) { + Assertions.checkArgument(!TextUtils.isEmpty(colorExpression)); + colorExpression = colorExpression.replace(" ", ""); + if (colorExpression.charAt(0) == '#') { + // Use a long to avoid rollovers on #ffXXXXXX + long color = Long.parseLong(colorExpression.substring(1), 16); + if (colorExpression.length() == 7) { + // Set the alpha value + color |= 0x00000000ff000000; + } else if (colorExpression.length() != 9) { + throw new IllegalArgumentException(); + } + return (int) color; + } else if (colorExpression.startsWith(RGBA)) { + Matcher matcher = RGBA_PATTERN.matcher(colorExpression); + if (matcher.matches()) { + return argb( + 255 - Integer.parseInt(matcher.group(4), 10), + Integer.parseInt(matcher.group(1), 10), + Integer.parseInt(matcher.group(2), 10), + Integer.parseInt(matcher.group(3), 10) + ); + } + } else if (colorExpression.startsWith(RGB)) { + Matcher matcher = RGB_PATTERN.matcher(colorExpression); + if (matcher.matches()) { + return rgb( + Integer.parseInt(matcher.group(1), 10), + Integer.parseInt(matcher.group(2), 10), + Integer.parseInt(matcher.group(3), 10) + ); + } + } else { + // we use our own color map + Integer color = COLOR_NAME_MAP.get(Util.toLowerInvariant(colorExpression)); + if (color != null) { + return color; + } + } + throw new IllegalArgumentException(); + } + + private static int argb(int alpha, int red, int green, int blue) { + return (alpha << 24) | (red << 16) | (green << 8) | blue; + } + + private static int rgb(int red, int green, int blue) { + return argb(0xFF, red, green, blue); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java index a9e6c1b99a..87670e7f8b 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParserUtil; import com.google.android.exoplayer.util.Util; -import android.graphics.Color; import android.text.Layout; import android.util.Log; @@ -207,7 +206,7 @@ public final class TtmlParser implements SubtitleParser { case TtmlNode.ATTR_TTS_BACKGROUND_COLOR: style = createIfNull(style); try { - style.setBackgroundColor(Color.parseColor(attributeValue)); + style.setBackgroundColor(TtmlColorParser.parseColor(attributeValue)); } catch (IllegalArgumentException e) { Log.w(TAG, "failed parsing background value: '" + attributeValue + "'"); } @@ -215,7 +214,7 @@ public final class TtmlParser implements SubtitleParser { case TtmlNode.ATTR_TTS_COLOR: style = createIfNull(style); try { - style.setColor(Color.parseColor(attributeValue)); + style.setColor(TtmlColorParser.parseColor(attributeValue)); } catch (IllegalArgumentException e) { Log.w(TAG, "failed parsing color value: '" + attributeValue + "'"); } From 0b1c8897bc4e589ce6a96a353abc8782fd535707 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 17:58:01 +0100 Subject: [PATCH 18/22] Bump bintray release version. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cd01d15c17..96547cc1b3 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:1.2.3' - classpath 'com.novoda:bintray-release:0.3.2' + classpath 'com.novoda:bintray-release:0.3.4' } } From aa647745a28138b744a7536491f8960f05d1460f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 12 Oct 2015 17:59:14 +0100 Subject: [PATCH 19/22] No-op re-orderings. --- .../com/google/android/exoplayer/MediaCodecUtil.java | 1 - .../exoplayer/extractor/webm/WebmExtractor.java | 10 +++++----- .../com/google/android/exoplayer/util/MimeTypes.java | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index 01c814560d..8c1a7b3030 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -98,7 +98,6 @@ public final class MediaCodecUtil { * @param mimeType The mime type. * @param secure Whether the decoder is required to support secure decryption. Always pass false * unless secure decryption really is required. - * * @return The name of the best decoder and its capabilities for the given mimeType, or null if * no decoder exists. */ diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java index dae9cc22ca..58d85370ee 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java @@ -75,10 +75,10 @@ public final class WebmExtractor implements Extractor { private static final String CODEC_ID_AAC = "A_AAC"; private static final String CODEC_ID_MP3 = "A_MPEG/L3"; private static final String CODEC_ID_AC3 = "A_AC3"; + private static final String CODEC_ID_TRUEHD = "A_TRUEHD"; private static final String CODEC_ID_DTS = "A_DTS"; private static final String CODEC_ID_DTS_EXPRESS = "A_DTS/EXPRESS"; private static final String CODEC_ID_DTS_LOSSLESS = "A_DTS/LOSSLESS"; - private static final String CODEC_ID_TRUEHD = "A_TRUEHD"; private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; private static final int VORBIS_MAX_INPUT_SIZE = 8192; @@ -1045,10 +1045,10 @@ public final class WebmExtractor implements Extractor { || CODEC_ID_AAC.equals(codecId) || CODEC_ID_MP3.equals(codecId) || CODEC_ID_AC3.equals(codecId) + || CODEC_ID_TRUEHD.equals(codecId) || CODEC_ID_DTS.equals(codecId) || CODEC_ID_DTS_EXPRESS.equals(codecId) || CODEC_ID_DTS_LOSSLESS.equals(codecId) - || CODEC_ID_TRUEHD.equals(codecId) || CODEC_ID_SUBRIP.equals(codecId); } @@ -1210,6 +1210,9 @@ public final class WebmExtractor implements Extractor { case CODEC_ID_AC3: mimeType = MimeTypes.AUDIO_AC3; break; + case CODEC_ID_TRUEHD: + mimeType = MimeTypes.AUDIO_TRUEHD; + break; case CODEC_ID_DTS: case CODEC_ID_DTS_EXPRESS: mimeType = MimeTypes.AUDIO_DTS; @@ -1217,9 +1220,6 @@ public final class WebmExtractor implements Extractor { case CODEC_ID_DTS_LOSSLESS: mimeType = MimeTypes.AUDIO_DTS_HD; break; - case CODEC_ID_TRUEHD: - mimeType = MimeTypes.AUDIO_TRUEHD; - break; case CODEC_ID_SUBRIP: mimeType = MimeTypes.APPLICATION_SUBRIP; break; diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index 23fd5f811c..15cf132217 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -46,11 +46,11 @@ public final class MimeTypes { public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_EC3 = BASE_TYPE_AUDIO + "/eac3"; + public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis"; public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus"; - public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From 6d44ec560eb5d9ac38644d0ecb6a942f8f9a58b3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 14 Oct 2015 11:51:16 +0100 Subject: [PATCH 20/22] Skip to the sample/auxiliary data offset in fragmented MP4 streams. The sample data position is the sum of the data offset and the base data offset. The base data offset is either specified in the stream, or defaults to the first byte position in the moof box. (We only support one traf per moof currently, so the offset does not need to be assigned for later track fragments.) The data position can optionally be offset by a data position read from the trun. The auxiliary information offset is calculated in the same way, but using an offset read from the saio box. Issue: #837 Issue: #861 --- .../android/exoplayer/extractor/mp4/Atom.java | 26 ++++++ .../extractor/mp4/FragmentedMp4Extractor.java | 87 ++++++++++++++++--- .../extractor/mp4/TrackFragment.java | 14 ++- 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java index e0b74bb2f9..21b5356367 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java @@ -92,6 +92,7 @@ import java.util.List; public static final int TYPE_enca = Util.getIntegerCodeForString("enca"); public static final int TYPE_frma = Util.getIntegerCodeForString("frma"); public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz"); + public static final int TYPE_saio = Util.getIntegerCodeForString("saio"); public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid"); public static final int TYPE_senc = Util.getIntegerCodeForString("senc"); public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp"); @@ -219,6 +220,31 @@ import java.util.List; return null; } + /** + * Returns the total number of leaf/container children of this atom with the given type. + * + * @param type The type of child atoms to count. + * @return The total number of leaf/container children of this atom with the given type. + */ + public int getChildAtomOfTypeCount(int type) { + int count = 0; + int size = leafChildren.size(); + for (int i = 0; i < size; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == type) { + count++; + } + } + size = containerChildren.size(); + for (int i = 0; i < size; i++) { + ContainerAtom atom = containerChildren.get(i); + if (atom.type == type) { + count++; + } + } + return count; + } + @Override public String toString() { return getAtomTypeString(type) diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java index 0df0f40e08..996eab4578 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java @@ -86,6 +86,7 @@ public final class FragmentedMp4Extractor implements Extractor { private long atomSize; private int atomHeaderBytesRead; private ParsableByteArray atomData; + private long endOfMdatPosition; private int sampleIndex; private int sampleSize; @@ -204,7 +205,15 @@ public final class FragmentedMp4Extractor implements Extractor { atomSize = atomHeader.readUnsignedLongToLong(); } + long atomPosition = input.getPosition() - atomHeaderBytesRead; + if (atomType == Atom.TYPE_moof) { + // The data positions may be updated when parsing the tfhd/trun. + fragmentRun.auxiliaryDataPosition = atomPosition; + fragmentRun.dataPosition = atomPosition; + } + if (atomType == Atom.TYPE_mdat) { + endOfMdatPosition = atomPosition + atomSize; if (!haveOutputSeekMap) { extractorOutput.seekMap(SeekMap.UNSEEKABLE); haveOutputSeekMap = true; @@ -324,6 +333,8 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseMoof(Track track, DefaultSampleValues extendsDefaults, ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) { + // This extractor only supports one traf per moof. + Assertions.checkArgument(1 == moof.getChildAtomOfTypeCount(Atom.TYPE_traf)); parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf), out, workaroundFlags, extendedTypeScratch); } @@ -333,6 +344,8 @@ public final class FragmentedMp4Extractor implements Extractor { */ private static void parseTraf(Track track, DefaultSampleValues extendsDefaults, ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) { + // This extractor only supports one trun per traf. + Assertions.checkArgument(1 == traf.getChildAtomOfTypeCount(Atom.TYPE_trun)); LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); long decodeTime; if (tfdtAtom == null || (workaroundFlags & WORKAROUND_IGNORE_TFDT_BOX) != 0) { @@ -342,19 +355,23 @@ public final class FragmentedMp4Extractor implements Extractor { } LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); - DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data); - out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex; + parseTfhd(extendsDefaults, tfhd.data, out); LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun); - parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out); + parseTrun(track, out.header, decodeTime, workaroundFlags, trun.data, out); LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { TrackEncryptionBox trackEncryptionBox = - track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex]; + track.sampleDescriptionEncryptionBoxes[out.header.sampleDescriptionIndex]; parseSaiz(trackEncryptionBox, saiz.data, out); } + LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); + if (saio != null) { + parseSaio(saio.data, out); + } + LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); if (senc != null) { parseSenc(senc.data, out); @@ -401,21 +418,49 @@ public final class FragmentedMp4Extractor implements Extractor { out.initEncryptionData(totalSize); } + /** + * Parses a saio atom (defined in 14496-12). + * + * @param saio The saio atom to parse. + * @param out The track fragment to populate with data from the saio atom. + */ + private static void parseSaio(ParsableByteArray saio, TrackFragment out) { + saio.setPosition(Atom.HEADER_SIZE); + int fullAtom = saio.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + if ((flags & 0x01) == 1) { + saio.skipBytes(8); + } + + int entryCount = saio.readUnsignedIntToInt(); + if (entryCount != 1) { + // We only support one trun element currently, so always expect one entry. + throw new IllegalStateException("Unexpected saio entry count: " + entryCount); + } + + int version = Atom.parseFullAtomVersion(fullAtom); + out.auxiliaryDataPosition += + version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong(); + } + /** * Parses a tfhd atom (defined in 14496-12). * * @param extendsDefaults Default sample values from the trex atom. - * @return The parsed default sample values. + * @param tfhd The tfhd atom to parse. + * @param out The track fragment to populate with data from the tfhd atom. */ - private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults, - ParsableByteArray tfhd) { + private static void parseTfhd(DefaultSampleValues extendsDefaults, ParsableByteArray tfhd, + TrackFragment out) { tfhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tfhd.readInt(); int flags = Atom.parseFullAtomFlags(fullAtom); tfhd.skipBytes(4); // trackId if ((flags & 0x01 /* base_data_offset_present */) != 0) { - tfhd.skipBytes(8); + long baseDataPosition = tfhd.readUnsignedLongToLong(); + out.dataPosition = baseDataPosition; + out.auxiliaryDataPosition = baseDataPosition; } int defaultSampleDescriptionIndex = @@ -427,7 +472,7 @@ public final class FragmentedMp4Extractor implements Extractor { ? tfhd.readUnsignedIntToInt() : extendsDefaults.size; int defaultSampleFlags = ((flags & 0x20 /* default_sample_flags_present */) != 0) ? tfhd.readUnsignedIntToInt() : extendsDefaults.flags; - return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration, + out.header = new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration, defaultSampleSize, defaultSampleFlags); } @@ -461,7 +506,7 @@ public final class FragmentedMp4Extractor implements Extractor { int sampleCount = trun.readUnsignedIntToInt(); if ((flags & 0x01 /* data_offset_present */) != 0) { - trun.skipBytes(4); + out.dataPosition += trun.readInt(); } boolean firstSampleFlagsPresent = (flags & 0x04 /* first_sample_flags_present */) != 0; @@ -610,6 +655,9 @@ public final class FragmentedMp4Extractor implements Extractor { } private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { + int bytesToSkip = (int) (fragmentRun.auxiliaryDataPosition - input.getPosition()); + Assertions.checkState(bytesToSkip >= 0, "Offset to encryption data was negative."); + input.skipFully(bytesToSkip); fragmentRun.fillEncryptionData(input); parserState = STATE_READING_SAMPLE_START; } @@ -629,7 +677,16 @@ public final class FragmentedMp4Extractor implements Extractor { * @throws InterruptedException If the thread is interrupted. */ private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { + if (sampleIndex == 0) { + int bytesToSkip = (int) (fragmentRun.dataPosition - input.getPosition()); + Assertions.checkState(bytesToSkip >= 0, "Offset to sample data was negative."); + input.skipFully(bytesToSkip); + } + if (sampleIndex >= fragmentRun.length) { + int bytesToSkip = (int) (endOfMdatPosition - input.getPosition()); + Assertions.checkState(bytesToSkip >= 0, "Offset to end of mdat was negative."); + input.skipFully(bytesToSkip); // We've run out of samples in the current mdat atom. enterReadingAtomHeaderState(); return false; @@ -687,8 +744,9 @@ public final class FragmentedMp4Extractor implements Extractor { long sampleTimeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L; int sampleFlags = (fragmentRun.definesEncryptionData ? C.SAMPLE_FLAG_ENCRYPTED : 0) | (fragmentRun.sampleIsSyncFrameTable[sampleIndex] ? C.SAMPLE_FLAG_SYNC : 0); + int sampleDescriptionIndex = fragmentRun.header.sampleDescriptionIndex; byte[] encryptionKey = fragmentRun.definesEncryptionData - ? track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex].keyId : null; + ? track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId : null; trackOutput.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); sampleIndex++; @@ -697,8 +755,9 @@ public final class FragmentedMp4Extractor implements Extractor { } private int appendSampleEncryptionData(ParsableByteArray sampleEncryptionData) { + int sampleDescriptionIndex = fragmentRun.header.sampleDescriptionIndex; TrackEncryptionBox encryptionBox = - track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex]; + track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; int vectorSize = encryptionBox.initializationVectorSize; boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex]; @@ -730,8 +789,8 @@ public final class FragmentedMp4Extractor implements Extractor { || atom == Atom.TYPE_traf || atom == Atom.TYPE_trak || atom == Atom.TYPE_trex || atom == Atom.TYPE_trun || atom == Atom.TYPE_mvex || atom == Atom.TYPE_mdia || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_pssh - || atom == Atom.TYPE_saiz || atom == Atom.TYPE_uuid || atom == Atom.TYPE_senc - || atom == Atom.TYPE_pasp || atom == Atom.TYPE_s263; + || atom == Atom.TYPE_saiz || atom == Atom.TYPE_saio || atom == Atom.TYPE_uuid + || atom == Atom.TYPE_senc || atom == Atom.TYPE_pasp || atom == Atom.TYPE_s263; } /** Returns whether the extractor should parse a container atom with type {@code atom}. */ diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackFragment.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackFragment.java index 7cb7badedd..c128495707 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackFragment.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/TrackFragment.java @@ -25,8 +25,18 @@ import java.io.IOException; */ /* package */ final class TrackFragment { - public int sampleDescriptionIndex; - + /** + * The default values for samples from the track fragment header. + */ + public DefaultSampleValues header; + /** + * The position (byte offset) of the start of sample data. + */ + public long dataPosition; + /** + * The position (byte offset) of the start of auxiliary data. + */ + public long auxiliaryDataPosition; /** * The number of samples contained by the fragment. */ From b89339f5f74e32ee5d2ad278f75fb40435a6dc7f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 14 Oct 2015 12:01:30 +0100 Subject: [PATCH 21/22] Update release notes --- RELEASENOTES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index db07bc7076..7e568fcd5e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,13 @@ ### Current dev branch (from r1.5.0) ### +* Enable smooth frame release by default. * Added OkHttpDataSource extension. +* AndroidTV: Correctly detect 4K display size on Bravia devices. +* FMP4: Handle non-sample data in mdat boxes. +* TTML: Fix parsing of some colors on Jellybean. +* SmoothStreaming: Ignore tfdt boxes. +* Misc bug fixes. ### r1.5.0 ### From 0545c58dee9380ead1d483b99e16d8cc041afef5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 14 Oct 2015 12:12:03 +0100 Subject: [PATCH 22/22] Bump version to 1.5.1 --- RELEASENOTES.md | 6 +++++- demo/src/main/AndroidManifest.xml | 4 ++-- demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml | 4 ++-- library/build.gradle | 2 +- .../com/google/android/exoplayer/ExoPlayerLibraryInfo.java | 4 ++-- playbacktests/src/main/AndroidManifest.xml | 4 ++-- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7e568fcd5e..94426695a6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,10 @@ # Release notes # -### Current dev branch (from r1.5.0) ### +### Current dev branch (from r1.5.1) ### + +* [Nothing yet] + +### r1.5.0 ### * Enable smooth frame release by default. * Added OkHttpDataSource extension. diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 230fd31a93..6dd5b055e7 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ diff --git a/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml b/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml index be4dd9c0cf..05f231f072 100644 --- a/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml +++ b/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml @@ -17,8 +17,8 @@ diff --git a/library/build.gradle b/library/build.gradle index 257c5b4a34..a68596bad6 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -77,7 +77,7 @@ publish { userOrg = 'google' groupId = 'com.google.android.exoplayer' artifactId = 'exoplayer' - version = 'r1.5.0' + version = 'r1.5.1' description = 'The ExoPlayer library.' website = 'https://github.com/google/ExoPlayer' } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index f746fd76c8..aad604859e 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -23,7 +23,7 @@ public final class ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - public static final String VERSION = "1.5.0"; + public static final String VERSION = "1.5.1"; /** * The version of the library, expressed as an integer. @@ -31,7 +31,7 @@ public final class ExoPlayerLibraryInfo { * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 001002003. */ - public static final int VERSION_INT = 001005000; + public static final int VERSION_INT = 001005001; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} diff --git a/playbacktests/src/main/AndroidManifest.xml b/playbacktests/src/main/AndroidManifest.xml index 01165915c2..d39b180ad8 100644 --- a/playbacktests/src/main/AndroidManifest.xml +++ b/playbacktests/src/main/AndroidManifest.xml @@ -17,8 +17,8 @@ + android:versionCode="1501" + android:versionName="1.5.1">