Merge pull request #4434 from google/dev-v2-r2.8.2

r2.8.2
This commit is contained in:
ojw28 2018-06-26 15:54:16 +01:00 committed by GitHub
commit f7ed789fc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
93 changed files with 1632 additions and 1018 deletions

View file

@ -1,5 +1,31 @@
# Release notes #
### 2.8.2 ###
* IMA: Don't advertise support for video/mpeg ad media, as we don't have an
extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)).
* DASH: Fix playback getting stuck when playing representations that have both
sidx atoms and non-zero presentationTimeOffset values.
* HLS:
* Allow injection of custom playlist trackers.
* Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags.
* Mitigate memory leaks when `MediaSource` loads are slow to cancel
([#4249](https://github.com/google/ExoPlayer/issues/4249)).
* Fix inconsistent `Player.EventListener` invocations for recursive player state
changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)).
* Fix `MediaCodec.native_setSurface` crash on Moto C
([#4315](https://github.com/google/ExoPlayer/issues/4315)).
* Fix missing whitespace in CEA-608
([#3906](https://github.com/google/ExoPlayer/issues/3906)).
* Fix crash downloading HLS media playlists
([#4396](https://github.com/google/ExoPlayer/issues/4396)).
* Fix a bug where download cancellation was ignored
([#4403](https://github.com/google/ExoPlayer/issues/4403)).
* Set `METADATA_KEY_TITLE` on media descriptions
([#4292](https://github.com/google/ExoPlayer/issues/4292)).
* Allow apps to register custom MIME types
([#4264](https://github.com/google/ExoPlayer/issues/4264)).
### 2.8.1 ###
* HLS:
@ -59,7 +85,7 @@
periods are created, released and being read from.
* Support live stream clipping with `ClippingMediaSource`.
* Allow setting tags for all media sources in their factories. The tag of the
current window can be retrieved with `ExoPlayer.getCurrentTag`.
current window can be retrieved with `Player.getCurrentTag`.
* UI components:
* Add support for displaying error messages and a buffering spinner in
`PlayerView`.

View file

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.8.1'
releaseVersionCode = 2801
releaseVersion = '2.8.2'
releaseVersionCode = 2802
// 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
@ -25,7 +25,7 @@ project.ext {
buildToolsVersion = '27.0.3'
testSupportLibraryVersion = '0.5'
supportLibraryVersion = '27.0.0'
playServicesLibraryVersion = '12.0.0'
playServicesLibraryVersion = '15.0.1'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
junitVersion = '4.12'

View file

@ -136,6 +136,7 @@ public class PlayerActivity extends Activity
private DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player;
private FrameworkMediaDrm mediaDrm;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters;
@ -487,8 +488,9 @@ public class PlayerActivity extends Activity
keyRequestPropertiesArray[i + 1]);
}
}
return new DefaultDrmSessionManager<>(
uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
releaseMediaDrm();
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
}
private void releasePlayer() {
@ -502,6 +504,23 @@ public class PlayerActivity extends Activity
mediaSource = null;
trackSelector = null;
}
releaseMediaDrm();
}
private void releaseMediaDrm() {
if (mediaDrm != null) {
mediaDrm.release();
mediaDrm = null;
}
}
private void releaseAdsLoader() {
if (adsLoader != null) {
adsLoader.release();
adsLoader = null;
loadedAdTagUri = null;
playerView.getOverlayFrameLayout().removeAllViews();
}
}
private void updateTrackSelectorParameters() {
@ -576,15 +595,6 @@ public class PlayerActivity extends Activity
}
}
private void releaseAdsLoader() {
if (adsLoader != null) {
adsLoader.release();
adsLoader = null;
loadedAdTagUri = null;
playerView.getOverlayFrameLayout().removeAllViews();
}
}
// User controls
private void updateButtonVisibilities() {

View file

@ -94,9 +94,15 @@ public class SampleChooserActivity extends Activity
SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris);
// Ping the download service in case it's not running (but should be).
startService(
new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT));
// Start the download service if it should be running but it's not currently.
// Starting the service in the foreground causes notification flicker if there is no scheduled
// action. Starting it in the background throws an exception if the app is in the background too
// (e.g. if device screen is locked).
try {
DownloadService.start(this, DemoDownloadService.class);
} catch (IllegalStateException e) {
DownloadService.startForeground(this, DemoDownloadService.class);
}
}
@Override

View file

@ -26,16 +26,6 @@ android {
}
dependencies {
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example:
// com.google.android.gms:play-services-cast-framework:12.0.0
// |-- com.google.android.gms:play-services-basement:12.0.0
// |-- com.android.support:support-v4:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:appcompat-v7:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
@ -44,6 +34,15 @@ dependencies {
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example via:
// com.google.android.gms:play-services-cast-framework:15.0.1
// |-- com.android.support:mediarouter-v7:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
}
ext {

View file

@ -26,17 +26,16 @@ android {
}
dependencies {
// This dependency is necessary to force the supportLibraryVersion of
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
// is included via:
// com.google.android.gms:play-services-ads:12.0.0
// |-- com.google.android.gms:play-services-ads-lite:12.0.0
// |-- com.google.android.gms:play-services-basement:12.0.0
// |-- com.android.support:support-v4:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5'
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.7'
implementation project(modulePrefix + 'library-core')
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
// 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.android.support:customtabs:26.1.0
implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:customtabs:' + supportLibraryVersion
}
ext {

View file

@ -447,9 +447,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} else if (contentType == C.TYPE_HLS) {
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
} else if (contentType == C.TYPE_OTHER) {
supportedMimeTypes.addAll(Arrays.asList(
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
supportedMimeTypes.addAll(
Arrays.asList(
MimeTypes.VIDEO_MP4,
MimeTypes.VIDEO_WEBM,
MimeTypes.VIDEO_H263,
MimeTypes.AUDIO_MP4,
MimeTypes.AUDIO_MPEG));
} else if (contentType == C.TYPE_SS) {
// IMA does not support Smooth Streaming ad media.
}

View file

@ -600,8 +600,9 @@ public final class MediaSessionConnector {
}
}
if (description.getTitle() != null) {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
String.valueOf(description.getTitle()));
String title = String.valueOf(description.getTitle());
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
}
if (description.getSubtitle() != null) {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,

View file

@ -89,12 +89,12 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* model">
*
* <ul>
* <li>It is strongly recommended that ExoPlayer instances are created and accessed from a single
* application thread. The application's main thread is ideal. Accessing an instance from
* multiple threads is discouraged as it may cause synchronization problems.
* <li>Registered listeners are called on the thread that created the ExoPlayer instance, unless
* the thread that created the ExoPlayer instance does not have a {@link Looper}. In that
* case, registered listeners will be called on the application's main thread.
* <li>ExoPlayer instances must be accessed from a single application thread. This must be the
* thread the player is created on if that thread has a {@link Looper}, or the application's
* main thread otherwise.
* <li>Registered listeners are called on the thread the player is created on if that thread has a
* {@link Looper}, or the application's main thread otherwise. Note that this means registered
* listeners are called on the same thread which must be used to access the player.
* <li>An internal playback thread is responsible for playback. Injected player components such as
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
* thread.

View file

@ -33,8 +33,10 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
@ -53,6 +55,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final CopyOnWriteArraySet<Player.EventListener> listeners;
private final Timeline.Window window;
private final Timeline.Period period;
private final ArrayDeque<PlaybackInfoUpdate> pendingPlaybackInfoUpdates;
private boolean playWhenReady;
private @RepeatMode int repeatMode;
@ -112,6 +115,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* startPositionUs= */ 0,
TrackGroupArray.EMPTY,
emptyTrackSelectorResult);
pendingPlaybackInfoUpdates = new ArrayDeque<>();
internalPlayer =
new ExoPlayerImplInternal(
renderers,
@ -185,7 +189,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* playWhenReadyChanged= */ false);
}
@Override
@ -193,10 +198,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (this.playWhenReady != playWhenReady) {
this.playWhenReady = playWhenReady;
internalPlayer.setPlayWhenReady(playWhenReady);
PlaybackInfo playbackInfo = this.playbackInfo;
for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
}
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ TIMELINE_CHANGE_REASON_RESET,
/* seekProcessed= */ false,
/* playWhenReadyChanged= */ true);
}
}
@ -352,7 +360,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_RESET,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* playWhenReadyChanged= */ false);
}
@Override
@ -615,7 +624,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
seekProcessed);
seekProcessed,
/* playWhenReadyChanged= */ false);
}
}
@ -643,51 +653,33 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
private void updatePlaybackInfo(
PlaybackInfo newPlaybackInfo,
PlaybackInfo playbackInfo,
boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason,
boolean seekProcessed) {
boolean timelineOrManifestChanged =
playbackInfo.timeline != newPlaybackInfo.timeline
|| playbackInfo.manifest != newPlaybackInfo.manifest;
boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState;
boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading;
boolean trackSelectorResultChanged =
playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
playbackInfo = newPlaybackInfo;
if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(
playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason);
}
boolean seekProcessed,
boolean playWhenReadyChanged) {
boolean isRunningRecursiveListenerNotification = !pendingPlaybackInfoUpdates.isEmpty();
pendingPlaybackInfoUpdates.addLast(
new PlaybackInfoUpdate(
playbackInfo,
/* previousPlaybackInfo= */ this.playbackInfo,
listeners,
trackSelector,
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
seekProcessed,
playWhenReady,
playWhenReadyChanged));
// Assign playback info immediately such that all getters return the right values.
this.playbackInfo = playbackInfo;
if (isRunningRecursiveListenerNotification) {
return;
}
if (positionDiscontinuity) {
for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity(positionDiscontinuityReason);
}
}
if (trackSelectorResultChanged) {
trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
for (Player.EventListener listener : listeners) {
listener.onTracksChanged(
playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections);
}
}
if (isLoadingChanged) {
for (Player.EventListener listener : listeners) {
listener.onLoadingChanged(playbackInfo.isLoading);
}
}
if (playbackStateChanged) {
for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
}
}
if (seekProcessed) {
for (Player.EventListener listener : listeners) {
listener.onSeekProcessed();
}
while (!pendingPlaybackInfoUpdates.isEmpty()) {
pendingPlaybackInfoUpdates.peekFirst().notifyListeners();
pendingPlaybackInfoUpdates.removeFirst();
}
}
@ -703,4 +695,85 @@ import java.util.concurrent.CopyOnWriteArraySet;
private boolean shouldMaskPosition() {
return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0;
}
private static final class PlaybackInfoUpdate {
private final PlaybackInfo playbackInfo;
private final Set<Player.EventListener> listeners;
private final TrackSelector trackSelector;
private final boolean positionDiscontinuity;
private final @Player.DiscontinuityReason int positionDiscontinuityReason;
private final @Player.TimelineChangeReason int timelineChangeReason;
private final boolean seekProcessed;
private final boolean playWhenReady;
private final boolean playbackStateOrPlayWhenReadyChanged;
private final boolean timelineOrManifestChanged;
private final boolean isLoadingChanged;
private final boolean trackSelectorResultChanged;
public PlaybackInfoUpdate(
PlaybackInfo playbackInfo,
PlaybackInfo previousPlaybackInfo,
Set<Player.EventListener> listeners,
TrackSelector trackSelector,
boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason,
boolean seekProcessed,
boolean playWhenReady,
boolean playWhenReadyChanged) {
this.playbackInfo = playbackInfo;
this.listeners = listeners;
this.trackSelector = trackSelector;
this.positionDiscontinuity = positionDiscontinuity;
this.positionDiscontinuityReason = positionDiscontinuityReason;
this.timelineChangeReason = timelineChangeReason;
this.seekProcessed = seekProcessed;
this.playWhenReady = playWhenReady;
playbackStateOrPlayWhenReadyChanged =
playWhenReadyChanged || previousPlaybackInfo.playbackState != playbackInfo.playbackState;
timelineOrManifestChanged =
previousPlaybackInfo.timeline != playbackInfo.timeline
|| previousPlaybackInfo.manifest != playbackInfo.manifest;
isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
trackSelectorResultChanged =
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
}
public void notifyListeners() {
if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(
playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason);
}
}
if (positionDiscontinuity) {
for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity(positionDiscontinuityReason);
}
}
if (trackSelectorResultChanged) {
trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
for (Player.EventListener listener : listeners) {
listener.onTracksChanged(
playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections);
}
}
if (isLoadingChanged) {
for (Player.EventListener listener : listeners) {
listener.onLoadingChanged(playbackInfo.isLoading);
}
}
if (playbackStateOrPlayWhenReadyChanged) {
for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
}
}
if (seekProcessed) {
for (Player.EventListener listener : listeners) {
listener.onSeekProcessed();
}
}
}
}
}

View file

@ -1543,6 +1543,7 @@ import java.util.Collections;
}
}
@SuppressWarnings("ParameterNotNullable")
private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder)
throws ExoPlaybackException {
MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod();

View file

@ -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.8.1";
public static final String VERSION = "2.8.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.8.1";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.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 = 2008001;
public static final int VERSION_INT = 2008002;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View file

@ -46,6 +46,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Data collector which is able to forward analytics events to {@link AnalyticsListener}s by
@ -66,29 +67,34 @@ public class AnalyticsCollector
/**
* Creates an analytics collector for the specified player.
*
* @param player The {@link Player} for which data will be collected.
* @param player The {@link Player} for which data will be collected. Can be null, if the player
* is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics
* collector.
* @param clock A {@link Clock} used to generate timestamps.
* @return An analytics collector.
*/
public AnalyticsCollector createAnalyticsCollector(Player player, Clock clock) {
public AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Clock clock) {
return new AnalyticsCollector(player, clock);
}
}
private final CopyOnWriteArraySet<AnalyticsListener> listeners;
private final Player player;
private final Clock clock;
private final Window window;
private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
private @MonotonicNonNull Player player;
/**
* Creates an analytics collector for the specified player.
*
* @param player The {@link Player} for which data will be collected.
* @param player The {@link Player} for which data will be collected. Can be null, if the player
* is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics
* collector.
* @param clock A {@link Clock} used to generate timestamps.
*/
protected AnalyticsCollector(Player player, Clock clock) {
this.player = Assertions.checkNotNull(player);
protected AnalyticsCollector(@Nullable Player player, Clock clock) {
this.player = player;
this.clock = Assertions.checkNotNull(clock);
listeners = new CopyOnWriteArraySet<>();
mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
@ -113,6 +119,17 @@ public class AnalyticsCollector
listeners.remove(listener);
}
/**
* Sets the player for which data will be collected. Must only be called if no player has been set
* yet.
*
* @param player The {@link Player} for which data will be collected.
*/
public void setPlayer(Player player) {
Assertions.checkState(this.player == null);
this.player = Assertions.checkNotNull(player);
}
// External events.
/**
@ -541,6 +558,7 @@ public class AnalyticsCollector
/** Returns a new {@link EventTime} for the specified window index and media period id. */
protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
Assertions.checkNotNull(player);
long realtimeMs = clock.elapsedRealtime();
Timeline timeline = player.getCurrentTimeline();
long eventPositionMs;
@ -579,7 +597,7 @@ public class AnalyticsCollector
private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) {
if (mediaPeriod == null) {
int windowIndex = player.getCurrentWindowIndex();
int windowIndex = Assertions.checkNotNull(player).getCurrentWindowIndex();
MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);
return generateEventTime(windowIndex, mediaPeriodId);
}

View file

@ -17,7 +17,7 @@ package com.google.android.exoplayer2.decoder;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import java.util.LinkedList;
import java.util.ArrayDeque;
/**
* Base class for {@link Decoder}s that use their own decode thread.
@ -28,8 +28,8 @@ public abstract class SimpleDecoder<I extends DecoderInputBuffer, O extends Outp
private final Thread decodeThread;
private final Object lock;
private final LinkedList<I> queuedInputBuffers;
private final LinkedList<O> queuedOutputBuffers;
private final ArrayDeque<I> queuedInputBuffers;
private final ArrayDeque<O> queuedOutputBuffers;
private final I[] availableInputBuffers;
private final O[] availableOutputBuffers;
@ -48,8 +48,8 @@ public abstract class SimpleDecoder<I extends DecoderInputBuffer, O extends Outp
*/
protected SimpleDecoder(I[] inputBuffers, O[] outputBuffers) {
lock = new Object();
queuedInputBuffers = new LinkedList<>();
queuedOutputBuffers = new LinkedList<>();
queuedInputBuffers = new ArrayDeque<>();
queuedOutputBuffers = new ArrayDeque<>();
availableInputBuffers = inputBuffers;
availableInputBufferCount = inputBuffers.length;
for (int i = 0; i < availableInputBufferCount; i++) {

View file

@ -108,7 +108,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
@Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
String url =
request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData());
return executePost(dataSourceFactory, url, new byte[0], null);
}

