Commit 6b0e1758 by Oliver Woodman Committed by GitHub

Merge pull request #5164 from google/dev-v2-r2.9.2

r2.9.2
parents b5beb326 a94fa330
Showing with 640 additions and 389 deletions
...@@ -16,9 +16,8 @@ all of the information requested in the issue template. ...@@ -16,9 +16,8 @@ all of the information requested in the issue template.
## Pull requests ## ## Pull requests ##
We will also consider high quality pull requests. These should normally merge We will also consider high quality pull requests. These should normally merge
into the `dev-vX` branch with the highest major version number. Bug fixes may into the `dev-v2` branch. Before a pull request can be accepted you must submit
be suitable for merging into older `dev-vX` branches. Before a pull request can a Contributor License Agreement, as described below.
be accepted you must submit a Contributor License Agreement, as described below.
[dev]: https://github.com/google/ExoPlayer/tree/dev [dev]: https://github.com/google/ExoPlayer/tree/dev
......
# Release notes # # Release notes #
### 2.9.2 ###
* HLS:
* Fix issue causing unnecessary media playlist requests when playing live
streams ([#5059](https://github.com/google/ExoPlayer/issues/5059)).
* Fix decoder re-instantiation issue for packed audio streams
([#5063](https://github.com/google/ExoPlayer/issues/5063)).
* MP4: Support Opus and FLAC in the MP4 container, and in DASH
([#4883](https://github.com/google/ExoPlayer/issues/4883)).
* DASH: Fix detecting the end of live events
([#4780](https://github.com/google/ExoPlayer/issues/4780)).
* Spherical video: Fall back to `TYPE_ROTATION_VECTOR` if
`TYPE_GAME_ROTATION_VECTOR` is unavailable
([#5119](https://github.com/google/ExoPlayer/issues/5119)).
* Support seeking for a wider range of MPEG-TS streams
([#5097](https://github.com/google/ExoPlayer/issues/5097)).
* Include channel count in audio capabilities check
([#4690](https://github.com/google/ExoPlayer/issues/4690)).
* Fix issue with applying the `show_buffering` attribute in `PlayerView`
([#5139](https://github.com/google/ExoPlayer/issues/5139)).
* Fix issue where null `Metadata` was output when it failed to decode
([#5149](https://github.com/google/ExoPlayer/issues/5149)).
* Fix playback of some invalid but playable MP4 streams by replacing assertions
with logged warnings in sample table parsing code
([#5162](https://github.com/google/ExoPlayer/issues/5162)).
* Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27.
### 2.9.1 ### ### 2.9.1 ###
* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext` * Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext`
...@@ -20,7 +47,7 @@ ...@@ -20,7 +47,7 @@
* DASH: Parse ProgramInformation element if present in the manifest. * DASH: Parse ProgramInformation element if present in the manifest.
* HLS: * HLS:
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload * Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
reader factory flags reader factory flags.
* Fix bug in segment sniffing * Fix bug in segment sniffing
([#5039](https://github.com/google/ExoPlayer/issues/5039)). ([#5039](https://github.com/google/ExoPlayer/issues/5039)).
([#4861](https://github.com/google/ExoPlayer/issues/4861)). ([#4861](https://github.com/google/ExoPlayer/issues/4861)).
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.9.1' releaseVersion = '2.9.2'
releaseVersionCode = 2009001 releaseVersionCode = 2009002
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
......
...@@ -81,8 +81,6 @@ import java.util.List; ...@@ -81,8 +81,6 @@ import java.util.List;
+ "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS)); + "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
MIME_TYPE_VIDEO_MP4)); MIME_TYPE_VIDEO_MP4));
SAMPLES = Collections.unmodifiableList(samples); SAMPLES = Collections.unmodifiableList(samples);
} }
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<string name="application_name">Exo IMA Demo</string> <string name="application_name">Exo IMA Demo</string>
<string name="content_url"><![CDATA[http://rmcdn.2mdn.net/MotifFiles/html/1248596/android_1330378998288.mp4]]></string> <string name="content_url"><![CDATA[https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv]]></string>
<string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string> <string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
...@@ -455,6 +456,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { ...@@ -455,6 +456,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
} }
} }
/** Returns current {@link UrlRequest}. May be null if the data source is not opened. */
@Nullable
protected UrlRequest getCurrentUrlRequest() {
return currentUrlRequest;
}
/** Returns current {@link UrlResponseInfo}. May be null if the data source is not opened. */
@Nullable
protected UrlResponseInfo getCurrentUrlResponseInfo() {
return responseInfo;
}
// Internal methods. // Internal methods.
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException { private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
......
...@@ -46,7 +46,7 @@ HOST_PLATFORM="linux-x86_64" ...@@ -46,7 +46,7 @@ HOST_PLATFORM="linux-x86_64"
be supported. See the [Supported formats][] page for more details of the be supported. See the [Supported formats][] page for more details of the
available flags. available flags.
For example, to fetch and build for armeabi-v7a, For example, to fetch and build FFmpeg release 4.0 for armeabi-v7a,
arm64-v8a and x86 on Linux x86_64: arm64-v8a and x86 on Linux x86_64:
``` ```
...@@ -71,7 +71,7 @@ COMMON_OPTIONS="\ ...@@ -71,7 +71,7 @@ COMMON_OPTIONS="\
" && \ " && \
cd "${FFMPEG_EXT_PATH}/jni" && \ cd "${FFMPEG_EXT_PATH}/jni" && \
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \ (git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
cd ffmpeg && \ cd ffmpeg && git checkout release/4.0 && \
./configure \ ./configure \
--libdir=android-libs/armeabi-v7a \ --libdir=android-libs/armeabi-v7a \
--arch=arm \ --arch=arm \
......
...@@ -145,12 +145,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -145,12 +145,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
private boolean isOutputSupported(Format inputFormat) { private boolean isOutputSupported(Format inputFormat) {
return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT); return shouldUseFloatOutput(inputFormat)
|| supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_16BIT);
} }
private boolean shouldUseFloatOutput(Format inputFormat) { private boolean shouldUseFloatOutput(Format inputFormat) {
Assertions.checkNotNull(inputFormat.sampleMimeType); Assertions.checkNotNull(inputFormat.sampleMimeType);
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) { if (!enableFloatOutput || !supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_FLOAT)) {
return false; return false;
} }
switch (inputFormat.sampleMimeType) { switch (inputFormat.sampleMimeType) {
......
...@@ -53,7 +53,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -53,7 +53,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
if (!FlacLibrary.isAvailable() if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) { } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
......
...@@ -31,13 +31,13 @@ android { ...@@ -31,13 +31,13 @@ android {
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.9.4' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.2'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.google.android.gms:play-services-ads:15.0.1' implementation 'com.google.android.gms:play-services-ads:17.1.1'
// These dependencies are necessary to force the supportLibraryVersion of // These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4 and com.android.support:customtabs to be // com.android.support:support-v4 and com.android.support:customtabs to be
// used. Else older versions are used, for example via: // used. Else older versions are used, for example via:
// com.google.android.gms:play-services-ads:15.0.1 // com.google.android.gms:play-services-ads:17.1.1
// |-- com.android.support:customtabs:26.1.0 // |-- com.android.support:customtabs:26.1.0
implementation 'com.android.support:support-v4:' + supportLibraryVersion implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:customtabs:' + supportLibraryVersion implementation 'com.android.support:customtabs:' + supportLibraryVersion
......
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.ext.ima"> package="com.google.android.exoplayer2.ext.ima">
<meta-data android:name="com.google.android.gms.version" <application>
android:value="@integer/google_play_services_version"/> <meta-data android:name="com.google.android.gms.ads.AD_MANAGER_APP"
android:value="true"/>
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
</application>
</manifest> </manifest>
# Proguard rules specific to the IMA extension.
-keep class com.google.ads.interactivemedia.** { *; }
-keep interface com.google.ads.interactivemedia.** { *; }
-keep class com.google.obf.** { *; }
-keep interface com.google.obf.** { *; }
...@@ -31,7 +31,7 @@ android { ...@@ -31,7 +31,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-media-compat:' + supportLibraryVersion api 'com.android.support:support-media-compat:' + supportLibraryVersion
} }
ext { ext {
......
...@@ -49,6 +49,11 @@ import java.util.Map; ...@@ -49,6 +49,11 @@ import java.util.Map;
/** /**
* Connects a {@link MediaSessionCompat} to a {@link Player}. * Connects a {@link MediaSessionCompat} to a {@link Player}.
* *
* <p>This connector does <em>not</em> call {@link MediaSessionCompat#setActive(boolean)}, and so
* application code is responsible for making the session active when desired. A session must be
* active for transport controls to be displayed (e.g. on the lock screen) and for it to receive
* media button events.
*
* <p>The connector listens for actions sent by the media session's controller and implements these * <p>The connector listens for actions sent by the media session's controller and implements these
* actions by calling appropriate player methods. The playback state of the media session is * actions by calling appropriate player methods. The playback state of the media session is
* automatically synced with the player. The connector can also be optionally extended by providing * automatically synced with the player. The connector can also be optionally extended by providing
......
...@@ -78,7 +78,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -78,7 +78,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
if (!OpusLibrary.isAvailable() if (!OpusLibrary.isAvailable()
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) { } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
......
...@@ -33,6 +33,8 @@ dependencies { ...@@ -33,6 +33,8 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'net.butterflytv.utils:rtmp-client:3.0.1' implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest package="com.google.android.exoplayer2.ext.rtmp"/>
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.rtmp;
import android.net.Uri;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** Unit test for {@link DefaultDataSource} with RTMP URIs. */
@RunWith(RobolectricTestRunner.class)
public final class DefaultDataSourceTest {
@Test
public void openRtmpDataSpec_instantiatesRtmpDataSourceViaReflection() throws IOException {
DefaultDataSource dataSource =
new DefaultDataSource(
RuntimeEnvironment.application, "userAgent", /* allowCrossProtocolRedirects= */ false);
DataSpec dataSpec = new DataSpec(Uri.parse("rtmp://test.com/stream"));
try {
dataSource.open(dataSpec);
} catch (UnsatisfiedLinkError e) {
// RtmpDataSource was successfully instantiated (test run using Gradle).
} catch (UnsupportedOperationException e) {
// RtmpDataSource was successfully instantiated (test run using Blaze).
}
}
}
...@@ -79,6 +79,7 @@ public class DefaultLoadControl implements LoadControl { ...@@ -79,6 +79,7 @@ public class DefaultLoadControl implements LoadControl {
private PriorityTaskManager priorityTaskManager; private PriorityTaskManager priorityTaskManager;
private int backBufferDurationMs; private int backBufferDurationMs;
private boolean retainBackBufferFromKeyframe; private boolean retainBackBufferFromKeyframe;
private boolean createDefaultLoadControlCalled;
/** Constructs a new instance. */ /** Constructs a new instance. */
public Builder() { public Builder() {
...@@ -99,8 +100,10 @@ public class DefaultLoadControl implements LoadControl { ...@@ -99,8 +100,10 @@ public class DefaultLoadControl implements LoadControl {
* *
* @param allocator The {@link DefaultAllocator}. * @param allocator The {@link DefaultAllocator}.
* @return This builder, for convenience. * @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/ */
public Builder setAllocator(DefaultAllocator allocator) { public Builder setAllocator(DefaultAllocator allocator) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.allocator = allocator; this.allocator = allocator;
return this; return this;
} }
...@@ -118,12 +121,14 @@ public class DefaultLoadControl implements LoadControl { ...@@ -118,12 +121,14 @@ public class DefaultLoadControl implements LoadControl {
* for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be * for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be
* caused by buffer depletion rather than a user action. * caused by buffer depletion rather than a user action.
* @return This builder, for convenience. * @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/ */
public Builder setBufferDurationsMs( public Builder setBufferDurationsMs(
int minBufferMs, int minBufferMs,
int maxBufferMs, int maxBufferMs,
int bufferForPlaybackMs, int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs) { int bufferForPlaybackAfterRebufferMs) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.minBufferMs = minBufferMs; this.minBufferMs = minBufferMs;
this.maxBufferMs = maxBufferMs; this.maxBufferMs = maxBufferMs;
this.bufferForPlaybackMs = bufferForPlaybackMs; this.bufferForPlaybackMs = bufferForPlaybackMs;
...@@ -137,8 +142,10 @@ public class DefaultLoadControl implements LoadControl { ...@@ -137,8 +142,10 @@ public class DefaultLoadControl implements LoadControl {
* *
* @param targetBufferBytes The target buffer size in bytes. * @param targetBufferBytes The target buffer size in bytes.
* @return This builder, for convenience. * @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/ */
public Builder setTargetBufferBytes(int targetBufferBytes) { public Builder setTargetBufferBytes(int targetBufferBytes) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.targetBufferBytes = targetBufferBytes; this.targetBufferBytes = targetBufferBytes;
return this; return this;
} }
...@@ -150,8 +157,10 @@ public class DefaultLoadControl implements LoadControl { ...@@ -150,8 +157,10 @@ public class DefaultLoadControl implements LoadControl {
* @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
* constraints over buffer size constraints. * constraints over buffer size constraints.
* @return This builder, for convenience. * @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/ */
public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) { public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
return this; return this;
} }
...@@ -161,8 +170,10 @@ public class DefaultLoadControl implements LoadControl { ...@@ -161,8 +170,10 @@ public class DefaultLoadControl implements LoadControl {
* *
* @param priorityTaskManager The {@link PriorityTaskManager} to use. * @param priorityTaskManager The {@link PriorityTaskManager} to use.
* @return This builder, for convenience. * @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/ */
public Builder setPriorityTaskManager(PriorityTaskManager priorityTaskManager) { public Builder setPriorityTaskManager(PriorityTaskManager priorityTaskManager) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.priorityTaskManager = priorityTaskManager; this.priorityTaskManager = priorityTaskManager;
return this; return this;
} }
...@@ -175,8 +186,10 @@ public class DefaultLoadControl implements LoadControl { ...@@ -175,8 +186,10 @@ public class DefaultLoadControl implements LoadControl {
* @param retainBackBufferFromKeyframe Whether the back buffer is retained from the previous * @param retainBackBufferFromKeyframe Whether the back buffer is retained from the previous
* keyframe. * keyframe.
* @return This builder, for convenience. * @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/ */
public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.backBufferDurationMs = backBufferDurationMs; this.backBufferDurationMs = backBufferDurationMs;
this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe; this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
return this; return this;
...@@ -184,6 +197,7 @@ public class DefaultLoadControl implements LoadControl { ...@@ -184,6 +197,7 @@ public class DefaultLoadControl implements LoadControl {
/** Creates a {@link DefaultLoadControl}. */ /** Creates a {@link DefaultLoadControl}. */
public DefaultLoadControl createDefaultLoadControl() { public DefaultLoadControl createDefaultLoadControl() {
createDefaultLoadControlCalled = true;
if (allocator == null) { if (allocator == null) {
allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
} }
...@@ -371,7 +385,7 @@ public class DefaultLoadControl implements LoadControl { ...@@ -371,7 +385,7 @@ public class DefaultLoadControl implements LoadControl {
} }
if (bufferedDurationUs < minBufferUs) { if (bufferedDurationUs < minBufferUs) {
isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached; isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
} else if (bufferedDurationUs > maxBufferUs || targetBufferSizeReached) { } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
isBuffering = false; isBuffering = false;
} // Else don't change the buffering state } // Else don't change the buffering state
if (priorityTaskManager != null && isBuffering != wasBuffering) { if (priorityTaskManager != null && isBuffering != wasBuffering) {
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.9.1"; public static final String VERSION = "2.9.2";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.1"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.2";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2009001; public static final int VERSION_INT = 2009002;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -139,9 +139,12 @@ public abstract class Timeline { ...@@ -139,9 +139,12 @@ public abstract class Timeline {
*/ */
public boolean isSeekable; public boolean isSeekable;
/** // TODO: Split this to better describe which parts of the window might change. For example it
* Whether this window may change when the timeline is updated. // should be possible to individually determine whether the start and end positions of the
*/ // window may change relative to the underlying periods. For an example of where it's useful to
// know that the end position is fixed whilst the start position may still change, see:
// https://github.com/google/ExoPlayer/issues/4780.
/** Whether this window may change when the timeline is updated. */
public boolean isDynamic; public boolean isDynamic;
/** /**
......
...@@ -29,11 +29,11 @@ import java.util.Arrays; ...@@ -29,11 +29,11 @@ import java.util.Arrays;
@TargetApi(21) @TargetApi(21)
public final class AudioCapabilities { public final class AudioCapabilities {
/** private static final int DEFAULT_MAX_CHANNEL_COUNT = 8;
* The minimum audio capabilities supported by all devices.
*/ /** The minimum audio capabilities supported by all devices. */
public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES = public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2); new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, DEFAULT_MAX_CHANNEL_COUNT);
/** /**
* Returns the current audio capabilities for the device. * Returns the current audio capabilities for the device.
...@@ -52,8 +52,10 @@ public final class AudioCapabilities { ...@@ -52,8 +52,10 @@ public final class AudioCapabilities {
if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) { if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) {
return DEFAULT_AUDIO_CAPABILITIES; return DEFAULT_AUDIO_CAPABILITIES;
} }
return new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS), return new AudioCapabilities(
intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0)); intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),
intent.getIntExtra(
AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT));
} }
private final int[] supportedEncodings; private final int[] supportedEncodings;
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio;
import android.media.AudioTrack; import android.media.AudioTrack;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -165,12 +166,13 @@ public interface AudioSink { ...@@ -165,12 +166,13 @@ public interface AudioSink {
void setListener(Listener listener); void setListener(Listener listener);
/** /**
* Returns whether it's possible to play audio in the specified encoding. * Returns whether the sink supports the audio format.
* *
* @param encoding The audio encoding. * @param channelCount The number of channels, or {@link Format#NO_VALUE} if not known.
* @return Whether it's possible to play audio in the specified encoding. * @param encoding The audio encoding, or {@link Format#NO_VALUE} if not known.
* @return Whether the sink supports the audio format.
*/ */
boolean isEncodingSupported(@C.Encoding int encoding); boolean supportsOutput(int channelCount, @C.Encoding int encoding);
/** /**
* Returns the playback position in the stream starting at zero, in microseconds, or * Returns the playback position in the stream starting at zero, in microseconds, or
......
...@@ -377,14 +377,18 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -377,14 +377,18 @@ public final class DefaultAudioSink implements AudioSink {
} }
@Override @Override
public boolean isEncodingSupported(@C.Encoding int encoding) { public boolean supportsOutput(int channelCount, @C.Encoding int encoding) {
if (Util.isEncodingLinearPcm(encoding)) { if (Util.isEncodingLinearPcm(encoding)) {
// AudioTrack supports 16-bit integer PCM output in all platform API versions, and float // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float
// output from platform API version 21 only. Other integer PCM encodings are resampled by this // output from platform API version 21 only. Other integer PCM encodings are resampled by this
// sink to 16-bit PCM. // sink to 16-bit PCM. We assume that the audio framework will downsample any number of
// channels to the output device's required number of channels.
return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21; return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21;
} else { } else {
return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); return audioCapabilities != null
&& audioCapabilities.supportsEncoding(encoding)
&& (channelCount == Format.NO_VALUE
|| channelCount <= audioCapabilities.getMaxChannelCount());
} }
} }
...@@ -415,7 +419,7 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -415,7 +419,7 @@ public final class DefaultAudioSink implements AudioSink {
isInputPcm = Util.isEncodingLinearPcm(inputEncoding); isInputPcm = Util.isEncodingLinearPcm(inputEncoding);
shouldConvertHighResIntPcmToFloat = shouldConvertHighResIntPcmToFloat =
enableConvertHighResIntPcmToFloat enableConvertHighResIntPcmToFloat
&& isEncodingSupported(C.ENCODING_PCM_32BIT) && supportsOutput(channelCount, C.ENCODING_PCM_32BIT)
&& Util.isEncodingHighResolutionIntegerPcm(inputEncoding); && Util.isEncodingHighResolutionIntegerPcm(inputEncoding);
if (isInputPcm) { if (isInputPcm) {
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
......
...@@ -272,12 +272,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -272,12 +272,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} }
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData); boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData);
if (supportsFormatDrm && allowPassthrough(mimeType) if (supportsFormatDrm
&& allowPassthrough(format.channelCount, mimeType)
&& mediaCodecSelector.getPassthroughDecoderInfo() != null) { && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
} }
if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.isEncodingSupported(format.pcmEncoding)) if ((MimeTypes.AUDIO_RAW.equals(mimeType)
|| !audioSink.isEncodingSupported(C.ENCODING_PCM_16BIT)) { && !audioSink.supportsOutput(format.channelCount, format.pcmEncoding))
|| !audioSink.supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
// Assume the decoder outputs 16-bit PCM, unless the input is raw. // Assume the decoder outputs 16-bit PCM, unless the input is raw.
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} }
...@@ -316,7 +318,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -316,7 +318,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
protected List<MediaCodecInfo> getDecoderInfos( protected List<MediaCodecInfo> getDecoderInfos(
MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)
throws DecoderQueryException { throws DecoderQueryException {
if (allowPassthrough(format.sampleMimeType)) { if (allowPassthrough(format.channelCount, format.sampleMimeType)) {
MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();
if (passthroughDecoderInfo != null) { if (passthroughDecoderInfo != null) {
return Collections.singletonList(passthroughDecoderInfo); return Collections.singletonList(passthroughDecoderInfo);
...@@ -330,12 +332,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -330,12 +332,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* This implementation returns true if the {@link AudioSink} indicates that encoded audio output * This implementation returns true if the {@link AudioSink} indicates that encoded audio output
* is supported. * is supported.
* *
* @param channelCount The number of channels in the input media, or {@link Format#NO_VALUE} if
* not known.
* @param mimeType The type of input media. * @param mimeType The type of input media.
* @return Whether passthrough playback is supported. * @return Whether passthrough playback is supported.
*/ */
protected boolean allowPassthrough(String mimeType) { protected boolean allowPassthrough(int channelCount, String mimeType) {
@C.Encoding int encoding = MimeTypes.getEncoding(mimeType); return audioSink.supportsOutput(channelCount, MimeTypes.getEncoding(mimeType));
return encoding != C.ENCODING_INVALID && audioSink.isEncodingSupported(encoding);
} }
@Override @Override
......
...@@ -249,13 +249,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -249,13 +249,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format); DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format);
/** /**
* Returns whether the audio sink can accept audio in the specified encoding. * Returns whether the sink supports the audio format.
* *
* @param encoding The audio encoding. * @see AudioSink#supportsOutput(int, int)
* @return Whether the audio sink can accept audio in the specified encoding.
*/ */
protected final boolean supportsOutputEncoding(@C.Encoding int encoding) { protected final boolean supportsOutput(int channelCount, @C.Encoding int encoding) {
return audioSink.isEncodingSupported(encoding); return audioSink.supportsOutput(channelCount, encoding);
} }
@Override @Override
......
...@@ -70,9 +70,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -70,9 +70,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
Assertions.checkNotNull(uuid); Assertions.checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
this.uuid = uuid; this.uuid = uuid;
// ClearKey had to be accessed using the Common PSSH UUID prior to API level 27. this.mediaDrm = new MediaDrm(adjustUuid(uuid));
this.mediaDrm =
new MediaDrm(Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid);
if (C.WIDEVINE_UUID.equals(uuid) && needsForceWidevineL3Workaround()) { if (C.WIDEVINE_UUID.equals(uuid) && needsForceWidevineL3Workaround()) {
forceWidevineL3(mediaDrm); forceWidevineL3(mediaDrm);
} }
...@@ -152,7 +150,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -152,7 +150,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
@Override @Override
public byte[] provideKeyResponse(byte[] scope, byte[] response) public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException { throws NotProvisionedException, DeniedByServerException {
if (C.CLEARKEY_UUID.equals(uuid)) { if (C.CLEARKEY_UUID.equals(uuid)) {
response = ClearKeyUtil.adjustResponseData(response); response = ClearKeyUtil.adjustResponseData(response);
} }
...@@ -212,8 +209,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -212,8 +209,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
// indicate that it required secure video decoders [Internal ref: b/11428937]. // indicate that it required secure video decoders [Internal ref: b/11428937].
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21 boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel")); && C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
return new FrameworkMediaCrypto(new MediaCrypto(uuid, initData), return new FrameworkMediaCrypto(
forceAllowInsecureDecoderComponents); new MediaCrypto(adjustUuid(uuid), initData), forceAllowInsecureDecoderComponents);
} }
private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) { private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
...@@ -269,6 +266,11 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -269,6 +266,11 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
return schemeDatas.get(0); return schemeDatas.get(0);
} }
private static UUID adjustUuid(UUID uuid) {
// ClearKey had to be accessed using the Common PSSH UUID prior to API level 27.
return Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid;
}
private static byte[] adjustRequestInitData(UUID uuid, byte[] initData) { private static byte[] adjustRequestInitData(UUID uuid, byte[] initData) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon
// devices also required data to be extracted from the PSSH atom for PlayReady. // devices also required data to be extracted from the PSSH atom for PlayReady.
......
...@@ -42,37 +42,25 @@ public abstract class BinarySearchSeeker { ...@@ -42,37 +42,25 @@ public abstract class BinarySearchSeeker {
protected interface TimestampSeeker { protected interface TimestampSeeker {
/** /**
* Searches for a given timestamp from the input. * Searches a limited window of the provided input for a target timestamp. The size of the
* window is implementation specific, but should be small enough such that it's reasonable for
* multiple such reads to occur during a seek operation.
* *
* <p>Given a target timestamp and an input stream, this seeker will try to read up to a range * @param input The {@link ExtractorInput} from which data should be peeked.
* of {@code searchRangeBytes} bytes from that input, look for all available timestamps from all * @param targetTimestamp The target timestamp.
* frames in that range, compare those with the target timestamp, and return one of the {@link * @param outputFrameHolder If {@link TimestampSearchResult#TYPE_TARGET_TIMESTAMP_FOUND} is
* TimestampSearchResult}.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @param targetTimestamp The target timestamp that we are looking for.
* @param outputFrameHolder If {@link TimestampSearchResult#RESULT_TARGET_TIMESTAMP_FOUND} is
* returned, this holder may be updated to hold the extracted frame that contains the target * returned, this holder may be updated to hold the extracted frame that contains the target
* frame/sample associated with the target timestamp. * frame/sample associated with the target timestamp.
* @return A {@link TimestampSearchResult}, that includes a {@link TimestampSearchResult#result} * @return A {@link TimestampSearchResult} that describes the result of the search.
* value, and other necessary info:
* <ul>
* <li>{@link TimestampSearchResult#RESULT_NO_TIMESTAMP} is returned if there is no
* timestamp in the reading range.
* <li>{@link TimestampSearchResult#RESULT_POSITION_UNDERESTIMATED} is returned if all
* timestamps in the range are smaller than the target timestamp.
* <li>{@link TimestampSearchResult#RESULT_POSITION_OVERESTIMATED} is returned if all
* timestamps in the range are larger than the target timestamp.
* <li>{@link TimestampSearchResult#RESULT_TARGET_TIMESTAMP_FOUND} is returned if this
* seeker can find a timestamp that it deems close enough to the given target.
* </ul>
*
* @throws IOException If an error occurred reading from the input. * @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted. * @throws InterruptedException If the thread was interrupted.
*/ */
TimestampSearchResult searchForTimestamp( TimestampSearchResult searchForTimestamp(
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException; throws IOException, InterruptedException;
/** Called when a seek operation finishes. */
default void onSeekFinished() {}
} }
/** /**
...@@ -231,22 +219,22 @@ public abstract class BinarySearchSeeker { ...@@ -231,22 +219,22 @@ public abstract class BinarySearchSeeker {
timestampSeeker.searchForTimestamp( timestampSeeker.searchForTimestamp(
input, seekOperationParams.getTargetTimePosition(), outputFrameHolder); input, seekOperationParams.getTargetTimePosition(), outputFrameHolder);
switch (timestampSearchResult.result) { switch (timestampSearchResult.type) {
case TimestampSearchResult.RESULT_POSITION_OVERESTIMATED: case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED:
seekOperationParams.updateSeekCeiling( seekOperationParams.updateSeekCeiling(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break; break;
case TimestampSearchResult.RESULT_POSITION_UNDERESTIMATED: case TimestampSearchResult.TYPE_POSITION_UNDERESTIMATED:
seekOperationParams.updateSeekFloor( seekOperationParams.updateSeekFloor(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate); timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break; break;
case TimestampSearchResult.RESULT_TARGET_TIMESTAMP_FOUND: case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND:
markSeekOperationFinished( markSeekOperationFinished(
/* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate); /* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate);
skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate); skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate);
return seekToPosition( return seekToPosition(
input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder); input, timestampSearchResult.bytePositionToUpdate, seekPositionHolder);
case TimestampSearchResult.RESULT_NO_TIMESTAMP: case TimestampSearchResult.TYPE_NO_TIMESTAMP:
// We can't find any timestamp in the search range from the search position. // We can't find any timestamp in the search range from the search position.
// Give up, and just continue reading from the last search position in this case. // Give up, and just continue reading from the last search position in this case.
markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition); markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition);
...@@ -270,6 +258,7 @@ public abstract class BinarySearchSeeker { ...@@ -270,6 +258,7 @@ public abstract class BinarySearchSeeker {
protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) { protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
seekOperationParams = null; seekOperationParams = null;
timestampSeeker.onSeekFinished();
onSeekOperationFinished(foundTargetFrame, resultPosition); onSeekOperationFinished(foundTargetFrame, resultPosition);
} }
...@@ -433,45 +422,49 @@ public abstract class BinarySearchSeeker { ...@@ -433,45 +422,49 @@ public abstract class BinarySearchSeeker {
*/ */
public static final class TimestampSearchResult { public static final class TimestampSearchResult {
public static final int RESULT_TARGET_TIMESTAMP_FOUND = 0; /** The search found a timestamp that it deems close enough to the given target. */
public static final int RESULT_POSITION_OVERESTIMATED = -1; public static final int TYPE_TARGET_TIMESTAMP_FOUND = 0;
public static final int RESULT_POSITION_UNDERESTIMATED = -2; /** The search found only timestamps larger than the target timestamp. */
public static final int RESULT_NO_TIMESTAMP = -3; public static final int TYPE_POSITION_OVERESTIMATED = -1;
/** The search found only timestamps smaller than the target timestamp. */
public static final int TYPE_POSITION_UNDERESTIMATED = -2;
/** The search didn't find any timestamps. */
public static final int TYPE_NO_TIMESTAMP = -3;
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({
RESULT_TARGET_TIMESTAMP_FOUND, TYPE_TARGET_TIMESTAMP_FOUND,
RESULT_POSITION_OVERESTIMATED, TYPE_POSITION_OVERESTIMATED,
RESULT_POSITION_UNDERESTIMATED, TYPE_POSITION_UNDERESTIMATED,
RESULT_NO_TIMESTAMP TYPE_NO_TIMESTAMP
}) })
@interface SearchResult {} @interface Type {}
public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT = public static final TimestampSearchResult NO_TIMESTAMP_IN_RANGE_RESULT =
new TimestampSearchResult(RESULT_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET); new TimestampSearchResult(TYPE_NO_TIMESTAMP, C.TIME_UNSET, C.POSITION_UNSET);
/** @see TimestampSeeker */ /** The type of the result. */
private final @SearchResult int result; @Type private final int type;
/** /**
* When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@code * SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@link
* result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link
* SeekOperationParams#floorTimePosition} should be updated with this value. * SeekOperationParams#floorTimePosition} should be updated with this value.
*/ */
private final long timestampToUpdate; private final long timestampToUpdate;
/** /**
* When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link * When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@code * SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@link
* result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link * #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link
* SeekOperationParams#floorBytePosition} should be updated with this value. * SeekOperationParams#floorBytePosition} should be updated with this value.
*/ */
private final long bytePositionToUpdate; private final long bytePositionToUpdate;
private TimestampSearchResult( private TimestampSearchResult(
@SearchResult int result, long timestampToUpdate, long bytePositionToUpdate) { @Type int type, long timestampToUpdate, long bytePositionToUpdate) {
this.result = result; this.type = type;
this.timestampToUpdate = timestampToUpdate; this.timestampToUpdate = timestampToUpdate;
this.bytePositionToUpdate = bytePositionToUpdate; this.bytePositionToUpdate = bytePositionToUpdate;
} }
...@@ -484,7 +477,7 @@ public abstract class BinarySearchSeeker { ...@@ -484,7 +477,7 @@ public abstract class BinarySearchSeeker {
public static TimestampSearchResult overestimatedResult( public static TimestampSearchResult overestimatedResult(
long newCeilingTimestamp, long newCeilingBytePosition) { long newCeilingTimestamp, long newCeilingBytePosition) {
return new TimestampSearchResult( return new TimestampSearchResult(
RESULT_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition); TYPE_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition);
} }
/** /**
...@@ -495,11 +488,11 @@ public abstract class BinarySearchSeeker { ...@@ -495,11 +488,11 @@ public abstract class BinarySearchSeeker {
public static TimestampSearchResult underestimatedResult( public static TimestampSearchResult underestimatedResult(
long newFloorTimestamp, long newCeilingBytePosition) { long newFloorTimestamp, long newCeilingBytePosition) {
return new TimestampSearchResult( return new TimestampSearchResult(
RESULT_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition); TYPE_POSITION_UNDERESTIMATED, newFloorTimestamp, newCeilingBytePosition);
} }
/** /**
* Returns a result to signal that the target timestamp has been found at the {@code * Returns a result to signal that the target timestamp has been found at {@code
* resultBytePosition}, and the seek operation can stop. * resultBytePosition}, and the seek operation can stop.
* *
* <p>Note that when this value is returned from {@link * <p>Note that when this value is returned from {@link
...@@ -508,7 +501,7 @@ public abstract class BinarySearchSeeker { ...@@ -508,7 +501,7 @@ public abstract class BinarySearchSeeker {
*/ */
public static TimestampSearchResult targetFoundResult(long resultBytePosition) { public static TimestampSearchResult targetFoundResult(long resultBytePosition) {
return new TimestampSearchResult( return new TimestampSearchResult(
RESULT_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition); TYPE_TARGET_TIMESTAMP_FOUND, C.TIME_UNSET, resultBytePosition);
} }
} }
......
...@@ -145,6 +145,10 @@ import java.util.List; ...@@ -145,6 +145,10 @@ import java.util.List;
public static final int TYPE_alac = Util.getIntegerCodeForString("alac"); public static final int TYPE_alac = Util.getIntegerCodeForString("alac");
public static final int TYPE_alaw = Util.getIntegerCodeForString("alaw"); public static final int TYPE_alaw = Util.getIntegerCodeForString("alaw");
public static final int TYPE_ulaw = Util.getIntegerCodeForString("ulaw"); public static final int TYPE_ulaw = Util.getIntegerCodeForString("ulaw");
public static final int TYPE_Opus = Util.getIntegerCodeForString("Opus");
public static final int TYPE_dOps = Util.getIntegerCodeForString("dOps");
public static final int TYPE_fLaC = Util.getIntegerCodeForString("fLaC");
public static final int TYPE_dfLa = Util.getIntegerCodeForString("dfLa");
public final int type; public final int type;
......
...@@ -58,6 +58,9 @@ import java.util.List; ...@@ -58,6 +58,9 @@ import java.util.List;
*/ */
private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3; private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3;
/** The magic signature for an Opus Identification header, as defined in RFC-7845. */
private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead");
/** /**
* Parses a trak atom (defined in 14496-12). * Parses a trak atom (defined in 14496-12).
* *
...@@ -233,8 +236,6 @@ import java.util.List; ...@@ -233,8 +236,6 @@ import java.util.List;
sizes = Arrays.copyOf(sizes, sampleCount); sizes = Arrays.copyOf(sizes, sampleCount);
timestamps = Arrays.copyOf(timestamps, sampleCount); timestamps = Arrays.copyOf(timestamps, sampleCount);
flags = Arrays.copyOf(flags, sampleCount); flags = Arrays.copyOf(flags, sampleCount);
remainingSamplesAtTimestampOffset = 0;
remainingTimestampOffsetChanges = 0;
break; break;
} }
...@@ -290,23 +291,38 @@ import java.util.List; ...@@ -290,23 +291,38 @@ import java.util.List;
} }
duration = timestampTimeUnits + timestampOffset; duration = timestampTimeUnits + timestampOffset;
Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0); // If the stbl's child boxes are not consistent the container is malformed, but the stream may
// Remove trailing ctts entries with 0-valued sample counts. // still be playable.
boolean isCttsValid = true;
while (remainingTimestampOffsetChanges > 0) { while (remainingTimestampOffsetChanges > 0) {
Assertions.checkArgument(ctts.readUnsignedIntToInt() == 0); if (ctts.readUnsignedIntToInt() != 0) {
isCttsValid = false;
break;
}
ctts.readInt(); // Ignore offset. ctts.readInt(); // Ignore offset.
remainingTimestampOffsetChanges--; remainingTimestampOffsetChanges--;
} }
if (remainingSynchronizationSamples != 0
// If the stbl's child boxes are not consistent the container is malformed, but the stream may || remainingSamplesAtTimestampDelta != 0
// still be playable. || remainingSamplesInChunk != 0
if (remainingSynchronizationSamples != 0 || remainingSamplesAtTimestampDelta != 0 || remainingTimestampDeltaChanges != 0
|| remainingSamplesInChunk != 0 || remainingTimestampDeltaChanges != 0) { || remainingSamplesAtTimestampOffset != 0
Log.w(TAG, "Inconsistent stbl box for track " + track.id || !isCttsValid) {
+ ": remainingSynchronizationSamples " + remainingSynchronizationSamples Log.w(
+ ", remainingSamplesAtTimestampDelta " + remainingSamplesAtTimestampDelta TAG,
+ ", remainingSamplesInChunk " + remainingSamplesInChunk "Inconsistent stbl box for track "
+ ", remainingTimestampDeltaChanges " + remainingTimestampDeltaChanges); + track.id
+ ": remainingSynchronizationSamples "
+ remainingSynchronizationSamples
+ ", remainingSamplesAtTimestampDelta "
+ remainingSamplesAtTimestampDelta
+ ", remainingSamplesInChunk "
+ remainingSamplesInChunk
+ ", remainingTimestampDeltaChanges "
+ remainingTimestampDeltaChanges
+ ", remainingSamplesAtTimestampOffset "
+ remainingSamplesAtTimestampOffset
+ (!isCttsValid ? ", ctts invalid" : ""));
} }
} else { } else {
long[] chunkOffsetsBytes = new long[chunkIterator.length]; long[] chunkOffsetsBytes = new long[chunkIterator.length];
...@@ -679,7 +695,9 @@ import java.util.List; ...@@ -679,7 +695,9 @@ import java.util.List;
|| childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE__mp3
|| childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alac
|| childAtomType == Atom.TYPE_alaw || childAtomType == Atom.TYPE_alaw
|| childAtomType == Atom.TYPE_ulaw) { || childAtomType == Atom.TYPE_ulaw
|| childAtomType == Atom.TYPE_Opus
|| childAtomType == Atom.TYPE_fLaC) {
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
language, isQuickTime, drmInitData, out, i); language, isQuickTime, drmInitData, out, i);
} else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g } else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g
...@@ -976,6 +994,10 @@ import java.util.List; ...@@ -976,6 +994,10 @@ import java.util.List;
mimeType = MimeTypes.AUDIO_ALAW; mimeType = MimeTypes.AUDIO_ALAW;
} else if (atomType == Atom.TYPE_ulaw) { } else if (atomType == Atom.TYPE_ulaw) {
mimeType = MimeTypes.AUDIO_MLAW; mimeType = MimeTypes.AUDIO_MLAW;
} else if (atomType == Atom.TYPE_Opus) {
mimeType = MimeTypes.AUDIO_OPUS;
} else if (atomType == Atom.TYPE_fLaC) {
mimeType = MimeTypes.AUDIO_FLAC;
} }
byte[] initializationData = null; byte[] initializationData = null;
...@@ -1016,7 +1038,20 @@ import java.util.List; ...@@ -1016,7 +1038,20 @@ import java.util.List;
} else if (childAtomType == Atom.TYPE_alac) { } else if (childAtomType == Atom.TYPE_alac) {
initializationData = new byte[childAtomSize]; initializationData = new byte[childAtomSize];
parent.setPosition(childPosition); parent.setPosition(childPosition);
parent.readBytes(initializationData, 0, childAtomSize); parent.readBytes(initializationData, /* offset= */ 0, childAtomSize);
} else if (childAtomType == Atom.TYPE_dOps) {
// Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic
// Signature and the body of the dOps atom.
int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE;
initializationData = new byte[opusMagic.length + childAtomBodySize];
System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length);
parent.setPosition(childPosition + Atom.HEADER_SIZE);
parent.readBytes(initializationData, opusMagic.length, childAtomBodySize);
} else if (childAtomSize == Atom.TYPE_dfLa) {
int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;
initializationData = new byte[childAtomBodySize];
parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);
parent.readBytes(initializationData, /* offset= */ 0, childAtomBodySize);
} }
childPosition += childAtomSize; childPosition += childAtomSize;
} }
......
...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker; ...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** /**
...@@ -53,10 +54,9 @@ import java.io.IOException; ...@@ -53,10 +54,9 @@ import java.io.IOException;
/** /**
* A seeker that looks for a given SCR timestamp at a given position in a PS stream. * A seeker that looks for a given SCR timestamp at a given position in a PS stream.
* *
* <p>Given a SCR timestamp, and a position within a PS stream, this seeker will try to read a * <p>Given a SCR timestamp, and a position within a PS stream, this seeker will peek up to {@link
* range of up to {@link #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all * #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all packs in that range, and
* packs in that range, and then compare the SCR timestamps (if available) of these packets vs the * then compare the SCR timestamps (if available) of these packets to the target timestamp.
* target timestamp.
*/ */
private static final class PsScrSeeker implements TimestampSeeker { private static final class PsScrSeeker implements TimestampSeeker {
...@@ -65,7 +65,7 @@ import java.io.IOException; ...@@ -65,7 +65,7 @@ import java.io.IOException;
private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) { private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) {
this.scrTimestampAdjuster = scrTimestampAdjuster; this.scrTimestampAdjuster = scrTimestampAdjuster;
packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES); packetBuffer = new ParsableByteArray();
} }
@Override @Override
...@@ -73,14 +73,19 @@ import java.io.IOException; ...@@ -73,14 +73,19 @@ import java.io.IOException;
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException { throws IOException, InterruptedException {
long inputPosition = input.getPosition(); long inputPosition = input.getPosition();
int bytesToRead = int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
(int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition());
packetBuffer.reset(bytesToRead); packetBuffer.reset(bytesToSearch);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition); return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
} }
@Override
public void onSeekFinished() {
packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
}
private TimestampSearchResult searchForScrValueInBuffer( private TimestampSearchResult searchForScrValueInBuffer(
ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) { ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) {
int startOfLastPacketPosition = C.POSITION_UNSET; int startOfLastPacketPosition = C.POSITION_UNSET;
......
...@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; ...@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** /**
...@@ -38,7 +39,7 @@ import java.io.IOException; ...@@ -38,7 +39,7 @@ import java.io.IOException;
*/ */
/* package */ final class PsDurationReader { /* package */ final class PsDurationReader {
private static final int DURATION_READ_BYTES = 20000; private static final int TIMESTAMP_SEARCH_BYTES = 20000;
private final TimestampAdjuster scrTimestampAdjuster; private final TimestampAdjuster scrTimestampAdjuster;
private final ParsableByteArray packetBuffer; private final ParsableByteArray packetBuffer;
...@@ -56,7 +57,7 @@ import java.io.IOException; ...@@ -56,7 +57,7 @@ import java.io.IOException;
firstScrValue = C.TIME_UNSET; firstScrValue = C.TIME_UNSET;
lastScrValue = C.TIME_UNSET; lastScrValue = C.TIME_UNSET;
durationUs = C.TIME_UNSET; durationUs = C.TIME_UNSET;
packetBuffer = new ParsableByteArray(DURATION_READ_BYTES); packetBuffer = new ParsableByteArray();
} }
/** Returns true if a PS duration has been read. */ /** Returns true if a PS duration has been read. */
...@@ -129,6 +130,7 @@ import java.io.IOException; ...@@ -129,6 +130,7 @@ import java.io.IOException;
} }
private int finishReadDuration(ExtractorInput input) { private int finishReadDuration(ExtractorInput input) {
packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
isDurationRead = true; isDurationRead = true;
input.resetPeekPosition(); input.resetPeekPosition();
return Extractor.RESULT_CONTINUE; return Extractor.RESULT_CONTINUE;
...@@ -136,16 +138,16 @@ import java.io.IOException; ...@@ -136,16 +138,16 @@ import java.io.IOException;
private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder) private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (input.getPosition() != 0) { int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength());
seekPositionHolder.position = 0; int searchStartPosition = 0;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK; return Extractor.RESULT_SEEK;
} }
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); packetBuffer.reset(bytesToSearch);
input.resetPeekPosition(); input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);
firstScrValue = readFirstScrValueFromBuffer(packetBuffer); firstScrValue = readFirstScrValueFromBuffer(packetBuffer);
isFirstScrValueRead = true; isFirstScrValueRead = true;
...@@ -172,17 +174,17 @@ import java.io.IOException; ...@@ -172,17 +174,17 @@ import java.io.IOException;
private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder) private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException, InterruptedException { throws IOException, InterruptedException {
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); long inputLength = input.getLength();
long bufferStartStreamPosition = input.getLength() - bytesToRead; int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength);
if (input.getPosition() != bufferStartStreamPosition) { long searchStartPosition = inputLength - bytesToSearch;
seekPositionHolder.position = bufferStartStreamPosition; if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK; return Extractor.RESULT_SEEK;
} }
packetBuffer.reset(bytesToSearch);
input.resetPeekPosition(); input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);
lastScrValue = readLastScrValueFromBuffer(packetBuffer); lastScrValue = readLastScrValueFromBuffer(packetBuffer);
isLastScrValueRead = true; isLastScrValueRead = true;
......
...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker; ...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** /**
...@@ -33,10 +34,8 @@ import java.io.IOException; ...@@ -33,10 +34,8 @@ import java.io.IOException;
/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker { /* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker {
private static final long SEEK_TOLERANCE_US = 100_000; private static final long SEEK_TOLERANCE_US = 100_000;
private static final int MINIMUM_SEARCH_RANGE_BYTES = TsExtractor.TS_PACKET_SIZE * 5; private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE;
private static final int TIMESTAMP_SEARCH_PACKETS = 200; private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
private static final int TIMESTAMP_SEARCH_BYTES =
TsExtractor.TS_PACKET_SIZE * TIMESTAMP_SEARCH_PACKETS;
public TsBinarySearchSeeker( public TsBinarySearchSeeker(
TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) { TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) {
...@@ -56,10 +55,10 @@ import java.io.IOException; ...@@ -56,10 +55,10 @@ import java.io.IOException;
* A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given * A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given
* position in a TS stream. * position in a TS stream.
* *
* <p>Given a PCR timestamp, and a position within a TS stream, this seeker will try to read up to * <p>Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link
* {@link #TIMESTAMP_SEARCH_PACKETS} TS packets from that stream position, look for all packet * #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to
* with PID equals to PCR_PID, and then compare the PCR timestamps (if available) of these packets * PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target
* vs the target timestamp. * timestamp.
*/ */
private static final class TsPcrSeeker implements TimestampSeeker { private static final class TsPcrSeeker implements TimestampSeeker {
...@@ -70,7 +69,7 @@ import java.io.IOException; ...@@ -70,7 +69,7 @@ import java.io.IOException;
public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) { public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) {
this.pcrPid = pcrPid; this.pcrPid = pcrPid;
this.pcrTimestampAdjuster = pcrTimestampAdjuster; this.pcrTimestampAdjuster = pcrTimestampAdjuster;
packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES); packetBuffer = new ParsableByteArray();
} }
@Override @Override
...@@ -78,10 +77,10 @@ import java.io.IOException; ...@@ -78,10 +77,10 @@ import java.io.IOException;
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException { throws IOException, InterruptedException {
long inputPosition = input.getPosition(); long inputPosition = input.getPosition();
int bytesToRead = int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
(int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition());
packetBuffer.reset(bytesToRead); packetBuffer.reset(bytesToSearch);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition); return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
} }
...@@ -133,5 +132,10 @@ import java.io.IOException; ...@@ -133,5 +132,10 @@ import java.io.IOException;
return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT; return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
} }
} }
@Override
public void onSeekFinished() {
packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
}
} }
} }
...@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; ...@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** /**
...@@ -35,8 +36,7 @@ import java.io.IOException; ...@@ -35,8 +36,7 @@ import java.io.IOException;
*/ */
/* package */ final class TsDurationReader { /* package */ final class TsDurationReader {
private static final int DURATION_READ_PACKETS = 200; private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
private static final int DURATION_READ_BYTES = TsExtractor.TS_PACKET_SIZE * DURATION_READ_PACKETS;
private final TimestampAdjuster pcrTimestampAdjuster; private final TimestampAdjuster pcrTimestampAdjuster;
private final ParsableByteArray packetBuffer; private final ParsableByteArray packetBuffer;
...@@ -54,7 +54,7 @@ import java.io.IOException; ...@@ -54,7 +54,7 @@ import java.io.IOException;
firstPcrValue = C.TIME_UNSET; firstPcrValue = C.TIME_UNSET;
lastPcrValue = C.TIME_UNSET; lastPcrValue = C.TIME_UNSET;
durationUs = C.TIME_UNSET; durationUs = C.TIME_UNSET;
packetBuffer = new ParsableByteArray(DURATION_READ_BYTES); packetBuffer = new ParsableByteArray();
} }
/** Returns true if a TS duration has been read. */ /** Returns true if a TS duration has been read. */
...@@ -117,6 +117,7 @@ import java.io.IOException; ...@@ -117,6 +117,7 @@ import java.io.IOException;
} }
private int finishReadDuration(ExtractorInput input) { private int finishReadDuration(ExtractorInput input) {
packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
isDurationRead = true; isDurationRead = true;
input.resetPeekPosition(); input.resetPeekPosition();
return Extractor.RESULT_CONTINUE; return Extractor.RESULT_CONTINUE;
...@@ -124,16 +125,16 @@ import java.io.IOException; ...@@ -124,16 +125,16 @@ import java.io.IOException;
private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (input.getPosition() != 0) { int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength());
seekPositionHolder.position = 0; int searchStartPosition = 0;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK; return Extractor.RESULT_SEEK;
} }
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); packetBuffer.reset(bytesToSearch);
input.resetPeekPosition(); input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);
firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid); firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid);
isFirstPcrValueRead = true; isFirstPcrValueRead = true;
...@@ -159,17 +160,17 @@ import java.io.IOException; ...@@ -159,17 +160,17 @@ import java.io.IOException;
private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
throws IOException, InterruptedException { throws IOException, InterruptedException {
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength()); long inputLength = input.getLength();
long bufferStartStreamPosition = input.getLength() - bytesToRead; int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength);
if (input.getPosition() != bufferStartStreamPosition) { long searchStartPosition = inputLength - bytesToSearch;
seekPositionHolder.position = bufferStartStreamPosition; if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK; return Extractor.RESULT_SEEK;
} }
packetBuffer.reset(bytesToSearch);
input.resetPeekPosition(); input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead); input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);
lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid); lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid);
isLastPcrValueRead = true; isLastPcrValueRead = true;
......
...@@ -129,9 +129,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { ...@@ -129,9 +129,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
buffer.flip(); buffer.flip();
int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT; int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT;
pendingMetadata[index] = decoder.decode(buffer); Metadata metadata = decoder.decode(buffer);
pendingMetadataTimestamps[index] = buffer.timeUs; if (metadata != null) {
pendingMetadataCount++; pendingMetadata[index] = metadata;
pendingMetadataTimestamps[index] = buffer.timeUs;
pendingMetadataCount++;
}
} }
} }
} }
......
...@@ -365,6 +365,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -365,6 +365,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public final synchronized void moveMediaSource( public final synchronized void moveMediaSource(
int currentIndex, int newIndex, @Nullable Runnable actionOnCompletion) { int currentIndex, int newIndex, @Nullable Runnable actionOnCompletion) {
if (currentIndex == newIndex) { if (currentIndex == newIndex) {
if (actionOnCompletion != null) {
actionOnCompletion.run();
}
return; return;
} }
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
...@@ -570,9 +573,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -570,9 +573,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) { if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) {
shuffleOrder = shuffleOrder.cloneAndClear(); shuffleOrder = shuffleOrder.cloneAndClear();
} else { } else {
for (int index = toIndex - 1; index >= fromIndex; index--) { shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndex);
shuffleOrder = shuffleOrder.cloneAndRemove(index);
}
} }
for (int index = toIndex - 1; index >= fromIndex; index--) { for (int index = toIndex - 1; index >= fromIndex; index--) {
removeMediaSourceInternal(index); removeMediaSourceInternal(index);
...@@ -581,7 +582,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -581,7 +582,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
break; break;
case MSG_MOVE: case MSG_MOVE:
MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(message); MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(message);
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index); shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
moveMediaSourceInternal(moveMessage.index, moveMessage.customData); moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
scheduleListenerNotification(moveMessage.actionOnCompletion); scheduleListenerNotification(moveMessage.actionOnCompletion);
......
...@@ -30,10 +30,8 @@ import java.io.EOFException; ...@@ -30,10 +30,8 @@ import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /** A queue of media samples. */
* A queue of media samples. public class SampleQueue implements TrackOutput {
*/
public final class SampleQueue implements TrackOutput {
/** /**
* A listener for changes to the upstream format. * A listener for changes to the upstream format.
......
...@@ -55,6 +55,17 @@ public interface ShuffleOrder { ...@@ -55,6 +55,17 @@ public interface ShuffleOrder {
this(length, new Random(randomSeed)); this(length, new Random(randomSeed));
} }
/**
* Creates an instance with a specified shuffle order and the specified random seed. The random
* seed is used for {@link #cloneAndInsert(int, int)} invocations.
*
* @param shuffledIndices The shuffled indices to use as order.
* @param randomSeed A random seed.
*/
public DefaultShuffleOrder(int[] shuffledIndices, long randomSeed) {
this(Arrays.copyOf(shuffledIndices, shuffledIndices.length), new Random(randomSeed));
}
private DefaultShuffleOrder(int length, Random random) { private DefaultShuffleOrder(int length, Random random) {
this(createShuffledList(length, random), random); this(createShuffledList(length, random), random);
} }
...@@ -124,15 +135,16 @@ public interface ShuffleOrder { ...@@ -124,15 +135,16 @@ public interface ShuffleOrder {
} }
@Override @Override
public ShuffleOrder cloneAndRemove(int removalIndex) { public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {
int[] newShuffled = new int[shuffled.length - 1]; int numberOfElementsToRemove = indexToExclusive - indexFrom;
boolean foundRemovedElement = false; int[] newShuffled = new int[shuffled.length - numberOfElementsToRemove];
int foundElementsCount = 0;
for (int i = 0; i < shuffled.length; i++) { for (int i = 0; i < shuffled.length; i++) {
if (shuffled[i] == removalIndex) { if (shuffled[i] >= indexFrom && shuffled[i] < indexToExclusive) {
foundRemovedElement = true; foundElementsCount++;
} else { } else {
newShuffled[foundRemovedElement ? i - 1 : i] = shuffled[i] > removalIndex newShuffled[i - foundElementsCount] =
? shuffled[i] - 1 : shuffled[i]; shuffled[i] >= indexFrom ? shuffled[i] - numberOfElementsToRemove : shuffled[i];
} }
} }
return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
...@@ -202,8 +214,8 @@ public interface ShuffleOrder { ...@@ -202,8 +214,8 @@ public interface ShuffleOrder {
} }
@Override @Override
public ShuffleOrder cloneAndRemove(int removalIndex) { public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {
return new UnshuffledShuffleOrder(length - 1); return new UnshuffledShuffleOrder(length - indexToExclusive + indexFrom);
} }
@Override @Override
...@@ -257,12 +269,14 @@ public interface ShuffleOrder { ...@@ -257,12 +269,14 @@ public interface ShuffleOrder {
ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount); ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount);
/** /**
* Returns a copy of the shuffle order with one element removed. * Returns a copy of the shuffle order with a range of elements removed.
* *
* @param removalIndex The index of the element in the unshuffled order which is to be removed. * @param indexFrom The starting index in the unshuffled order of the range to remove.
* @return A copy of this {@link ShuffleOrder} without the removed element. * @param indexToExclusive The smallest index (must be greater or equal to {@code indexFrom}) that
* will not be removed.
* @return A copy of this {@link ShuffleOrder} without the elements in the removed range.
*/ */
ShuffleOrder cloneAndRemove(int removalIndex); ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive);
/** Returns a copy of the shuffle order with all elements removed. */ /** Returns a copy of the shuffle order with all elements removed. */
ShuffleOrder cloneAndClear(); ShuffleOrder cloneAndClear();
......
...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; ...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
...@@ -451,7 +452,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -451,7 +452,7 @@ public final class Cea608Decoder extends CeaDecoder {
switch (cc2) { switch (cc2) {
case CTRL_ERASE_DISPLAYED_MEMORY: case CTRL_ERASE_DISPLAYED_MEMORY:
cues = null; cues = Collections.emptyList();
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
resetCueBuilders(); resetCueBuilders();
} }
...@@ -506,7 +507,7 @@ public final class Cea608Decoder extends CeaDecoder { ...@@ -506,7 +507,7 @@ public final class Cea608Decoder extends CeaDecoder {
if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP
|| captionMode == CC_MODE_UNKNOWN) { || captionMode == CC_MODE_UNKNOWN) {
// When switching from paint-on or to roll-up or unknown, we also need to clear the caption. // When switching from paint-on or to roll-up or unknown, we also need to clear the caption.
cues = null; cues = Collections.emptyList();
} }
} }
......
...@@ -110,7 +110,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { ...@@ -110,7 +110,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
foundEvent = EVENT_END_OF_FILE; foundEvent = EVENT_END_OF_FILE;
} else if (STYLE_START.equals(line)) { } else if (STYLE_START.equals(line)) {
foundEvent = EVENT_STYLE_BLOCK; foundEvent = EVENT_STYLE_BLOCK;
} else if (COMMENT_START.startsWith(line)) { } else if (line.startsWith(COMMENT_START)) {
foundEvent = EVENT_COMMENT; foundEvent = EVENT_COMMENT;
} else { } else {
foundEvent = EVENT_CUE; foundEvent = EVENT_CUE;
......
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format; ...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
...@@ -160,7 +161,10 @@ public abstract class BaseTrackSelection implements TrackSelection { ...@@ -160,7 +161,10 @@ public abstract class BaseTrackSelection implements TrackSelection {
if (!canBlacklist) { if (!canBlacklist) {
return false; return false;
} }
blacklistUntilTimes[index] = Math.max(blacklistUntilTimes[index], nowMs + blacklistDurationMs); blacklistUntilTimes[index] =
Math.max(
blacklistUntilTimes[index],
Util.addWithOverflowDefault(nowMs, blacklistDurationMs, Long.MAX_VALUE));
return true; return true;
} }
......
...@@ -207,7 +207,7 @@ public final class MimeTypes { ...@@ -207,7 +207,7 @@ public final class MimeTypes {
if (codec == null) { if (codec == null) {
return null; return null;
} }
codec = codec.trim(); codec = Util.toLowerInvariant(codec.trim());
if (codec.startsWith("avc1") || codec.startsWith("avc3")) { if (codec.startsWith("avc1") || codec.startsWith("avc3")) {
return MimeTypes.VIDEO_H264; return MimeTypes.VIDEO_H264;
} else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) {
...@@ -245,6 +245,8 @@ public final class MimeTypes { ...@@ -245,6 +245,8 @@ public final class MimeTypes {
return MimeTypes.AUDIO_OPUS; return MimeTypes.AUDIO_OPUS;
} else if (codec.startsWith("vorbis")) { } else if (codec.startsWith("vorbis")) {
return MimeTypes.AUDIO_VORBIS; return MimeTypes.AUDIO_VORBIS;
} else if (codec.startsWith("flac")) {
return MimeTypes.AUDIO_FLAC;
} else { } else {
return getCustomMimeTypeForCodec(codec); return getCustomMimeTypeForCodec(codec);
} }
......
...@@ -67,6 +67,12 @@ public final class ParsableByteArray { ...@@ -67,6 +67,12 @@ public final class ParsableByteArray {
this.limit = limit; this.limit = limit;
} }
/** Sets the position and limit to zero. */
public void reset() {
position = 0;
limit = 0;
}
/** /**
* Resets the position to zero and the limit to the specified value. If the limit exceeds the * Resets the position to zero and the limit to the specified value. If the limit exceeds the
* capacity, {@code data} is replaced with a new array of sufficient size. * capacity, {@code data} is replaced with a new array of sufficient size.
...@@ -78,6 +84,16 @@ public final class ParsableByteArray { ...@@ -78,6 +84,16 @@ public final class ParsableByteArray {
} }
/** /**
* Updates the instance to wrap {@code data}, and resets the position to zero and the limit to
* {@code data.length}.
*
* @param data The array to wrap.
*/
public void reset(byte[] data) {
reset(data, data.length);
}
/**
* Updates the instance to wrap {@code data}, and resets the position to zero. * Updates the instance to wrap {@code data}, and resets the position to zero.
* *
* @param data The array to wrap. * @param data The array to wrap.
...@@ -90,14 +106,6 @@ public final class ParsableByteArray { ...@@ -90,14 +106,6 @@ public final class ParsableByteArray {
} }
/** /**
* Sets the position and limit to zero.
*/
public void reset() {
position = 0;
limit = 0;
}
/**
* Returns the number of bytes yet to be read. * Returns the number of bytes yet to be read.
*/ */
public int bytesLeft() { public int bytesLeft() {
......
...@@ -26,6 +26,7 @@ import java.lang.annotation.RetentionPolicy; ...@@ -26,6 +26,7 @@ import java.lang.annotation.RetentionPolicy;
*/ */
public final class RepeatModeUtil { public final class RepeatModeUtil {
// LINT.IfChange
/** /**
* Set of repeat toggle modes. Can be combined using bit-wise operations. Possible flag values are * Set of repeat toggle modes. Can be combined using bit-wise operations. Possible flag values are
* {@link #REPEAT_TOGGLE_MODE_NONE}, {@link #REPEAT_TOGGLE_MODE_ONE} and {@link * {@link #REPEAT_TOGGLE_MODE_NONE}, {@link #REPEAT_TOGGLE_MODE_ONE} and {@link
...@@ -47,6 +48,7 @@ public final class RepeatModeUtil { ...@@ -47,6 +48,7 @@ public final class RepeatModeUtil {
public static final int REPEAT_TOGGLE_MODE_ONE = 1; public static final int REPEAT_TOGGLE_MODE_ONE = 1;
/** "Repeat All" button enabled. */ /** "Repeat All" button enabled. */
public static final int REPEAT_TOGGLE_MODE_ALL = 1 << 1; // 2 public static final int REPEAT_TOGGLE_MODE_ALL = 1 << 1; // 2
// LINT.ThenChange(../../../../../../../../../ui/src/main/res/values/attrs.xml)
private RepeatModeUtil() { private RepeatModeUtil() {
// Prevent instantiation. // Prevent instantiation.
......
...@@ -1329,6 +1329,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1329,6 +1329,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "A7010a48": case "A7010a48":
case "A7020a48": case "A7020a48":
case "AquaPowerM": case "AquaPowerM":
case "ASUS_X00AD_2":
case "Aura_Note_2": case "Aura_Note_2":
case "BLACK-1X": case "BLACK-1X":
case "BRAVIA_ATV2": case "BRAVIA_ATV2":
...@@ -1369,6 +1370,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1369,6 +1370,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "HWBLN-H": case "HWBLN-H":
case "HWCAM-H": case "HWCAM-H":
case "HWVNS-H": case "HWVNS-H":
case "i9031":
case "iball8735_9806": case "iball8735_9806":
case "Infinix-X572": case "Infinix-X572":
case "iris60": case "iris60":
...@@ -1376,6 +1378,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1376,6 +1378,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "j2xlteins": case "j2xlteins":
case "JGZ": case "JGZ":
case "K50a40": case "K50a40":
case "kate":
case "le_x6": case "le_x6":
case "LS-5017": case "LS-5017":
case "M5c": case "M5c":
......
...@@ -26,10 +26,14 @@ track 256: ...@@ -26,10 +26,14 @@ track 256:
drmInitData = - drmInitData = -
initializationData: initializationData:
data = length 22, hash CE183139 data = length 22, hash CE183139
total output bytes = 24315 total output bytes = 45026
sample count = 1 sample count = 2
sample 0: sample 0:
time = 55611 time = 55610
flags = 1
data = length 20711, hash 34341E8
sample 1:
time = 88977
flags = 0 flags = 0
data = length 18112, hash EC44B35B data = length 18112, hash EC44B35B
track 257: track 257:
...@@ -57,19 +61,19 @@ track 257: ...@@ -57,19 +61,19 @@ track 257:
total output bytes = 5015 total output bytes = 5015
sample count = 4 sample count = 4
sample 0: sample 0:
time = 11333 time = 44699
flags = 1 flags = 1
data = length 1253, hash 727FD1C6 data = length 1253, hash 727FD1C6
sample 1: sample 1:
time = 37455 time = 70821
flags = 1 flags = 1
data = length 1254, hash 73FB07B8 data = length 1254, hash 73FB07B8
sample 2: sample 2:
time = 63578 time = 96944
flags = 1 flags = 1
data = length 1254, hash 73FB07B8 data = length 1254, hash 73FB07B8
sample 3: sample 3:
time = 89700 time = 123066
flags = 1 flags = 1
data = length 1254, hash 73FB07B8 data = length 1254, hash 73FB07B8
track 8448: track 8448:
......
...@@ -26,10 +26,14 @@ track 256: ...@@ -26,10 +26,14 @@ track 256:
drmInitData = - drmInitData = -
initializationData: initializationData:
data = length 22, hash CE183139 data = length 22, hash CE183139
total output bytes = 24315 total output bytes = 45026
sample count = 1 sample count = 2
sample 0: sample 0:
time = 77855 time = 77854
flags = 1
data = length 20711, hash 34341E8
sample 1:
time = 111221
flags = 0 flags = 0
data = length 18112, hash EC44B35B data = length 18112, hash EC44B35B
track 257: track 257:
...@@ -57,19 +61,19 @@ track 257: ...@@ -57,19 +61,19 @@ track 257:
total output bytes = 5015 total output bytes = 5015
sample count = 4 sample count = 4
sample 0: sample 0:
time = 33577 time = 66943
flags = 1 flags = 1
data = length 1253, hash 727FD1C6 data = length 1253, hash 727FD1C6
sample 1: sample 1:
time = 59699 time = 93065
flags = 1 flags = 1
data = length 1254, hash 73FB07B8 data = length 1254, hash 73FB07B8
sample 2: sample 2:
time = 85822 time = 119188
flags = 1 flags = 1
data = length 1254, hash 73FB07B8 data = length 1254, hash 73FB07B8
sample 3: sample 3:
time = 111944 time = 145310
flags = 1 flags = 1
data = length 1254, hash 73FB07B8 data = length 1254, hash 73FB07B8
track 8448: track 8448:
......
...@@ -48,15 +48,15 @@ public class DefaultLoadControlTest { ...@@ -48,15 +48,15 @@ public class DefaultLoadControlTest {
createDefaultLoadControl(); createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
} }
@Test @Test
public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() { public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() {
createDefaultLoadControl(); createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue();
} }
...@@ -69,7 +69,7 @@ public class DefaultLoadControlTest { ...@@ -69,7 +69,7 @@ public class DefaultLoadControlTest {
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
} }
@Test @Test
...@@ -82,7 +82,7 @@ public class DefaultLoadControlTest { ...@@ -82,7 +82,7 @@ public class DefaultLoadControlTest {
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, SPEED)).isFalse(); assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
} }
@Test @Test
...@@ -100,7 +100,7 @@ public class DefaultLoadControlTest { ...@@ -100,7 +100,7 @@ public class DefaultLoadControlTest {
public void testShouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() { public void testShouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() {
createDefaultLoadControl(); createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, /* playbackSpeed= */ 100f)) assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, /* playbackSpeed= */ 100f))
.isFalse(); .isFalse();
} }
......
...@@ -45,10 +45,32 @@ public final class ShuffleOrderTest { ...@@ -45,10 +45,32 @@ public final class ShuffleOrderTest {
testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5); testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5);
} }
} }
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0); testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0, 1);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2); testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2, 3);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4); testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4, 5);
testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0); testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0, 1);
testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 1000);
testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 999);
testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 0, 500);
testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 100, 600);
testCloneAndRemove(new DefaultShuffleOrder(1000, RANDOM_SEED), 500, 1000);
}
@Test
public void testDefaultShuffleOrderSideloaded() {
int[] shuffledIndices = new int[] {2, 1, 0, 4, 3};
ShuffleOrder shuffleOrder = new DefaultShuffleOrder(shuffledIndices, RANDOM_SEED);
assertThat(shuffleOrder.getFirstIndex()).isEqualTo(2);
assertThat(shuffleOrder.getLastIndex()).isEqualTo(3);
for (int i = 0; i < 4; i++) {
assertThat(shuffleOrder.getNextIndex(shuffledIndices[i])).isEqualTo(shuffledIndices[i + 1]);
}
assertThat(shuffleOrder.getNextIndex(3)).isEqualTo(C.INDEX_UNSET);
for (int i = 4; i > 0; i--) {
assertThat(shuffleOrder.getPreviousIndex(shuffledIndices[i]))
.isEqualTo(shuffledIndices[i - 1]);
}
assertThat(shuffleOrder.getPreviousIndex(2)).isEqualTo(C.INDEX_UNSET);
} }
@Test @Test
...@@ -63,10 +85,15 @@ public final class ShuffleOrderTest { ...@@ -63,10 +85,15 @@ public final class ShuffleOrderTest {
testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5); testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5);
} }
} }
testCloneAndRemove(new UnshuffledShuffleOrder(5), 0); testCloneAndRemove(new UnshuffledShuffleOrder(5), 0, 1);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 2); testCloneAndRemove(new UnshuffledShuffleOrder(5), 2, 3);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 4); testCloneAndRemove(new UnshuffledShuffleOrder(5), 4, 5);
testCloneAndRemove(new UnshuffledShuffleOrder(1), 0); testCloneAndRemove(new UnshuffledShuffleOrder(1), 0, 1);
testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 1000);
testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 999);
testCloneAndRemove(new UnshuffledShuffleOrder(1000), 0, 500);
testCloneAndRemove(new UnshuffledShuffleOrder(1000), 100, 600);
testCloneAndRemove(new UnshuffledShuffleOrder(1000), 500, 1000);
} }
@Test @Test
...@@ -120,22 +147,24 @@ public final class ShuffleOrderTest { ...@@ -120,22 +147,24 @@ public final class ShuffleOrderTest {
} }
} }
private static void testCloneAndRemove(ShuffleOrder shuffleOrder, int position) { private static void testCloneAndRemove(
ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(position); ShuffleOrder shuffleOrder, int indexFrom, int indexToExclusive) {
assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - 1); int numberOfElementsToRemove = indexToExclusive - indexFrom;
ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(indexFrom, indexToExclusive);
assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - numberOfElementsToRemove);
// Assert all elements still have the relative same order // Assert all elements still have the relative same order
for (int i = 0; i < shuffleOrder.getLength(); i++) { for (int i = 0; i < shuffleOrder.getLength(); i++) {
if (i == position) { if (i >= indexFrom && i < indexToExclusive) {
continue; continue;
} }
int expectedNextIndex = shuffleOrder.getNextIndex(i); int expectedNextIndex = shuffleOrder.getNextIndex(i);
if (expectedNextIndex == position) { while (expectedNextIndex >= indexFrom && expectedNextIndex < indexToExclusive) {
expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex); expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex);
} }
if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) { if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= indexFrom) {
expectedNextIndex--; expectedNextIndex -= numberOfElementsToRemove;
} }
int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1); int newNextIndex = newOrder.getNextIndex(i < indexFrom ? i : i - numberOfElementsToRemove);
assertThat(newNextIndex).isEqualTo(expectedNextIndex); assertThat(newNextIndex).isEqualTo(expectedNextIndex);
} }
} }
......
...@@ -376,7 +376,6 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -376,7 +376,6 @@ public final class DashMediaSource extends BaseMediaSource {
private int staleManifestReloadAttempt; private int staleManifestReloadAttempt;
private long expiredManifestPublishTimeUs; private long expiredManifestPublishTimeUs;
private boolean dynamicMediaPresentationEnded;
private int firstPeriodId; private int firstPeriodId;
...@@ -679,7 +678,6 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -679,7 +678,6 @@ public final class DashMediaSource extends BaseMediaSource {
elapsedRealtimeOffsetMs = 0; elapsedRealtimeOffsetMs = 0;
staleManifestReloadAttempt = 0; staleManifestReloadAttempt = 0;
expiredManifestPublishTimeUs = C.TIME_UNSET; expiredManifestPublishTimeUs = C.TIME_UNSET;
dynamicMediaPresentationEnded = false;
firstPeriodId = 0; firstPeriodId = 0;
periodsById.clear(); periodsById.clear();
} }
...@@ -691,10 +689,6 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -691,10 +689,6 @@ public final class DashMediaSource extends BaseMediaSource {
startLoadingManifest(); startLoadingManifest();
} }
/* package */ void onDashLiveMediaPresentationEndSignalEncountered() {
this.dynamicMediaPresentationEnded = true;
}
/* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { /* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {
if (this.expiredManifestPublishTimeUs == C.TIME_UNSET if (this.expiredManifestPublishTimeUs == C.TIME_UNSET
|| this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) { || this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) {
...@@ -734,9 +728,8 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -734,9 +728,8 @@ public final class DashMediaSource extends BaseMediaSource {
// behind. // behind.
Log.w(TAG, "Loaded out of sync manifest"); Log.w(TAG, "Loaded out of sync manifest");
isManifestStale = true; isManifestStale = true;
} else if (dynamicMediaPresentationEnded } else if (expiredManifestPublishTimeUs != C.TIME_UNSET
|| (expiredManifestPublishTimeUs != C.TIME_UNSET && newManifest.publishTimeMs * 1000 <= expiredManifestPublishTimeUs) {
&& newManifest.publishTimeMs * 1000 <= expiredManifestPublishTimeUs)) {
// If we receive a dynamic manifest that's older than expected (i.e. its publish time has // If we receive a dynamic manifest that's older than expected (i.e. its publish time has
// expired, or it's dynamic and we know the presentation has ended), then this manifest is // expired, or it's dynamic and we know the presentation has ended), then this manifest is
// stale. // stale.
...@@ -745,8 +738,6 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -745,8 +738,6 @@ public final class DashMediaSource extends BaseMediaSource {
"Loaded stale dynamic manifest: " "Loaded stale dynamic manifest: "
+ newManifest.publishTimeMs + newManifest.publishTimeMs
+ ", " + ", "
+ dynamicMediaPresentationEnded
+ ", "
+ expiredManifestPublishTimeUs); + expiredManifestPublishTimeUs);
isManifestStale = true; isManifestStale = true;
} }
...@@ -763,7 +754,6 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -763,7 +754,6 @@ public final class DashMediaSource extends BaseMediaSource {
staleManifestReloadAttempt = 0; staleManifestReloadAttempt = 0;
} }
manifest = newManifest; manifest = newManifest;
manifestLoadPending &= manifest.dynamic; manifestLoadPending &= manifest.dynamic;
manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs;
...@@ -1170,12 +1160,16 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -1170,12 +1160,16 @@ public final class DashMediaSource extends BaseMediaSource {
long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs(
defaultPositionProjectionUs); defaultPositionProjectionUs);
Object tag = setTag ? windowTag : null; Object tag = setTag ? windowTag : null;
boolean isDynamic =
manifest.dynamic
&& manifest.minUpdatePeriodMs != C.TIME_UNSET
&& manifest.durationMs == C.TIME_UNSET;
return window.set( return window.set(
tag, tag,
presentationStartTimeMs, presentationStartTimeMs,
windowStartTimeMs, windowStartTimeMs,
/* isSeekable= */ true, /* isSeekable= */ true,
manifest.dynamic, isDynamic,
windowDefaultStartPositionUs, windowDefaultStartPositionUs,
windowDurationUs, windowDurationUs,
/* firstPeriodIndex= */ 0, /* firstPeriodIndex= */ 0,
...@@ -1253,11 +1247,6 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -1253,11 +1247,6 @@ public final class DashMediaSource extends BaseMediaSource {
public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {
DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);
} }
@Override
public void onDashLiveMediaPresentationEndSignalEncountered() {
DashMediaSource.this.onDashLiveMediaPresentationEndSignalEncountered();
}
} }
private final class ManifestCallback implements Loader.Callback<ParsingLoadable<DashManifest>> { private final class ManifestCallback implements Loader.Callback<ParsingLoadable<DashManifest>> {
......
...@@ -318,9 +318,12 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -318,9 +318,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
} }
long periodDurationUs = representationHolder.periodDurationUs;
boolean periodEnded = periodDurationUs != C.TIME_UNSET;
if (representationHolder.getSegmentCount() == 0) { if (representationHolder.getSegmentCount() == 0) {
// The index doesn't define any segments. // The index doesn't define any segments.
out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); out.endOfStream = periodEnded;
return; return;
} }
...@@ -343,17 +346,15 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -343,17 +346,15 @@ public class DefaultDashChunkSource implements DashChunkSource {
fatalError = new BehindLiveWindowException(); fatalError = new BehindLiveWindowException();
return; return;
} }
if (segmentNum > lastAvailableSegmentNum if (segmentNum > lastAvailableSegmentNum
|| (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) { || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) {
// The segment is beyond the end of the period. We know the period will not be extended if the // The segment is beyond the end of the period.
// manifest is static, or if there's a period after this one. out.endOfStream = periodEnded;
out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1);
return; return;
} }
long periodDurationUs = representationHolder.periodDurationUs; if (periodEnded && representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) {
if (periodDurationUs != C.TIME_UNSET
&& representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) {
// The period duration clips the period to a position before the segment. // The period duration clips the period to a position before the segment.
out.endOfStream = true; out.endOfStream = true;
return; return;
......
...@@ -60,8 +60,7 @@ import java.util.TreeMap; ...@@ -60,8 +60,7 @@ import java.util.TreeMap;
*/ */
public final class PlayerEmsgHandler implements Handler.Callback { public final class PlayerEmsgHandler implements Handler.Callback {
private static final int EMSG_MEDIA_PRESENTATION_ENDED = 1; private static final int EMSG_MANIFEST_EXPIRED = 1;
private static final int EMSG_MANIFEST_EXPIRED = 2;
/** Callbacks for player emsg events encountered during DASH live stream. */ /** Callbacks for player emsg events encountered during DASH live stream. */
public interface PlayerEmsgCallback { public interface PlayerEmsgCallback {
...@@ -75,9 +74,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -75,9 +74,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
* @param expiredManifestPublishTimeUs The manifest publish time that has been expired. * @param expiredManifestPublishTimeUs The manifest publish time that has been expired.
*/ */
void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs); void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs);
/** Called when a media presentation end signal is encountered during live stream. * */
void onDashLiveMediaPresentationEndSignalEncountered();
} }
private final Allocator allocator; private final Allocator allocator;
...@@ -88,7 +84,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -88,7 +84,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
private DashManifest manifest; private DashManifest manifest;
private boolean dynamicMediaPresentationEnded;
private long expiredManifestPublishTimeUs; private long expiredManifestPublishTimeUs;
private long lastLoadedChunkEndTimeUs; private long lastLoadedChunkEndTimeUs;
private long lastLoadedChunkEndTimeBeforeRefreshUs; private long lastLoadedChunkEndTimeBeforeRefreshUs;
...@@ -134,21 +129,15 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -134,21 +129,15 @@ public final class PlayerEmsgHandler implements Handler.Callback {
return true; return true;
} }
boolean manifestRefreshNeeded = false; boolean manifestRefreshNeeded = false;
if (dynamicMediaPresentationEnded) { // Find the smallest publishTime (greater than or equal to the current manifest's publish time)
// The manifest we have is dynamic, but we know a non-dynamic one representing the final state // that has a corresponding expiry time.
// should be available. Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs);
manifestRefreshNeeded = true; if (expiredEntry != null) {
} else { long expiredPointUs = expiredEntry.getValue();
// Find the smallest publishTime (greater than or equal to the current manifest's publish if (expiredPointUs < presentationPositionUs) {
// time) that has a corresponding expiry time. expiredManifestPublishTimeUs = expiredEntry.getKey();
Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs); notifyManifestPublishTimeExpired();
if (expiredEntry != null) { manifestRefreshNeeded = true;
long expiredPointUs = expiredEntry.getValue();
if (expiredPointUs < presentationPositionUs) {
expiredManifestPublishTimeUs = expiredEntry.getKey();
notifyManifestPublishTimeExpired();
manifestRefreshNeeded = true;
}
} }
} }
if (manifestRefreshNeeded) { if (manifestRefreshNeeded) {
...@@ -221,9 +210,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -221,9 +210,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
return true; return true;
} }
switch (message.what) { switch (message.what) {
case (EMSG_MEDIA_PRESENTATION_ENDED):
handleMediaPresentationEndedMessageEncountered();
return true;
case (EMSG_MANIFEST_EXPIRED): case (EMSG_MANIFEST_EXPIRED):
ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj; ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj;
handleManifestExpiredMessage( handleManifestExpiredMessage(
...@@ -248,11 +234,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -248,11 +234,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
} }
} }
private void handleMediaPresentationEndedMessageEncountered() {
dynamicMediaPresentationEnded = true;
notifySourceMediaPresentationEnded();
}
private @Nullable Map.Entry<Long, Long> ceilingExpiryEntryForPublishTime(long publishTimeMs) { private @Nullable Map.Entry<Long, Long> ceilingExpiryEntryForPublishTime(long publishTimeMs) {
return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs); return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs);
} }
...@@ -273,10 +254,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -273,10 +254,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);
} }
private void notifySourceMediaPresentationEnded() {
playerEmsgCallback.onDashLiveMediaPresentationEndSignalEncountered();
}
/** Requests DASH media manifest to be refreshed if necessary. */ /** Requests DASH media manifest to be refreshed if necessary. */
private void maybeNotifyDashManifestRefreshNeeded() { private void maybeNotifyDashManifestRefreshNeeded() {
if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET
...@@ -298,12 +275,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -298,12 +275,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
} }
} }
private static boolean isMessageSignalingMediaPresentationEnded(EventMessage eventMessage) {
// According to section 4.5.2.1 DASH-IF IOP, if both presentation time delta and event duration
// are zero, the media presentation is ended.
return eventMessage.presentationTimeUs == 0 && eventMessage.durationMs == 0;
}
/** Handles emsg messages for a specific track for the player. */ /** Handles emsg messages for a specific track for the player. */
public final class PlayerTrackEmsgHandler implements TrackOutput { public final class PlayerTrackEmsgHandler implements TrackOutput {
...@@ -413,16 +384,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -413,16 +384,7 @@ public final class PlayerEmsgHandler implements Handler.Callback {
if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) { if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) {
return; return;
} }
onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg);
if (isMessageSignalingMediaPresentationEnded(eventMessage)) {
onMediaPresentationEndedMessageEncountered();
} else {
onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg);
}
}
private void onMediaPresentationEndedMessageEncountered() {
handler.sendMessage(handler.obtainMessage(EMSG_MEDIA_PRESENTATION_ENDED));
} }
private void onManifestExpiredMessageEncountered( private void onManifestExpiredMessageEncountered(
......
# Proguard rules specific to the dash module.
# Constructors accessed via reflection in SegmentDownloadAction
-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloadAction
-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloadAction {
static ** DESERIALIZER;
}
...@@ -262,7 +262,8 @@ import java.util.List; ...@@ -262,7 +262,8 @@ import java.util.List;
// Retry when playlist is refreshed. // Retry when playlist is refreshed.
return; return;
} }
HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); HlsMediaPlaylist mediaPlaylist =
playlistTracker.getPlaylistSnapshot(selectedUrl, /* isForPlayback= */ true);
independentSegments = mediaPlaylist.hasIndependentSegments; independentSegments = mediaPlaylist.hasIndependentSegments;
updateLiveEdgeTimeUs(mediaPlaylist); updateLiveEdgeTimeUs(mediaPlaylist);
...@@ -279,7 +280,7 @@ import java.util.List; ...@@ -279,7 +280,7 @@ import java.util.List;
// behind the live window. // behind the live window.
selectedVariantIndex = oldVariantIndex; selectedVariantIndex = oldVariantIndex;
selectedUrl = variants[selectedVariantIndex]; selectedUrl = variants[selectedVariantIndex];
mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl, /* isForPlayback= */ true);
startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
chunkMediaSequence = previous.getNextChunkIndex(); chunkMediaSequence = previous.getNextChunkIndex();
...@@ -435,7 +436,8 @@ import java.util.List; ...@@ -435,7 +436,8 @@ import java.util.List;
chunkIterators[i] = MediaChunkIterator.EMPTY; chunkIterators[i] = MediaChunkIterator.EMPTY;
continue; continue;
} }
HlsMediaPlaylist playlist = playlistTracker.getPlaylistSnapshot(variantUrl); HlsMediaPlaylist playlist =
playlistTracker.getPlaylistSnapshot(variantUrl, /* isForPlayback= */ false);
long startOfPlaylistInPeriodUs = long startOfPlaylistInPeriodUs =
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
boolean switchingVariant = variantIndex != oldVariantIndex; boolean switchingVariant = variantIndex != oldVariantIndex;
......
...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.upstream.DataSpec; ...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
...@@ -41,8 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -41,8 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger;
*/ */
/* package */ final class HlsMediaChunk extends MediaChunk { /* package */ final class HlsMediaChunk extends MediaChunk {
public static final String PRIV_TIMESTAMP_FRAME_OWNER =
private static final String PRIV_TIMESTAMP_FRAME_OWNER =
"com.apple.streaming.transportStreamTimestamp"; "com.apple.streaming.transportStreamTimestamp";
private static final AtomicInteger uidSource = new AtomicInteger(); private static final AtomicInteger uidSource = new AtomicInteger();
...@@ -313,8 +313,10 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -313,8 +313,10 @@ import java.util.concurrent.atomic.AtomicInteger;
*/ */
private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException { private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition(); input.resetPeekPosition();
if (input.getLength() < Id3Decoder.ID3_HEADER_LENGTH try {
|| !input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) { input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
} catch (EOFException e) {
// The input isn't long enough for there to be any ID3 data.
return C.TIME_UNSET; return C.TIME_UNSET;
} }
id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH); id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
...@@ -330,9 +332,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -330,9 +332,7 @@ import java.util.concurrent.atomic.AtomicInteger;
id3Data.reset(requiredCapacity); id3Data.reset(requiredCapacity);
System.arraycopy(data, 0, id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH); System.arraycopy(data, 0, id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
} }
if (!input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size, true)) { input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size);
return C.TIME_UNSET;
}
Metadata metadata = id3Decoder.decode(id3Data.data, id3Size); Metadata metadata = id3Decoder.decode(id3Data.data, id3Size);
if (metadata == null) { if (metadata == null) {
return C.TIME_UNSET; return C.TIME_UNSET;
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.hls; package com.google.android.exoplayer2.source.hls;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
...@@ -24,6 +25,8 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput; ...@@ -24,6 +25,8 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
...@@ -791,7 +794,7 @@ import java.util.List; ...@@ -791,7 +794,7 @@ import java.util.List;
return createDummyTrackOutput(id, type); return createDummyTrackOutput(id, type);
} }
} }
SampleQueue trackOutput = new SampleQueue(allocator); SampleQueue trackOutput = new PrivTimestampStrippingSampleQueue(allocator);
trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.sourceId(chunkUid); trackOutput.sourceId(chunkUid);
trackOutput.setUpstreamFormatChangeListener(this); trackOutput.setUpstreamFormatChangeListener(this);
...@@ -1126,4 +1129,53 @@ import java.util.List; ...@@ -1126,4 +1129,53 @@ import java.util.List;
Log.w(TAG, "Unmapped track with id " + id + " of type " + type); Log.w(TAG, "Unmapped track with id " + id + " of type " + type);
return new DummyTrackOutput(); return new DummyTrackOutput();
} }
private static final class PrivTimestampStrippingSampleQueue extends SampleQueue {
public PrivTimestampStrippingSampleQueue(Allocator allocator) {
super(allocator);
}
@Override
public void format(Format format) {
super.format(format.copyWithMetadata(getAdjustedMetadata(format.metadata)));
}
/**
* Strips the private timestamp frame from metadata, if present. See:
* https://github.com/google/ExoPlayer/issues/5063
*/
@Nullable
private Metadata getAdjustedMetadata(@Nullable Metadata metadata) {
if (metadata == null) {
return null;
}
int length = metadata.length();
int transportStreamTimestampMetadataIndex = C.INDEX_UNSET;
for (int i = 0; i < length; i++) {
Metadata.Entry metadataEntry = metadata.get(i);
if (metadataEntry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) metadataEntry;
if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) {
transportStreamTimestampMetadataIndex = i;
break;
}
}
}
if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) {
return metadata;
}
if (length == 1) {
return null;
}
Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1];
for (int i = 0; i < length; i++) {
if (i != transportStreamTimestampMetadataIndex) {
int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1;
newMetadataEntries[newIndex] = metadata.get(i);
}
}
return new Metadata(newMetadataEntries);
}
}
} }
...@@ -162,9 +162,9 @@ public final class DefaultHlsPlaylistTracker ...@@ -162,9 +162,9 @@ public final class DefaultHlsPlaylistTracker
} }
@Override @Override
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url, boolean isForPlayback) {
HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
if (snapshot != null) { if (snapshot != null && isForPlayback) {
maybeSetPrimaryUrl(url); maybeSetPrimaryUrl(url);
} }
return snapshot; return snapshot;
......
...@@ -167,11 +167,13 @@ public interface HlsPlaylistTracker { ...@@ -167,11 +167,13 @@ public interface HlsPlaylistTracker {
* HlsUrl}. * HlsUrl}.
* *
* @param url The {@link HlsUrl} corresponding to the requested media playlist. * @param url The {@link HlsUrl} corresponding to the requested media playlist.
* @param isForPlayback Whether the caller might use the snapshot to request media segments for
* playback. If true, the primary playlist may be updated to the one requested.
* @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May * @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May
* be null if no snapshot has been loaded yet. * be null if no snapshot has been loaded yet.
*/ */
@Nullable @Nullable
HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url); HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url, boolean isForPlayback);
/** /**
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no * Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
......
# Proguard rules specific to the hls module.
# Constructors accessed via reflection in SegmentDownloadAction
-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction
-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction {
static ** DESERIALIZER;
}
...@@ -378,8 +378,12 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> { ...@@ -378,8 +378,12 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid, DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid,
MimeTypes.VIDEO_MP4, protectionElement.data)); MimeTypes.VIDEO_MP4, protectionElement.data));
for (StreamElement streamElement : streamElementArray) { for (StreamElement streamElement : streamElementArray) {
for (int i = 0; i < streamElement.formats.length; i++) { int type = streamElement.type;
streamElement.formats[i] = streamElement.formats[i].copyWithDrmInitData(drmInitData); if (type == C.TRACK_TYPE_VIDEO || type == C.TRACK_TYPE_AUDIO) {
Format[] formats = streamElement.formats;
for (int i = 0; i < formats.length; i++) {
formats[i] = formats[i].copyWithDrmInitData(drmInitData);
}
} }
} }
} }
......
# Proguard rules specific to the smoothstreaming module.
# Constructors accessed via reflection in SegmentDownloadAction
-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction
-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction {
static ** DESERIALIZER;
}
...@@ -241,11 +241,7 @@ import java.util.List; ...@@ -241,11 +241,7 @@ import java.util.List;
*/ */
public class PlayerView extends FrameLayout { public class PlayerView extends FrameLayout {
private static final int SURFACE_TYPE_NONE = 0; // LINT.IfChange
private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;
private static final int SURFACE_TYPE_MONO360_VIEW = 3;
/** /**
* Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link * Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link
* #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}. * #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}.
...@@ -266,6 +262,14 @@ public class PlayerView extends FrameLayout { ...@@ -266,6 +262,14 @@ public class PlayerView extends FrameLayout {
* buffering} state. * buffering} state.
*/ */
public static final int SHOW_BUFFERING_ALWAYS = 2; public static final int SHOW_BUFFERING_ALWAYS = 2;
// LINT.ThenChange(../../../../../../res/values/attrs.xml)
// LINT.IfChange
private static final int SURFACE_TYPE_NONE = 0;
private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;
private static final int SURFACE_TYPE_MONO360_VIEW = 3;
// LINT.ThenChange(../../../../../../res/values/attrs.xml)
private final AspectRatioFrameLayout contentFrame; private final AspectRatioFrameLayout contentFrame;
private final View shutterView; private final View shutterView;
......
...@@ -104,12 +104,18 @@ public final class SphericalSurfaceView extends GLSurfaceView { ...@@ -104,12 +104,18 @@ public final class SphericalSurfaceView extends GLSurfaceView {
// Configure sensors and touch. // Configure sensors and touch.
sensorManager = sensorManager =
(SensorManager) Assertions.checkNotNull(context.getSystemService(Context.SENSOR_SERVICE)); (SensorManager) Assertions.checkNotNull(context.getSystemService(Context.SENSOR_SERVICE));
// TYPE_GAME_ROTATION_VECTOR is the easiest sensor since it handles all the complex math for Sensor orientationSensor = null;
// fusion. It's used instead of TYPE_ROTATION_VECTOR since the latter uses the magnetometer on if (Util.SDK_INT >= 18) {
// devices. When used indoors, the magnetometer can take some time to settle depending on the // TYPE_GAME_ROTATION_VECTOR is the easiest sensor since it handles all the complex math for
// device and amount of metal in the environment. // fusion. It's used instead of TYPE_ROTATION_VECTOR since the latter uses the magnetometer on
int type = Util.SDK_INT >= 18 ? Sensor.TYPE_GAME_ROTATION_VECTOR : Sensor.TYPE_ROTATION_VECTOR; // devices. When used indoors, the magnetometer can take some time to settle depending on the
orientationSensor = sensorManager.getDefaultSensor(type); // device and amount of metal in the environment.
orientationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);
}
if (orientationSensor == null) {
orientationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
}
this.orientationSensor = orientationSensor;
scene = new SceneRenderer(); scene = new SceneRenderer();
renderer = new Renderer(scene); renderer = new Renderer(scene);
......
...@@ -44,11 +44,10 @@ import android.view.View; ...@@ -44,11 +44,10 @@ import android.view.View;
* a nicer UI. An even more advanced UI would reproject the user's touch point into 3D and drag the * a nicer UI. An even more advanced UI would reproject the user's touch point into 3D and drag the
* Mesh as the user moves their finger. However, that requires quaternion interpolation. * Mesh as the user moves their finger. However, that requires quaternion interpolation.
*/ */
// @VisibleForTesting /* package */ class TouchTracker extends GestureDetector.SimpleOnGestureListener
/*package*/ class TouchTracker extends GestureDetector.SimpleOnGestureListener
implements View.OnTouchListener { implements View.OnTouchListener {
/*package*/ interface Listener { /* package */ interface Listener {
void onScrollChange(PointF scrollOffsetDegrees); void onScrollChange(PointF scrollOffsetDegrees);
} }
......
...@@ -53,8 +53,8 @@ ...@@ -53,8 +53,8 @@
<attr name="auto_show" format="boolean"/> <attr name="auto_show" format="boolean"/>
<attr name="show_buffering" format="enum"> <attr name="show_buffering" format="enum">
<enum name="never" value="0"/> <enum name="never" value="0"/>
<enum name="always" value="1"/> <enum name="when_playing" value="1"/>
<enum name="when_playing" value="2"/> <enum name="always" value="2"/>
</attr> </attr>
<attr name="keep_content_on_player_reset" format="boolean"/> <attr name="keep_content_on_player_reset" format="boolean"/>
<attr name="resize_mode"/> <attr name="resize_mode"/>
......
...@@ -61,8 +61,8 @@ public final class FakeShuffleOrder implements ShuffleOrder { ...@@ -61,8 +61,8 @@ public final class FakeShuffleOrder implements ShuffleOrder {
} }
@Override @Override
public ShuffleOrder cloneAndRemove(int removalIndex) { public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {
return new FakeShuffleOrder(length - 1); return new FakeShuffleOrder(length - indexToExclusive + indexFrom);
} }
@Override @Override
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment