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.
## Pull requests ##
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
be suitable for merging into older `dev-vX` branches. Before a pull request can
be accepted you must submit a Contributor License Agreement, as described below.
into the `dev-v2` branch. Before a pull request can be accepted you must submit
a Contributor License Agreement, as described below.
[dev]: https://github.com/google/ExoPlayer/tree/dev
......
# 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 ###
* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext`
......@@ -20,7 +47,7 @@
* DASH: Parse ProgramInformation element if present in the manifest.
* HLS:
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
reader factory flags
reader factory flags.
* Fix bug in segment sniffing
([#5039](https://github.com/google/ExoPlayer/issues/5039)).
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
......
......@@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.9.1'
releaseVersionCode = 2009001
releaseVersion = '2.9.2'
releaseVersionCode = 2009002
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided
......
......@@ -81,8 +81,6 @@ import java.util.List;
+ "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
MIME_TYPE_VIDEO_MP4));
SAMPLES = Collections.unmodifiableList(samples);
}
......
......@@ -17,7 +17,7 @@
<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>
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.cronet;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
......@@ -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.
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
......
......@@ -46,7 +46,7 @@ HOST_PLATFORM="linux-x86_64"
be supported. See the [Supported formats][] page for more details of the
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:
```
......@@ -71,7 +71,7 @@ COMMON_OPTIONS="\
" && \
cd "${FFMPEG_EXT_PATH}/jni" && \
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
cd ffmpeg && \
cd ffmpeg && git checkout release/4.0 && \
./configure \
--libdir=android-libs/armeabi-v7a \
--arch=arm \
......
......@@ -145,12 +145,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
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) {
Assertions.checkNotNull(inputFormat.sampleMimeType);
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
if (!enableFloatOutput || !supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_FLOAT)) {
return false;
}
switch (inputFormat.sampleMimeType) {
......
......@@ -53,7 +53,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
} else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
......
......@@ -31,13 +31,13 @@ android {
}
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 '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
// com.android.support:support-v4 and com.android.support:customtabs to be
// 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
implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:customtabs:' + supportLibraryVersion
......
......@@ -15,6 +15,10 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.ext.ima">
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<application>
<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>
# 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 {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-media-compat:' + supportLibraryVersion
api 'com.android.support:support-media-compat:' + supportLibraryVersion
}
ext {
......
......@@ -49,6 +49,11 @@ import java.util.Map;
/**
* 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
* 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
......
......@@ -78,7 +78,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
if (!OpusLibrary.isAvailable()
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
} else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
......
......@@ -33,6 +33,8 @@ dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
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 {
private PriorityTaskManager priorityTaskManager;
private int backBufferDurationMs;
private boolean retainBackBufferFromKeyframe;
private boolean createDefaultLoadControlCalled;
/** Constructs a new instance. */
public Builder() {
......@@ -99,8 +100,10 @@ public class DefaultLoadControl implements LoadControl {
*
* @param allocator The {@link DefaultAllocator}.
* @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setAllocator(DefaultAllocator allocator) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.allocator = allocator;
return this;
}
......@@ -118,12 +121,14 @@ public class DefaultLoadControl implements LoadControl {
* for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be
* caused by buffer depletion rather than a user action.
* @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setBufferDurationsMs(
int minBufferMs,
int maxBufferMs,
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.minBufferMs = minBufferMs;
this.maxBufferMs = maxBufferMs;
this.bufferForPlaybackMs = bufferForPlaybackMs;
......@@ -137,8 +142,10 @@ public class DefaultLoadControl implements LoadControl {
*
* @param targetBufferBytes The target buffer size in bytes.
* @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setTargetBufferBytes(int targetBufferBytes) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.targetBufferBytes = targetBufferBytes;
return this;
}
......@@ -150,8 +157,10 @@ public class DefaultLoadControl implements LoadControl {
* @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
* constraints over buffer size constraints.
* @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setPrioritizeTimeOverSizeThresholds(boolean prioritizeTimeOverSizeThresholds) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
return this;
}
......@@ -161,8 +170,10 @@ public class DefaultLoadControl implements LoadControl {
*
* @param priorityTaskManager The {@link PriorityTaskManager} to use.
* @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setPriorityTaskManager(PriorityTaskManager priorityTaskManager) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.priorityTaskManager = priorityTaskManager;
return this;
}
......@@ -175,8 +186,10 @@ public class DefaultLoadControl implements LoadControl {
* @param retainBackBufferFromKeyframe Whether the back buffer is retained from the previous
* keyframe.
* @return This builder, for convenience.
* @throws IllegalStateException If {@link #createDefaultLoadControl()} has already been called.
*/
public Builder setBackBuffer(int backBufferDurationMs, boolean retainBackBufferFromKeyframe) {
Assertions.checkState(!createDefaultLoadControlCalled);
this.backBufferDurationMs = backBufferDurationMs;
this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe;
return this;
......@@ -184,6 +197,7 @@ public class DefaultLoadControl implements LoadControl {
/** Creates a {@link DefaultLoadControl}. */
public DefaultLoadControl createDefaultLoadControl() {
createDefaultLoadControlCalled = true;
if (allocator == null) {
allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
}
......@@ -371,7 +385,7 @@ public class DefaultLoadControl implements LoadControl {
}
if (bufferedDurationUs < minBufferUs) {
isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached;
} else if (bufferedDurationUs > maxBufferUs || targetBufferSizeReached) {
} else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
isBuffering = false;
} // Else don't change the buffering state
if (priorityTaskManager != null && isBuffering != wasBuffering) {
......
......@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.9.1";
public static final String VERSION = "2.9.2";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.1";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.2";
/**
* The version of the library expressed as an integer, for example 1002003.
......@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2009001;
public static final int VERSION_INT = 2009002;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
......@@ -139,9 +139,12 @@ public abstract class Timeline {
*/
public boolean isSeekable;
/**
* Whether this window may change when the timeline is updated.
*/
// TODO: Split this to better describe which parts of the window might change. For example it
// 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;
/**
......
......@@ -29,11 +29,11 @@ import java.util.Arrays;
@TargetApi(21)
public final class AudioCapabilities {
/**
* The minimum audio capabilities supported by all devices.
*/
private static final int DEFAULT_MAX_CHANNEL_COUNT = 8;
/** The minimum audio capabilities supported by all devices. */
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.
......@@ -52,8 +52,10 @@ public final class AudioCapabilities {
if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) {
return DEFAULT_AUDIO_CAPABILITIES;
}
return new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),
intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0));
return new AudioCapabilities(
intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),
intent.getIntExtra(
AudioManager.EXTRA_MAX_CHANNEL_COUNT, /* defaultValue= */ DEFAULT_MAX_CHANNEL_COUNT));
}
private final int[] supportedEncodings;
......
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio;
import android.media.AudioTrack;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import java.nio.ByteBuffer;
......@@ -165,12 +166,13 @@ public interface AudioSink {
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.
* @return Whether it's possible to play audio in the specified encoding.
* @param channelCount The number of channels, or {@link Format#NO_VALUE} if not known.
* @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
......
......@@ -377,14 +377,18 @@ public final class DefaultAudioSink implements AudioSink {
}
@Override
public boolean isEncodingSupported(@C.Encoding int encoding) {
public boolean supportsOutput(int channelCount, @C.Encoding int encoding) {
if (Util.isEncodingLinearPcm(encoding)) {
// 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
// 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;
} 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 {
isInputPcm = Util.isEncodingLinearPcm(inputEncoding);
shouldConvertHighResIntPcmToFloat =
enableConvertHighResIntPcmToFloat
&& isEncodingSupported(C.ENCODING_PCM_32BIT)
&& supportsOutput(channelCount, C.ENCODING_PCM_32BIT)
&& Util.isEncodingHighResolutionIntegerPcm(inputEncoding);
if (isInputPcm) {
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
......
......@@ -272,12 +272,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData);
if (supportsFormatDrm && allowPassthrough(mimeType)
if (supportsFormatDrm
&& allowPassthrough(format.channelCount, mimeType)
&& mediaCodecSelector.getPassthroughDecoderInfo() != null) {
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
}
if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.isEncodingSupported(format.pcmEncoding))
|| !audioSink.isEncodingSupported(C.ENCODING_PCM_16BIT)) {
if ((MimeTypes.AUDIO_RAW.equals(mimeType)
&& !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.
return FORMAT_UNSUPPORTED_SUBTYPE;
}
......@@ -316,7 +318,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
protected List<MediaCodecInfo> getDecoderInfos(
MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder)
throws DecoderQueryException {
if (allowPassthrough(format.sampleMimeType)) {
if (allowPassthrough(format.channelCount, format.sampleMimeType)) {
MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();
if (passthroughDecoderInfo != null) {
return Collections.singletonList(passthroughDecoderInfo);
......@@ -330,12 +332,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* This implementation returns true if the {@link AudioSink} indicates that encoded audio output
* 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.
* @return Whether passthrough playback is supported.
*/
protected boolean allowPassthrough(String mimeType) {
@C.Encoding int encoding = MimeTypes.getEncoding(mimeType);
return encoding != C.ENCODING_INVALID && audioSink.isEncodingSupported(encoding);
protected boolean allowPassthrough(int channelCount, String mimeType) {
return audioSink.supportsOutput(channelCount, MimeTypes.getEncoding(mimeType));
}
@Override
......
......@@ -249,13 +249,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
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.
* @return Whether the audio sink can accept audio in the specified encoding.
* @see AudioSink#supportsOutput(int, int)
*/
protected final boolean supportsOutputEncoding(@C.Encoding int encoding) {
return audioSink.isEncodingSupported(encoding);
protected final boolean supportsOutput(int channelCount, @C.Encoding int encoding) {
return audioSink.supportsOutput(channelCount, encoding);
}
@Override
......
......@@ -70,9 +70,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
Assertions.checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
this.uuid = uuid;
// ClearKey had to be accessed using the Common PSSH UUID prior to API level 27.
this.mediaDrm =
new MediaDrm(Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid);
this.mediaDrm = new MediaDrm(adjustUuid(uuid));
if (C.WIDEVINE_UUID.equals(uuid) && needsForceWidevineL3Workaround()) {
forceWidevineL3(mediaDrm);
}
......@@ -152,7 +150,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
@Override
public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException {
if (C.CLEARKEY_UUID.equals(uuid)) {
response = ClearKeyUtil.adjustResponseData(response);
}
......@@ -212,8 +209,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
// indicate that it required secure video decoders [Internal ref: b/11428937].
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
return new FrameworkMediaCrypto(new MediaCrypto(uuid, initData),
forceAllowInsecureDecoderComponents);
return new FrameworkMediaCrypto(
new MediaCrypto(adjustUuid(uuid), initData), forceAllowInsecureDecoderComponents);
}
private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
......@@ -269,6 +266,11 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
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) {
// 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.
......
......@@ -42,37 +42,25 @@ public abstract class BinarySearchSeeker {
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
* of {@code searchRangeBytes} bytes from that input, look for all available timestamps from all
* frames in that range, compare those with the target timestamp, and return one of the {@link
* 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
* @param input The {@link ExtractorInput} from which data should be peeked.
* @param targetTimestamp The target timestamp.
* @param outputFrameHolder If {@link TimestampSearchResult#TYPE_TARGET_TIMESTAMP_FOUND} is
* returned, this holder may be updated to hold the extracted frame that contains the target
* frame/sample associated with the target timestamp.
* @return A {@link TimestampSearchResult}, that includes a {@link TimestampSearchResult#result}
* 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>
*
* @return A {@link TimestampSearchResult} that describes the result of the search.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
TimestampSearchResult searchForTimestamp(
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException;
/** Called when a seek operation finishes. */
default void onSeekFinished() {}
}
/**
......@@ -231,22 +219,22 @@ public abstract class BinarySearchSeeker {
timestampSeeker.searchForTimestamp(
input, seekOperationParams.getTargetTimePosition(), outputFrameHolder);
switch (timestampSearchResult.result) {
case TimestampSearchResult.RESULT_POSITION_OVERESTIMATED:
switch (timestampSearchResult.type) {
case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED:
seekOperationParams.updateSeekCeiling(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break;
case TimestampSearchResult.RESULT_POSITION_UNDERESTIMATED:
case TimestampSearchResult.TYPE_POSITION_UNDERESTIMATED:
seekOperationParams.updateSeekFloor(
timestampSearchResult.timestampToUpdate, timestampSearchResult.bytePositionToUpdate);
break;
case TimestampSearchResult.RESULT_TARGET_TIMESTAMP_FOUND:
case TimestampSearchResult.TYPE_TARGET_TIMESTAMP_FOUND:
markSeekOperationFinished(
/* foundTargetFrame= */ true, timestampSearchResult.bytePositionToUpdate);
skipInputUntilPosition(input, timestampSearchResult.bytePositionToUpdate);
return seekToPosition(
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.
// Give up, and just continue reading from the last search position in this case.
markSeekOperationFinished(/* foundTargetFrame= */ false, searchPosition);
......@@ -270,6 +258,7 @@ public abstract class BinarySearchSeeker {
protected final void markSeekOperationFinished(boolean foundTargetFrame, long resultPosition) {
seekOperationParams = null;
timestampSeeker.onSeekFinished();
onSeekOperationFinished(foundTargetFrame, resultPosition);
}
......@@ -433,45 +422,49 @@ public abstract class BinarySearchSeeker {
*/
public static final class TimestampSearchResult {
public static final int RESULT_TARGET_TIMESTAMP_FOUND = 0;
public static final int RESULT_POSITION_OVERESTIMATED = -1;
public static final int RESULT_POSITION_UNDERESTIMATED = -2;
public static final int RESULT_NO_TIMESTAMP = -3;
/** The search found a timestamp that it deems close enough to the given target. */
public static final int TYPE_TARGET_TIMESTAMP_FOUND = 0;
/** The search found only timestamps larger than the target timestamp. */
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
@Retention(RetentionPolicy.SOURCE)
@IntDef({
RESULT_TARGET_TIMESTAMP_FOUND,
RESULT_POSITION_OVERESTIMATED,
RESULT_POSITION_UNDERESTIMATED,
RESULT_NO_TIMESTAMP
TYPE_TARGET_TIMESTAMP_FOUND,
TYPE_POSITION_OVERESTIMATED,
TYPE_POSITION_UNDERESTIMATED,
TYPE_NO_TIMESTAMP
})
@interface SearchResult {}
@interface Type {}
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 */
private final @SearchResult int result;
/** The type of the result. */
@Type private final int type;
/**
* When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@code
* result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link
* When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingTimePosition} should be updated with this value. When {@link
* #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link
* SeekOperationParams#floorTimePosition} should be updated with this value.
*/
private final long timestampToUpdate;
/**
* When {@code result} is {@link #RESULT_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@code
* result} is {@link #RESULT_POSITION_UNDERESTIMATED}, the {@link
* When {@link #type} is {@link #TYPE_POSITION_OVERESTIMATED}, the {@link
* SeekOperationParams#ceilingBytePosition} should be updated with this value. When {@link
* #type} is {@link #TYPE_POSITION_UNDERESTIMATED}, the {@link
* SeekOperationParams#floorBytePosition} should be updated with this value.
*/
private final long bytePositionToUpdate;
private TimestampSearchResult(
@SearchResult int result, long timestampToUpdate, long bytePositionToUpdate) {
this.result = result;
@Type int type, long timestampToUpdate, long bytePositionToUpdate) {
this.type = type;
this.timestampToUpdate = timestampToUpdate;
this.bytePositionToUpdate = bytePositionToUpdate;
}
......@@ -484,7 +477,7 @@ public abstract class BinarySearchSeeker {
public static TimestampSearchResult overestimatedResult(
long newCeilingTimestamp, long newCeilingBytePosition) {
return new TimestampSearchResult(
RESULT_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition);
TYPE_POSITION_OVERESTIMATED, newCeilingTimestamp, newCeilingBytePosition);
}
/**
......@@ -495,11 +488,11 @@ public abstract class BinarySearchSeeker {
public static TimestampSearchResult underestimatedResult(
long newFloorTimestamp, long newCeilingBytePosition) {
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.
*
* <p>Note that when this value is returned from {@link
......@@ -508,7 +501,7 @@ public abstract class BinarySearchSeeker {
*/
public static TimestampSearchResult targetFoundResult(long resultBytePosition) {
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;
public static final int TYPE_alac = Util.getIntegerCodeForString("alac");
public static final int TYPE_alaw = Util.getIntegerCodeForString("alaw");
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;
......
......@@ -58,6 +58,9 @@ import java.util.List;
*/
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).
*
......@@ -233,8 +236,6 @@ import java.util.List;
sizes = Arrays.copyOf(sizes, sampleCount);
timestamps = Arrays.copyOf(timestamps, sampleCount);
flags = Arrays.copyOf(flags, sampleCount);
remainingSamplesAtTimestampOffset = 0;
remainingTimestampOffsetChanges = 0;
break;
}
......@@ -290,23 +291,38 @@ import java.util.List;
}
duration = timestampTimeUnits + timestampOffset;
Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0);
// Remove trailing ctts entries with 0-valued sample counts.
// If the stbl's child boxes are not consistent the container is malformed, but the stream may
// still be playable.
boolean isCttsValid = true;
while (remainingTimestampOffsetChanges > 0) {
Assertions.checkArgument(ctts.readUnsignedIntToInt() == 0);
if (ctts.readUnsignedIntToInt() != 0) {
isCttsValid = false;
break;
}
ctts.readInt(); // Ignore offset.
remainingTimestampOffsetChanges--;
}
// If the stbl's child boxes are not consistent the container is malformed, but the stream may
// still be playable.
if (remainingSynchronizationSamples != 0 || remainingSamplesAtTimestampDelta != 0
|| remainingSamplesInChunk != 0 || remainingTimestampDeltaChanges != 0) {
Log.w(TAG, "Inconsistent stbl box for track " + track.id
+ ": remainingSynchronizationSamples " + remainingSynchronizationSamples
+ ", remainingSamplesAtTimestampDelta " + remainingSamplesAtTimestampDelta
+ ", remainingSamplesInChunk " + remainingSamplesInChunk
+ ", remainingTimestampDeltaChanges " + remainingTimestampDeltaChanges);
if (remainingSynchronizationSamples != 0
|| remainingSamplesAtTimestampDelta != 0
|| remainingSamplesInChunk != 0
|| remainingTimestampDeltaChanges != 0
|| remainingSamplesAtTimestampOffset != 0
|| !isCttsValid) {
Log.w(
TAG,
"Inconsistent stbl box for track "
+ track.id
+ ": remainingSynchronizationSamples "
+ remainingSynchronizationSamples
+ ", remainingSamplesAtTimestampDelta "
+ remainingSamplesAtTimestampDelta
+ ", remainingSamplesInChunk "
+ remainingSamplesInChunk
+ ", remainingTimestampDeltaChanges "
+ remainingTimestampDeltaChanges
+ ", remainingSamplesAtTimestampOffset "
+ remainingSamplesAtTimestampOffset
+ (!isCttsValid ? ", ctts invalid" : ""));
}
} else {
long[] chunkOffsetsBytes = new long[chunkIterator.length];
......@@ -679,7 +695,9 @@ import java.util.List;
|| childAtomType == Atom.TYPE__mp3
|| childAtomType == Atom.TYPE_alac
|| 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,
language, isQuickTime, drmInitData, out, i);
} else if (childAtomType == Atom.TYPE_TTML || childAtomType == Atom.TYPE_tx3g
......@@ -976,6 +994,10 @@ import java.util.List;
mimeType = MimeTypes.AUDIO_ALAW;
} else if (atomType == Atom.TYPE_ulaw) {
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;
......@@ -1016,7 +1038,20 @@ import java.util.List;
} else if (childAtomType == Atom.TYPE_alac) {
initializationData = new byte[childAtomSize];
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;
}
......
......@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
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.
*
* <p>Given a SCR timestamp, and a position within a PS stream, this seeker will try to read a
* range of up to {@link #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all
* packs in that range, and then compare the SCR timestamps (if available) of these packets vs the
* target timestamp.
* <p>Given a SCR timestamp, and a position within a PS stream, this seeker will peek up to {@link
* #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all packs in that range, and
* then compare the SCR timestamps (if available) of these packets to the target timestamp.
*/
private static final class PsScrSeeker implements TimestampSeeker {
......@@ -65,7 +65,7 @@ import java.io.IOException;
private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) {
this.scrTimestampAdjuster = scrTimestampAdjuster;
packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES);
packetBuffer = new ParsableByteArray();
}
@Override
......@@ -73,14 +73,19 @@ import java.io.IOException;
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException {
long inputPosition = input.getPosition();
int bytesToRead =
(int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition());
packetBuffer.reset(bytesToRead);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
packetBuffer.reset(bytesToSearch);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
}
@Override
public void onSeekFinished() {
packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
}
private TimestampSearchResult searchForScrValueInBuffer(
ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) {
int startOfLastPacketPosition = C.POSITION_UNSET;
......
......@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
......@@ -38,7 +39,7 @@ import java.io.IOException;
*/
/* 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 ParsableByteArray packetBuffer;
......@@ -56,7 +57,7 @@ import java.io.IOException;
firstScrValue = C.TIME_UNSET;
lastScrValue = 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. */
......@@ -129,6 +130,7 @@ import java.io.IOException;
}
private int finishReadDuration(ExtractorInput input) {
packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
isDurationRead = true;
input.resetPeekPosition();
return Extractor.RESULT_CONTINUE;
......@@ -136,16 +138,16 @@ import java.io.IOException;
private int readFirstScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException, InterruptedException {
if (input.getPosition() != 0) {
seekPositionHolder.position = 0;
int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength());
int searchStartPosition = 0;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK;
}
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
packetBuffer.reset(bytesToSearch);
input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
firstScrValue = readFirstScrValueFromBuffer(packetBuffer);
isFirstScrValueRead = true;
......@@ -172,17 +174,17 @@ import java.io.IOException;
private int readLastScrValue(ExtractorInput input, PositionHolder seekPositionHolder)
throws IOException, InterruptedException {
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
long bufferStartStreamPosition = input.getLength() - bytesToRead;
if (input.getPosition() != bufferStartStreamPosition) {
seekPositionHolder.position = bufferStartStreamPosition;
long inputLength = input.getLength();
int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength);
long searchStartPosition = inputLength - bytesToSearch;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK;
}
packetBuffer.reset(bytesToSearch);
input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
lastScrValue = readLastScrValueFromBuffer(packetBuffer);
isLastScrValueRead = true;
......
......@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
......@@ -33,10 +34,8 @@ import java.io.IOException;
/* package */ final class TsBinarySearchSeeker extends BinarySearchSeeker {
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 TIMESTAMP_SEARCH_PACKETS = 200;
private static final int TIMESTAMP_SEARCH_BYTES =
TsExtractor.TS_PACKET_SIZE * TIMESTAMP_SEARCH_PACKETS;
private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE;
private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
public TsBinarySearchSeeker(
TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) {
......@@ -56,10 +55,10 @@ import java.io.IOException;
* A {@link TimestampSeeker} implementation that looks for a given PCR timestamp at a given
* 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
* {@link #TIMESTAMP_SEARCH_PACKETS} TS packets from that stream position, look for all packet
* with PID equals to PCR_PID, and then compare the PCR timestamps (if available) of these packets
* vs the target timestamp.
* <p>Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link
* #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to
* PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target
* timestamp.
*/
private static final class TsPcrSeeker implements TimestampSeeker {
......@@ -70,7 +69,7 @@ import java.io.IOException;
public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) {
this.pcrPid = pcrPid;
this.pcrTimestampAdjuster = pcrTimestampAdjuster;
packetBuffer = new ParsableByteArray(TIMESTAMP_SEARCH_BYTES);
packetBuffer = new ParsableByteArray();
}
@Override
......@@ -78,10 +77,10 @@ import java.io.IOException;
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException {
long inputPosition = input.getPosition();
int bytesToRead =
(int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - input.getPosition());
packetBuffer.reset(bytesToRead);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
packetBuffer.reset(bytesToSearch);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
return searchForPcrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
}
......@@ -133,5 +132,10 @@ import java.io.IOException;
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;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
......@@ -35,8 +36,7 @@ import java.io.IOException;
*/
/* package */ final class TsDurationReader {
private static final int DURATION_READ_PACKETS = 200;
private static final int DURATION_READ_BYTES = TsExtractor.TS_PACKET_SIZE * DURATION_READ_PACKETS;
private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
private final TimestampAdjuster pcrTimestampAdjuster;
private final ParsableByteArray packetBuffer;
......@@ -54,7 +54,7 @@ import java.io.IOException;
firstPcrValue = C.TIME_UNSET;
lastPcrValue = 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. */
......@@ -117,6 +117,7 @@ import java.io.IOException;
}
private int finishReadDuration(ExtractorInput input) {
packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
isDurationRead = true;
input.resetPeekPosition();
return Extractor.RESULT_CONTINUE;
......@@ -124,16 +125,16 @@ import java.io.IOException;
private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
throws IOException, InterruptedException {
if (input.getPosition() != 0) {
seekPositionHolder.position = 0;
int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength());
int searchStartPosition = 0;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK;
}
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
packetBuffer.reset(bytesToSearch);
input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
firstPcrValue = readFirstPcrValueFromBuffer(packetBuffer, pcrPid);
isFirstPcrValueRead = true;
......@@ -159,17 +160,17 @@ import java.io.IOException;
private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
throws IOException, InterruptedException {
int bytesToRead = (int) Math.min(DURATION_READ_BYTES, input.getLength());
long bufferStartStreamPosition = input.getLength() - bytesToRead;
if (input.getPosition() != bufferStartStreamPosition) {
seekPositionHolder.position = bufferStartStreamPosition;
long inputLength = input.getLength();
int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, inputLength);
long searchStartPosition = inputLength - bytesToSearch;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
return Extractor.RESULT_SEEK;
}
packetBuffer.reset(bytesToSearch);
input.resetPeekPosition();
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToRead);
packetBuffer.setPosition(0);
packetBuffer.setLimit(bytesToRead);
input.peekFully(packetBuffer.data, /* offset= */ 0, bytesToSearch);
lastPcrValue = readLastPcrValueFromBuffer(packetBuffer, pcrPid);
isLastPcrValueRead = true;
......
......@@ -129,9 +129,12 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
buffer.flip();
int index = (pendingMetadataIndex + pendingMetadataCount) % MAX_PENDING_METADATA_COUNT;
pendingMetadata[index] = decoder.decode(buffer);
pendingMetadataTimestamps[index] = buffer.timeUs;
pendingMetadataCount++;
Metadata metadata = decoder.decode(buffer);
if (metadata != null) {
pendingMetadata[index] = metadata;
pendingMetadataTimestamps[index] = buffer.timeUs;
pendingMetadataCount++;
}
}
}
}
......
......@@ -365,6 +365,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public final synchronized void moveMediaSource(
int currentIndex, int newIndex, @Nullable Runnable actionOnCompletion) {
if (currentIndex == newIndex) {
if (actionOnCompletion != null) {
actionOnCompletion.run();
}
return;
}
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
......@@ -570,9 +573,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) {
shuffleOrder = shuffleOrder.cloneAndClear();
} else {
for (int index = toIndex - 1; index >= fromIndex; index--) {
shuffleOrder = shuffleOrder.cloneAndRemove(index);
}
shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndex);
}
for (int index = toIndex - 1; index >= fromIndex; index--) {
removeMediaSourceInternal(index);
......@@ -581,7 +582,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
break;
case MSG_MOVE:
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);
moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
scheduleListenerNotification(moveMessage.actionOnCompletion);
......
......@@ -30,10 +30,8 @@ import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* A queue of media samples.
*/
public final class SampleQueue implements TrackOutput {
/** A queue of media samples. */
public class SampleQueue implements TrackOutput {
/**
* A listener for changes to the upstream format.
......
......@@ -55,6 +55,17 @@ public interface ShuffleOrder {
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) {
this(createShuffledList(length, random), random);
}
......@@ -124,15 +135,16 @@ public interface ShuffleOrder {
}
@Override
public ShuffleOrder cloneAndRemove(int removalIndex) {
int[] newShuffled = new int[shuffled.length - 1];
boolean foundRemovedElement = false;
public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {
int numberOfElementsToRemove = indexToExclusive - indexFrom;
int[] newShuffled = new int[shuffled.length - numberOfElementsToRemove];
int foundElementsCount = 0;
for (int i = 0; i < shuffled.length; i++) {
if (shuffled[i] == removalIndex) {
foundRemovedElement = true;
if (shuffled[i] >= indexFrom && shuffled[i] < indexToExclusive) {
foundElementsCount++;
} else {
newShuffled[foundRemovedElement ? i - 1 : i] = shuffled[i] > removalIndex
? shuffled[i] - 1 : shuffled[i];
newShuffled[i - foundElementsCount] =
shuffled[i] >= indexFrom ? shuffled[i] - numberOfElementsToRemove : shuffled[i];
}
}
return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
......@@ -202,8 +214,8 @@ public interface ShuffleOrder {
}
@Override
public ShuffleOrder cloneAndRemove(int removalIndex) {
return new UnshuffledShuffleOrder(length - 1);
public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {
return new UnshuffledShuffleOrder(length - indexToExclusive + indexFrom);
}
@Override
......@@ -257,12 +269,14 @@ public interface ShuffleOrder {
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.
* @return A copy of this {@link ShuffleOrder} without the removed element.
* @param indexFrom The starting index in the unshuffled order of the range to remove.
* @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. */
ShuffleOrder cloneAndClear();
......
......@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
......@@ -451,7 +452,7 @@ public final class Cea608Decoder extends CeaDecoder {
switch (cc2) {
case CTRL_ERASE_DISPLAYED_MEMORY:
cues = null;
cues = Collections.emptyList();
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
resetCueBuilders();
}
......@@ -506,7 +507,7 @@ public final class Cea608Decoder extends CeaDecoder {
if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP
|| captionMode == CC_MODE_UNKNOWN) {
// 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 {
foundEvent = EVENT_END_OF_FILE;
} else if (STYLE_START.equals(line)) {
foundEvent = EVENT_STYLE_BLOCK;
} else if (COMMENT_START.startsWith(line)) {
} else if (line.startsWith(COMMENT_START)) {
foundEvent = EVENT_COMMENT;
} else {
foundEvent = EVENT_CUE;
......
......@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
......@@ -160,7 +161,10 @@ public abstract class BaseTrackSelection implements TrackSelection {
if (!canBlacklist) {
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;
}
......
......@@ -207,7 +207,7 @@ public final class MimeTypes {
if (codec == null) {
return null;
}
codec = codec.trim();
codec = Util.toLowerInvariant(codec.trim());
if (codec.startsWith("avc1") || codec.startsWith("avc3")) {
return MimeTypes.VIDEO_H264;
} else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) {
......@@ -245,6 +245,8 @@ public final class MimeTypes {
return MimeTypes.AUDIO_OPUS;
} else if (codec.startsWith("vorbis")) {
return MimeTypes.AUDIO_VORBIS;
} else if (codec.startsWith("flac")) {
return MimeTypes.AUDIO_FLAC;
} else {
return getCustomMimeTypeForCodec(codec);
}
......
......@@ -67,6 +67,12 @@ public final class ParsableByteArray {
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
* capacity, {@code data} is replaced with a new array of sufficient size.
......@@ -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.
*
* @param data The array to wrap.
......@@ -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.
*/
public int bytesLeft() {
......
......@@ -26,6 +26,7 @@ import java.lang.annotation.RetentionPolicy;
*/
public final class RepeatModeUtil {
// LINT.IfChange
/**
* 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
......@@ -47,6 +48,7 @@ public final class RepeatModeUtil {
public static final int REPEAT_TOGGLE_MODE_ONE = 1;
/** "Repeat All" button enabled. */
public static final int REPEAT_TOGGLE_MODE_ALL = 1 << 1; // 2
// LINT.ThenChange(../../../../../../../../../ui/src/main/res/values/attrs.xml)
private RepeatModeUtil() {
// Prevent instantiation.
......
......@@ -1329,6 +1329,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "A7010a48":
case "A7020a48":
case "AquaPowerM":
case "ASUS_X00AD_2":
case "Aura_Note_2":
case "BLACK-1X":
case "BRAVIA_ATV2":
......@@ -1369,6 +1370,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "HWBLN-H":
case "HWCAM-H":
case "HWVNS-H":
case "i9031":
case "iball8735_9806":
case "Infinix-X572":
case "iris60":
......@@ -1376,6 +1378,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "j2xlteins":
case "JGZ":
case "K50a40":
case "kate":
case "le_x6":
case "LS-5017":
case "M5c":
......
......@@ -26,10 +26,14 @@ track 256:
drmInitData = -
initializationData:
data = length 22, hash CE183139
total output bytes = 24315
sample count = 1
total output bytes = 45026
sample count = 2
sample 0:
time = 55611
time = 55610
flags = 1
data = length 20711, hash 34341E8
sample 1:
time = 88977
flags = 0
data = length 18112, hash EC44B35B
track 257:
......@@ -57,19 +61,19 @@ track 257:
total output bytes = 5015
sample count = 4
sample 0:
time = 11333
time = 44699
flags = 1
data = length 1253, hash 727FD1C6
sample 1:
time = 37455
time = 70821
flags = 1
data = length 1254, hash 73FB07B8
sample 2:
time = 63578
time = 96944
flags = 1
data = length 1254, hash 73FB07B8
sample 3:
time = 89700
time = 123066
flags = 1
data = length 1254, hash 73FB07B8
track 8448:
......
......@@ -26,10 +26,14 @@ track 256:
drmInitData = -
initializationData:
data = length 22, hash CE183139
total output bytes = 24315
sample count = 1
total output bytes = 45026
sample count = 2
sample 0:
time = 77855
time = 77854
flags = 1
data = length 20711, hash 34341E8
sample 1:
time = 111221
flags = 0
data = length 18112, hash EC44B35B
track 257:
......@@ -57,19 +61,19 @@ track 257:
total output bytes = 5015
sample count = 4
sample 0:
time = 33577
time = 66943
flags = 1
data = length 1253, hash 727FD1C6
sample 1:
time = 59699
time = 93065
flags = 1
data = length 1254, hash 73FB07B8
sample 2:
time = 85822
time = 119188
flags = 1
data = length 1254, hash 73FB07B8
sample 3:
time = 111944
time = 145310
flags = 1
data = length 1254, hash 73FB07B8
track 8448:
......
......@@ -48,15 +48,15 @@ public class DefaultLoadControlTest {
createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, 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)).isFalse();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
}
@Test
public void testShouldNotContinueLoadingOnceBufferingStopped_untilBelowMinBuffer() {
createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, 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 - 1, SPEED)).isTrue();
}
......@@ -69,7 +69,7 @@ public class DefaultLoadControlTest {
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();
assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue();
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
......@@ -82,7 +82,7 @@ public class DefaultLoadControlTest {
assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, 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
......@@ -100,7 +100,7 @@ public class DefaultLoadControlTest {
public void testShouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() {
createDefaultLoadControl();
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US + 1, /* playbackSpeed= */ 100f))
assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, /* playbackSpeed= */ 100f))
.isFalse();
}
......
......@@ -45,10 +45,32 @@ public final class ShuffleOrderTest {
testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5);
}
}
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4);
testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0, 1);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2, 3);
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4, 5);
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
......@@ -63,10 +85,15 @@ public final class ShuffleOrderTest {
testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5);
}
}
testCloneAndRemove(new UnshuffledShuffleOrder(5), 0);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 2);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 4);
testCloneAndRemove(new UnshuffledShuffleOrder(1), 0);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 0, 1);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 2, 3);
testCloneAndRemove(new UnshuffledShuffleOrder(5), 4, 5);
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
......@@ -120,22 +147,24 @@ public final class ShuffleOrderTest {
}
}
private static void testCloneAndRemove(ShuffleOrder shuffleOrder, int position) {
ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(position);
assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - 1);
private static void testCloneAndRemove(
ShuffleOrder shuffleOrder, int indexFrom, int indexToExclusive) {
int numberOfElementsToRemove = indexToExclusive - indexFrom;
ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(indexFrom, indexToExclusive);
assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - numberOfElementsToRemove);
// Assert all elements still have the relative same order
for (int i = 0; i < shuffleOrder.getLength(); i++) {
if (i == position) {
if (i >= indexFrom && i < indexToExclusive) {
continue;
}
int expectedNextIndex = shuffleOrder.getNextIndex(i);
if (expectedNextIndex == position) {
while (expectedNextIndex >= indexFrom && expectedNextIndex < indexToExclusive) {
expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex);
}
if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {
expectedNextIndex--;
if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= indexFrom) {
expectedNextIndex -= numberOfElementsToRemove;
}
int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1);
int newNextIndex = newOrder.getNextIndex(i < indexFrom ? i : i - numberOfElementsToRemove);
assertThat(newNextIndex).isEqualTo(expectedNextIndex);
}
}
......
......@@ -376,7 +376,6 @@ public final class DashMediaSource extends BaseMediaSource {
private int staleManifestReloadAttempt;
private long expiredManifestPublishTimeUs;
private boolean dynamicMediaPresentationEnded;
private int firstPeriodId;
......@@ -679,7 +678,6 @@ public final class DashMediaSource extends BaseMediaSource {
elapsedRealtimeOffsetMs = 0;
staleManifestReloadAttempt = 0;
expiredManifestPublishTimeUs = C.TIME_UNSET;
dynamicMediaPresentationEnded = false;
firstPeriodId = 0;
periodsById.clear();
}
......@@ -691,10 +689,6 @@ public final class DashMediaSource extends BaseMediaSource {
startLoadingManifest();
}
/* package */ void onDashLiveMediaPresentationEndSignalEncountered() {
this.dynamicMediaPresentationEnded = true;
}
/* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {
if (this.expiredManifestPublishTimeUs == C.TIME_UNSET
|| this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) {
......@@ -734,9 +728,8 @@ public final class DashMediaSource extends BaseMediaSource {
// behind.
Log.w(TAG, "Loaded out of sync manifest");
isManifestStale = true;
} else if (dynamicMediaPresentationEnded
|| (expiredManifestPublishTimeUs != C.TIME_UNSET
&& newManifest.publishTimeMs * 1000 <= expiredManifestPublishTimeUs)) {
} else if (expiredManifestPublishTimeUs != C.TIME_UNSET
&& newManifest.publishTimeMs * 1000 <= expiredManifestPublishTimeUs) {
// 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
// stale.
......@@ -745,8 +738,6 @@ public final class DashMediaSource extends BaseMediaSource {
"Loaded stale dynamic manifest: "
+ newManifest.publishTimeMs
+ ", "
+ dynamicMediaPresentationEnded
+ ", "
+ expiredManifestPublishTimeUs);
isManifestStale = true;
}
......@@ -763,7 +754,6 @@ public final class DashMediaSource extends BaseMediaSource {
staleManifestReloadAttempt = 0;
}
manifest = newManifest;
manifestLoadPending &= manifest.dynamic;
manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs;
......@@ -1170,12 +1160,16 @@ public final class DashMediaSource extends BaseMediaSource {
long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs(
defaultPositionProjectionUs);
Object tag = setTag ? windowTag : null;
boolean isDynamic =
manifest.dynamic
&& manifest.minUpdatePeriodMs != C.TIME_UNSET
&& manifest.durationMs == C.TIME_UNSET;
return window.set(
tag,
presentationStartTimeMs,
windowStartTimeMs,
/* isSeekable= */ true,
manifest.dynamic,
isDynamic,
windowDefaultStartPositionUs,
windowDurationUs,
/* firstPeriodIndex= */ 0,
......@@ -1253,11 +1247,6 @@ public final class DashMediaSource extends BaseMediaSource {
public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) {
DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);
}
@Override
public void onDashLiveMediaPresentationEndSignalEncountered() {
DashMediaSource.this.onDashLiveMediaPresentationEndSignalEncountered();
}
}
private final class ManifestCallback implements Loader.Callback<ParsingLoadable<DashManifest>> {
......
......@@ -318,9 +318,12 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
}
long periodDurationUs = representationHolder.periodDurationUs;
boolean periodEnded = periodDurationUs != C.TIME_UNSET;
if (representationHolder.getSegmentCount() == 0) {
// The index doesn't define any segments.
out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1);
out.endOfStream = periodEnded;
return;
}
......@@ -343,17 +346,15 @@ public class DefaultDashChunkSource implements DashChunkSource {
fatalError = new BehindLiveWindowException();
return;
}
if (segmentNum > lastAvailableSegmentNum
|| (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) {
// The segment is beyond the end of the period. We know the period will not be extended if the
// manifest is static, or if there's a period after this one.
out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1);
// The segment is beyond the end of the period.
out.endOfStream = periodEnded;
return;
}
long periodDurationUs = representationHolder.periodDurationUs;
if (periodDurationUs != C.TIME_UNSET
&& representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) {
if (periodEnded && representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) {
// The period duration clips the period to a position before the segment.
out.endOfStream = true;
return;
......
......@@ -60,8 +60,7 @@ import java.util.TreeMap;
*/
public final class PlayerEmsgHandler implements Handler.Callback {
private static final int EMSG_MEDIA_PRESENTATION_ENDED = 1;
private static final int EMSG_MANIFEST_EXPIRED = 2;
private static final int EMSG_MANIFEST_EXPIRED = 1;
/** Callbacks for player emsg events encountered during DASH live stream. */
public interface PlayerEmsgCallback {
......@@ -75,9 +74,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
* @param expiredManifestPublishTimeUs The manifest publish time that has been expired.
*/
void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs);
/** Called when a media presentation end signal is encountered during live stream. * */
void onDashLiveMediaPresentationEndSignalEncountered();
}
private final Allocator allocator;
......@@ -88,7 +84,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
private DashManifest manifest;
private boolean dynamicMediaPresentationEnded;
private long expiredManifestPublishTimeUs;
private long lastLoadedChunkEndTimeUs;
private long lastLoadedChunkEndTimeBeforeRefreshUs;
......@@ -134,21 +129,15 @@ public final class PlayerEmsgHandler implements Handler.Callback {
return true;
}
boolean manifestRefreshNeeded = false;
if (dynamicMediaPresentationEnded) {
// The manifest we have is dynamic, but we know a non-dynamic one representing the final state
// should be available.
manifestRefreshNeeded = true;
} else {
// Find the smallest publishTime (greater than or equal to the current manifest's publish
// time) that has a corresponding expiry time.
Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs);
if (expiredEntry != null) {
long expiredPointUs = expiredEntry.getValue();
if (expiredPointUs < presentationPositionUs) {
expiredManifestPublishTimeUs = expiredEntry.getKey();
notifyManifestPublishTimeExpired();
manifestRefreshNeeded = true;
}
// Find the smallest publishTime (greater than or equal to the current manifest's publish time)
// that has a corresponding expiry time.
Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs);
if (expiredEntry != null) {
long expiredPointUs = expiredEntry.getValue();
if (expiredPointUs < presentationPositionUs) {
expiredManifestPublishTimeUs = expiredEntry.getKey();
notifyManifestPublishTimeExpired();
manifestRefreshNeeded = true;
}
}
if (manifestRefreshNeeded) {
......@@ -221,9 +210,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
return true;
}
switch (message.what) {
case (EMSG_MEDIA_PRESENTATION_ENDED):
handleMediaPresentationEndedMessageEncountered();
return true;
case (EMSG_MANIFEST_EXPIRED):
ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj;
handleManifestExpiredMessage(
......@@ -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) {
return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs);
}
......@@ -273,10 +254,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);
}
private void notifySourceMediaPresentationEnded() {
playerEmsgCallback.onDashLiveMediaPresentationEndSignalEncountered();
}
/** Requests DASH media manifest to be refreshed if necessary. */
private void maybeNotifyDashManifestRefreshNeeded() {
if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET
......@@ -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. */
public final class PlayerTrackEmsgHandler implements TrackOutput {
......@@ -413,16 +384,7 @@ public final class PlayerEmsgHandler implements Handler.Callback {
if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) {
return;
}
if (isMessageSignalingMediaPresentationEnded(eventMessage)) {
onMediaPresentationEndedMessageEncountered();
} else {
onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg);
}
}
private void onMediaPresentationEndedMessageEncountered() {
handler.sendMessage(handler.obtainMessage(EMSG_MEDIA_PRESENTATION_ENDED));
onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg);
}
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;
// Retry when playlist is refreshed.
return;
}
HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
HlsMediaPlaylist mediaPlaylist =
playlistTracker.getPlaylistSnapshot(selectedUrl, /* isForPlayback= */ true);
independentSegments = mediaPlaylist.hasIndependentSegments;
updateLiveEdgeTimeUs(mediaPlaylist);
......@@ -279,7 +280,7 @@ import java.util.List;
// behind the live window.
selectedVariantIndex = oldVariantIndex;
selectedUrl = variants[selectedVariantIndex];
mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl, /* isForPlayback= */ true);
startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
chunkMediaSequence = previous.getNextChunkIndex();
......@@ -435,7 +436,8 @@ import java.util.List;
chunkIterators[i] = MediaChunkIterator.EMPTY;
continue;
}
HlsMediaPlaylist playlist = playlistTracker.getPlaylistSnapshot(variantUrl);
HlsMediaPlaylist playlist =
playlistTracker.getPlaylistSnapshot(variantUrl, /* isForPlayback= */ false);
long startOfPlaylistInPeriodUs =
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
boolean switchingVariant = variantIndex != oldVariantIndex;
......
......@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
......@@ -41,8 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
/* package */ final class HlsMediaChunk extends MediaChunk {
private static final String PRIV_TIMESTAMP_FRAME_OWNER =
public static final String PRIV_TIMESTAMP_FRAME_OWNER =
"com.apple.streaming.transportStreamTimestamp";
private static final AtomicInteger uidSource = new AtomicInteger();
......@@ -313,8 +313,10 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
private long peekId3PrivTimestamp(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
if (input.getLength() < Id3Decoder.ID3_HEADER_LENGTH
|| !input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) {
try {
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;
}
id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
......@@ -330,9 +332,7 @@ import java.util.concurrent.atomic.AtomicInteger;
id3Data.reset(requiredCapacity);
System.arraycopy(data, 0, id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
}
if (!input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size, true)) {
return C.TIME_UNSET;
}
input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size);
Metadata metadata = id3Decoder.decode(id3Data.data, id3Size);
if (metadata == null) {
return C.TIME_UNSET;
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.hls;
import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
......@@ -24,6 +25,8 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.SeekMap;
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.SampleQueue;
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
......@@ -791,7 +794,7 @@ import java.util.List;
return createDummyTrackOutput(id, type);
}
}
SampleQueue trackOutput = new SampleQueue(allocator);
SampleQueue trackOutput = new PrivTimestampStrippingSampleQueue(allocator);
trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.sourceId(chunkUid);
trackOutput.setUpstreamFormatChangeListener(this);
......@@ -1126,4 +1129,53 @@ import java.util.List;
Log.w(TAG, "Unmapped track with id " + id + " of type " + type);
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
}
@Override
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url, boolean isForPlayback) {
HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
if (snapshot != null) {
if (snapshot != null && isForPlayback) {
maybeSetPrimaryUrl(url);
}
return snapshot;
......
......@@ -167,11 +167,13 @@ public interface HlsPlaylistTracker {
* HlsUrl}.
*
* @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
* be null if no snapshot has been loaded yet.
*/
@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
......
# 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> {
DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid,
MimeTypes.VIDEO_MP4, protectionElement.data));
for (StreamElement streamElement : streamElementArray) {
for (int i = 0; i < streamElement.formats.length; i++) {
streamElement.formats[i] = streamElement.formats[i].copyWithDrmInitData(drmInitData);
int type = streamElement.type;
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;
*/
public class PlayerView extends FrameLayout {
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.IfChange
/**
* Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link
* #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}.
......@@ -266,6 +262,14 @@ public class PlayerView extends FrameLayout {
* buffering} state.
*/
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 View shutterView;
......
......@@ -104,12 +104,18 @@ public final class SphericalSurfaceView extends GLSurfaceView {
// Configure sensors and touch.
sensorManager =
(SensorManager) Assertions.checkNotNull(context.getSystemService(Context.SENSOR_SERVICE));
// TYPE_GAME_ROTATION_VECTOR is the easiest sensor since it handles all the complex math for
// fusion. It's used instead of TYPE_ROTATION_VECTOR since the latter uses the magnetometer on
// devices. When used indoors, the magnetometer can take some time to settle depending on the
// device and amount of metal in the environment.
int type = Util.SDK_INT >= 18 ? Sensor.TYPE_GAME_ROTATION_VECTOR : Sensor.TYPE_ROTATION_VECTOR;
orientationSensor = sensorManager.getDefaultSensor(type);
Sensor orientationSensor = null;
if (Util.SDK_INT >= 18) {
// TYPE_GAME_ROTATION_VECTOR is the easiest sensor since it handles all the complex math for
// fusion. It's used instead of TYPE_ROTATION_VECTOR since the latter uses the magnetometer on
// devices. When used indoors, the magnetometer can take some time to settle depending on the
// 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();
renderer = new Renderer(scene);
......
......@@ -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
* 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 {
/*package*/ interface Listener {
/* package */ interface Listener {
void onScrollChange(PointF scrollOffsetDegrees);
}
......
......@@ -53,8 +53,8 @@
<attr name="auto_show" format="boolean"/>
<attr name="show_buffering" format="enum">
<enum name="never" value="0"/>
<enum name="always" value="1"/>
<enum name="when_playing" value="2"/>
<enum name="when_playing" value="1"/>
<enum name="always" value="2"/>
</attr>
<attr name="keep_content_on_player_reset" format="boolean"/>
<attr name="resize_mode"/>
......
......@@ -61,8 +61,8 @@ public final class FakeShuffleOrder implements ShuffleOrder {
}
@Override
public ShuffleOrder cloneAndRemove(int removalIndex) {
return new FakeShuffleOrder(length - 1);
public ShuffleOrder cloneAndRemove(int indexFrom, int indexToExclusive) {
return new FakeShuffleOrder(length - indexToExclusive + indexFrom);
}
@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