View file

@ -24,7 +24,7 @@ import java.io.EOFException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Stack;
import java.util.ArrayDeque;
/**
* Default implementation of {@link EbmlReader}.
@ -46,15 +46,21 @@ import java.util.Stack;
private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
private final byte[] scratch = new byte[8];
private final Stack<MasterElement> masterElementsStack = new Stack<>();
private final VarintReader varintReader = new VarintReader();
private final byte[] scratch;
private final ArrayDeque<MasterElement> masterElementsStack;
private final VarintReader varintReader;
private EbmlReaderOutput output;
private @ElementState int elementState;
private int elementId;
private long elementContentSize;
public DefaultEbmlReader() {
scratch = new byte[8];
masterElementsStack = new ArrayDeque<>();
varintReader = new VarintReader();
}
@Override
public void init(EbmlReaderOutput eventHandler) {
this.output = eventHandler;
@ -100,7 +106,7 @@ import java.util.Stack;
case EbmlReaderOutput.TYPE_MASTER:
long elementContentPosition = input.getPosition();
long elementEndPosition = elementContentPosition + elementContentSize;
masterElementsStack.add(new MasterElement(elementId, elementEndPosition));
masterElementsStack.push(new MasterElement(elementId, elementEndPosition));
output.startMasterElement(elementId, elementContentPosition, elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
return true;

View file

@ -78,8 +78,9 @@ import java.io.IOException;
return false;
}
if (size != 0) {
input.advancePeekPosition((int) size);
peekLength += size;
int sizeInt = (int) size;
input.advancePeekPosition(sizeInt);
peekLength += sizeInt;
}
}
return peekLength == headerStart + headerSize;

View file

@ -108,4 +108,7 @@ import com.google.android.exoplayer2.util.Util;
return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);
}
private FixedSampleSizeRechunker() {
// Prevent instantiation.
}
}

View file

@ -50,7 +50,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.UUID;
/**
@ -141,7 +140,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// Parser state.
private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch;
private final Stack<ContainerAtom> containerAtoms;
private final ArrayDeque<ContainerAtom> containerAtoms;
private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos;
private final @Nullable TrackOutput additionalEmsgTrackOutput;
@ -257,7 +256,7 @@ public final class FragmentedMp4Extractor implements Extractor {
nalPrefix = new ParsableByteArray(5);
nalBuffer = new ParsableByteArray();
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<>();
containerAtoms = new ArrayDeque<>();
pendingMetadataSampleInfos = new ArrayDeque<>();
trackBundles = new SparseArray<>();
durationUs = C.TIME_UNSET;
@ -390,7 +389,7 @@ public final class FragmentedMp4Extractor implements Extractor {
if (shouldParseContainerAtom(atomType)) {
long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
containerAtoms.add(new ContainerAtom(atomType, endPosition));
containerAtoms.push(new ContainerAtom(atomType, endPosition));
if (atomSize == atomHeaderBytesRead) {
processAtomEnded(endPosition);
} else {

View file

@ -37,9 +37,9 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* Extracts data from the MP4 container format.
@ -101,7 +101,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private final ParsableByteArray nalLength;
private final ParsableByteArray atomHeader;
private final Stack<ContainerAtom> containerAtoms;
private final ArrayDeque<ContainerAtom> containerAtoms;
@State private int parserState;
private int atomType;
@ -137,7 +137,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
public Mp4Extractor(@Flags int flags) {
this.flags = flags;
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
containerAtoms = new Stack<>();
containerAtoms = new ArrayDeque<>();
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
sampleTrackIndex = C.INDEX_UNSET;
@ -303,7 +303,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
if (shouldParseContainerAtom(atomType)) {
long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;
containerAtoms.add(new ContainerAtom(atomType, endPosition));
containerAtoms.push(new ContainerAtom(atomType, endPosition));
if (atomSize == atomHeaderBytesRead) {
processAtomEnded(endPosition);
} else {

View file

@ -49,6 +49,7 @@ public final class PsshAtomUtil {
* @param data The scheme specific data.
* @return The PSSH atom.
*/
@SuppressWarnings("ParameterNotNullable")
public static byte[] buildPsshAtom(
UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) {
boolean buildV1Atom = keyIds != null;

View file

@ -130,6 +130,6 @@ import java.util.List;
} else {
length = 10000 << length;
}
return frames * length;
return (long) frames * length;
}
}

View file

