mirror of
https://github.com/samsonjs/media.git
synced 2026-04-01 10:35:48 +00:00
commit
f7ed789fc3
93 changed files with 1632 additions and 1018 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1543,6 +1543,7 @@ import java.util.Collections;
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ParameterNotNullable")
|
||||
private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder)
|
||||
throws ExoPlaybackException {
|
||||
MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod();
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -108,4 +108,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);
|
||||
}
|
||||
|
||||
private FixedSampleSizeRechunker() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -130,6 +130,6 @@ import java.util.List;
|
|||
} else {
|
||||
length = 10000 << length;
|
||||
}
|
||||
return frames * length;
|
||||
return (long) frames * length;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue