diff --git a/RELEASENOTES.md b/RELEASENOTES.md index feaa240220..8ddbe4068c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,9 +1,19 @@ # Release notes # -### r2.1.0 ### +### r2.1.1 ### -This release contains important bug fixes. Users of r2.0.x should proactively -update to this version. +Bugfix release only. Users of r2.1.0 and r2.0.x should proactively update to +this version. + +* Fix some subtitle types (e.g. WebVTT) being displayed out of sync + ([#2208](https://github.com/google/ExoPlayer/issues/2208)). +* Fix incorrect position reporting for on-demand HLS media that includes + EXT-X-PROGRAM-DATE-TIME tags + ([#2224](https://github.com/google/ExoPlayer/issues/2224)). +* Fix issue where playbacks could get stuck in the initial buffering state if + over 1MB of data needs to be read to initialize the playback. + +### r2.1.0 ### * HLS: Support for seeking in live streams ([#87](https://github.com/google/ExoPlayer/issues/87)). diff --git a/build.gradle b/build.gradle index 0ea3ad66f3..358b8f1404 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ allprojects { releaseRepoName = 'exoplayer' releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.1.0' + releaseVersion = 'r2.1.1' releaseWebsite = 'https://github.com/google/ExoPlayer' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index d1b44abafe..4c6d832211 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2101" + android:versionName="2.1.1"> diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 6fa46d7451..5a3015d506 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -183,29 +183,53 @@ "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" }, { - "name": "WV: Secure SD & HD (WebM,VP9)", + "name": "WV: Secure Fullsample SD & HD (WebM,VP9)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure SD (WebM,VP9)", + "name": "WV: Secure Fullsample SD (WebM,VP9)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure HD (WebM,VP9)", + "name": "WV: Secure Fullsample HD (WebM,VP9)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure UHD (WebM,VP9)", + "name": "WV: Secure Fullsample UHD (WebM,VP9)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, + { + "name": "WV: Secure Subsample SD & HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample SD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample UHD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, { "name": "WV: Secure Subsample (WebM, VP9 with altref)", "uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_subsample/sintel_1080p_vp9_altref_subsample.mpd", diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index be18d64195..0f6f3b07b1 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -48,12 +49,25 @@ public final class ExoPlayerTest extends TestCase { */ private static final int TIMEOUT_MS = 10000; + /** + * Tests playback of a source that exposes a single period. + */ public void testPlayToEnd() throws Exception { PlayerWrapper playerWrapper = new PlayerWrapper(); Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null); - playerWrapper.setup(new SinglePeriodTimeline(0, false), new Object(), format); - playerWrapper.blockUntilEndedOrError(TIMEOUT_MS); + playerWrapper.setup(new SinglePeriodTimeline(0, false), null, format); + playerWrapper.blockUntilEnded(TIMEOUT_MS); + } + + /** + * Tests playback of a source that exposes an empty timeline. Playback is expected to end without + * error. + */ + public void testPlayEmptyTimeline() throws Exception { + PlayerWrapper playerWrapper = new PlayerWrapper(); + playerWrapper.setup(Timeline.EMPTY, null, null); + playerWrapper.blockUntilEnded(TIMEOUT_MS); } /** @@ -70,7 +84,6 @@ public final class ExoPlayerTest extends TestCase { private Format expectedFormat; private ExoPlayer player; private Exception exception; - private boolean seenPositionDiscontinuity; public PlayerWrapper() { endedCountDownLatch = new CountDownLatch(1); @@ -81,12 +94,11 @@ public final class ExoPlayerTest extends TestCase { // Called on the test thread. - public void blockUntilEndedOrError(long timeoutMs) throws Exception { + public void blockUntilEnded(long timeoutMs) throws Exception { if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out."); } release(); - // Throw any pending exception (from playback, timing out or releasing). if (exception != null) { throw exception; @@ -108,7 +120,7 @@ public final class ExoPlayerTest extends TestCase { player.setPlayWhenReady(true); player.prepare(new FakeMediaSource(timeline, manifest, format)); } catch (Exception e) { - handlePlayerException(e); + handleError(e); } } }); @@ -123,7 +135,7 @@ public final class ExoPlayerTest extends TestCase { player.release(); } } catch (Exception e) { - handlePlayerException(e); + handleError(e); } finally { playerThread.quit(); } @@ -132,7 +144,7 @@ public final class ExoPlayerTest extends TestCase { playerThread.join(); } - private void handlePlayerException(Exception exception) { + private void handleError(Exception exception) { if (this.exception == null) { this.exception = exception; } @@ -167,20 +179,13 @@ public final class ExoPlayerTest extends TestCase { @Override public void onPlayerError(ExoPlaybackException exception) { - this.exception = exception; - endedCountDownLatch.countDown(); + handleError(exception); } @Override public void onPositionDiscontinuity() { - assertFalse(seenPositionDiscontinuity); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(0, player.getCurrentPeriodIndex()); - assertEquals(0, player.getCurrentPosition()); - assertEquals(0, player.getBufferedPosition()); - assertEquals(expectedTimeline, player.getCurrentTimeline()); - assertEquals(expectedManifest, player.getCurrentManifest()); - seenPositionDiscontinuity = true; + // Should never happen. + handleError(new IllegalStateException("Received position discontinuity")); } } @@ -194,17 +199,16 @@ public final class ExoPlayerTest extends TestCase { private final Timeline timeline; private final Object manifest; private final Format format; + private final ArrayList activeMediaPeriods; - private FakeMediaPeriod mediaPeriod; private boolean preparedSource; - private boolean releasedPeriod; private boolean releasedSource; public FakeMediaSource(Timeline timeline, Object manifest, Format format) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); this.timeline = timeline; this.manifest = manifest; this.format = format; + activeMediaPeriods = new ArrayList<>(); } @Override @@ -221,33 +225,29 @@ public final class ExoPlayerTest extends TestCase { @Override public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + Assertions.checkIndex(index, 0, timeline.getPeriodCount()); assertTrue(preparedSource); - assertNull(mediaPeriod); - assertFalse(releasedPeriod); assertFalse(releasedSource); assertEquals(0, index); assertEquals(0, positionUs); - mediaPeriod = new FakeMediaPeriod(format); + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(format); + activeMediaPeriods.add(mediaPeriod); return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { assertTrue(preparedSource); - assertNotNull(this.mediaPeriod); - assertFalse(releasedPeriod); assertFalse(releasedSource); - assertEquals(this.mediaPeriod, mediaPeriod); - this.mediaPeriod.release(); - releasedPeriod = true; + assertTrue(activeMediaPeriods.remove(mediaPeriod)); + ((FakeMediaPeriod) mediaPeriod).release(); } @Override public void releaseSource() { assertTrue(preparedSource); - assertNotNull(this.mediaPeriod); - assertTrue(releasedPeriod); assertFalse(releasedSource); + assertTrue(activeMediaPeriods.isEmpty()); releasedSource = true; } @@ -400,7 +400,6 @@ public final class ExoPlayerTest extends TestCase { public FakeVideoRenderer(Format expectedFormat) { super(C.TRACK_TYPE_VIDEO); - Assertions.checkArgument(MimeTypes.isVideo(expectedFormat.sampleMimeType)); this.expectedFormat = expectedFormat; } diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 447e39bf52..514bbca8f4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -267,6 +267,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ; } buffer.timeUs += streamOffsetUs; + } else if (result == C.RESULT_FORMAT_READ) { + Format format = formatHolder.format; + if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { + format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs); + formatHolder.format = format; + } } return result; } diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 3e6fac4a5e..5cef177517 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -96,6 +96,13 @@ public final class C { @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; + /** + * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to + * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. + */ + @SuppressWarnings("InlinedApi") + public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; + /** * Represents an audio encoding, or an invalid or unset value. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index a7cbeb524c..ab4e59e08f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -332,8 +332,10 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SEEK_ACK: { if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(); + if (msg.arg1 != 0) { + for (EventListener listener : listeners) { + listener.onPositionDiscontinuity(); + } } } break; diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 66be6b7478..afae56f1aa 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -559,7 +559,7 @@ import java.io.IOException; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. playbackInfo = new PlaybackInfo(0, 0); - eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't // ignored. playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); @@ -569,6 +569,7 @@ import java.io.IOException; return; } + boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; int periodIndex = periodPosition.first; long periodPositionUs = periodPosition.second; @@ -578,10 +579,13 @@ import java.io.IOException; // Seek position equals the current position. Do nothing. return; } - periodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); + long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); + seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; + periodPositionUs = newPeriodPositionUs; } finally { playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); - eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) + .sendToTarget(); } } @@ -676,6 +680,7 @@ import java.io.IOException; standaloneMediaClock.stop(); rendererMediaClock = null; rendererMediaClockSource = null; + rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { try { ensureStopped(renderer); @@ -823,9 +828,6 @@ import java.io.IOException; } private boolean haveSufficientBuffer(boolean rebuffering) { - if (loadingPeriodHolder == null) { - return false; - } long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared ? loadingPeriodHolder.startPositionUs : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); @@ -1287,7 +1289,8 @@ import java.io.IOException; } private void maybeContinueLoading() { - long nextLoadPositionUs = loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); + long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0 + : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { setIsLoading(false); } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 45f63d713d..ea522ac4c8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - String VERSION = "2.1.0"; + String VERSION = "2.1.1"; /** * The version of the library, expressed as an integer. @@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2001000; + int VERSION_INT = 2001001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 36753309e2..73df6a1e7a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -29,7 +29,6 @@ import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -178,7 +177,7 @@ public class SimpleExoPlayer implements ExoPlayer { // Set initial values. audioVolume = 1; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioStreamType = C.STREAM_TYPE_DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; @@ -393,7 +392,7 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Returns the audio session identifier, or {@code AudioTrack.SESSION_ID_NOT_SET} if not set. + * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */ public int getAudioSessionId() { return audioSessionId; @@ -949,7 +948,7 @@ public class SimpleExoPlayer implements ExoPlayer { } audioFormat = null; audioDecoderCounters = null; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; } // TextRenderer.Output implementation diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 8e6cf68dc8..072180db94 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioTimestamp; import android.media.PlaybackParams; @@ -30,26 +31,28 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles * playback position smoothing, non-blocking writes and reconfiguration. *