@ -357,12 +357,12 @@ import java.util.Arrays;
for (int i = 0; i < lengthMap.length; i++) {
if (isSparse) {
if (bitArray.readBit()) {
lengthMap[i] = bitArray.readBits(5) + 1;
lengthMap[i] = (long) (bitArray.readBits(5) + 1);
} else { // entry unused
lengthMap[i] = 0;
}
} else { // not sparse
lengthMap[i] = bitArray.readBits(5) + 1;
lengthMap[i] = (long) (bitArray.readBits(5) + 1);
}
}
} else {
@ -392,7 +392,7 @@ import java.util.Arrays;
lookupValuesCount = 0;
}
} else {
lookupValuesCount = entries * dimensions;
lookupValuesCount = (long) entries * dimensions;
}
// discard (no decoding required yet)
bitArray.skipBits((int) (lookupValuesCount * valueBits));
@ -407,6 +407,10 @@ import java.util.Arrays;
return (long) Math.floor(Math.pow(entries, 1.d / dimension));
}
private VorbisUtil() {
// Prevent instantiation.
}
public static final class CodeBook {
public final int dimensions;

View file

@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
/*package*/ final class WavHeaderReader {
/* package */ final class WavHeaderReader {
private static final String TAG = "WavHeaderReader";
@ -158,6 +158,10 @@ import java.io.IOException;
wavHeader.setDataBounds(input.getPosition(), chunkHeader.size);
}
private WavHeaderReader() {
// Prevent instantiation.
}
/** Container for a WAV chunk header. */
private static final class ChunkHeader {

View file

@ -262,12 +262,23 @@ public final class DownloadManager {
return task.id;
}
/** Returns the current number of tasks. */
/** Returns the number of tasks. */
public int getTaskCount() {
Assertions.checkState(!released);
return tasks.size();
}
/** Returns the number of download tasks. */
public int getDownloadCount() {
int count = 0;
for (int i = 0; i < tasks.size(); i++) {
if (!tasks.get(i).action.isRemoveAction) {
count++;
}
}
return count;
}
/** Returns the state of a task, or null if no such task exists */
public @Nullable TaskState getTaskState(int taskId) {
Assertions.checkState(!released);

View file

@ -160,9 +160,9 @@ public abstract class DownloadService extends Service {
* Starts the service, adding an action to be executed.
*
* @param context A {@link Context}.
* @param clazz The concrete download service being targeted by the intent.
* @param clazz The concrete download service to be started.
* @param downloadAction The action to be executed.
* @param foreground Whether this intent will be used to start the service in the foreground.
* @param foreground Whether the service is started in the foreground.
*/
public static void startWithAction(
Context context,
@ -177,6 +177,33 @@ public abstract class DownloadService extends Service {
}
}
/**
* Starts the service without adding a new action. If there are any not finished actions and the
* requirements are met, the service resumes executing actions. Otherwise it stops immediately.
*
* @param context A {@link Context}.
* @param clazz The concrete download service to be started.
* @see #startForeground(Context, Class)
*/
public static void start(Context context, Class<? extends DownloadService> clazz) {
context.startService(new Intent(context, clazz).setAction(ACTION_INIT));
}
/**
* Starts the service in the foreground without adding a new action. If there are any not finished
* actions and the requirements are met, the service resumes executing actions. Otherwise it stops
* immediately.
*
* @param context A {@link Context}.
* @param clazz The concrete download service to be started.
* @see #start(Context, Class)
*/
public static void startForeground(Context context, Class<? extends DownloadService> clazz) {
Intent intent =
new Intent(context, clazz).setAction(ACTION_INIT).putExtra(KEY_FOREGROUND, true);
Util.startForegroundService(context, intent);
}
@Override
public void onCreate() {
logd("onCreate");
@ -187,17 +214,6 @@ public abstract class DownloadService extends Service {
downloadManager = getDownloadManager();
downloadManagerListener = new DownloadManagerListener();
downloadManager.addListener(downloadManagerListener);
RequirementsHelper requirementsHelper;
synchronized (requirementsHelpers) {
Class<? extends DownloadService> clazz = getClass();
requirementsHelper = requirementsHelpers.get(clazz);
if (requirementsHelper == null) {
requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz);
requirementsHelpers.put(clazz, requirementsHelper);
}
}
requirementsHelper.start();
}
@Override
@ -237,6 +253,7 @@ public abstract class DownloadService extends Service {
Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
break;
}
maybeStartWatchingRequirements();
if (downloadManager.isIdle()) {
stop();
}
@ -248,14 +265,7 @@ public abstract class DownloadService extends Service {
logd("onDestroy");
foregroundNotificationUpdater.stopPeriodicUpdates();
downloadManager.removeListener(downloadManagerListener);
if (downloadManager.getTaskCount() == 0) {
synchronized (requirementsHelpers) {
RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
if (requirementsHelper != null) {
requirementsHelper.stop();
}
}
}
maybeStopWatchingRequirements();
}
@Nullable
@ -312,6 +322,31 @@ public abstract class DownloadService extends Service {
// Do nothing.
}
private void maybeStartWatchingRequirements() {
if (downloadManager.getDownloadCount() == 0) {
return;
}
Class<? extends DownloadService> clazz = getClass();
RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz);
if (requirementsHelper == null) {
requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz);
requirementsHelpers.put(clazz, requirementsHelper);
requirementsHelper.start();
logd("started watching requirements");
}
}
private void maybeStopWatchingRequirements() {
if (downloadManager.getDownloadCount() > 0) {
return;
}
RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
if (requirementsHelper != null) {
requirementsHelper.stop();
logd("stopped watching requirements");
}
}
private void stop() {
foregroundNotificationUpdater.stopPeriodicUpdates();
// Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260].
@ -331,7 +366,7 @@ public abstract class DownloadService extends Service {
private final class DownloadManagerListener implements DownloadManager.Listener {
@Override
public void onInitialized(DownloadManager downloadManager) {
// Do nothing.
maybeStartWatchingRequirements();
}
@Override

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
package com.google.android.exoplayer2.offline;
import android.net.Uri;
import com.google.android.exoplayer2.C;

View file

@ -201,6 +201,8 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
throws InterruptedException, IOException;
/** Initializes the download, returning a list of {@link Segment}s that need to be downloaded. */
// Writes to downloadedSegments and downloadedBytes are safe. See the comment on download().
@SuppressWarnings("NonAtomicVolatileUpdate")
private List<Segment> initDownload() throws IOException, InterruptedException {
M manifest = getManifest(dataSource, manifestUri);
if (!streamKeys.isEmpty()) {

View file

@ -19,7 +19,6 @@ import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
@ -34,6 +33,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
@ -656,7 +656,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
/* package */ static final class MediaSourceHolder implements Comparable<MediaSourceHolder> {
public final MediaSource mediaSource;
public final int uid;
public final Object uid;
public DeferredTimeline timeline;
public int childIndex;
@ -668,9 +668,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public MediaSourceHolder(MediaSource mediaSource) {
this.mediaSource = mediaSource;
this.uid = System.identityHashCode(this);
this.timeline = new DeferredTimeline();
this.activeMediaPeriods = new ArrayList<>();
this.uid = new Object();
}
public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) {
@ -728,8 +728,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private final int[] firstPeriodInChildIndices;
private final int[] firstWindowInChildIndices;
private final Timeline[] timelines;
private final int[] uids;
private final SparseIntArray childIndexByUid;
private final Object[] uids;
private final HashMap<Object, Integer> childIndexByUid;
public ConcatenatedTimeline(
Collection<MediaSourceHolder> mediaSourceHolders,
@ -744,8 +744,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
firstPeriodInChildIndices = new int[childCount];
firstWindowInChildIndices = new int[childCount];
timelines = new Timeline[childCount];
uids = new int[childCount];
childIndexByUid = new SparseIntArray();
uids = new Object[childCount];
childIndexByUid = new HashMap<>();
int index = 0;
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
timelines[index] = mediaSourceHolder.timeline;
@ -768,11 +768,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
@Override
protected int getChildIndexByChildUid(Object childUid) {
if (!(childUid instanceof Integer)) {
return C.INDEX_UNSET;
}
int index = childIndexByUid.get((int) childUid, -1);
return index == -1 ? C.INDEX_UNSET : index;
Integer index = childIndexByUid.get(childUid);
return index == null ? C.INDEX_UNSET : index;
}
@Override
@ -804,7 +801,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public int getPeriodCount() {
return periodCount;
}
}
/**

View file

@ -19,19 +19,25 @@ package com.google.android.exoplayer2.source;
@Deprecated
public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource {
/** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource()} instead. */
/**
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(MediaSource...)}
* instead.
*/
@Deprecated
public DynamicConcatenatingMediaSource() {}
/** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean)} instead. */
/**
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean,
* MediaSource...)} instead.
*/
@Deprecated
public DynamicConcatenatingMediaSource(boolean isAtomic) {
super(isAtomic);
}
/**
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean,
* ShuffleOrder)} instead.
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, ShuffleOrder,
* MediaSource...)} instead.
*/
@Deprecated
public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) {

View file

@ -90,7 +90,7 @@ import java.util.Arrays;
private final Runnable onContinueLoadingRequestedRunnable;
private final Handler handler;
private Callback callback;
private @Nullable Callback callback;
private SeekMap seekMap;
private SampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds;
@ -190,6 +190,7 @@ import java.util.Arrays;
}
loader.release(this);
handler.removeCallbacksAndMessages(null);
callback = null;
released = true;
eventDispatcher.mediaPeriodReleased();
}
@ -832,11 +833,6 @@ import java.util.Arrays;
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public void load() throws IOException, InterruptedException {
int result = Extractor.RESULT_CONTINUE;

View file

@ -348,11 +348,6 @@ import java.util.Arrays;
// Never happens.
}
@Override
public boolean isLoadCanceled() {
return false;
}
@Override
public void load() throws IOException, InterruptedException {
// We always load from the beginning, so reset the sampleSize to 0.

View file

@ -72,11 +72,14 @@ public final class TrackGroup implements Parcelable {
}
/**
* Returns the index of the track with the given format in the group.
* Returns the index of the track with the given format in the group. The format is located by
* identity so, for example, {@code group.indexOf(group.getFormat(index)) == index} even if
* multiple tracks have formats that contain the same values.
*
* @param format The format.
* @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.
*/
@SuppressWarnings("ReferenceEquality")
public int indexOf(Format format) {
for (int i = 0; i < formats.length; i++) {
if (format == formats[i]) {

View file

@ -44,7 +44,7 @@ public abstract class BaseMediaChunk extends MediaChunk {
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the
* whole chunk should be output.
* @param chunkIndex The index of the chunk.
* @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
*/
public BaseMediaChunk(
DataSource dataSource,

View file

@ -21,10 +21,8 @@ import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
/**
* An output for {@link BaseMediaChunk}s.
*/
/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider {
/** An output for {@link BaseMediaChunk}s. */
public final class BaseMediaChunkOutput implements TrackOutputProvider {
private static final String TAG = "BaseMediaChunkOutput";

View file

@ -49,7 +49,7 @@ public class ContainerMediaChunk extends BaseMediaChunk {
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the
* whole chunk should be output.
* @param chunkIndex The index of the chunk.
* @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
* @param chunkCount The number of chunks in the underlying media that are spanned by this
* instance. Normally equal to one, but may be larger if multiple chunks as defined by the
* underlying media are being merged into a single load.
@ -106,11 +106,6 @@ public class ContainerMediaChunk extends BaseMediaChunk {
loadCanceled = true;
}
@Override
public final boolean isLoadCanceled() {
return loadCanceled;
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public final void load() throws IOException, InterruptedException {

View file

@ -75,11 +75,6 @@ public abstract class DataChunk extends Chunk {
loadCanceled = true;
}
@Override
public final boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public final void load() throws IOException, InterruptedException {
try {

View file

@ -69,11 +69,6 @@ public final class InitializationChunk extends Chunk {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {

View file

@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.Assertions;
*/
public abstract class MediaChunk extends Chunk {
/** The chunk index. */
/** The chunk index, or {@link C#INDEX_UNSET} if it is not known. */
public final long chunkIndex;
/**
@ -37,7 +37,7 @@ public abstract class MediaChunk extends Chunk {
* @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
*/
public MediaChunk(
DataSource dataSource,
@ -54,9 +54,9 @@ public abstract class MediaChunk extends Chunk {
this.chunkIndex = chunkIndex;
}
/** Returns the next chunk index. */
/** Returns the next chunk index or {@link C#INDEX_UNSET} if it is not known. */
public long getNextChunkIndex() {
return chunkIndex + 1;
return chunkIndex != C.INDEX_UNSET ? chunkIndex + 1 : C.INDEX_UNSET;
}
/**

View file

@ -34,7 +34,6 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
private final Format sampleFormat;
private volatile int bytesLoaded;
private volatile boolean loadCanceled;
private volatile boolean loadCompleted;
/**
@ -45,7 +44,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
* @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
* @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*}
* constants.
* @param sampleFormat The {@link Format} of the sample in the chunk.
@ -90,12 +89,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
// Do nothing.
}
@SuppressWarnings("NonAtomicVolatileUpdate")

View file

@ -374,6 +374,9 @@ public final class Cea608Decoder extends CeaDecoder {
private void handleMidrowCtrl(byte cc2) {
// TODO: support the extended styles (i.e. backgrounds and transparencies)
// A midrow control code advances the cursor.
currentCueBuilder.append(' ');
// cc2 - 0|0|1|0|ATRBT|U
// ATRBT is the 3-byte encoded attribute, and U is the underline toggle
boolean isUnderlined = (cc2 & 0x01) == 0x01;

View file

@ -38,7 +38,6 @@ import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
@ -196,7 +195,10 @@ public final class Cea708Decoder extends CeaDecoder {
@Override
protected void decode(SubtitleInputBuffer inputBuffer) {
ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
// Subtitle input buffers are non-direct and the position is zero, so calling array() is safe.
@SuppressWarnings("ByteBufferBackingArray")
byte[] inputBufferData = inputBuffer.data.array();
ccData.reset(inputBufferData, inputBuffer.data.limit());
while (ccData.bytesLeft() >= 3) {
int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07);
@ -879,7 +881,7 @@ public final class Cea708Decoder extends CeaDecoder {
private int row;
public CueBuilder() {
rolledUpCaptions = new LinkedList<>();
rolledUpCaptions = new ArrayList<>();
captionStringBuilder = new SpannableStringBuilder();
reset();
}

View file

@ -24,7 +24,7 @@ import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import java.util.LinkedList;
import java.util.ArrayDeque;
import java.util.PriorityQueue;
/**
@ -35,8 +35,8 @@ import java.util.PriorityQueue;
private static final int NUM_INPUT_BUFFERS = 10;
private static final int NUM_OUTPUT_BUFFERS = 2;
private final LinkedList<CeaInputBuffer> availableInputBuffers;
private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
private final ArrayDeque<CeaInputBuffer> availableInputBuffers;
private final ArrayDeque<SubtitleOutputBuffer> availableOutputBuffers;
private final PriorityQueue<CeaInputBuffer> queuedInputBuffers;
private CeaInputBuffer dequeuedInputBuffer;
@ -44,11 +44,11 @@ import java.util.PriorityQueue;
private long queuedInputBufferCount;
public CeaDecoder() {
availableInputBuffers = new LinkedList<>();
availableInputBuffers = new ArrayDeque<>();
for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
availableInputBuffers.add(new CeaInputBuffer());
}
availableOutputBuffers = new LinkedList<>();
availableOutputBuffers = new ArrayDeque<>();
for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
availableOutputBuffers.add(new CeaOutputBuffer());
}

View file

@ -62,7 +62,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
super("SsaDecoder");
if (initializationData != null && !initializationData.isEmpty()) {
haveInitializationData = true;
String formatLine = new String(initializationData.get(0));
String formatLine = Util.fromUtf8Bytes(initializationData.get(0));
Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));
parseFormatLine(formatLine);
parseHeader(new ParsableByteArray(initializationData.get(1)));

View file

@ -26,8 +26,8 @@ import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.util.XmlPullParserUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -109,13 +109,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
xmlParser.setInput(inputStream, null);
TtmlSubtitle ttmlSubtitle = null;
LinkedList<TtmlNode> nodeStack = new LinkedList<>();
ArrayDeque<TtmlNode> nodeStack = new ArrayDeque<>();
int unsupportedNodeDepth = 0;
int eventType = xmlParser.getEventType();
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peekLast();
TtmlNode parent = nodeStack.peek();
if (unsupportedNodeDepth == 0) {
String name = xmlParser.getName();
if (eventType == XmlPullParser.START_TAG) {
@ -131,7 +131,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
} else {
try {
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
nodeStack.addLast(node);
nodeStack.push(node);
if (parent != null) {
parent.addChild(node);
}
@ -145,9 +145,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
} else if (eventType == XmlPullParser.END_TAG) {
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles, regionMap);
ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap);
}
nodeStack.removeLast();
nodeStack.pop();
}
} else {
if (eventType == XmlPullParser.START_TAG) {
@ -178,7 +178,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
float frameRateMultiplier = 1;
String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier");
if (frameRateMultiplierString != null) {
String[] parts = frameRateMultiplierString.split(" ");
String[] parts = Util.split(frameRateMultiplierString, " ");
if (parts.length != 2) {
throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts");
}
@ -354,7 +354,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
}
private String[] parseStyleIds(String parentStyleIds) {
return parentStyleIds.split("\\s+");
parentStyleIds = parentStyleIds.trim();
return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, "\\s+");
}
private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) {
@ -531,7 +532,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static void parseFontSize(String expression, TtmlStyle out) throws
SubtitleDecoderException {
String[] expressions = expression.split("\\s+");
String[] expressions = Util.split(expression, "\\s+");
Matcher matcher;
if (expressions.length == 1) {
matcher = FONT_SIZE.matcher(expression);

View file

@ -92,7 +92,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
| ((initializationBytes[27] & 0xFF) << 16)
| ((initializationBytes[28] & 0xFF) << 8)
| (initializationBytes[29] & 0xFF);
String fontFamily = new String(initializationBytes, 43, initializationBytes.length - 43);
String fontFamily =
Util.fromUtf8Bytes(initializationBytes, 43, initializationBytes.length - 43);
defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME;
//font size (initializationBytes[25]) is 5% of video height
calculatedVideoTrackHeight = 20 * initializationBytes[25];

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.webvtt;
import android.text.TextUtils;
import com.google.android.exoplayer2.util.ColorParser;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -314,7 +315,7 @@ import java.util.regex.Pattern;
}
selector = selector.substring(0, voiceStartIndex);
}
String[] classDivision = selector.split("\\.");
String[] classDivision = Util.split(selector, "\\.");
String tagAndIdDivision = classDivision[0];
int idPrefixIndex = tagAndIdDivision.indexOf('#');
if (idPrefixIndex != -1) {

View file

@ -78,7 +78,8 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
int boxType = sampleData.readInt();
remainingCueBoxBytes -= BOX_HEADER_SIZE;
int payloadLength = boxSize - BOX_HEADER_SIZE;
String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength);
String boxPayload =
Util.fromUtf8Bytes(sampleData.data, sampleData.getPosition(), payloadLength);
sampleData.skipBytes(payloadLength);
remainingCueBoxBytes -= payloadLength;
if (boxType == TYPE_sttg) {

View file

@ -34,11 +34,12 @@ import android.text.style.UnderlineSpan;
import android.util.Log;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -157,7 +158,7 @@ public final class WebvttCueParser {
/* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,
List<WebvttCssStyle> styles) {
SpannableStringBuilder spannedText = new SpannableStringBuilder();
Stack<StartTag> startTagStack = new Stack<>();
ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();
List<StyleMatch> scratchStyleMatches = new ArrayList<>();
int pos = 0;
while (pos < markup.length()) {
@ -456,7 +457,7 @@ public final class WebvttCueParser {
if (tagExpression.isEmpty()) {
return null;
}
return tagExpression.split("[ \\.]")[0];
return Util.splitAtFirst(tagExpression, "[ \\.]")[0];
}
private static void getApplicableStyles(List<WebvttCssStyle> declaredStyles, String id,
@ -518,7 +519,7 @@ public final class WebvttCueParser {
voice = fullTagExpression.substring(voiceStartIndex).trim();
fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);
}
String[] nameAndClasses = fullTagExpression.split("\\.");
String[] nameAndClasses = Util.split(fullTagExpression, "\\.");
String name = nameAndClasses[0];
String[] classes;
if (nameAndClasses.length > 1) {

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text.webvtt;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -53,8 +54,8 @@ public final class WebvttParserUtil {
*/
public static long parseTimestampUs(String timestamp) throws NumberFormatException {
long value = 0;
String[] parts = timestamp.split("\\.", 2);
String[] subparts = parts[0].split(":");
String[] parts = Util.splitAtFirst(timestamp, "\\.");
String[] subparts = Util.split(parts[0], ":");
for (String subpart : subparts) {
value = (value * 60) + Long.parseLong(subpart);
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.trackselection;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
@ -242,9 +243,11 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
this.clock = clock;
playbackSpeed = 1f;
selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
reason = C.SELECTION_REASON_INITIAL;
lastBufferEvaluationMs = C.TIME_UNSET;
@SuppressWarnings("nullness:method.invocation.invalid")
int selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
this.selectedIndex = selectedIndex;
}
@Override
@ -301,7 +304,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
}
@Override
public Object getSelectionData() {
public @Nullable Object getSelectionData() {
return null;
}

View file

@ -110,6 +110,7 @@ public abstract class BaseTrackSelection implements TrackSelection {
}
@Override
@SuppressWarnings("ReferenceEquality")
public final int indexOf(Format format) {
for (int i = 0; i < length; i++) {
if (formats[i] == format) {
@ -183,7 +184,9 @@ public abstract class BaseTrackSelection implements TrackSelection {
return hashCode;
}
// Track groups are compared by identity not value, as distinct groups may have the same value.
@Override
@SuppressWarnings("ReferenceEquality")
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;

View file

@ -19,7 +19,6 @@ import android.content.Context;
import android.graphics.Point;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Pair;
@ -161,8 +160,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private final SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides;
private final SparseBooleanArray rendererDisabledFlags;
private String preferredAudioLanguage;
private String preferredTextLanguage;
private @Nullable String preferredAudioLanguage;
private @Nullable String preferredTextLanguage;
private boolean selectUndeterminedTextLanguage;
private int disabledTextTrackSelectionFlags;
private boolean forceLowestBitrate;
@ -572,14 +571,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag.
* {@code null} selects the default track, or the first track if there's no default.
*/
public final String preferredAudioLanguage;
public final @Nullable String preferredAudioLanguage;
// Text
/**
* The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the
* default track if there is one, or no track otherwise.
*/
public final String preferredTextLanguage;
public final @Nullable String preferredTextLanguage;
/**
* Whether a text track with undetermined language should be selected if no track with
* {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset.
@ -673,8 +672,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/* package */ Parameters(
SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides,
SparseBooleanArray rendererDisabledFlags,
String preferredAudioLanguage,
String preferredTextLanguage,
@Nullable String preferredAudioLanguage,
@Nullable String preferredTextLanguage,
boolean selectUndeterminedTextLanguage,
int disabledTextTrackSelectionFlags,
boolean forceLowestBitrate,
@ -759,7 +758,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @param groups The {@link TrackGroupArray}.
* @return The override, or null if no override exists.
*/
public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {
public final @Nullable SelectionOverride getSelectionOverride(
int rendererIndex, TrackGroupArray groups) {
Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(rendererIndex);
return overrides != null ? overrides.get(groups) : null;
}
@ -816,8 +816,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
result = 31 * result + viewportHeight;
result = 31 * result + maxVideoBitrate;
result = 31 * result + tunnelingAudioSessionId;
result = 31 * result + preferredAudioLanguage.hashCode();
result = 31 * result + preferredTextLanguage.hashCode();
result =
31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode());
result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode());
return result;
}
@ -1042,7 +1043,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private static final int[] NO_TRACKS = new int[0];
private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
private final TrackSelection.Factory adaptiveTrackSelectionFactory;
private final @Nullable TrackSelection.Factory adaptiveTrackSelectionFactory;
private final AtomicReference<Parameters> parametersReference;
/**
@ -1069,7 +1070,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null if
* the selector should not support adaptive tracks.
*/
public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) {
public DefaultTrackSelector(@Nullable TrackSelection.Factory adaptiveTrackSelectionFactory) {
this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
parametersReference = new AtomicReference<>(Parameters.DEFAULT);
}
@ -1139,7 +1140,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */
@Deprecated
public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {
public final @Nullable SelectionOverride getSelectionOverride(
int rendererIndex, TrackGroupArray groups) {
return getParameters().getSelectionOverride(rendererIndex, groups);
}
@ -1170,11 +1172,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// MappingTrackSelector implementation.
@Override
protected final Pair<RendererConfiguration[], TrackSelection[]> selectTracks(
MappedTrackInfo mappedTrackInfo,
int[][][] rendererFormatSupports,
int[] rendererMixedMimeTypeAdaptationSupports)
throws ExoPlaybackException {
protected final Pair<RendererConfiguration[], TrackSelection[]>
selectTracks(
MappedTrackInfo mappedTrackInfo,
int[][][] rendererFormatSupports,
int[] rendererMixedMimeTypeAdaptationSupports)
throws ExoPlaybackException {
Parameters params = parametersReference.get();
int rendererCount = mappedTrackInfo.getRendererCount();
TrackSelection[] rendererTrackSelections =
@ -1200,8 +1203,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
rendererTrackGroups.get(override.groupIndex), override.tracks[0]);
} else {
rendererTrackSelections[i] =
adaptiveTrackSelectionFactory.createTrackSelection(
rendererTrackGroups.get(override.groupIndex), override.tracks);
Assertions.checkNotNull(adaptiveTrackSelectionFactory)
.createTrackSelection(
rendererTrackGroups.get(override.groupIndex), override.tracks);
}
}
}
@ -1209,7 +1213,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Initialize the renderer configurations to the default configuration for all renderers with
// selections, and null otherwise.
RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount];
RendererConfiguration[] rendererConfigurations =
new RendererConfiguration[rendererCount];
for (int i = 0; i < rendererCount; i++) {
boolean forceRendererDisabled = params.getRendererDisabled(i);
boolean rendererEnabled =
@ -1331,12 +1336,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @return The {@link TrackSelection} for the renderer, or null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
protected TrackSelection selectVideoTrack(
protected @Nullable TrackSelection selectVideoTrack(
TrackGroupArray groups,
int[][] formatSupports,
int mixedMimeTypeAdaptationSupports,
Parameters params,
TrackSelection.Factory adaptiveTrackSelectionFactory)
@Nullable TrackSelection.Factory adaptiveTrackSelectionFactory)
throws ExoPlaybackException {
TrackSelection selection = null;
if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) {
@ -1354,7 +1359,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return selection;
}
private static TrackSelection selectAdaptiveVideoTrack(
private static @Nullable TrackSelection selectAdaptiveVideoTrack(
TrackGroupArray groups,
int[][] formatSupport,
int mixedMimeTypeAdaptationSupports,
@ -1374,7 +1379,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
params.maxVideoBitrate, params.viewportWidth, params.viewportHeight,
params.viewportOrientationMayChange);
if (adaptiveTracks.length > 0) {
return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks);
return Assertions.checkNotNull(adaptiveTrackSelectionFactory)
.createTrackSelection(group, adaptiveTracks);
}
}
return null;
@ -1421,9 +1427,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices);
}
private static int getAdaptiveVideoTrackCountForMimeType(TrackGroup group, int[] formatSupport,
int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight,
int maxVideoBitrate, List<Integer> selectedTrackIndices) {
private static int getAdaptiveVideoTrackCountForMimeType(
TrackGroup group,
int[] formatSupport,
int requiredAdaptiveSupport,
@Nullable String mimeType,
int maxVideoWidth,
int maxVideoHeight,
int maxVideoBitrate,
List<Integer> selectedTrackIndices) {
int adaptiveTrackCount = 0;
for (int i = 0; i < selectedTrackIndices.size(); i++) {
int trackIndex = selectedTrackIndices.get(i);
@ -1436,9 +1448,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return adaptiveTrackCount;
}
private static void filterAdaptiveVideoTrackCountForMimeType(TrackGroup group,
int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth,
int maxVideoHeight, int maxVideoBitrate, List<Integer> selectedTrackIndices) {
private static void filterAdaptiveVideoTrackCountForMimeType(
TrackGroup group,
int[] formatSupport,
int requiredAdaptiveSupport,
@Nullable String mimeType,
int maxVideoWidth,
int maxVideoHeight,
int maxVideoBitrate,
List<Integer> selectedTrackIndices) {
for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {
int trackIndex = selectedTrackIndices.get(i);
if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType,
@ -1449,8 +1467,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
}
private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType,
int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight,
private static boolean isSupportedAdaptiveVideoTrack(
Format format,
@Nullable String mimeType,
int formatSupport,
int requiredAdaptiveSupport,
int maxVideoWidth,
int maxVideoHeight,
int maxVideoBitrate) {
return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0)
&& (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType))
@ -1459,7 +1482,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
&& (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate);
}
private static TrackSelection selectFixedVideoTrack(
private static @Nullable TrackSelection selectFixedVideoTrack(
TrackGroupArray groups, int[][] formatSupports, Parameters params) {
TrackGroup selectedGroup = null;
int selectedTrackIndex = 0;
@ -1537,12 +1560,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @return The {@link TrackSelection} for the renderer, or null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
protected TrackSelection selectAudioTrack(
protected @Nullable TrackSelection selectAudioTrack(
TrackGroupArray groups,
int[][] formatSupports,
int mixedMimeTypeAdaptationSupports,
Parameters params,
TrackSelection.Factory adaptiveTrackSelectionFactory)
@Nullable TrackSelection.Factory adaptiveTrackSelectionFactory)
throws ExoPlaybackException {
int selectedTrackIndex = C.INDEX_UNSET;
int selectedGroupIndex = C.INDEX_UNSET;
@ -1606,8 +1629,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
int index = 0;
for (int i = 0; i < group.length; i++) {
if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i],
selectedConfiguration)) {
if (isSupportedAdaptiveAudioTrack(
group.getFormat(i), formatSupport[i], Assertions.checkNotNull(selectedConfiguration))) {
adaptiveIndices[index++] = i;
}
}
@ -1648,7 +1671,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @return The {@link TrackSelection} for the renderer, or null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
protected TrackSelection selectTextTrack(
protected @Nullable TrackSelection selectTextTrack(
TrackGroupArray groups, int[][] formatSupport, Parameters params)
throws ExoPlaybackException {
TrackGroup selectedGroup = null;
@ -1721,7 +1744,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @return The {@link TrackSelection} for the renderer, or null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
protected TrackSelection selectOtherTrack(
protected @Nullable TrackSelection selectOtherTrack(
int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params)
throws ExoPlaybackException {
TrackGroup selectedGroup = null;
@ -1883,15 +1906,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
* Returns whether a {@link Format} specifies a particular language, or {@code false} if
* {@code language} is null.
* Returns whether a {@link Format} specifies a particular language, or {@code false} if {@code
* language} is null.
*
* @param format The {@link Format}.
* @param language The language.
* @return Whether the format specifies the language, or {@code false} if {@code language} is
* null.
*/
protected static boolean formatHasLanguage(Format format, String language) {
protected static boolean formatHasLanguage(Format format, @Nullable String language) {
return language != null
&& TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
}
@ -1997,7 +2020,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* negative integer if this score is worse than the other.
*/
@Override
public int compareTo(@NonNull AudioTrackScore other) {
public int compareTo(AudioTrackScore other) {
if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) {
return compareInts(this.withinRendererCapabilitiesScore,
other.withinRendererCapabilitiesScore);
@ -2066,9 +2089,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
public final int channelCount;
public final int sampleRate;
public final String mimeType;
public final @Nullable String mimeType;
public AudioConfigurationTuple(int channelCount, int sampleRate, String mimeType) {
public AudioConfigurationTuple(int channelCount, int sampleRate, @Nullable String mimeType) {
this.channelCount = channelCount;
this.sampleRate = sampleRate;
this.mimeType = mimeType;

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.trackselection;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.util.Assertions;
@ -30,7 +31,7 @@ public final class FixedTrackSelection extends BaseTrackSelection {
public static final class Factory implements TrackSelection.Factory {
private final int reason;
private final Object data;
private final @Nullable Object data;
public Factory() {
this.reason = C.SELECTION_REASON_UNKNOWN;
@ -41,7 +42,7 @@ public final class FixedTrackSelection extends BaseTrackSelection {
* @param reason A reason for the track selection.
* @param data Optional data associated with the track selection.
*/
public Factory(int reason, Object data) {
public Factory(int reason, @Nullable Object data) {
this.reason = reason;
this.data = data;
}
@ -51,11 +52,10 @@ public final class FixedTrackSelection extends BaseTrackSelection {
Assertions.checkArgument(tracks.length == 1);
return new FixedTrackSelection(group, tracks[0], reason, data);
}
}
private final int reason;
private final Object data;
private final @Nullable Object data;
/**
* @param group The {@link TrackGroup}. Must not be null.
@ -71,7 +71,7 @@ public final class FixedTrackSelection extends BaseTrackSelection {
* @param reason A reason for the track selection.
* @param data Optional data associated with the track selection.
*/
public FixedTrackSelection(TrackGroup group, int track, int reason, Object data) {
public FixedTrackSelection(TrackGroup group, int track, int reason, @Nullable Object data) {
super(group, track);
this.reason = reason;
this.data = data;
@ -94,7 +94,7 @@ public final class FixedTrackSelection extends BaseTrackSelection {
}
@Override
public Object getSelectionData() {
public @Nullable Object getSelectionData() {
return data;
}

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.trackselection;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
@ -301,13 +302,13 @@ public abstract class MappingTrackSelector extends TrackSelector {
}
private MappedTrackInfo currentMappedTrackInfo;
private @Nullable MappedTrackInfo currentMappedTrackInfo;
/**
* Returns the mapping information for the currently active track selection, or null if no
* selection is currently active.
*/
public final MappedTrackInfo getCurrentMappedTrackInfo() {
public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() {
return currentMappedTrackInfo;
}
@ -357,9 +358,11 @@ public abstract class MappingTrackSelector extends TrackSelector {
int[] rendererTrackTypes = new int[rendererCapabilities.length];
for (int i = 0; i < rendererCapabilities.length; i++) {
int rendererTrackGroupCount = rendererTrackGroupCounts[i];
rendererTrackGroupArrays[i] = new TrackGroupArray(
Arrays.copyOf(rendererTrackGroups[i], rendererTrackGroupCount));
rendererFormatSupports[i] = Arrays.copyOf(rendererFormatSupports[i], rendererTrackGroupCount);
rendererTrackGroupArrays[i] =
new TrackGroupArray(
Util.nullSafeArrayCopy(rendererTrackGroups[i], rendererTrackGroupCount));
rendererFormatSupports[i] =
Util.nullSafeArrayCopy(rendererFormatSupports[i], rendererTrackGroupCount);
rendererTrackTypes[i] = rendererCapabilities[i].getTrackType();
}
@ -367,7 +370,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length];
TrackGroupArray unmappedTrackGroupArray =
new TrackGroupArray(
Arrays.copyOf(
Util.nullSafeArrayCopy(
rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount));
// Package up the track information and selections.
@ -399,11 +402,12 @@ public abstract class MappingTrackSelector extends TrackSelector {
* RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
protected abstract Pair<RendererConfiguration[], TrackSelection[]> selectTracks(
MappedTrackInfo mappedTrackInfo,
int[][][] rendererFormatSupports,
int[] rendererMixedMimeTypeAdaptationSupport)
throws ExoPlaybackException;
protected abstract Pair<RendererConfiguration[], TrackSelection[]>
selectTracks(
MappedTrackInfo mappedTrackInfo,
int[][][] rendererFormatSupports,
int[] rendererMixedMimeTypeAdaptationSupport)
throws ExoPlaybackException;
/**
* Finds the renderer to which the provided {@link TrackGroup} should be mapped.

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.trackselection;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.TrackGroup;
import java.util.Random;
@ -47,7 +48,6 @@ public final class RandomTrackSelection extends BaseTrackSelection {
public RandomTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
return new RandomTrackSelection(group, tracks, random);
}
}
private final Random random;
@ -123,7 +123,7 @@ public final class RandomTrackSelection extends BaseTrackSelection {
}
@Override
public Object getSelectionData() {
public @Nullable Object getSelectionData() {
return null;
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.trackselection;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
@ -90,7 +91,9 @@ public interface TrackSelection {
int getIndexInTrackGroup(int index);
/**
* Returns the index in the selection of the track with the specified format.
* Returns the index in the selection of the track with the specified format. The format is
* located by identity so, for example, {@code selection.indexOf(selection.getFormat(index)) ==
* index} even if multiple selected tracks have formats that contain the same values.
*
* @param format The format.
* @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
@ -129,10 +132,8 @@ public interface TrackSelection {
*/
int getSelectionReason();
/**
* Returns optional data associated with the current track selection.
*/
Object getSelectionData();
/** Returns optional data associated with the current track selection. */
@Nullable Object getSelectionData();
// Adaptation.

View file

@ -29,9 +29,7 @@ public final class TrackSelectionArray {
// Lazily initialized hashcode.
private int hashCode;
/**
* @param trackSelections The selections. Must not be null, but may contain null elements.
*/
/** @param trackSelections The selections. Must not be null, but may contain null elements. */
public TrackSelectionArray(TrackSelection... trackSelections) {
this.trackSelections = trackSelections;
this.length = trackSelections.length;
@ -43,13 +41,11 @@ public final class TrackSelectionArray {
* @param index The index of the selection.
* @return The selection.
*/
public TrackSelection get(int index) {
public @Nullable TrackSelection get(int index) {
return trackSelections[index];
}
/**
* Returns the selections in a newly allocated array.
*/
/** Returns the selections in a newly allocated array. */
public TrackSelection[] getAll() {
return trackSelections.clone();
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.trackselection;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Renderer;
@ -89,7 +90,7 @@ public abstract class TrackSelector {
}
private InvalidationListener listener;
private @Nullable InvalidationListener listener;
/**
* Called by the player to initialize the selector.

View file

@ -48,7 +48,9 @@ public final class TrackSelectorResult {
* TrackSelector#onSelectionActivated(Object)} should the selection be activated.
*/
public TrackSelectorResult(
RendererConfiguration[] rendererConfigurations, TrackSelection[] selections, Object info) {
RendererConfiguration[] rendererConfigurations,
TrackSelection[] selections,
Object info) {
this.rendererConfigurations = rendererConfigurations;
this.selections = new TrackSelectionArray(selections);
this.info = info;

View file

@ -19,6 +19,7 @@ import android.net.Uri;
import android.util.Base64;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.net.URLDecoder;
@ -41,8 +42,8 @@ public final class DataSchemeDataSource implements DataSource {
if (!SCHEME_DATA.equals(scheme)) {
throw new ParserException("Unsupported scheme: " + scheme);
}
String[] uriParts = uri.getSchemeSpecificPart().split(",");
if (uriParts.length > 2) {
String[] uriParts = Util.split(uri.getSchemeSpecificPart(), ",");
if (uriParts.length != 2) {
throw new ParserException("Unexpected URI format: " + uri);
}
String dataString = uriParts[1];

View file

@ -57,11 +57,6 @@ public final class Loader implements LoaderErrorThrower {
*/
void cancelLoad();
/**
* Returns whether the load has been canceled.
*/
boolean isLoadCanceled();
/**
* Performs the load, returning on completion or cancellation.
*
@ -250,15 +245,17 @@ public final class Loader implements LoaderErrorThrower {
private static final int MSG_IO_EXCEPTION = 3;
private static final int MSG_FATAL_ERROR = 4;
private final T loadable;
private final Loader.Callback<T> callback;
public final int defaultMinRetryCount;
private final T loadable;
private final long startTimeMs;
private @Nullable Loader.Callback<T> callback;
private IOException currentError;
private int errorCount;
private volatile Thread executorThread;
private volatile boolean canceled;
private volatile boolean released;
public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
@ -295,6 +292,7 @@ public final class Loader implements LoaderErrorThrower {
sendEmptyMessage(MSG_CANCEL);
}
} else {
canceled = true;
loadable.cancelLoad();
if (executorThread != null) {
executorThread.interrupt();
@ -304,6 +302,11 @@ public final class Loader implements LoaderErrorThrower {
finish();
long nowMs = SystemClock.elapsedRealtime();
callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true);
// If loading, this task will be referenced from a GC root (the loading thread) until
// cancellation completes. The time taken for cancellation to complete depends on the
// implementation of the Loadable that the task is loading. We null the callback reference
// here so that it doesn't prevent garbage collection whilst cancellation is ongoing.
callback = null;
}
}
@ -311,7 +314,7 @@ public final class Loader implements LoaderErrorThrower {
public void run() {
try {
executorThread = Thread.currentThread();
if (!loadable.isLoadCanceled()) {
if (!canceled) {
TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
try {
loadable.load();
@ -328,7 +331,7 @@ public final class Loader implements LoaderErrorThrower {
}
} catch (InterruptedException e) {
// The load was canceled.
Assertions.checkState(loadable.isLoadCanceled());
Assertions.checkState(canceled);
if (!released) {
sendEmptyMessage(MSG_END_OF_SOURCE);
}
@ -373,7 +376,7 @@ public final class Loader implements LoaderErrorThrower {
finish();
long nowMs = SystemClock.elapsedRealtime();
long durationMs = nowMs - startTimeMs;
if (loadable.isLoadCanceled()) {
if (canceled) {
callback.onLoadCanceled(loadable, nowMs, durationMs, false);
return;
}

View file

@ -78,7 +78,6 @@ public final class ParsingLoadable<T> implements Loadable {
private final Parser<? extends T> parser;
private volatile T result;
private volatile boolean isCanceled;
private volatile long bytesLoaded;
/**
@ -128,14 +127,7 @@ public final class ParsingLoadable<T> implements Loadable {
@Override
public final void cancelLoad() {
// We don't actually cancel anything, but we need to record the cancellation so that
// isLoadCanceled can return the correct value.
isCanceled = true;
}
@Override
public final boolean isLoadCanceled() {
return isCanceled;
// Do nothing.
}
@Override

View file

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource;
@ -52,8 +51,6 @@ public final class CacheDataSource implements DataSource {
*/
public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
private static final String TAG = "CacheDataSource";
/**
* Flags controlling the cache's behavior.
*/
@ -221,7 +218,7 @@ public final class CacheDataSource implements DataSource {
try {
key = CacheUtil.getKey(dataSpec);
uri = dataSpec.uri;
actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri);
actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri);
flags = dataSpec.flags;
readPosition = dataSpec.position;
@ -272,7 +269,7 @@ public final class CacheDataSource implements DataSource {
bytesRemaining -= bytesRead;
}
} else if (currentDataSpecLengthUnset) {
setBytesRemainingAndMaybeStoreLength(0);
setNoBytesRemainingAndMaybeStoreLength();
} else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
closeCurrentSource();
openNextSource(false);
@ -281,7 +278,7 @@ public final class CacheDataSource implements DataSource {
return bytesRead;
} catch (IOException e) {
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
setBytesRemainingAndMaybeStoreLength(0);
setNoBytesRemainingAndMaybeStoreLength();
return C.RESULT_END_OF_INPUT;
}
handleBeforeThrow(e);
@ -402,46 +399,38 @@ public final class CacheDataSource implements DataSource {
currentDataSource = nextDataSource;
currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;
long resolvedLength = nextDataSource.open(nextDataSpec);
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
setBytesRemainingAndMaybeStoreLength(resolvedLength);
}
// TODO find a way to store length and redirected uri in one metadata mutation.
maybeUpdateActualUriFieldAndRedirectedUriMetadata();
}
private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() {
if (!isReadingFromUpstream()) {
return;
}
actualUri = currentDataSource.getUri();
maybeUpdateRedirectedUriMetadata();
}
private void maybeUpdateRedirectedUriMetadata() {
if (!isWritingToCache()) {
return;
}
// Update bytesRemaining, actualUri and (if writing to cache) the cache metadata.
ContentMetadataMutations mutations = new ContentMetadataMutations();
boolean isRedirected = !uri.equals(actualUri);
if (isRedirected) {
ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
} else {
ContentMetadataInternal.removeRedirectedUri(mutations);
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
bytesRemaining = resolvedLength;
ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining);
}
try {
if (isReadingFromUpstream()) {
actualUri = currentDataSource.getUri();
boolean isRedirected = !uri.equals(actualUri);
if (isRedirected) {
ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
} else {
ContentMetadataInternal.removeRedirectedUri(mutations);
}
}
if (isWritingToCache()) {
cache.applyContentMetadataMutations(key, mutations);
} catch (CacheException e) {
String message =
"Couldn't update redirected URI. "
+ "This might cause relative URIs get resolved incorrectly.";
Log.w(TAG, message, e);
}
}
private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) {
private void setNoBytesRemainingAndMaybeStoreLength() throws IOException {
bytesRemaining = 0;
if (isWritingToCache()) {
cache.setContentLength(key, readPosition);
}
}
private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {
ContentMetadata contentMetadata = cache.getContentMetadata(key);
Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata);
return redirectedUri == null ? uri : redirectedUri;
return redirectedUri == null ? defaultUri : redirectedUri;
}
private static boolean isCausedByPositionOutOfRange(IOException e) {
@ -458,13 +447,6 @@ public final class CacheDataSource implements DataSource {
return false;
}
private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException {
this.bytesRemaining = bytesRemaining;
if (isWritingToCache()) {
cache.setContentLength(key, readPosition + bytesRemaining);
}
}
private boolean isReadingFromUpstream() {
return !isReadingFromCache();
}

View file

@ -129,11 +129,11 @@ public final class CacheUtil {
cache,
new CacheDataSource(cache, upstream),
new byte[DEFAULT_BUFFER_SIZE_BYTES],
null,
0,
/* priorityTaskManager= */ null,
/* priority= */ 0,
counters,
null,
false);
isCanceled,
/* enableEOFException= */ false);
}
/**

View file

@ -20,7 +20,7 @@ import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
/** Helper classes to easily access and modify internal metadata values. */
/*package*/ final class ContentMetadataInternal {
/* package */ final class ContentMetadataInternal {
private static final String PREFIX = ContentMetadata.INTERNAL_METADATA_NAME_PREFIX;
private static final String METADATA_NAME_REDIRECTED_URI = PREFIX + "redir";
@ -59,4 +59,8 @@ import com.google.android.exoplayer2.C;
public static void removeRedirectedUri(ContentMetadataMutations mutations) {
mutations.remove(METADATA_NAME_REDIRECTED_URI);
}
private ContentMetadataInternal() {
// Prevent instantiation.
}
}

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream.crypto;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@ -49,7 +50,9 @@ public final class AesFlushingCipher {
flushedBlock = new byte[blockSize];
long counter = offset / blockSize;
int startPadding = (int) (offset % blockSize);
cipher.init(mode, new SecretKeySpec(secretKey, cipher.getAlgorithm().split("/")[0]),
cipher.init(
mode,
new SecretKeySpec(secretKey, Util.splitAtFirst(cipher.getAlgorithm(), "/")[0]),
new IvParameterSpec(getInitializationVector(nonce, counter)));
if (startPadding != 0) {
updateInPlace(new byte[startPadding], 0, startPadding);

View file

@ -26,7 +26,7 @@ import java.util.regex.Pattern;
*
* @see <a href="https://w3c.github.io/webvtt/#styling">WebVTT CSS Styling</a>
* @see <a href="https://www.w3.org/TR/ttml2/">Timed Text Markup Language 2 (TTML2) - 10.3.5</a>
**/
*/
public final class ColorParser {
private static final String RGB = "rgb";
@ -271,4 +271,7 @@ public final class ColorParser {
COLOR_MAP.put("yellowgreen", 0xFF9ACD32);
}
private ColorParser() {
// Prevent instantiation.
}
}

View file

@ -111,12 +111,20 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
GLES20.glDeleteTextures(1, textureIdHolder, 0);
}
} finally {
if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) {
EGL14.eglMakeCurrent(
display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
}
if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) {
EGL14.eglDestroySurface(display, surface);
}
if (context != null) {
EGL14.eglDestroyContext(display, context);
}
// EGL14.eglReleaseThread could crash before Android K (see [internal: b/11327779]).
if (Util.SDK_INT >= 19) {
EGL14.eglReleaseThread();
}
display = null;
context = null;
surface = null;

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import java.util.ArrayList;
/**
* Defines common MIME types and helper methods.
@ -92,7 +93,29 @@ public final class MimeTypes {
public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs";
public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif";
private MimeTypes() {}
private static final ArrayList<CustomMimeType> customMimeTypes = new ArrayList<>();
/**
* Registers a custom MIME type. Most applications do not need to call this method, as handling of
* standard MIME types is built in. These built-in MIME types take precedence over any registered
* via this method. If this method is used, it must be called before creating any player(s).
*
* @param mimeType The custom MIME type to register.
* @param codecPrefix The RFC 6381-style codec string prefix associated with the MIME type.
* @param trackType The {@link C}{@code .TRACK_TYPE_*} constant associated with the MIME type.
* This value is ignored if the top-level type of {@code mimeType} is audio, video or text.
*/
public static void registerCustomMimeType(String mimeType, String codecPrefix, int trackType) {
CustomMimeType customMimeType = new CustomMimeType(mimeType, codecPrefix, trackType);
int customMimeTypeCount = customMimeTypes.size();
for (int i = 0; i < customMimeTypeCount; i++) {
if (mimeType.equals(customMimeTypes.get(i).mimeType)) {
customMimeTypes.remove(i);
break;
}
}
customMimeTypes.add(customMimeType);
}
/**
* Whether the top-level type of {@code mimeType} is audio.
@ -144,7 +167,7 @@ public final class MimeTypes {
if (codecs == null) {
return null;
}
String[] codecList = codecs.split(",");
String[] codecList = Util.split(codecs, ",");
for (String codec : codecList) {
String mimeType = getMediaMimeType(codec);
if (mimeType != null && isVideo(mimeType)) {
@ -164,7 +187,7 @@ public final class MimeTypes {
if (codecs == null) {
return null;
}
String[] codecList = codecs.split(",");
String[] codecList = Util.split(codecs, ",");
for (String codec : codecList) {
String mimeType = getMediaMimeType(codec);
if (mimeType != null && isAudio(mimeType)) {
@ -222,8 +245,9 @@ public final class MimeTypes {
return MimeTypes.AUDIO_OPUS;
} else if (codec.startsWith("vorbis")) {
return MimeTypes.AUDIO_VORBIS;
} else {
return getCustomMimeTypeForCodec(codec);
}
return null;
}
/**
@ -236,18 +260,28 @@ public final class MimeTypes {
@Nullable
public static String getMimeTypeFromMp4ObjectType(int objectType) {
switch (objectType) {
case 0x60:
case 0x61:
return MimeTypes.VIDEO_MPEG2;
case 0x20:
return MimeTypes.VIDEO_MP4V;
case 0x21:
return MimeTypes.VIDEO_H264;
case 0x23:
return MimeTypes.VIDEO_H265;
case 0x60:
case 0x61:
case 0x62:
case 0x63:
case 0x64:
case 0x65:
return MimeTypes.VIDEO_MPEG2;
case 0x6A:
return MimeTypes.VIDEO_MPEG;
case 0x69:
case 0x6B:
return MimeTypes.AUDIO_MPEG;
case 0xA3:
return MimeTypes.VIDEO_VC1;
case 0xB1:
return MimeTypes.VIDEO_VP9;
case 0x40:
case 0x66:
case 0x67:
@ -298,7 +332,7 @@ public final class MimeTypes {
|| APPLICATION_CAMERA_MOTION.equals(mimeType)) {
return C.TRACK_TYPE_METADATA;
} else {
return C.TRACK_TYPE_UNKNOWN;
return getTrackTypeForCustomMimeType(mimeType);
}
}
@ -355,4 +389,41 @@ public final class MimeTypes {
return mimeType.substring(0, indexOfSlash);
}
private static @Nullable String getCustomMimeTypeForCodec(String codec) {
int customMimeTypeCount = customMimeTypes.size();
for (int i = 0; i < customMimeTypeCount; i++) {
CustomMimeType customMimeType = customMimeTypes.get(i);
if (codec.startsWith(customMimeType.codecPrefix)) {
return customMimeType.mimeType;
}
}
return null;
}
private static int getTrackTypeForCustomMimeType(String mimeType) {
int customMimeTypeCount = customMimeTypes.size();
for (int i = 0; i < customMimeTypeCount; i++) {
CustomMimeType customMimeType = customMimeTypes.get(i);
if (mimeType.equals(customMimeType.mimeType)) {
return customMimeType.trackType;
}
}
return C.TRACK_TYPE_UNKNOWN;
}
private MimeTypes() {
// Prevent instantiation.
}
private static final class CustomMimeType {
public final String mimeType;
public final String codecPrefix;
public final int trackType;
public CustomMimeType(String mimeType, String codecPrefix, int trackType) {
this.mimeType = mimeType;
this.codecPrefix = codecPrefix;
this.trackType = trackType;
}
}
}

View file

@ -175,7 +175,7 @@ public final class ParsableBitArray {
bitOffset -= 8;
returnValue |= (data[byteOffset++] & 0xFF) << bitOffset;
}
returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset;
returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
returnValue &= 0xFFFFFFFF >>> (32 - numBits);
if (bitOffset == 8) {
bitOffset = 0;
@ -199,17 +199,18 @@ public final class ParsableBitArray {
int to = offset + (numBits >> 3) /* numBits / 8 */;
for (int i = offset; i < to; i++) {
buffer[i] = (byte) (data[byteOffset++] << bitOffset);
buffer[i] |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
buffer[i] = (byte) (buffer[i] | ((data[byteOffset] & 0xFF) >> (8 - bitOffset)));
}
// Trailing bits.
int bitsLeft = numBits & 7 /* numBits % 8 */;
if (bitsLeft == 0) {
return;
}
buffer[to] &= 0xFF >> bitsLeft; // Set to 0 the bits that are going to be overwritten.
// Set bits that are going to be overwritten to 0.
buffer[to] = (byte) (buffer[to] & (0xFF >> bitsLeft));
if (bitOffset + bitsLeft > 8) {
// We read the rest of data[byteOffset] and increase byteOffset.
buffer[to] |= (byte) ((data[byteOffset++] & 0xFF) << bitOffset);
buffer[to] = (byte) (buffer[to] | ((data[byteOffset++] & 0xFF) << bitOffset));
bitOffset -= 8;
}
bitOffset += bitsLeft;
@ -280,9 +281,10 @@ public final class ParsableBitArray {
int firstByteReadSize = Math.min(8 - bitOffset, numBits);
int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize;
int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1);
data[byteOffset] &= firstByteBitmask;
data[byteOffset] = (byte) (data[byteOffset] & firstByteBitmask);
int firstByteInputBits = value >>> (numBits - firstByteReadSize);
data[byteOffset] |= firstByteInputBits << firstByteRightPaddingSize;
data[byteOffset] =
(byte) (data[byteOffset] | (firstByteInputBits << firstByteRightPaddingSize));
remainingBitsToRead -= firstByteReadSize;
int currentByteIndex = byteOffset + 1;
while (remainingBitsToRead > 8) {
@ -290,9 +292,11 @@ public final class ParsableBitArray {
remainingBitsToRead -= 8;
}
int lastByteRightPaddingSize = 8 - remainingBitsToRead;
data[currentByteIndex] &= (1 << lastByteRightPaddingSize) - 1;
data[currentByteIndex] =
(byte) (data[currentByteIndex] & ((1 << lastByteRightPaddingSize) - 1));
int lastByteInput = value & ((1 << remainingBitsToRead) - 1);
data[currentByteIndex] |= lastByteInput << lastByteRightPaddingSize;
data[currentByteIndex] =
(byte) (data[currentByteIndex] | (lastByteInput << lastByteRightPaddingSize));
skipBits(numBits);
assertValidOffset();
}

View file

@ -470,7 +470,7 @@ public final class ParsableByteArray {
if (lastIndex < limit && data[lastIndex] == 0) {
stringLength--;
}
String result = new String(data, position, stringLength);
String result = Util.fromUtf8Bytes(data, position, stringLength);
position += length;
return result;
}
@ -489,7 +489,7 @@ public final class ParsableByteArray {
while (stringLimit < limit && data[stringLimit] != 0) {
stringLimit++;
}
String string = new String(data, position, stringLimit - position);
String string = Util.fromUtf8Bytes(data, position, stringLimit - position);
position = stringLimit;
if (position < limit) {
position++;
@ -520,7 +520,7 @@ public final class ParsableByteArray {
// There's a byte order mark at the start of the line. Discard it.
position += 3;
}
String line = new String(data, position, lineLimit - position);
String line = Util.fromUtf8Bytes(data, position, lineLimit - position);
position = lineLimit;
if (position == limit) {
return line;

View file

@ -140,7 +140,7 @@ public final class ParsableNalUnitBitArray {
returnValue |= (data[byteOffset] & 0xFF) << bitOffset;
byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
}
returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset;
returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
returnValue &= 0xFFFFFFFF >>> (32 - numBits);
if (bitOffset == 8) {
bitOffset = 0;

View file

@ -311,10 +311,10 @@ public final class Util {
* Returns a normalized RFC 639-2/T code for {@code language}.
*
* @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language code.
* @return The all-lowercase normalized code, or null if the input was null, or
* {@code language.toLowerCase()} if the language could not be normalized.
* @return The all-lowercase normalized code, or null if the input was null, or {@code
* language.toLowerCase()} if the language could not be normalized.
*/
public static String normalizeLanguageCode(String language) {
public static @Nullable String normalizeLanguageCode(@Nullable String language) {
try {
return language == null ? null : new Locale(language).getISO3Language();
} catch (MissingResourceException e) {
@ -332,6 +332,18 @@ public final class Util {
return new String(bytes, Charset.forName(C.UTF8_NAME));
}
/**
* Returns a new {@link String} constructed by decoding UTF-8 encoded bytes in a subarray.
*
* @param bytes The UTF-8 encoded bytes to decode.
* @param offset The index of the first byte to decode.
* @param length The number of bytes to decode.
* @return The string.
*/
public static String fromUtf8Bytes(byte[] bytes, int offset, int length) {
return new String(bytes, offset, length, Charset.forName(C.UTF8_NAME));
}
/**
* Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.
*
@ -342,6 +354,33 @@ public final class Util {
return value.getBytes(Charset.forName(C.UTF8_NAME));
}
/**
* Splits a string using {@code value.split(regex, -1}). Note: this is is similar to {@link
* String#split(String)} but empty matches at the end of the string will not be omitted from the
* returned array.
*
* @param value The string to split.
* @param regex A delimiting regular expression.
* @return The array of strings resulting from splitting the string.
*/
public static String[] split(String value, String regex) {
return value.split(regex, /* limit= */ -1);
}
/**
* Splits the string at the first occurrence of the delimiter {@code regex}. If the delimiter does
* not match, returns an array with one element which is the input string. If the delimiter does
* match, returns an array with the portion of the string before the delimiter and the rest of the
* string.
*
* @param value The string.
* @param regex A delimiting regular expression.
* @return The string split by the first occurrence of the delimiter.
*/
public static String[] splitAtFirst(String value, String regex) {
return value.split(regex, /* limit= */ 2);
}
/**
* Returns whether the given character is a carriage return ('\r') or a line feed ('\n').
*
@ -978,7 +1017,7 @@ public final class Util {
if (TextUtils.isEmpty(codecs)) {
return null;
}
String[] codecArray = codecs.trim().split("(\\s*,\\s*)");
String[] codecArray = split(codecs.trim(), "(\\s*,\\s*)");
StringBuilder builder = new StringBuilder();
for (String codec : codecArray) {
if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
@ -1454,7 +1493,7 @@ public final class Util {
// If we managed to read sys.display-size, attempt to parse it.
if (!TextUtils.isEmpty(sysDisplaySize)) {
try {
String[] sysDisplaySizeParts = sysDisplaySize.trim().split("x");
String[] sysDisplaySizeParts = split(sysDisplaySize.trim(), "x");
if (sysDisplaySizeParts.length == 2) {
int width = Integer.parseInt(sysDisplaySizeParts[0]);
int height = Integer.parseInt(sysDisplaySizeParts[1]);

View file

@ -156,7 +156,7 @@ public final class DummySurface extends Surface {
private static final int MSG_INIT = 1;
private static final int MSG_RELEASE = 2;
private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure;
private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture;
private @MonotonicNonNull Handler handler;
private @Nullable Error initError;
private @Nullable RuntimeException initException;
@ -169,7 +169,7 @@ public final class DummySurface extends Surface {
public DummySurface init(@SecureMode int secureMode) {
start();
handler = new Handler(getLooper(), /* callback= */ this);
eglSurfaceTexure = new EGLSurfaceTexture(handler);
eglSurfaceTexture = new EGLSurfaceTexture(handler);
boolean wasInterrupted = false;
synchronized (this) {
handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget();
@ -232,16 +232,16 @@ public final class DummySurface extends Surface {
}
private void initInternal(@SecureMode int secureMode) {
Assertions.checkNotNull(eglSurfaceTexure);
eglSurfaceTexure.init(secureMode);
Assertions.checkNotNull(eglSurfaceTexture);
eglSurfaceTexture.init(secureMode);
this.surface =
new DummySurface(
this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);
this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);
}
private void releaseInternal() {
Assertions.checkNotNull(eglSurfaceTexure);
eglSurfaceTexure.release();
Assertions.checkNotNull(eglSurfaceTexture);
eglSurfaceTexture.release();
}
}

View file

@ -205,7 +205,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
this.context = context.getApplicationContext();
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context);
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
@ -1177,8 +1177,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// https://github.com/google/ExoPlayer/issues/3835,
// https://github.com/google/ExoPlayer/issues/4006,
// https://github.com/google/ExoPlayer/issues/4084,
// https://github.com/google/ExoPlayer/issues/4104.
// https://github.com/google/ExoPlayer/issues/4134.
// https://github.com/google/ExoPlayer/issues/4104,
// https://github.com/google/ExoPlayer/issues/4134,
// https://github.com/google/ExoPlayer/issues/4315.
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013)
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013)
|| "mido".equals(Util.DEVICE) // Redmi Note 4
@ -1192,7 +1193,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|| "M5c".equals(Util.DEVICE) // Meizu M5C
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E
|| "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note
|| "woods_f".equals(Util.MODEL)) // Moto E (4)
|| "woods_f".equals(Util.MODEL) // Moto E (4)
|| "watson".equals(Util.DEVICE)) // Moto C
&& "OMX.MTK.VIDEO.DECODER.AVC".equals(name))
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite
|| "CAM-L21".equals(Util.MODEL)) // Huawei Y6II

View file

@ -72,8 +72,12 @@ public final class VideoFrameReleaseTimeHelper {
* @param context A context from which information about the default display can be retrieved.
*/
public VideoFrameReleaseTimeHelper(@Nullable Context context) {
windowManager = context == null ? null
: (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (context != null) {
context = context.getApplicationContext();
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
} else {
windowManager = null;
}
if (windowManager != null) {
displayListener = Util.SDK_INT >= 17 ? maybeBuildDefaultDisplayListenerV17(context) : null;
vsyncSampler = VSyncSampler.getInstance();

View file

@ -1980,6 +1980,105 @@ public final class ExoPlayerTest {
.inOrder();
}
@Test
public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception {
// We add two listeners to the player. The first stops the player as soon as it's ready and both
// record the state change events they receive.
final AtomicReference<Player> playerReference = new AtomicReference<>();
final List<Integer> eventListener1States = new ArrayList<>();
final List<Integer> eventListener2States = new ArrayList<>();
final EventListener eventListener1 =
new DefaultEventListener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
eventListener1States.add(playbackState);
if (playbackState == Player.STATE_READY) {
playerReference.get().stop(/* reset= */ true);
}
}
};
final EventListener eventListener2 =
new DefaultEventListener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
eventListener2States.add(playbackState);
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testRecursivePlayerChanges")
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
playerReference.set(player);
player.addListener(eventListener1);
player.addListener(eventListener2);
}
})
.build();
new ExoPlayerTestRunner.Builder()
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(eventListener1States)
.containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE)
.inOrder();
assertThat(eventListener2States)
.containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE)
.inOrder();
}
@Test
public void testRecursivePlayerChangesAreReportedInCorrectOrder() throws Exception {
// The listener stops the player as soon as it's ready (which should report a timeline and state
// change) and sets playWhenReady to false when the timeline callback is received.
final AtomicReference<Player> playerReference = new AtomicReference<>();
final List<Boolean> eventListenerPlayWhenReady = new ArrayList<>();
final List<Integer> eventListenerStates = new ArrayList<>();
final EventListener eventListener =
new DefaultEventListener() {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
if (timeline.isEmpty()) {
playerReference.get().setPlayWhenReady(/* playWhenReady= */ false);
}
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
eventListenerPlayWhenReady.add(playWhenReady);
eventListenerStates.add(playbackState);
if (playbackState == Player.STATE_READY) {
playerReference.get().stop(/* reset= */ true);
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testRecursivePlayerChanges")
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
playerReference.set(player);
player.addListener(eventListener);
}
})
.build();
new ExoPlayerTestRunner.Builder()
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(eventListenerStates)
.containsExactly(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE, Player.STATE_IDLE)
.inOrder();
assertThat(eventListenerPlayWhenReady).containsExactly(true, true, true, false).inOrder();
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {

View file

@ -391,11 +391,6 @@ public final class AdaptiveTrackSelectionTest {
// Do nothing.
}
@Override
public boolean isLoadCanceled() {
return false;
}
@Override
public void load() throws IOException, InterruptedException {
// Do nothing.

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.dash;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Pair;
import android.util.SparseArray;
import android.util.SparseIntArray;
@ -72,7 +73,7 @@ import java.util.List;
private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>
trackEmsgHandlerBySampleStream;
private Callback callback;
private @Nullable Callback callback;
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
private EventSampleStream[] eventSampleStreams;
private SequenceableLoader compositeSequenceableLoader;
@ -150,6 +151,7 @@ import java.util.List;
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
sampleStream.release(this);
}
callback = null;
eventDispatcher.mediaPeriodReleased();
}

View file

@ -25,12 +25,15 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
public final class DashWrappingSegmentIndex implements DashSegmentIndex {
private final ChunkIndex chunkIndex;
private final long timeOffsetUs;
/**
* @param chunkIndex The {@link ChunkIndex} to wrap.
* @param timeOffsetUs An offset to subtract from the times in the wrapped index, in microseconds.
*/
public DashWrappingSegmentIndex(ChunkIndex chunkIndex) {
public DashWrappingSegmentIndex(ChunkIndex chunkIndex, long timeOffsetUs) {
this.chunkIndex = chunkIndex;
this.timeOffsetUs = timeOffsetUs;
}
@Override
@ -45,7 +48,7 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex {
@Override
public long getTimeUs(long segmentNum) {
return chunkIndex.timesUs[(int) segmentNum];
return chunkIndex.timesUs[(int) segmentNum] - timeOffsetUs;
}
@Override
@ -61,7 +64,7 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex {
@Override
public long getSegmentNum(long timeUs, long periodDurationUs) {
return chunkIndex.getChunkIndex(timeUs);
return chunkIndex.getChunkIndex(timeUs + timeOffsetUs);
}
@Override

View file

@ -354,7 +354,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
if (representationHolder.segmentIndex == null) {
SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap();
if (seekMap != null) {
representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap);
representationHolder.segmentIndex =
new DashWrappingSegmentIndex(
(ChunkIndex) seekMap,
representationHolder.representation.presentationTimeOffsetUs);
}
}
}

View file

@ -167,7 +167,9 @@ public final class DashDownloader extends SegmentDownloader<DashManifest, Repres
return index;
}
ChunkIndex seekMap = DashUtil.loadChunkIndex(dataSource, trackType, representation);
return seekMap == null ? null : new DashWrappingSegmentIndex(seekMap);
return seekMap == null
? null
: new DashWrappingSegmentIndex(seekMap, representation.presentationTimeOffsetUs);
}
}

View file

@ -104,7 +104,7 @@ import java.util.List;
// the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods
// in TrackSelection to avoid unexpected behavior.
private TrackSelection trackSelection;
private long liveEdgeTimeUs;
private long liveEdgeInPeriodTimeUs;
private boolean seenExpectedPlaylistError;
/**
@ -128,7 +128,7 @@ import java.util.List;
this.variants = variants;
this.timestampAdjusterProvider = timestampAdjusterProvider;
this.muxedCaptionFormats = muxedCaptionFormats;
liveEdgeTimeUs = C.TIME_UNSET;
liveEdgeInPeriodTimeUs = C.TIME_UNSET;
Format[] variantFormats = new Format[variants.length];
int[] initialTrackSelection = new int[variants.length];
for (int i = 0; i < variants.length; i++) {
@ -254,16 +254,17 @@ import java.util.List;
// Select the chunk.
long chunkMediaSequence;
long startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
if (previous == null || switchingVariant) {
long targetPositionUs = (previous == null || independentSegments) ? loadPositionUs
: previous.startTimeUs;
if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) {
long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs;
long targetPositionInPeriodUs =
(previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs;
if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) {
// If the playlist is too old to contain the chunk, we need to refresh it.
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
} else {
long positionOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs;
long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs;
chunkMediaSequence =
Util.binarySearchFloor(
mediaPlaylist.segments,
@ -277,6 +278,8 @@ import java.util.List;
selectedVariantIndex = oldVariantIndex;
selectedUrl = variants[selectedVariantIndex];
mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
chunkMediaSequence = previous.getNextChunkIndex();
}
}
@ -331,9 +334,7 @@ import java.util.List;
}
// Compute start time of the next chunk.
long positionOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
int discontinuitySequence = mediaPlaylist.discontinuitySequence
+ segment.relativeDiscontinuitySequence;
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
@ -420,12 +421,17 @@ import java.util.List;
// Private methods.
private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {
final boolean resolveTimeToLiveEdgePossible = liveEdgeTimeUs != C.TIME_UNSET;
return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET;
final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET;
return resolveTimeToLiveEdgePossible
? liveEdgeInPeriodTimeUs - playbackPositionUs
: C.TIME_UNSET;
}
private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) {
liveEdgeTimeUs = mediaPlaylist.hasEndTag ? C.TIME_UNSET : mediaPlaylist.getEndTimeUs();
liveEdgeInPeriodTimeUs =
mediaPlaylist.hasEndTag
? C.TIME_UNSET
: (mediaPlaylist.getEndTimeUs() - playlistTracker.getInitialStartTimeUs());
}
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex,

View file

@ -206,11 +206,6 @@ import java.util.concurrent.atomic.AtomicInteger;
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public void load() throws IOException, InterruptedException {
maybeLoadInitData();
@ -242,7 +237,7 @@ import java.util.concurrent.atomic.AtomicInteger;
initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition);
}
} finally {
Util.closeQuietly(dataSource);
Util.closeQuietly(initDataSource);
}
initLoadCompleted = true;
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.hls;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
@ -57,7 +58,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final boolean allowChunklessPreparation;
private Callback callback;
private @Nullable Callback callback;
private int pendingPrepareCount;
private TrackGroupArray trackGroups;
private HlsSampleStreamWrapper[] sampleStreamWrappers;
@ -96,6 +97,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.release();
}
callback = null;
eventDispatcher.mediaPeriodReleased();
}

View file

@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat
import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
@ -58,6 +59,7 @@ public final class HlsMediaSource extends BaseMediaSource
private HlsExtractorFactory extractorFactory;
private @Nullable ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private @Nullable HlsPlaylistTracker playlistTracker;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private int minLoadableRetryCount;
private boolean allowChunklessPreparation;
@ -136,16 +138,37 @@ public final class HlsMediaSource extends BaseMediaSource
* Sets the parser to parse HLS playlists. The default is an instance of {@link
* HlsPlaylistParser}.
*
* <p>Must not be called after calling {@link #setPlaylistTracker} on the same builder.
*
* @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setPlaylistParser(ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
Assertions.checkState(!isCreateCalled);
Assertions.checkState(playlistTracker == null, "A playlist tracker has already been set.");
this.playlistParser = Assertions.checkNotNull(playlistParser);
return this;
}
/**
* Sets the HLS playlist tracker. The default is an instance of {@link
* DefaultHlsPlaylistTracker}. Playlist trackers must not be shared by {@link HlsMediaSource}
* instances.
*
* <p>Must not be called after calling {@link #setPlaylistParser} on the same builder.
*
* @param playlistTracker A tracker for HLS playlists.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setPlaylistTracker(HlsPlaylistTracker playlistTracker) {
Assertions.checkState(!isCreateCalled);
Assertions.checkState(playlistParser == null, "A playlist parser has already been set.");
this.playlistTracker = Assertions.checkNotNull(playlistTracker);
return this;
}
/**
* Sets the factory to create composite {@link SequenceableLoader}s for when this media source
* loads data from multiple streams (video, audio etc...). The default is an instance of {@link
@ -187,8 +210,12 @@ public final class HlsMediaSource extends BaseMediaSource
@Override
public HlsMediaSource createMediaSource(Uri playlistUri) {
isCreateCalled = true;
if (playlistParser == null) {
playlistParser = new HlsPlaylistParser();
if (playlistTracker == null) {
playlistTracker =
new DefaultHlsPlaylistTracker(
hlsDataSourceFactory,
minLoadableRetryCount,
playlistParser != null ? playlistParser : new HlsPlaylistParser());
}
return new HlsMediaSource(
playlistUri,
@ -196,7 +223,7 @@ public final class HlsMediaSource extends BaseMediaSource
extractorFactory,
compositeSequenceableLoaderFactory,
minLoadableRetryCount,
playlistParser,
playlistTracker,
allowChunklessPreparation,
tag);
}
@ -233,12 +260,10 @@ public final class HlsMediaSource extends BaseMediaSource
private final HlsDataSourceFactory dataSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final int minLoadableRetryCount;
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private final boolean allowChunklessPreparation;
private final HlsPlaylistTracker playlistTracker;
private final @Nullable Object tag;
private HlsPlaylistTracker playlistTracker;
/**
* @param manifestUri The {@link Uri} of the HLS manifest.
* @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests,
@ -276,8 +301,13 @@ public final class HlsMediaSource extends BaseMediaSource
int minLoadableRetryCount,
Handler eventHandler,
MediaSourceEventListener eventListener) {
this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory),
HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener,
this(
manifestUri,
new DefaultHlsDataSourceFactory(dataSourceFactory),
HlsExtractorFactory.DEFAULT,
minLoadableRetryCount,
eventHandler,
eventListener,
new HlsPlaylistParser());
}
@ -309,7 +339,8 @@ public final class HlsMediaSource extends BaseMediaSource
extractorFactory,
new DefaultCompositeSequenceableLoaderFactory(),
minLoadableRetryCount,
playlistParser,
new DefaultHlsPlaylistTracker(
dataSourceFactory, minLoadableRetryCount, new HlsPlaylistParser()),
/* allowChunklessPreparation= */ false,
/* tag= */ null);
if (eventHandler != null && eventListener != null) {
@ -323,7 +354,7 @@ public final class HlsMediaSource extends BaseMediaSource
HlsExtractorFactory extractorFactory,
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
int minLoadableRetryCount,
ParsingLoadable.Parser<HlsPlaylist> playlistParser,
HlsPlaylistTracker playlistTracker,
boolean allowChunklessPreparation,
@Nullable Object tag) {
this.manifestUri = manifestUri;
@ -331,7 +362,7 @@ public final class HlsMediaSource extends BaseMediaSource
this.extractorFactory = extractorFactory;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.minLoadableRetryCount = minLoadableRetryCount;
this.playlistParser = playlistParser;
this.playlistTracker = playlistTracker;
this.allowChunklessPreparation = allowChunklessPreparation;
this.tag = tag;
}
@ -339,9 +370,7 @@ public final class HlsMediaSource extends BaseMediaSource
@Override
public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher,
minLoadableRetryCount, this, playlistParser);
playlistTracker.start();
playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this);
}
@Override
@ -373,7 +402,6 @@ public final class HlsMediaSource extends BaseMediaSource
public void releaseSourceInternal() {
if (playlistTracker != null) {
playlistTracker.release();
playlistTracker = null;
}
}

View file

@ -73,6 +73,7 @@ public final class HlsDownloadHelper extends DownloadHelper {
public TrackGroupArray getTrackGroups(int periodIndex) {
Assertions.checkNotNull(playlist);
if (playlist instanceof HlsMediaPlaylist) {
renditionTypes = new int[0];
return TrackGroupArray.EMPTY;
}
// TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction.

View file

@ -0,0 +1,582 @@
/*
* Copyright (C) 2016 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.source.hls.playlist;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.UriUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
/** Default implementation for {@link HlsPlaylistTracker}. */
public final class DefaultHlsPlaylistTracker
implements HlsPlaylistTracker, Loader.Callback<ParsingLoadable<HlsPlaylist>> {
/**
* Coefficient applied on the target duration of a playlist to determine the amount of time after
* which an unchanging playlist is considered stuck.
*/
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5;
private final HlsDataSourceFactory dataSourceFactory;
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private final int minRetryCount;
private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles;
private final List<PlaylistEventListener> listeners;
private EventDispatcher eventDispatcher;
private Loader initialPlaylistLoader;
private Handler playlistRefreshHandler;
private PrimaryPlaylistListener primaryPlaylistListener;
private HlsMasterPlaylist masterPlaylist;
private HlsUrl primaryHlsUrl;
private HlsMediaPlaylist primaryUrlSnapshot;
private boolean isLive;
private long initialStartTimeUs;
/**
* @param dataSourceFactory A factory for {@link DataSource} instances.
* @param minRetryCount The minimum number of times loads must be retried before {@link
* #maybeThrowPlaylistRefreshError(HlsUrl)} and {@link
* #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors.
* @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists.
*/
public DefaultHlsPlaylistTracker(
HlsDataSourceFactory dataSourceFactory,
int minRetryCount,
ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
this.dataSourceFactory = dataSourceFactory;
this.minRetryCount = minRetryCount;
this.playlistParser = playlistParser;
listeners = new ArrayList<>();
playlistBundles = new IdentityHashMap<>();
initialStartTimeUs = C.TIME_UNSET;
}
// HlsPlaylistTracker implementation.
@Override
public void start(
Uri initialPlaylistUri,
EventDispatcher eventDispatcher,
PrimaryPlaylistListener primaryPlaylistListener) {
this.playlistRefreshHandler = new Handler();
this.eventDispatcher = eventDispatcher;
this.primaryPlaylistListener = primaryPlaylistListener;
ParsingLoadable<HlsPlaylist> masterPlaylistLoadable =
new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
initialPlaylistUri,
C.DATA_TYPE_MANIFEST,
playlistParser);
Assertions.checkState(initialPlaylistLoader == null);
initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist");
long elapsedRealtime =
initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount);
eventDispatcher.loadStarted(
masterPlaylistLoadable.dataSpec, masterPlaylistLoadable.type, elapsedRealtime);
}
@Override
public void release() {
primaryHlsUrl = null;
primaryUrlSnapshot = null;
masterPlaylist = null;
initialStartTimeUs = C.TIME_UNSET;
initialPlaylistLoader.release();
initialPlaylistLoader = null;
for (MediaPlaylistBundle bundle : playlistBundles.values()) {
bundle.release();
}
playlistRefreshHandler.removeCallbacksAndMessages(null);
playlistRefreshHandler = null;
playlistBundles.clear();
}
@Override
public void addListener(PlaylistEventListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(PlaylistEventListener listener) {
listeners.remove(listener);
}
@Override
public HlsMasterPlaylist getMasterPlaylist() {
return masterPlaylist;
}
@Override
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
if (snapshot != null) {
maybeSetPrimaryUrl(url);
}
return snapshot;
}
@Override
public long getInitialStartTimeUs() {
return initialStartTimeUs;
}
@Override
public boolean isSnapshotValid(HlsUrl url) {
return playlistBundles.get(url).isSnapshotValid();
}
@Override
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
if (initialPlaylistLoader != null) {
initialPlaylistLoader.maybeThrowError();
}
if (primaryHlsUrl != null) {
maybeThrowPlaylistRefreshError(primaryHlsUrl);
}
}
@Override
public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException {
playlistBundles.get(url).maybeThrowPlaylistRefreshError();
}
@Override
public void refreshPlaylist(HlsUrl url) {
playlistBundles.get(url).loadPlaylist();
}
@Override
public boolean isLive() {
return isLive;
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(
ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
HlsPlaylist result = loadable.getResult();
HlsMasterPlaylist masterPlaylist;
boolean isMediaPlaylist = result instanceof HlsMediaPlaylist;
if (isMediaPlaylist) {
masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri);
} else /* result instanceof HlsMasterPlaylist */ {
masterPlaylist = (HlsMasterPlaylist) result;
}
this.masterPlaylist = masterPlaylist;
primaryHlsUrl = masterPlaylist.variants.get(0);
ArrayList<HlsUrl> urls = new ArrayList<>();
urls.addAll(masterPlaylist.variants);
urls.addAll(masterPlaylist.audios);
urls.addAll(masterPlaylist.subtitles);
createBundles(urls);
MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl);
if (isMediaPlaylist) {
// We don't need to load the playlist again. We can use the same result.
primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result);
} else {
primaryBundle.loadPlaylist();
}
eventDispatcher.loadCompleted(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
}
@Override
public void onLoadCanceled(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
boolean released) {
eventDispatcher.loadCanceled(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
}
@Override
public @Loader.RetryAction int onLoadError(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error) {
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded(),
error,
isFatal);
return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
}
// Internal methods.
private boolean maybeSelectNewPrimaryUrl() {
List<HlsUrl> variants = masterPlaylist.variants;
int variantsSize = variants.size();
long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < variantsSize; i++) {
MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i));
if (currentTimeMs > bundle.blacklistUntilMs) {
primaryHlsUrl = bundle.playlistUrl;
bundle.loadPlaylist();
return true;
}
}
return false;
}
private void maybeSetPrimaryUrl(HlsUrl url) {
if (url == primaryHlsUrl
|| !masterPlaylist.variants.contains(url)
|| (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) {
// Ignore if the primary url is unchanged, if the url is not a variant url, or if the last
// primary snapshot contains an end tag.
return;
}
primaryHlsUrl = url;
playlistBundles.get(primaryHlsUrl).loadPlaylist();
}
private void createBundles(List<HlsUrl> urls) {
int listSize = urls.size();
for (int i = 0; i < listSize; i++) {
HlsUrl url = urls.get(i);
MediaPlaylistBundle bundle = new MediaPlaylistBundle(url);
playlistBundles.put(url, bundle);
}
}
/**
* Called by the bundles when a snapshot changes.
*
* @param url The url of the playlist.
* @param newSnapshot The new snapshot.
*/
private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) {
if (url == primaryHlsUrl) {
if (primaryUrlSnapshot == null) {
// This is the first primary url snapshot.
isLive = !newSnapshot.hasEndTag;
initialStartTimeUs = newSnapshot.startTimeUs;
}
primaryUrlSnapshot = newSnapshot;
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
}
int listenersSize = listeners.size();
for (int i = 0; i < listenersSize; i++) {
listeners.get(i).onPlaylistChanged();
}
}
private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) {
int listenersSize = listeners.size();
boolean anyBlacklistingFailed = false;
for (int i = 0; i < listenersSize; i++) {
anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist);
}
return anyBlacklistingFailed;
}
private HlsMediaPlaylist getLatestPlaylistSnapshot(
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
if (loadedPlaylist.hasEndTag) {
// If the loaded playlist has an end tag but is not newer than the old playlist then we have
// an inconsistent state. This is typically caused by the server incorrectly resetting the
// media sequence when appending the end tag. We resolve this case as best we can by
// returning the old playlist with the end tag appended.
return oldPlaylist.copyWithEndTag();
} else {
return oldPlaylist;
}
}
long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);
int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist);
return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);
}
private long getLoadedPlaylistStartTimeUs(
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasProgramDateTime) {
return loadedPlaylist.startTimeUs;
}
long primarySnapshotStartTimeUs =
primaryUrlSnapshot != null ? primaryUrlSnapshot.startTimeUs : 0;
if (oldPlaylist == null) {
return primarySnapshotStartTimeUs;
}
int oldPlaylistSize = oldPlaylist.segments.size();
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;
} else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) {
return oldPlaylist.getEndTimeUs();
} else {
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
return primarySnapshotStartTimeUs;
}
}
private int getLoadedPlaylistDiscontinuitySequence(
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasDiscontinuitySequence) {
return loadedPlaylist.discontinuitySequence;
}
// TODO: Improve cross-playlist discontinuity adjustment.
int primaryUrlDiscontinuitySequence =
primaryUrlSnapshot != null ? primaryUrlSnapshot.discontinuitySequence : 0;
if (oldPlaylist == null) {
return primaryUrlDiscontinuitySequence;
}
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.discontinuitySequence
+ firstOldOverlappingSegment.relativeDiscontinuitySequence
- loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;
}
return primaryUrlDiscontinuitySequence;
}
private static Segment getFirstOldOverlappingSegment(
HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) {
int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence);
List<Segment> oldSegments = oldPlaylist.segments;
return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;
}
/** Holds all information related to a specific Media Playlist. */
private final class MediaPlaylistBundle
implements Loader.Callback<ParsingLoadable<HlsPlaylist>>, Runnable {
private final HlsUrl playlistUrl;
private final Loader mediaPlaylistLoader;
private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
private HlsMediaPlaylist playlistSnapshot;
private long lastSnapshotLoadMs;
private long lastSnapshotChangeMs;
private long earliestNextLoadTimeMs;
private long blacklistUntilMs;
private boolean loadPending;
private IOException playlistError;
public MediaPlaylistBundle(HlsUrl playlistUrl) {
this.playlistUrl = playlistUrl;
mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist");
mediaPlaylistLoadable =
new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url),
C.DATA_TYPE_MANIFEST,
playlistParser);
}
public HlsMediaPlaylist getPlaylistSnapshot() {
return playlistSnapshot;
}
public boolean isSnapshotValid() {
if (playlistSnapshot == null) {
return false;
}
long currentTimeMs = SystemClock.elapsedRealtime();
long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs));
return playlistSnapshot.hasEndTag
|| playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
|| playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
|| lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs;
}
public void release() {
mediaPlaylistLoader.release();
}
public void loadPlaylist() {
blacklistUntilMs = 0;
if (loadPending || mediaPlaylistLoader.isLoading()) {
// Load already pending or in progress. Do nothing.
return;
}
long currentTimeMs = SystemClock.elapsedRealtime();
if (currentTimeMs < earliestNextLoadTimeMs) {
loadPending = true;
playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs);
} else {
loadPlaylistImmediately();
}
}
public void maybeThrowPlaylistRefreshError() throws IOException {
mediaPlaylistLoader.maybeThrowError();
if (playlistError != null) {
throw playlistError;
}
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(
ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, long loadDurationMs) {
HlsPlaylist result = loadable.getResult();
if (result instanceof HlsMediaPlaylist) {
processLoadedPlaylist((HlsMediaPlaylist) result);
eventDispatcher.loadCompleted(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
} else {
playlistError = new ParserException("Loaded playlist has unexpected type.");
}
}
@Override
public void onLoadCanceled(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
boolean released) {
eventDispatcher.loadCanceled(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded());
}
@Override
public @Loader.RetryAction int onLoadError(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error) {
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(
loadable.dataSpec,
C.DATA_TYPE_MANIFEST,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded(),
error,
isFatal);
boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error);
boolean shouldRetryIfNotFatal =
notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist;
if (isFatal) {
return Loader.DONT_RETRY_FATAL;
}
if (shouldBlacklist) {
shouldRetryIfNotFatal |= blacklistPlaylist();
}
return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY;
}
// Runnable implementation.
@Override
public void run() {
loadPending = false;
loadPlaylistImmediately();
}
// Internal methods.
private void loadPlaylistImmediately() {
long elapsedRealtime =
mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
eventDispatcher.loadStarted(
mediaPlaylistLoadable.dataSpec, mediaPlaylistLoadable.type, elapsedRealtime);
}
private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
HlsMediaPlaylist oldPlaylist = playlistSnapshot;
long currentTimeMs = SystemClock.elapsedRealtime();
lastSnapshotLoadMs = currentTimeMs;
playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
if (playlistSnapshot != oldPlaylist) {
playlistError = null;
lastSnapshotChangeMs = currentTimeMs;
onPlaylistUpdated(playlistUrl, playlistSnapshot);
} else if (!playlistSnapshot.hasEndTag) {
if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size()
< playlistSnapshot.mediaSequence) {
// The media sequence jumped backwards. The server has probably reset.
playlistError = new PlaylistResetException(playlistUrl.url);
notifyPlaylistError(playlistUrl, false);
} else if (currentTimeMs - lastSnapshotChangeMs
> C.usToMs(playlistSnapshot.targetDurationUs)
* PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) {
// The playlist seems to be stuck. Blacklist it.
playlistError = new PlaylistStuckException(playlistUrl.url);
notifyPlaylistError(playlistUrl, true);
blacklistPlaylist();
}
}
// Do not allow the playlist to load again within the target duration if we obtained a new
// snapshot, or half the target duration otherwise.
earliestNextLoadTimeMs =
currentTimeMs
+ C.usToMs(
playlistSnapshot != oldPlaylist
? playlistSnapshot.targetDurationUs
: (playlistSnapshot.targetDurationUs / 2));
// Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the
// next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
// the primary.
if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) {
loadPlaylist();
}
}
/**
* Blacklists the playlist.
*
* @return Whether the playlist is the primary, despite being blacklisted.
*/
private boolean blacklistPlaylist() {
blacklistUntilMs =
SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl();
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 The Android Open Source Project
* 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.
@ -16,66 +16,28 @@
package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.UriUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
/**
* Tracks playlists linked to a provided playlist url. The provided url might reference an HLS
* master playlist or a media playlist.
* Tracks playlists associated to an HLS stream and provides snapshots.
*
* <p>The playlist tracker is responsible for exposing the seeking window, which is defined by the
* segments that one of the playlists exposes. This playlist is called primary and needs to be
* periodically refreshed in the case of live streams. Note that the primary playlist is one of the
* media playlists while the master playlist is an optional kind of playlist defined by the HLS
* specification (RFC 8216).
*
* <p>Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a
* primary playlist is always available.
*/
public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {
public interface HlsPlaylistTracker {
/**
* Thrown when a playlist is considered to be stuck due to a server side error.
*/
public static final class PlaylistStuckException extends IOException {
/**
* The url of the stuck playlist.
*/
public final String url;
private PlaylistStuckException(String url) {
this.url = url;
}
}
/**
* Thrown when the media sequence of a new snapshot indicates the server has reset.
*/
public static final class PlaylistResetException extends IOException {
/**
* The url of the reset playlist.
*/
public final String url;
private PlaylistResetException(String url) {
this.url = url;
}
}
/**
* Listener for primary playlist changes.
*/
public interface PrimaryPlaylistListener {
/** Listener for primary playlist changes. */
interface PrimaryPlaylistListener {
/**
* Called when the primary playlist changes.
@ -85,10 +47,8 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
}
/**
* Called on playlist loading events.
*/
public interface PlaylistEventListener {
/** Called on playlist loading events. */
interface PlaylistEventListener {
/**
* Called a playlist changes.
@ -105,141 +65,107 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist);
}
/**
* Coefficient applied on the target duration of a playlist to determine the amount of time after
* which an unchanging playlist is considered stuck.
*/
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5;
/** Thrown when a playlist is considered to be stuck due to a server side error. */
final class PlaylistStuckException extends IOException {
private final Uri initialPlaylistUri;
private final HlsDataSourceFactory dataSourceFactory;
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
private final int minRetryCount;
private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles;
private final Handler playlistRefreshHandler;
private final PrimaryPlaylistListener primaryPlaylistListener;
private final List<PlaylistEventListener> listeners;
private final Loader initialPlaylistLoader;
private final EventDispatcher eventDispatcher;
/** The url of the stuck playlist. */
public final String url;
private HlsMasterPlaylist masterPlaylist;
private HlsUrl primaryHlsUrl;
private HlsMediaPlaylist primaryUrlSnapshot;
private boolean isLive;
private long initialStartTimeUs;
/**
* @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media
* playlist or a master playlist.
* @param dataSourceFactory A factory for {@link DataSource} instances.
* @param eventDispatcher A dispatcher to notify of events.
* @param minRetryCount The minimum number of times loads must be retried before
* {@link #maybeThrowPlaylistRefreshError(HlsUrl)} and
* {@link #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors.
* @param primaryPlaylistListener A callback for the primary playlist change events.
*/
public HlsPlaylistTracker(Uri initialPlaylistUri, HlsDataSourceFactory dataSourceFactory,
EventDispatcher eventDispatcher, int minRetryCount,
PrimaryPlaylistListener primaryPlaylistListener,
ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
this.initialPlaylistUri = initialPlaylistUri;
this.dataSourceFactory = dataSourceFactory;
this.eventDispatcher = eventDispatcher;
this.minRetryCount = minRetryCount;
this.primaryPlaylistListener = primaryPlaylistListener;
this.playlistParser = playlistParser;
listeners = new ArrayList<>();
initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist");
playlistBundles = new IdentityHashMap<>();
playlistRefreshHandler = new Handler();
initialStartTimeUs = C.TIME_UNSET;
/**
* Creates an instance.
*
* @param url See {@link #url}.
*/
public PlaylistStuckException(String url) {
this.url = url;
}
}
/** Thrown when the media sequence of a new snapshot indicates the server has reset. */
final class PlaylistResetException extends IOException {
/** The url of the reset playlist. */
public final String url;
/**
* Creates an instance.
*
* @param url See {@link #url}.
*/
public PlaylistResetException(String url) {
this.url = url;
}
}
/**
* Starts the playlist tracker.
*
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link
* #release()} call.
*
* @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master
* playlist.
* @param eventDispatcher A dispatcher to notify of events.
* @param listener A callback for the primary playlist change events.
*/
void start(
Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener);
/** Releases all acquired resources. Must be called once per {@link #start} call. */
void release();
/**
* Registers a listener to receive events from the playlist tracker.
*
* @param listener The listener.
*/
public void addListener(PlaylistEventListener listener) {
listeners.add(listener);
}
void addListener(PlaylistEventListener listener);
/**
* Unregisters a listener.
*
* @param listener The listener to unregister.
*/
public void removeListener(PlaylistEventListener listener) {
listeners.remove(listener);
}
/**
* Starts tracking all the playlists related to the provided Uri.
*/
public void start() {
ParsingLoadable<HlsPlaylist> masterPlaylistLoadable = new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri,
C.DATA_TYPE_MANIFEST, playlistParser);
initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount);
}
void removeListener(PlaylistEventListener listener);
/**
* Returns the master playlist.
*
* <p>If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist}
* with a single variant for said media playlist is returned.
*
* @return The master playlist. Null if the initial playlist has yet to be loaded.
*/
public HlsMasterPlaylist getMasterPlaylist() {
return masterPlaylist;
}
@Nullable
HlsMasterPlaylist getMasterPlaylist();
/**
* Returns the most recent snapshot available of the playlist referenced by the provided
* {@link HlsUrl}.
* Returns the most recent snapshot available of the playlist referenced by the provided {@link
* HlsUrl}.
*
* @param url The {@link HlsUrl} corresponding to the requested media playlist.
* @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May
* be null if no snapshot has been loaded yet.
*/
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
if (snapshot != null) {
maybeSetPrimaryUrl(url);
}
return snapshot;
}
@Nullable
HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url);
/**
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
* media playlist has been loaded.
*/
public long getInitialStartTimeUs() {
return initialStartTimeUs;
}
long getInitialStartTimeUs();
/**
* Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
* valid, meaning all the segments referenced by the playlist are expected to be available. If the
* playlist is not valid then some of the segments may no longer be available.
*
* @param url The {@link HlsUrl}.
* @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
* valid.
*/
public boolean isSnapshotValid(HlsUrl url) {
return playlistBundles.get(url).isSnapshotValid();
}
/**
* Releases the playlist tracker.
*/
public void release() {
initialPlaylistLoader.release();
for (MediaPlaylistBundle bundle : playlistBundles.values()) {
bundle.release();
}
playlistRefreshHandler.removeCallbacksAndMessages(null);
playlistBundles.clear();
}
boolean isSnapshotValid(HlsUrl url);
/**
* If the tracker is having trouble refreshing the master playlist or the primary playlist, this
@ -247,401 +173,31 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
*
* @throws IOException The underlying error.
*/
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
initialPlaylistLoader.maybeThrowError();
if (primaryHlsUrl != null) {
maybeThrowPlaylistRefreshError(primaryHlsUrl);
}
}
void maybeThrowPrimaryPlaylistRefreshError() throws IOException;
/**
* If the playlist is having trouble refreshing the playlist referenced by the given
* {@link HlsUrl}, this method throws the underlying error.
* If the playlist is having trouble refreshing the playlist referenced by the given {@link
* HlsUrl}, this method throws the underlying error.
*
* @param url The {@link HlsUrl}.
* @throws IOException The underyling error.
*/
public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException {
playlistBundles.get(url).maybeThrowPlaylistRefreshError();
}
void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException;
/**
* Triggers a playlist refresh and whitelists it.
* Requests a playlist refresh and whitelists it.
*
* <p>The playlist tracker may choose the delay the playlist refresh. The request is discarded if
* a refresh was already pending.
*
* @param url The {@link HlsUrl} of the playlist to be refreshed.
*/
public void refreshPlaylist(HlsUrl url) {
playlistBundles.get(url).loadPlaylist();
}
void refreshPlaylist(HlsUrl url);
/**
* Returns whether this is live content.
* Returns whether the tracked playlists describe a live stream.
*
* @return True if the content is live. False otherwise.
*/
public boolean isLive() {
return isLive;
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs) {
HlsPlaylist result = loadable.getResult();
HlsMasterPlaylist masterPlaylist;
boolean isMediaPlaylist = result instanceof HlsMediaPlaylist;
if (isMediaPlaylist) {
masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri);
} else /* result instanceof HlsMasterPlaylist */ {
masterPlaylist = (HlsMasterPlaylist) result;
}
this.masterPlaylist = masterPlaylist;
primaryHlsUrl = masterPlaylist.variants.get(0);
ArrayList<HlsUrl> urls = new ArrayList<>();
urls.addAll(masterPlaylist.variants);
urls.addAll(masterPlaylist.audios);
urls.addAll(masterPlaylist.subtitles);
createBundles(urls);
MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl);
if (isMediaPlaylist) {
// We don't need to load the playlist again. We can use the same result.
primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result);
} else {
primaryBundle.loadPlaylist();
}
eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public @Loader.RetryAction int onLoadError(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error) {
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded(), error, isFatal);
return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
}
// Internal methods.
private boolean maybeSelectNewPrimaryUrl() {
List<HlsUrl> variants = masterPlaylist.variants;
int variantsSize = variants.size();
long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < variantsSize; i++) {
MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i));
if (currentTimeMs > bundle.blacklistUntilMs) {
primaryHlsUrl = bundle.playlistUrl;
bundle.loadPlaylist();
return true;
}
}
return false;
}
private void maybeSetPrimaryUrl(HlsUrl url) {
if (url == primaryHlsUrl
|| !masterPlaylist.variants.contains(url)
|| (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) {
// Ignore if the primary url is unchanged, if the url is not a variant url, or if the last
// primary snapshot contains an end tag.
return;
}
primaryHlsUrl = url;
playlistBundles.get(primaryHlsUrl).loadPlaylist();
}
private void createBundles(List<HlsUrl> urls) {
int listSize = urls.size();
for (int i = 0; i < listSize; i++) {
HlsUrl url = urls.get(i);
MediaPlaylistBundle bundle = new MediaPlaylistBundle(url);
playlistBundles.put(url, bundle);
}
}
/**
* Called by the bundles when a snapshot changes.
*
* @param url The url of the playlist.
* @param newSnapshot The new snapshot.
*/
private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) {
if (url == primaryHlsUrl) {
if (primaryUrlSnapshot == null) {
// This is the first primary url snapshot.
isLive = !newSnapshot.hasEndTag;
initialStartTimeUs = newSnapshot.startTimeUs;
}
primaryUrlSnapshot = newSnapshot;
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
}
int listenersSize = listeners.size();
for (int i = 0; i < listenersSize; i++) {
listeners.get(i).onPlaylistChanged();
}
}
private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) {
int listenersSize = listeners.size();
boolean anyBlacklistingFailed = false;
for (int i = 0; i < listenersSize; i++) {
anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist);
}
return anyBlacklistingFailed;
}
private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
if (loadedPlaylist.hasEndTag) {
// If the loaded playlist has an end tag but is not newer than the old playlist then we have
// an inconsistent state. This is typically caused by the server incorrectly resetting the
// media sequence when appending the end tag. We resolve this case as best we can by
// returning the old playlist with the end tag appended.
return oldPlaylist.copyWithEndTag();
} else {
return oldPlaylist;
}
}
long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);
int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist);
return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);
}
private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasProgramDateTime) {
return loadedPlaylist.startTimeUs;
}
long primarySnapshotStartTimeUs = primaryUrlSnapshot != null
? primaryUrlSnapshot.startTimeUs : 0;
if (oldPlaylist == null) {
return primarySnapshotStartTimeUs;
}
int oldPlaylistSize = oldPlaylist.segments.size();
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;
} else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) {
return oldPlaylist.getEndTimeUs();
} else {
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
return primarySnapshotStartTimeUs;
}
}
private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasDiscontinuitySequence) {
return loadedPlaylist.discontinuitySequence;
}
// TODO: Improve cross-playlist discontinuity adjustment.
int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null
? primaryUrlSnapshot.discontinuitySequence : 0;
if (oldPlaylist == null) {
return primaryUrlDiscontinuitySequence;
}
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.discontinuitySequence
+ firstOldOverlappingSegment.relativeDiscontinuitySequence
- loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;
}
return primaryUrlDiscontinuitySequence;
}
private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence);
List<Segment> oldSegments = oldPlaylist.segments;
return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;
}
/**
* Holds all information related to a specific Media Playlist.
*/
private final class MediaPlaylistBundle implements Loader.Callback<ParsingLoadable<HlsPlaylist>>,
Runnable {
private final HlsUrl playlistUrl;
private final Loader mediaPlaylistLoader;
private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
private HlsMediaPlaylist playlistSnapshot;
private long lastSnapshotLoadMs;
private long lastSnapshotChangeMs;
private long earliestNextLoadTimeMs;
private long blacklistUntilMs;
private boolean loadPending;
private IOException playlistError;
public MediaPlaylistBundle(HlsUrl playlistUrl) {
this.playlistUrl = playlistUrl;
mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist");
mediaPlaylistLoadable = new ParsingLoadable<>(
dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST),
UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST,
playlistParser);
}
public HlsMediaPlaylist getPlaylistSnapshot() {
return playlistSnapshot;
}
public boolean isSnapshotValid() {
if (playlistSnapshot == null) {
return false;
}
long currentTimeMs = SystemClock.elapsedRealtime();
long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs));
return playlistSnapshot.hasEndTag
|| playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
|| playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
|| lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs;
}
public void release() {
mediaPlaylistLoader.release();
}
public void loadPlaylist() {
blacklistUntilMs = 0;
if (loadPending || mediaPlaylistLoader.isLoading()) {
// Load already pending or in progress. Do nothing.
return;
}
long currentTimeMs = SystemClock.elapsedRealtime();
if (currentTimeMs < earliestNextLoadTimeMs) {
loadPending = true;
playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs);
} else {
loadPlaylistImmediately();
}
}
public void maybeThrowPlaylistRefreshError() throws IOException {
mediaPlaylistLoader.maybeThrowError();
if (playlistError != null) {
throw playlistError;
}
}
// Loader.Callback implementation.
@Override
public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs) {
HlsPlaylist result = loadable.getResult();
if (result instanceof HlsMediaPlaylist) {
processLoadedPlaylist((HlsMediaPlaylist) result);
eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
} else {
playlistError = new ParserException("Loaded playlist has unexpected type.");
}
}
@Override
public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public @Loader.RetryAction int onLoadError(
ParsingLoadable<HlsPlaylist> loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error) {
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded(), error, isFatal);
boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error);
boolean shouldRetryIfNotFatal =
notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist;
if (isFatal) {
return Loader.DONT_RETRY_FATAL;
}
if (shouldBlacklist) {
shouldRetryIfNotFatal |= blacklistPlaylist();
}
return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY;
}
// Runnable implementation.
@Override
public void run() {
loadPending = false;
loadPlaylistImmediately();
}
// Internal methods.
private void loadPlaylistImmediately() {
mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
}
private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
HlsMediaPlaylist oldPlaylist = playlistSnapshot;
long currentTimeMs = SystemClock.elapsedRealtime();
lastSnapshotLoadMs = currentTimeMs;
playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
if (playlistSnapshot != oldPlaylist) {
playlistError = null;
lastSnapshotChangeMs = currentTimeMs;
onPlaylistUpdated(playlistUrl, playlistSnapshot);
} else if (!playlistSnapshot.hasEndTag) {
if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size()
< playlistSnapshot.mediaSequence) {
// The media sequence jumped backwards. The server has probably reset.
playlistError = new PlaylistResetException(playlistUrl.url);
notifyPlaylistError(playlistUrl, false);
} else if (currentTimeMs - lastSnapshotChangeMs
> C.usToMs(playlistSnapshot.targetDurationUs)
* PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) {
// The playlist seems to be stuck. Blacklist it.
playlistError = new PlaylistStuckException(playlistUrl.url);
notifyPlaylistError(playlistUrl, true);
blacklistPlaylist();
}
}
// Do not allow the playlist to load again within the target duration if we obtained a new
// snapshot, or half the target duration otherwise.
earliestNextLoadTimeMs = currentTimeMs + C.usToMs(playlistSnapshot != oldPlaylist
? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2));
// Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the
// next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
// the primary.
if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) {
loadPlaylist();
}
}
/**
* Blacklists the playlist.
*
* @return Whether the playlist is the primary, despite being blacklisted.
*/
private boolean blacklistPlaylist() {
blacklistUntilMs = SystemClock.elapsedRealtime()
+ ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl();
}
}
boolean isLive();
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.smoothstreaming;
import android.support.annotation.Nullable;
import android.util.Base64;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SeekParameters;
@ -52,7 +53,7 @@ import java.util.ArrayList;
private final TrackEncryptionBox[] trackEncryptionBoxes;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private Callback callback;
private @Nullable Callback callback;
private SsManifest manifest;
private ChunkSampleStream<SsChunkSource>[] sampleStreams;
private SequenceableLoader compositeSequenceableLoader;
@ -98,6 +99,7 @@ import java.util.ArrayList;
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
sampleStream.release();
}
callback = null;
eventDispatcher.mediaPeriodReleased();
}

View file

@ -251,7 +251,7 @@ public final class SubtitleView extends View implements TextOutput {
// Calculate the bounds after padding is taken into account.
int left = getLeft() + getPaddingLeft();
int top = rawTop + getPaddingTop();
int right = getRight() + getPaddingRight();
int right = getRight() - getPaddingRight();
int bottom = rawBottom - getPaddingBottom();
if (bottom <= top || right <= left) {
// No space to draw subtitles.

View file

@ -203,7 +203,9 @@ public class TrackSelectionView extends LinearLayout {
removeViewAt(i);
}
if (trackSelector == null) {
MappingTrackSelector.MappedTrackInfo trackInfo =
trackSelector == null ? null : trackSelector.getCurrentMappedTrackInfo();
if (trackSelector == null || trackInfo == null) {
// The view is not initialized.
disableView.setEnabled(false);
defaultView.setEnabled(false);
@ -212,7 +214,6 @@ public class TrackSelectionView extends LinearLayout {
disableView.setEnabled(true);
defaultView.setEnabled(true);
MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo();
trackGroups = trackInfo.getTrackGroups(rendererIndex);
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();