diff --git a/RELEASENOTES.md b/RELEASENOTES.md index db07bc7076..94426695a6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,8 +1,18 @@ # 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. +* 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 ### 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' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 1575a1e6b7..6dd5b055e7 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ @@ -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..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 @@ -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() { @@ -150,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); @@ -185,9 +187,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 +611,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; + } + } + } 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/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml b/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml index 72070a4162..05f231f072 100644 --- a/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml +++ b/demo_misc/vp9_opus_sw/src/main/AndroidManifest.xml @@ -16,9 +16,9 @@ @@ -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; 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/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/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/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index 145e151099..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 */ @@ -90,6 +95,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/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/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 9970758c90..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; @@ -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); } /** 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..8c1a7b3030 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,14 @@ 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)) { @@ -202,8 +208,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; } 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..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. @@ -560,6 +489,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/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java index d7e6e0c8d9..9157de7ef2 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java @@ -209,6 +209,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(id, trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, 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/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; 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) { 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/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index 91399644e7..4c7bff673b 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/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java index 291b167971..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 @@ -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}; @@ -81,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; @@ -199,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; @@ -319,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); } @@ -328,23 +344,34 @@ 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 = 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); - 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); @@ -391,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 = @@ -417,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); } @@ -451,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; @@ -475,8 +530,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() @@ -601,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; } @@ -620,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; @@ -678,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++; @@ -688,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]; @@ -721,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/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/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. */ 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; 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 1ca8ec9032..adcaae0741 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"; @@ -74,6 +75,7 @@ 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"; @@ -347,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) { @@ -639,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; @@ -1028,6 +1034,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) @@ -1038,6 +1045,7 @@ 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) @@ -1069,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 { @@ -1147,6 +1160,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: @@ -1194,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; 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 48a40a2868..fbfba8e7e8 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); 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 + "'"); } 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..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 @@ -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"; @@ -45,6 +46,7 @@ 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"; 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+))?" 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 + + + + 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"> 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