* Before starting playback, specify the input format by calling - * {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)}, optionally - * specifying an audio session. + * {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)} or + * {@link #initializeV21(int, boolean)}, optionally specifying an audio session and whether the + * track is to be used with tunneling video playback. *

* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. *

* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If * {@link #isInitialized()} returns {@code false} after the call, it is necessary to call - * {@link #initialize(int)} before writing more data. + * {@link #initialize(int)} or {@link #initializeV21(int, boolean)} before writing more data. *

* The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and * released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input - * format is unchanged). It is safe to call {@link #initialize(int)} after calling {@link #reset()} - * without reconfiguration. + * format is unchanged). It is safe to call {@link #initialize(int)} or + * {@link #initializeV21(int, boolean)} after calling {@link #reset()} without reconfiguration. *

* Call {@link #release()} when the instance is no longer required. */ @@ -143,11 +146,6 @@ public final class AudioTrack { */ public static final int RESULT_BUFFER_CONSUMED = 2; - /** - * Represents an unset {@link android.media.AudioTrack} session identifier. - */ - public static final int SESSION_ID_NOT_SET = 0; - /** * Returned by {@link #getCurrentPositionUs} when the position is not set. */ @@ -273,6 +271,10 @@ public final class AudioTrack { private int bufferSize; private long bufferSizeUs; + private boolean useHwAvSync; + private ByteBuffer avSyncHeader; + private int bytesUntilNextAvSync; + private int nextPlayheadOffsetIndex; private int playheadOffsetCount; private long smoothedPlayheadOffsetUs; @@ -341,8 +343,8 @@ public final class AudioTrack { } /** - * Returns whether the audio track has been successfully initialized via {@link #initialize} and - * not yet {@link #reset}. + * Returns whether the audio track has been successfully initialized via {@link #initialize} or + * {@link #initializeV21(int, boolean)}, and has not yet been {@link #reset}. */ public boolean isInitialized() { return audioTrack != null; @@ -442,6 +444,21 @@ public final class AudioTrack { throw new IllegalArgumentException("Unsupported channel count: " + channelCount); } + // Workaround for overly strict channel configuration checks on nVidia Shield. + if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { + switch(channelCount) { + case 7: + channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; + break; + case 3: + case 5: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + break; + default: + break; + } + } + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); @C.Encoding int sourceEncoding; if (passthrough) { @@ -498,11 +515,28 @@ public final class AudioTrack { /** * Initializes the audio track for writing new buffers using {@link #handleBuffer}. * - * @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to - * create a new one. - * @return The new (or re-used) session identifier. + * @param sessionId Audio track session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} to create + * one. + * @return The audio track session identifier. */ public int initialize(int sessionId) throws InitializationException { + return initializeInternal(sessionId, false); + } + + /** + * Initializes the audio track for writing new buffers using {@link #handleBuffer}. + * + * @param sessionId Audio track session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} to create + * one. + * @param tunneling Whether the audio track is to be used with tunneling video playback. + * @return The audio track session identifier. + */ + public int initializeV21(int sessionId, boolean tunneling) throws InitializationException { + Assertions.checkState(Util.SDK_INT >= 21); + return initializeInternal(sessionId, tunneling); + } + + private int initializeInternal(int sessionId, boolean tunneling) throws InitializationException { // If we're asynchronously releasing a previous audio track then we block until it has been // released. This guarantees that we cannot end up in a state where we have multiple audio // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust @@ -510,7 +544,11 @@ public final class AudioTrack { // initialization of the audio track to fail. releasingConditionVariable.block(); - if (sessionId == SESSION_ID_NOT_SET) { + useHwAvSync = tunneling; + if (useHwAvSync) { + audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding, + bufferSize, sessionId); + } else if (sessionId == C.AUDIO_SESSION_ID_UNSET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, targetEncoding, bufferSize, MODE_STREAM); } else { @@ -692,7 +730,9 @@ public final class AudioTrack { buffer.position(buffer.position() + bytesWritten); } } else { - bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + bytesWritten = useHwAvSync + ? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs) + : writeNonBlockingV21(audioTrack, buffer, bytesRemaining); } if (bytesWritten < 0) { @@ -718,6 +758,7 @@ public final class AudioTrack { public void handleEndOfStream() { if (isInitialized()) { audioTrackUtil.handleEndOfStream(getSubmittedFrames()); + bytesUntilNextAvSync = 0; } } @@ -743,19 +784,27 @@ public final class AudioTrack { } /** - * Sets the stream type for audio track. If the stream type has changed, {@link #isInitialized()} - * will return {@code false} and the caller must re-{@link #initialize(int)} the audio track - * before writing more data. The caller must not reuse the audio session identifier when - * re-initializing with a new stream type. + * Sets the stream type for audio track. If the stream type has changed and if the audio track + * is not configured for use with video tunneling, then the audio track is reset and the caller + * must re-initialize the audio track before writing more data. The caller must not reuse the + * audio session identifier when re-initializing with a new stream type. + *

+ * If the audio track is configured for use with video tunneling then the stream type is ignored + * and the audio track is not reset. The passed stream type will be used if the audio track is + * later re-configured into non-tunneled mode. * * @param streamType The {@link C.StreamType} to use for audio output. - * @return Whether the stream type changed. + * @return Whether the audio track was reset as a result of this call. */ public boolean setStreamType(@C.StreamType int streamType) { if (this.streamType == streamType) { return false; } this.streamType = streamType; + if (useHwAvSync) { + // The stream type is ignored in tunneling mode, so no need to reset. + return false; + } reset(); return true; } @@ -795,9 +844,9 @@ public final class AudioTrack { /** * Releases the underlying audio track asynchronously. *

- * Calling {@link #initialize(int)} will block until the audio track has been released, so it is - * safe to initialize immediately after a reset. The audio session may remain active until - * {@link #release()} is called. + * Calling {@link #initialize(int)} or {@link #initializeV21(int, boolean)} will block until the + * audio track has been released, so it is safe to initialize immediately after a reset. The audio + * session may remain active until {@link #release()} is called. */ public void reset() { if (isInitialized()) { @@ -805,6 +854,7 @@ public final class AudioTrack { submittedEncodedFrames = 0; framesPerEncodedSample = 0; currentSourceBuffer = null; + avSyncHeader = null; startMediaTimeState = START_NOT_SET; latencyUs = 0; resetSyncParams(); @@ -1020,6 +1070,26 @@ public final class AudioTrack { && audioTrack.getPlaybackHeadPosition() == 0; } + /** + * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback. + */ + @TargetApi(21) + private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate, + int channelConfig, int encoding, int bufferSize, int sessionId) { + AudioAttributes attributesBuilder = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(AudioAttributes.FLAG_HW_AV_SYNC) + .build(); + AudioFormat format = new AudioFormat.Builder() + .setChannelMask(channelConfig) + .setEncoding(encoding) + .setSampleRate(sampleRate) + .build(); + return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM, + sessionId); + } + /** * Converts the provided buffer into 16-bit PCM. * @@ -1125,11 +1195,50 @@ public final class AudioTrack { } @TargetApi(21) - private static int writeNonBlockingV21( - android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { + private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer, + int size) { return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); } + @TargetApi(21) + private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack, + ByteBuffer buffer, int size, long presentationTimeUs) { + // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed. + // if (Util.SDK_INT >= 23) { + // // The underlying platform AudioTrack writes AV sync headers directly. + // return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); + // } + if (avSyncHeader == null) { + avSyncHeader = ByteBuffer.allocate(16); + avSyncHeader.order(ByteOrder.BIG_ENDIAN); + avSyncHeader.putInt(0x55550001); + } + if (bytesUntilNextAvSync == 0) { + avSyncHeader.putInt(4, size); + avSyncHeader.putLong(8, presentationTimeUs * 1000); + avSyncHeader.position(0); + bytesUntilNextAvSync = size; + } + int avSyncHeaderBytesRemaining = avSyncHeader.remaining(); + if (avSyncHeaderBytesRemaining > 0) { + int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + if (result < avSyncHeaderBytesRemaining) { + return 0; + } + } + int result = writeNonBlockingV21(audioTrack, buffer, size); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + bytesUntilNextAvSync -= result; + return result; + } + @TargetApi(21) private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) { audioTrack.setVolume(volume); diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 648bfd5762..d3cde10afb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -129,7 +129,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media boolean playClearSamplesWithoutKeys, Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioTrack = new AudioTrack(audioCapabilities, this); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -274,7 +274,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onDisabled() { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; try { audioTrack.release(); } finally { @@ -328,8 +328,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (!audioTrack.isInitialized()) { // Initialize the AudioTrack now. try { - if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) { - audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET); + if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { + audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET); eventDispatcher.audioSessionId(audioSessionId); onAudioSessionId(audioSessionId); } else { @@ -387,7 +387,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media case C.MSG_SET_STREAM_TYPE: @C.StreamType int streamType = (Integer) message; if (audioTrack.setStreamType(streamType)) { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; } break; default: diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 572f7b54c1..5c9acc7739 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -145,7 +145,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements this.drmSessionManager = drmSessionManager; formatHolder = new FormatHolder(); this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; } @@ -245,8 +245,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } if (!audioTrack.isInitialized()) { - if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) { - audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET); + if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { + audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET); eventDispatcher.audioSessionId(audioSessionId); onAudioSessionId(audioSessionId); } else { @@ -425,7 +425,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onDisabled() { inputFormat = null; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioTrackNeedsConfigure = true; waitingForKeys = false; try { @@ -554,7 +554,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements case C.MSG_SET_STREAM_TYPE: @C.StreamType int streamType = (Integer) message; if (audioTrack.setStreamType(streamType)) { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; } break; default: diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 0b7190d382..8ab4d45c47 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -244,7 +244,7 @@ import java.io.IOException; @Override public long getNextLoadPositionUs() { - return getBufferedPositionUs(); + return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs(); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index a3c1c88df4..f4a9665b10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -133,4 +133,32 @@ public interface MediaPeriod extends SequenceableLoader { */ long seekToUs(long positionUs); + // SequenceableLoader interface. Overridden to provide more specific documentation. + + /** + * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. + *

+ * This method should only be called after the period has been prepared. It may be called when no + * tracks are selected. + */ + @Override + long getNextLoadPositionUs(); + + /** + * Attempts to continue loading. + *

+ * This method may be called both during and after the period has been prepared. + *

+ * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the + * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called + * when the period is permitted to continue loading data. A period may do this both during and + * after preparation. + * + * @param positionUs The current playback position. + * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return + * a different value than prior to the call. False otherwise. + */ + @Override + boolean continueLoading(long positionUs); + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 2f46fc694c..869efa6cdc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -104,15 +104,14 @@ public final class HlsMediaSource implements MediaSource, SinglePeriodTimeline timeline; if (playlistTracker.isLive()) { // TODO: fix windowPositionInPeriodUs when playlist is empty. - long windowPositionInPeriodUs = playlist.startTimeUs; List segments = playlist.segments; long windowDefaultStartPositionUs = segments.isEmpty() ? 0 : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs, - windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); + playlist.startTimeUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); } else /* not live */ { - timeline = new SinglePeriodTimeline(playlist.durationUs, playlist.durationUs, 0, 0, true, - false); + timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs, + playlist.durationUs, playlist.startTimeUs, 0, true, false); } sourceListener.onSourceInfoRefreshed(timeline, playlist); } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java index 1c29f10c84..c4c5a7e4ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import android.graphics.Color; import android.support.annotation.IntDef; import android.text.Layout.Alignment; import java.lang.annotation.Retention; @@ -36,19 +37,23 @@ public class Cue { @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END}) public @interface AnchorType {} + /** * An unset anchor or line type value. */ public static final int TYPE_UNSET = Integer.MIN_VALUE; + /** * Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue * box. */ public static final int ANCHOR_TYPE_START = 0; + /** * Anchors the middle of the cue box. */ public static final int ANCHOR_TYPE_MIDDLE = 1; + /** * Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue * box. @@ -61,10 +66,12 @@ public class Cue { @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNSET, LINE_TYPE_FRACTION, LINE_TYPE_NUMBER}) public @interface LineType {} + /** * Value for {@link #lineType} when {@link #line} is a fractional position. */ public static final int LINE_TYPE_FRACTION = 0; + /** * Value for {@link #lineType} when {@link #line} is a line number. */ @@ -74,10 +81,12 @@ public class Cue { * The cue text. Note the {@link CharSequence} may be decorated with styling spans. */ public final CharSequence text; + /** * The alignment of the cue text within the cue box, or null if the alignment is undefined. */ public final Alignment textAlignment; + /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of @@ -86,6 +95,7 @@ public class Cue { * For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the * fractional vertical position relative to the top of the viewport. */ + public final float line; /** * The type of the {@link #line} value. @@ -112,6 +122,7 @@ public class Cue { * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first * line is visible at the bottom of the viewport. */ + @LineType public final int lineType; /** @@ -122,6 +133,7 @@ public class Cue { * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box * respectively. */ + @AnchorType public final int lineAnchor; /** @@ -133,6 +145,7 @@ public class Cue { * text. */ public final float position; + /** * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. @@ -143,12 +156,23 @@ public class Cue { */ @AnchorType public final int positionAnchor; + /** * The size of the cue box in the writing direction specified as a fraction of the viewport size * in that direction, or {@link #DIMEN_UNSET}. */ public final float size; + /** + * Specifies whether or not the {@link #windowColor} property is set. + */ + public final boolean windowColorSet; + + /** + * The fill color of the window. + */ + public final int windowColor; + /** * Constructs a cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. @@ -171,6 +195,25 @@ public class Cue { */ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { + this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false, + Color.BLACK); + } + + /** + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param windowColorSet See {@link #windowColorSet}. + * @param windowColor See {@link #windowColor}. + */ + public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, + @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, + boolean windowColorSet, int windowColor) { this.text = text; this.textAlignment = textAlignment; this.line = line; @@ -179,6 +222,8 @@ public class Cue { this.position = position; this.positionAnchor = positionAnchor; this.size = size; + this.windowColorSet = windowColorSet; + this.windowColor = windowColor; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index d094266fcc..8ac0c64082 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -65,6 +65,13 @@ import java.util.List; *

  • Default: {@code true}
  • * * + *
  • {@code default_artwork} - Default artwork to use if no artwork available in audio + * streams. + *
      + *
    • Corresponding method: {@link #setDefaultArtwork(Bitmap)}
    • + *
    • Default: {@code null}
    • + *
    + *
  • *
  • {@code use_controller} - Whether playback controls are displayed. *
      *
    • Corresponding method: {@link #setUseController(boolean)}
    • @@ -179,6 +186,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private SimpleExoPlayer player; private boolean useController; private boolean useArtwork; + private Bitmap defaultArtwork; private int controllerShowTimeoutMs; public SimpleExoPlayerView(Context context) { @@ -194,6 +202,7 @@ public final class SimpleExoPlayerView extends FrameLayout { int playerLayoutId = R.layout.exo_simple_player_view; boolean useArtwork = true; + int defaultArtwork = 0; boolean useController = true; int surfaceType = SURFACE_TYPE_SURFACE_VIEW; int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; @@ -205,6 +214,8 @@ public final class SimpleExoPlayerView extends FrameLayout { playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); + defaultArtwork = a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork, + defaultArtwork); useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController); surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType); resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode); @@ -246,6 +257,9 @@ public final class SimpleExoPlayerView extends FrameLayout { // Artwork view. artworkView = (ImageView) findViewById(R.id.exo_artwork); this.useArtwork = useArtwork && artworkView != null; + if (defaultArtwork != 0) { + this.defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtwork); + } // Subtitle view. subtitleView = (SubtitleView) findViewById(R.id.exo_subtitles); @@ -351,6 +365,26 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + /** + * Returns the default artwork to display. + */ + public Bitmap getDefaultArtwork() { + return defaultArtwork; + } + + /** + * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is + * present in the media. + * + * @param defaultArtwork the default artwork to display. + */ + public void setDefaultArtwork(Bitmap defaultArtwork) { + if (this.defaultArtwork != defaultArtwork) { + this.defaultArtwork = defaultArtwork; + updateForCurrentTrackSelections(); + } + } + /** * Returns whether the playback controls are enabled. */ @@ -569,6 +603,9 @@ public final class SimpleExoPlayerView extends FrameLayout { } } } + if (setArtworkFromBitmap(defaultArtwork)) { + return; + } } // Artwork disabled or unavailable. hideArtwork(); @@ -580,18 +617,23 @@ public final class SimpleExoPlayerView extends FrameLayout { if (metadataEntry instanceof ApicFrame) { byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - if (bitmap != null) { - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getHeight(); - if (bitmapWidth > 0 && bitmapHeight > 0) { - if (contentFrame != null) { - contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); - } - artworkView.setImageBitmap(bitmap); - artworkView.setVisibility(VISIBLE); - return true; - } + return setArtworkFromBitmap(bitmap); + } + } + return false; + } + + private boolean setArtworkFromBitmap(Bitmap bitmap) { + if (bitmap != null) { + int bitmapWidth = bitmap.getWidth(); + int bitmapHeight = bitmap.getHeight(); + if (bitmapWidth > 0 && bitmapHeight > 0) { + if (contentFrame != null) { + contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); } + artworkView.setImageBitmap(bitmap); + artworkView.setVisibility(VISIBLE); + return true; } } return false; diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index de461ecf0d..04f3b986bd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -146,9 +146,13 @@ import com.google.android.exoplayer2.util.Util; // Nothing to draw. return; } + + int windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; + if (!applyEmbeddedStyles) { // Strip out any embedded styling. cueText = cueText.toString(); + windowColor = style.windowColor; } if (areCharSequencesEqual(this.cueText, cueText) && Util.areEqual(this.cueTextAlignment, cue.textAlignment) @@ -161,7 +165,7 @@ import com.google.android.exoplayer2.util.Util; && this.applyEmbeddedStyles == applyEmbeddedStyles && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor - && this.windowColor == style.windowColor + && this.windowColor == windowColor && this.edgeType == style.edgeType && this.edgeColor == style.edgeColor && Util.areEqual(this.textPaint.getTypeface(), style.typeface) @@ -187,7 +191,7 @@ import com.google.android.exoplayer2.util.Util; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; - this.windowColor = style.windowColor; + this.windowColor = windowColor; this.edgeType = style.edgeType; this.edgeColor = style.edgeColor; this.textPaint.setTypeface(style.typeface); diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 2a13953106..f68b72fb65 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -403,7 +403,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - if (earlyUs < -30000) { + if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { // We're more than 30ms late rendering the frame. dropOutputBuffer(codec, bufferIndex); return true; @@ -437,6 +437,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } + /** + * Returns true if the current frame should be dropped. + * + * @param earlyUs Time indicating how early the frame is. Negative values indicate late frame. + * @param elapsedRealtimeUs Wall clock time. + */ + protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { + // Drop the frame if we're more than 30ms late rendering the frame. + return earlyUs < -30000; + } + private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { TraceUtil.beginSection("skipVideoBuffer"); codec.releaseOutputBuffer(bufferIndex, false); diff --git a/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png index c65956ab7f..843df84091 100644 Binary files a/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-hdpi/exo_controls_fastforward.png differ diff --git a/library/src/main/res/drawable-hdpi/exo_controls_next.png b/library/src/main/res/drawable-hdpi/exo_controls_next.png index 6e27b8161e..c37541472e 100644 Binary files a/library/src/main/res/drawable-hdpi/exo_controls_next.png and b/library/src/main/res/drawable-hdpi/exo_controls_next.png differ diff --git a/library/src/main/res/drawable-hdpi/exo_controls_pause.png b/library/src/main/res/drawable-hdpi/exo_controls_pause.png index 1d465a41e4..0a23452746 100644 Binary files a/library/src/main/res/drawable-hdpi/exo_controls_pause.png and b/library/src/main/res/drawable-hdpi/exo_controls_pause.png differ diff --git a/library/src/main/res/drawable-hdpi/exo_controls_play.png b/library/src/main/res/drawable-hdpi/exo_controls_play.png index 2746d17fb1..e98e2b9cbe 100644 Binary files a/library/src/main/res/drawable-hdpi/exo_controls_play.png and b/library/src/main/res/drawable-hdpi/exo_controls_play.png differ diff --git a/library/src/main/res/drawable-hdpi/exo_controls_previous.png b/library/src/main/res/drawable-hdpi/exo_controls_previous.png index 85b3766904..3eae5c883b 100644 Binary files a/library/src/main/res/drawable-hdpi/exo_controls_previous.png and b/library/src/main/res/drawable-hdpi/exo_controls_previous.png differ diff --git a/library/src/main/res/drawable-hdpi/exo_controls_rewind.png b/library/src/main/res/drawable-hdpi/exo_controls_rewind.png index a4ac181777..36537d3b73 100644 Binary files a/library/src/main/res/drawable-hdpi/exo_controls_rewind.png and b/library/src/main/res/drawable-hdpi/exo_controls_rewind.png differ diff --git a/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png b/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png index 1b4d9dbef9..19b9e6015c 100644 Binary files a/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-ldpi/exo_controls_fastforward.png differ diff --git a/library/src/main/res/drawable-ldpi/exo_controls_next.png b/library/src/main/res/drawable-ldpi/exo_controls_next.png index 99927fd27b..d4872037aa 100644 Binary files a/library/src/main/res/drawable-ldpi/exo_controls_next.png and b/library/src/main/res/drawable-ldpi/exo_controls_next.png differ diff --git a/library/src/main/res/drawable-ldpi/exo_controls_pause.png b/library/src/main/res/drawable-ldpi/exo_controls_pause.png index 3b98d66688..616ec42f39 100644 Binary files a/library/src/main/res/drawable-ldpi/exo_controls_pause.png and b/library/src/main/res/drawable-ldpi/exo_controls_pause.png differ diff --git a/library/src/main/res/drawable-ldpi/exo_controls_play.png b/library/src/main/res/drawable-ldpi/exo_controls_play.png index e7c19724bb..5d1c702892 100644 Binary files a/library/src/main/res/drawable-ldpi/exo_controls_play.png and b/library/src/main/res/drawable-ldpi/exo_controls_play.png differ diff --git a/library/src/main/res/drawable-ldpi/exo_controls_previous.png b/library/src/main/res/drawable-ldpi/exo_controls_previous.png index df043228d0..930534d312 100644 Binary files a/library/src/main/res/drawable-ldpi/exo_controls_previous.png and b/library/src/main/res/drawable-ldpi/exo_controls_previous.png differ diff --git a/library/src/main/res/drawable-ldpi/exo_controls_rewind.png b/library/src/main/res/drawable-ldpi/exo_controls_rewind.png index 28843f9fb0..83d71782f6 100644 Binary files a/library/src/main/res/drawable-ldpi/exo_controls_rewind.png and b/library/src/main/res/drawable-ldpi/exo_controls_rewind.png differ diff --git a/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png index 170dd2daaa..ee3efe1d69 100644 Binary files a/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-mdpi/exo_controls_fastforward.png differ diff --git a/library/src/main/res/drawable-mdpi/exo_controls_next.png b/library/src/main/res/drawable-mdpi/exo_controls_next.png index fcd73d90e7..9d4d7469ed 100644 Binary files a/library/src/main/res/drawable-mdpi/exo_controls_next.png and b/library/src/main/res/drawable-mdpi/exo_controls_next.png differ diff --git a/library/src/main/res/drawable-mdpi/exo_controls_pause.png b/library/src/main/res/drawable-mdpi/exo_controls_pause.png index 3e6b2a17b5..f54c942201 100644 Binary files a/library/src/main/res/drawable-mdpi/exo_controls_pause.png and b/library/src/main/res/drawable-mdpi/exo_controls_pause.png differ diff --git a/library/src/main/res/drawable-mdpi/exo_controls_play.png b/library/src/main/res/drawable-mdpi/exo_controls_play.png index 7966bbc516..dd0c142859 100644 Binary files a/library/src/main/res/drawable-mdpi/exo_controls_play.png and b/library/src/main/res/drawable-mdpi/exo_controls_play.png differ diff --git a/library/src/main/res/drawable-mdpi/exo_controls_previous.png b/library/src/main/res/drawable-mdpi/exo_controls_previous.png index b653d05b9f..950e213d2f 100644 Binary files a/library/src/main/res/drawable-mdpi/exo_controls_previous.png and b/library/src/main/res/drawable-mdpi/exo_controls_previous.png differ diff --git a/library/src/main/res/drawable-mdpi/exo_controls_rewind.png b/library/src/main/res/drawable-mdpi/exo_controls_rewind.png index 5489180eb1..e75efae189 100644 Binary files a/library/src/main/res/drawable-mdpi/exo_controls_rewind.png and b/library/src/main/res/drawable-mdpi/exo_controls_rewind.png differ diff --git a/library/src/main/res/drawable-v21/exo_controls_fastforward.xml b/library/src/main/res/drawable-v21/exo_controls_fastforward.xml new file mode 100644 index 0000000000..945d70eef3 --- /dev/null +++ b/library/src/main/res/drawable-v21/exo_controls_fastforward.xml @@ -0,0 +1,8 @@ + + + + diff --git a/library/src/main/res/drawable-v21/exo_controls_next.xml b/library/src/main/res/drawable-v21/exo_controls_next.xml new file mode 100644 index 0000000000..c96f1e4afe --- /dev/null +++ b/library/src/main/res/drawable-v21/exo_controls_next.xml @@ -0,0 +1,8 @@ + + + + diff --git a/library/src/main/res/drawable-v21/exo_controls_pause.xml b/library/src/main/res/drawable-v21/exo_controls_pause.xml new file mode 100644 index 0000000000..69a17cb9a1 --- /dev/null +++ b/library/src/main/res/drawable-v21/exo_controls_pause.xml @@ -0,0 +1,8 @@ + + + + diff --git a/library/src/main/res/drawable-v21/exo_controls_play.xml b/library/src/main/res/drawable-v21/exo_controls_play.xml new file mode 100644 index 0000000000..c21978f7f2 --- /dev/null +++ b/library/src/main/res/drawable-v21/exo_controls_play.xml @@ -0,0 +1,8 @@ + + + + diff --git a/library/src/main/res/drawable-v21/exo_controls_previous.xml b/library/src/main/res/drawable-v21/exo_controls_previous.xml new file mode 100644 index 0000000000..c5789d5fe4 --- /dev/null +++ b/library/src/main/res/drawable-v21/exo_controls_previous.xml @@ -0,0 +1,8 @@ + + + + diff --git a/library/src/main/res/drawable-v21/exo_controls_rewind.xml b/library/src/main/res/drawable-v21/exo_controls_rewind.xml new file mode 100644 index 0000000000..6ecb8f8a27 --- /dev/null +++ b/library/src/main/res/drawable-v21/exo_controls_rewind.xml @@ -0,0 +1,8 @@ + + + + diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png index 60f7e92181..ead712cfe9 100644 Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png differ diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_next.png b/library/src/main/res/drawable-xhdpi/exo_controls_next.png index 4def965cec..bc1ebf83c5 100644 Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_next.png and b/library/src/main/res/drawable-xhdpi/exo_controls_next.png differ diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_pause.png b/library/src/main/res/drawable-xhdpi/exo_controls_pause.png index 6bd3d482e1..1c868f1831 100644 Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_pause.png and b/library/src/main/res/drawable-xhdpi/exo_controls_pause.png differ diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_play.png b/library/src/main/res/drawable-xhdpi/exo_controls_play.png index ccfef18056..f2f934413e 100644 Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_play.png and b/library/src/main/res/drawable-xhdpi/exo_controls_play.png differ diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_previous.png b/library/src/main/res/drawable-xhdpi/exo_controls_previous.png index c4472ae2d9..d197eff873 100644 Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_previous.png and b/library/src/main/res/drawable-xhdpi/exo_controls_previous.png differ diff --git a/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png b/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png index 167d10e58b..3340ef9bd2 100644 Binary files a/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png and b/library/src/main/res/drawable-xhdpi/exo_controls_rewind.png differ diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png index ab9e022fbf..e1c6cae292 100644 Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_fastforward.png differ diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_next.png b/library/src/main/res/drawable-xxhdpi/exo_controls_next.png index ce0a14325a..232f09e910 100644 Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_next.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_next.png differ diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png b/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png index 9a36b17cb8..50a545db4d 100644 Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_pause.png differ diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_play.png b/library/src/main/res/drawable-xxhdpi/exo_controls_play.png index 41f76bbf99..08508c5015 100644 Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_play.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_play.png differ diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png b/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png index d4688741b9..f71acc4875 100644 Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_previous.png differ diff --git a/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png b/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png index 8ebb2ccf30..db0555f9e5 100644 Binary files a/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png and b/library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png differ diff --git a/library/src/main/res/drawable-xxxhdpi/exo_controls_fastforward.png b/library/src/main/res/drawable-xxxhdpi/exo_controls_fastforward.png new file mode 100644 index 0000000000..af0435a8b7 Binary files /dev/null and b/library/src/main/res/drawable-xxxhdpi/exo_controls_fastforward.png differ diff --git a/library/src/main/res/drawable-xxxhdpi/exo_controls_next.png b/library/src/main/res/drawable-xxxhdpi/exo_controls_next.png new file mode 100644 index 0000000000..50916cf0e7 Binary files /dev/null and b/library/src/main/res/drawable-xxxhdpi/exo_controls_next.png differ diff --git a/library/src/main/res/drawable-xxxhdpi/exo_controls_pause.png b/library/src/main/res/drawable-xxxhdpi/exo_controls_pause.png new file mode 100644 index 0000000000..6a5c59335e Binary files /dev/null and b/library/src/main/res/drawable-xxxhdpi/exo_controls_pause.png differ diff --git a/library/src/main/res/drawable-xxxhdpi/exo_controls_play.png b/library/src/main/res/drawable-xxxhdpi/exo_controls_play.png new file mode 100644 index 0000000000..5d1f452d0d Binary files /dev/null and b/library/src/main/res/drawable-xxxhdpi/exo_controls_play.png differ diff --git a/library/src/main/res/drawable-xxxhdpi/exo_controls_previous.png b/library/src/main/res/drawable-xxxhdpi/exo_controls_previous.png new file mode 100644 index 0000000000..00f7b97d1b Binary files /dev/null and b/library/src/main/res/drawable-xxxhdpi/exo_controls_previous.png differ diff --git a/library/src/main/res/drawable-xxxhdpi/exo_controls_rewind.png b/library/src/main/res/drawable-xxxhdpi/exo_controls_rewind.png new file mode 100644 index 0000000000..5f3bb94440 Binary files /dev/null and b/library/src/main/res/drawable-xxxhdpi/exo_controls_rewind.png differ diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index b5c01b4575..c73bfb0a3c 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -37,6 +37,7 @@ +