mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
commit
f7ed789fc3
93 changed files with 1632 additions and 1018 deletions
|
|
@ -1,5 +1,31 @@
|
||||||
# Release notes #
|
# 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 ###
|
### 2.8.1 ###
|
||||||
|
|
||||||
* HLS:
|
* HLS:
|
||||||
|
|
@ -59,7 +85,7 @@
|
||||||
periods are created, released and being read from.
|
periods are created, released and being read from.
|
||||||
* Support live stream clipping with `ClippingMediaSource`.
|
* Support live stream clipping with `ClippingMediaSource`.
|
||||||
* Allow setting tags for all media sources in their factories. The tag of the
|
* 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:
|
* UI components:
|
||||||
* Add support for displaying error messages and a buffering spinner in
|
* Add support for displaying error messages and a buffering spinner in
|
||||||
`PlayerView`.
|
`PlayerView`.
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.8.1'
|
releaseVersion = '2.8.2'
|
||||||
releaseVersionCode = 2801
|
releaseVersionCode = 2802
|
||||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||||
// components provided by the library may be of use on older devices.
|
// components provided by the library may be of use on older devices.
|
||||||
// However, please note that the core media playback functionality provided
|
// However, please note that the core media playback functionality provided
|
||||||
|
|
@ -25,7 +25,7 @@ project.ext {
|
||||||
buildToolsVersion = '27.0.3'
|
buildToolsVersion = '27.0.3'
|
||||||
testSupportLibraryVersion = '0.5'
|
testSupportLibraryVersion = '0.5'
|
||||||
supportLibraryVersion = '27.0.0'
|
supportLibraryVersion = '27.0.0'
|
||||||
playServicesLibraryVersion = '12.0.0'
|
playServicesLibraryVersion = '15.0.1'
|
||||||
dexmakerVersion = '1.2'
|
dexmakerVersion = '1.2'
|
||||||
mockitoVersion = '1.9.5'
|
mockitoVersion = '1.9.5'
|
||||||
junitVersion = '4.12'
|
junitVersion = '4.12'
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ public class PlayerActivity extends Activity
|
||||||
|
|
||||||
private DataSource.Factory mediaDataSourceFactory;
|
private DataSource.Factory mediaDataSourceFactory;
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
|
private FrameworkMediaDrm mediaDrm;
|
||||||
private MediaSource mediaSource;
|
private MediaSource mediaSource;
|
||||||
private DefaultTrackSelector trackSelector;
|
private DefaultTrackSelector trackSelector;
|
||||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||||
|
|
@ -487,8 +488,9 @@ public class PlayerActivity extends Activity
|
||||||
keyRequestPropertiesArray[i + 1]);
|
keyRequestPropertiesArray[i + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new DefaultDrmSessionManager<>(
|
releaseMediaDrm();
|
||||||
uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession);
|
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
|
||||||
|
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releasePlayer() {
|
private void releasePlayer() {
|
||||||
|
|
@ -502,6 +504,23 @@ public class PlayerActivity extends Activity
|
||||||
mediaSource = null;
|
mediaSource = null;
|
||||||
trackSelector = 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() {
|
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
|
// User controls
|
||||||
|
|
||||||
private void updateButtonVisibilities() {
|
private void updateButtonVisibilities() {
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,15 @@ public class SampleChooserActivity extends Activity
|
||||||
SampleListLoader loaderTask = new SampleListLoader();
|
SampleListLoader loaderTask = new SampleListLoader();
|
||||||
loaderTask.execute(uris);
|
loaderTask.execute(uris);
|
||||||
|
|
||||||
// Ping the download service in case it's not running (but should be).
|
// Start the download service if it should be running but it's not currently.
|
||||||
startService(
|
// Starting the service in the foreground causes notification flicker if there is no scheduled
|
||||||
new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT));
|
// 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
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,6 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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
|
api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
|
|
@ -44,6 +34,15 @@ dependencies {
|
||||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
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 {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,16 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// This dependency is necessary to force the supportLibraryVersion of
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.7'
|
||||||
// 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'
|
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
|
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 {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -447,9 +447,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||||
} else if (contentType == C.TYPE_HLS) {
|
} else if (contentType == C.TYPE_HLS) {
|
||||||
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
|
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
|
||||||
} else if (contentType == C.TYPE_OTHER) {
|
} else if (contentType == C.TYPE_OTHER) {
|
||||||
supportedMimeTypes.addAll(Arrays.asList(
|
supportedMimeTypes.addAll(
|
||||||
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
|
Arrays.asList(
|
||||||
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
|
MimeTypes.VIDEO_MP4,
|
||||||
|
MimeTypes.VIDEO_WEBM,
|
||||||
|
MimeTypes.VIDEO_H263,
|
||||||
|
MimeTypes.AUDIO_MP4,
|
||||||
|
MimeTypes.AUDIO_MPEG));
|
||||||
} else if (contentType == C.TYPE_SS) {
|
} else if (contentType == C.TYPE_SS) {
|
||||||
// IMA does not support Smooth Streaming ad media.
|
// IMA does not support Smooth Streaming ad media.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -600,8 +600,9 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (description.getTitle() != null) {
|
if (description.getTitle() != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
|
String title = String.valueOf(description.getTitle());
|
||||||
String.valueOf(description.getTitle()));
|
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
|
||||||
|
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
|
||||||
}
|
}
|
||||||
if (description.getSubtitle() != null) {
|
if (description.getSubtitle() != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
|
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
|
||||||
|
|
|
||||||
|
|
@ -89,12 +89,12 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
||||||
* model">
|
* model">
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>It is strongly recommended that ExoPlayer instances are created and accessed from a single
|
* <li>ExoPlayer instances must be accessed from a single application thread. This must be the
|
||||||
* application thread. The application's main thread is ideal. Accessing an instance from
|
* thread the player is created on if that thread has a {@link Looper}, or the application's
|
||||||
* multiple threads is discouraged as it may cause synchronization problems.
|
* main thread otherwise.
|
||||||
* <li>Registered listeners are called on the thread that created the ExoPlayer instance, unless
|
* <li>Registered listeners are called on the thread the player is created on if that thread has a
|
||||||
* the thread that created the ExoPlayer instance does not have a {@link Looper}. In that
|
* {@link Looper}, or the application's main thread otherwise. Note that this means registered
|
||||||
* case, registered listeners will be called on the application's main thread.
|
* 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
|
* <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
|
* Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
|
||||||
* thread.
|
* 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.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -53,6 +55,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
private final CopyOnWriteArraySet<Player.EventListener> listeners;
|
private final CopyOnWriteArraySet<Player.EventListener> listeners;
|
||||||
private final Timeline.Window window;
|
private final Timeline.Window window;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
|
private final ArrayDeque<PlaybackInfoUpdate> pendingPlaybackInfoUpdates;
|
||||||
|
|
||||||
private boolean playWhenReady;
|
private boolean playWhenReady;
|
||||||
private @RepeatMode int repeatMode;
|
private @RepeatMode int repeatMode;
|
||||||
|
|
@ -112,6 +115,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
/* startPositionUs= */ 0,
|
/* startPositionUs= */ 0,
|
||||||
TrackGroupArray.EMPTY,
|
TrackGroupArray.EMPTY,
|
||||||
emptyTrackSelectorResult);
|
emptyTrackSelectorResult);
|
||||||
|
pendingPlaybackInfoUpdates = new ArrayDeque<>();
|
||||||
internalPlayer =
|
internalPlayer =
|
||||||
new ExoPlayerImplInternal(
|
new ExoPlayerImplInternal(
|
||||||
renderers,
|
renderers,
|
||||||
|
|
@ -185,7 +189,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
/* positionDiscontinuity= */ false,
|
/* positionDiscontinuity= */ false,
|
||||||
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
||||||
TIMELINE_CHANGE_REASON_RESET,
|
TIMELINE_CHANGE_REASON_RESET,
|
||||||
/* seekProcessed= */ false);
|
/* seekProcessed= */ false,
|
||||||
|
/* playWhenReadyChanged= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -193,10 +198,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
if (this.playWhenReady != playWhenReady) {
|
if (this.playWhenReady != playWhenReady) {
|
||||||
this.playWhenReady = playWhenReady;
|
this.playWhenReady = playWhenReady;
|
||||||
internalPlayer.setPlayWhenReady(playWhenReady);
|
internalPlayer.setPlayWhenReady(playWhenReady);
|
||||||
PlaybackInfo playbackInfo = this.playbackInfo;
|
updatePlaybackInfo(
|
||||||
for (Player.EventListener listener : listeners) {
|
playbackInfo,
|
||||||
listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
|
/* 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,
|
/* positionDiscontinuity= */ false,
|
||||||
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
||||||
TIMELINE_CHANGE_REASON_RESET,
|
TIMELINE_CHANGE_REASON_RESET,
|
||||||
/* seekProcessed= */ false);
|
/* seekProcessed= */ false,
|
||||||
|
/* playWhenReadyChanged= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -615,7 +624,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
positionDiscontinuity,
|
positionDiscontinuity,
|
||||||
positionDiscontinuityReason,
|
positionDiscontinuityReason,
|
||||||
timelineChangeReason,
|
timelineChangeReason,
|
||||||
seekProcessed);
|
seekProcessed,
|
||||||
|
/* playWhenReadyChanged= */ false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -643,51 +653,33 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePlaybackInfo(
|
private void updatePlaybackInfo(
|
||||||
PlaybackInfo newPlaybackInfo,
|
PlaybackInfo playbackInfo,
|
||||||
boolean positionDiscontinuity,
|
boolean positionDiscontinuity,
|
||||||
@Player.DiscontinuityReason int positionDiscontinuityReason,
|
@Player.DiscontinuityReason int positionDiscontinuityReason,
|
||||||
@Player.TimelineChangeReason int timelineChangeReason,
|
@Player.TimelineChangeReason int timelineChangeReason,
|
||||||
boolean seekProcessed) {
|
boolean seekProcessed,
|
||||||
boolean timelineOrManifestChanged =
|
boolean playWhenReadyChanged) {
|
||||||
playbackInfo.timeline != newPlaybackInfo.timeline
|
boolean isRunningRecursiveListenerNotification = !pendingPlaybackInfoUpdates.isEmpty();
|
||||||
|| playbackInfo.manifest != newPlaybackInfo.manifest;
|
pendingPlaybackInfoUpdates.addLast(
|
||||||
boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState;
|
new PlaybackInfoUpdate(
|
||||||
boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading;
|
playbackInfo,
|
||||||
boolean trackSelectorResultChanged =
|
/* previousPlaybackInfo= */ this.playbackInfo,
|
||||||
playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
|
listeners,
|
||||||
playbackInfo = newPlaybackInfo;
|
trackSelector,
|
||||||
if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
|
positionDiscontinuity,
|
||||||
for (Player.EventListener listener : listeners) {
|
positionDiscontinuityReason,
|
||||||
listener.onTimelineChanged(
|
timelineChangeReason,
|
||||||
playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason);
|
seekProcessed,
|
||||||
}
|
playWhenReady,
|
||||||
|
playWhenReadyChanged));
|
||||||
|
// Assign playback info immediately such that all getters return the right values.
|
||||||
|
this.playbackInfo = playbackInfo;
|
||||||
|
if (isRunningRecursiveListenerNotification) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (positionDiscontinuity) {
|
while (!pendingPlaybackInfoUpdates.isEmpty()) {
|
||||||
for (Player.EventListener listener : listeners) {
|
pendingPlaybackInfoUpdates.peekFirst().notifyListeners();
|
||||||
listener.onPositionDiscontinuity(positionDiscontinuityReason);
|
pendingPlaybackInfoUpdates.removeFirst();
|
||||||
}
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -703,4 +695,85 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
private boolean shouldMaskPosition() {
|
private boolean shouldMaskPosition() {
|
||||||
return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0;
|
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)
|
private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod();
|
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". */
|
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||||
public static final String VERSION = "2.8.1";
|
public static final String VERSION = "2.8.2";
|
||||||
|
|
||||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.1";
|
public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library expressed as an integer, for example 1002003.
|
* The version of the library expressed as an integer, for example 1002003.
|
||||||
|
|
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
|
||||||
* integer version 123045006 (123-045-006).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final int VERSION_INT = 2008001;
|
public static final int VERSION_INT = 2008002;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
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
|
* 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.
|
* 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.
|
* @param clock A {@link Clock} used to generate timestamps.
|
||||||
* @return An analytics collector.
|
* @return An analytics collector.
|
||||||
*/
|
*/
|
||||||
public AnalyticsCollector createAnalyticsCollector(Player player, Clock clock) {
|
public AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Clock clock) {
|
||||||
return new AnalyticsCollector(player, clock);
|
return new AnalyticsCollector(player, clock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final CopyOnWriteArraySet<AnalyticsListener> listeners;
|
private final CopyOnWriteArraySet<AnalyticsListener> listeners;
|
||||||
private final Player player;
|
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final Window window;
|
private final Window window;
|
||||||
private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
|
private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
|
||||||
|
|
||||||
|
private @MonotonicNonNull Player player;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an analytics collector for the specified 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.
|
* @param clock A {@link Clock} used to generate timestamps.
|
||||||
*/
|
*/
|
||||||
protected AnalyticsCollector(Player player, Clock clock) {
|
protected AnalyticsCollector(@Nullable Player player, Clock clock) {
|
||||||
this.player = Assertions.checkNotNull(player);
|
this.player = player;
|
||||||
this.clock = Assertions.checkNotNull(clock);
|
this.clock = Assertions.checkNotNull(clock);
|
||||||
listeners = new CopyOnWriteArraySet<>();
|
listeners = new CopyOnWriteArraySet<>();
|
||||||
mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
|
mediaPeriodQueueTracker = new MediaPeriodQueueTracker();
|
||||||
|
|
@ -113,6 +119,17 @@ public class AnalyticsCollector
|
||||||
listeners.remove(listener);
|
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.
|
// External events.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -541,6 +558,7 @@ public class AnalyticsCollector
|
||||||
|
|
||||||
/** Returns a new {@link EventTime} for the specified window index and media period id. */
|
/** Returns a new {@link EventTime} for the specified window index and media period id. */
|
||||||
protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
|
protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
|
||||||
|
Assertions.checkNotNull(player);
|
||||||
long realtimeMs = clock.elapsedRealtime();
|
long realtimeMs = clock.elapsedRealtime();
|
||||||
Timeline timeline = player.getCurrentTimeline();
|
Timeline timeline = player.getCurrentTimeline();
|
||||||
long eventPositionMs;
|
long eventPositionMs;
|
||||||
|
|
@ -579,7 +597,7 @@ public class AnalyticsCollector
|
||||||
|
|
||||||
private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) {
|
private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) {
|
||||||
if (mediaPeriod == null) {
|
if (mediaPeriod == null) {
|
||||||
int windowIndex = player.getCurrentWindowIndex();
|
int windowIndex = Assertions.checkNotNull(player).getCurrentWindowIndex();
|
||||||
MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);
|
MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex);
|
||||||
return generateEventTime(windowIndex, mediaPeriodId);
|
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.C;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
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.
|
* 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 Thread decodeThread;
|
||||||
|
|
||||||
private final Object lock;
|
private final Object lock;
|
||||||
private final LinkedList<I> queuedInputBuffers;
|
private final ArrayDeque<I> queuedInputBuffers;
|
||||||
private final LinkedList<O> queuedOutputBuffers;
|
private final ArrayDeque<O> queuedOutputBuffers;
|
||||||
private final I[] availableInputBuffers;
|
private final I[] availableInputBuffers;
|
||||||
private final O[] availableOutputBuffers;
|
private final O[] availableOutputBuffers;
|
||||||
|
|
||||||
|
|
@ -48,8 +48,8 @@ public abstract class SimpleDecoder<I extends DecoderInputBuffer, O extends Outp
|
||||||
*/
|
*/
|
||||||
protected SimpleDecoder(I[] inputBuffers, O[] outputBuffers) {
|
protected SimpleDecoder(I[] inputBuffers, O[] outputBuffers) {
|
||||||
lock = new Object();
|
lock = new Object();
|
||||||
queuedInputBuffers = new LinkedList<>();
|
queuedInputBuffers = new ArrayDeque<>();
|
||||||
queuedOutputBuffers = new LinkedList<>();
|
queuedOutputBuffers = new ArrayDeque<>();
|
||||||
availableInputBuffers = inputBuffers;
|
availableInputBuffers = inputBuffers;
|
||||||
availableInputBufferCount = inputBuffers.length;
|
availableInputBufferCount = inputBuffers.length;
|
||||||
for (int i = 0; i < availableInputBufferCount; i++) {
|
for (int i = 0; i < availableInputBufferCount; i++) {
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
|
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);
|
return executePost(dataSourceFactory, url, new byte[0], null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.Stack;
|
import java.util.ArrayDeque;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation of {@link EbmlReader}.
|
* 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_FLOAT32_ELEMENT_SIZE_BYTES = 4;
|
||||||
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
|
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
|
||||||
|
|
||||||
private final byte[] scratch = new byte[8];
|
private final byte[] scratch;
|
||||||
private final Stack<MasterElement> masterElementsStack = new Stack<>();
|
private final ArrayDeque<MasterElement> masterElementsStack;
|
||||||
private final VarintReader varintReader = new VarintReader();
|
private final VarintReader varintReader;
|
||||||
|
|
||||||
private EbmlReaderOutput output;
|
private EbmlReaderOutput output;
|
||||||
private @ElementState int elementState;
|
private @ElementState int elementState;
|
||||||
private int elementId;
|
private int elementId;
|
||||||
private long elementContentSize;
|
private long elementContentSize;
|
||||||
|
|
||||||
|
public DefaultEbmlReader() {
|
||||||
|
scratch = new byte[8];
|
||||||
|
masterElementsStack = new ArrayDeque<>();
|
||||||
|
varintReader = new VarintReader();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(EbmlReaderOutput eventHandler) {
|
public void init(EbmlReaderOutput eventHandler) {
|
||||||
this.output = eventHandler;
|
this.output = eventHandler;
|
||||||
|
|
@ -100,7 +106,7 @@ import java.util.Stack;
|
||||||
case EbmlReaderOutput.TYPE_MASTER:
|
case EbmlReaderOutput.TYPE_MASTER:
|
||||||
long elementContentPosition = input.getPosition();
|
long elementContentPosition = input.getPosition();
|
||||||
long elementEndPosition = elementContentPosition + elementContentSize;
|
long elementEndPosition = elementContentPosition + elementContentSize;
|
||||||
masterElementsStack.add(new MasterElement(elementId, elementEndPosition));
|
masterElementsStack.push(new MasterElement(elementId, elementEndPosition));
|
||||||
output.startMasterElement(elementId, elementContentPosition, elementContentSize);
|
output.startMasterElement(elementId, elementContentPosition, elementContentSize);
|
||||||
elementState = ELEMENT_STATE_READ_ID;
|
elementState = ELEMENT_STATE_READ_ID;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,9 @@ import java.io.IOException;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (size != 0) {
|
if (size != 0) {
|
||||||
input.advancePeekPosition((int) size);
|
int sizeInt = (int) size;
|
||||||
peekLength += size;
|
input.advancePeekPosition(sizeInt);
|
||||||
|
peekLength += sizeInt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return peekLength == headerStart + headerSize;
|
return peekLength == headerStart + headerSize;
|
||||||
|
|
|
||||||
|
|
@ -108,4 +108,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
return new Results(offsets, sizes, maximumSize, timestamps, flags, duration);
|
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.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Stack;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -141,7 +140,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
// Parser state.
|
// Parser state.
|
||||||
private final ParsableByteArray atomHeader;
|
private final ParsableByteArray atomHeader;
|
||||||
private final byte[] extendedTypeScratch;
|
private final byte[] extendedTypeScratch;
|
||||||
private final Stack<ContainerAtom> containerAtoms;
|
private final ArrayDeque<ContainerAtom> containerAtoms;
|
||||||
private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos;
|
private final ArrayDeque<MetadataSampleInfo> pendingMetadataSampleInfos;
|
||||||
private final @Nullable TrackOutput additionalEmsgTrackOutput;
|
private final @Nullable TrackOutput additionalEmsgTrackOutput;
|
||||||
|
|
||||||
|
|
@ -257,7 +256,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
nalPrefix = new ParsableByteArray(5);
|
nalPrefix = new ParsableByteArray(5);
|
||||||
nalBuffer = new ParsableByteArray();
|
nalBuffer = new ParsableByteArray();
|
||||||
extendedTypeScratch = new byte[16];
|
extendedTypeScratch = new byte[16];
|
||||||
containerAtoms = new Stack<>();
|
containerAtoms = new ArrayDeque<>();
|
||||||
pendingMetadataSampleInfos = new ArrayDeque<>();
|
pendingMetadataSampleInfos = new ArrayDeque<>();
|
||||||
trackBundles = new SparseArray<>();
|
trackBundles = new SparseArray<>();
|
||||||
durationUs = C.TIME_UNSET;
|
durationUs = C.TIME_UNSET;
|
||||||
|
|
@ -390,7 +389,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||||
|
|
||||||
if (shouldParseContainerAtom(atomType)) {
|
if (shouldParseContainerAtom(atomType)) {
|
||||||
long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
|
long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
|
||||||
containerAtoms.add(new ContainerAtom(atomType, endPosition));
|
containerAtoms.push(new ContainerAtom(atomType, endPosition));
|
||||||
if (atomSize == atomHeaderBytesRead) {
|
if (atomSize == atomHeaderBytesRead) {
|
||||||
processAtomEnded(endPosition);
|
processAtomEnded(endPosition);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,9 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Stack;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts data from the MP4 container format.
|
* 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 nalLength;
|
||||||
|
|
||||||
private final ParsableByteArray atomHeader;
|
private final ParsableByteArray atomHeader;
|
||||||
private final Stack<ContainerAtom> containerAtoms;
|
private final ArrayDeque<ContainerAtom> containerAtoms;
|
||||||
|
|
||||||
@State private int parserState;
|
@State private int parserState;
|
||||||
private int atomType;
|
private int atomType;
|
||||||
|
|
@ -137,7 +137,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
||||||
public Mp4Extractor(@Flags int flags) {
|
public Mp4Extractor(@Flags int flags) {
|
||||||
this.flags = flags;
|
this.flags = flags;
|
||||||
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
|
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
|
||||||
containerAtoms = new Stack<>();
|
containerAtoms = new ArrayDeque<>();
|
||||||
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
|
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
|
||||||
nalLength = new ParsableByteArray(4);
|
nalLength = new ParsableByteArray(4);
|
||||||
sampleTrackIndex = C.INDEX_UNSET;
|
sampleTrackIndex = C.INDEX_UNSET;
|
||||||
|
|
@ -303,7 +303,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
||||||
|
|
||||||
if (shouldParseContainerAtom(atomType)) {
|
if (shouldParseContainerAtom(atomType)) {
|
||||||
long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;
|
long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;
|
||||||
containerAtoms.add(new ContainerAtom(atomType, endPosition));
|
containerAtoms.push(new ContainerAtom(atomType, endPosition));
|
||||||
if (atomSize == atomHeaderBytesRead) {
|
if (atomSize == atomHeaderBytesRead) {
|
||||||
processAtomEnded(endPosition);
|
processAtomEnded(endPosition);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ public final class PsshAtomUtil {
|
||||||
* @param data The scheme specific data.
|
* @param data The scheme specific data.
|
||||||
* @return The PSSH atom.
|
* @return The PSSH atom.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("ParameterNotNullable")
|
||||||
public static byte[] buildPsshAtom(
|
public static byte[] buildPsshAtom(
|
||||||
UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) {
|
UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) {
|
||||||
boolean buildV1Atom = keyIds != null;
|
boolean buildV1Atom = keyIds != null;
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,6 @@ import java.util.List;
|
||||||
} else {
|
} else {
|
||||||
length = 10000 << length;
|
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++) {
|
for (int i = 0; i < lengthMap.length; i++) {
|
||||||
if (isSparse) {
|
if (isSparse) {
|
||||||
if (bitArray.readBit()) {
|
if (bitArray.readBit()) {
|
||||||
lengthMap[i] = bitArray.readBits(5) + 1;
|
lengthMap[i] = (long) (bitArray.readBits(5) + 1);
|
||||||
} else { // entry unused
|
} else { // entry unused
|
||||||
lengthMap[i] = 0;
|
lengthMap[i] = 0;
|
||||||
}
|
}
|
||||||
} else { // not sparse
|
} else { // not sparse
|
||||||
lengthMap[i] = bitArray.readBits(5) + 1;
|
lengthMap[i] = (long) (bitArray.readBits(5) + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -392,7 +392,7 @@ import java.util.Arrays;
|
||||||
lookupValuesCount = 0;
|
lookupValuesCount = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lookupValuesCount = entries * dimensions;
|
lookupValuesCount = (long) entries * dimensions;
|
||||||
}
|
}
|
||||||
// discard (no decoding required yet)
|
// discard (no decoding required yet)
|
||||||
bitArray.skipBits((int) (lookupValuesCount * valueBits));
|
bitArray.skipBits((int) (lookupValuesCount * valueBits));
|
||||||
|
|
@ -407,6 +407,10 @@ import java.util.Arrays;
|
||||||
return (long) Math.floor(Math.pow(entries, 1.d / dimension));
|
return (long) Math.floor(Math.pow(entries, 1.d / dimension));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private VorbisUtil() {
|
||||||
|
// Prevent instantiation.
|
||||||
|
}
|
||||||
|
|
||||||
public static final class CodeBook {
|
public static final class CodeBook {
|
||||||
|
|
||||||
public final int dimensions;
|
public final int dimensions;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
|
/** 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";
|
private static final String TAG = "WavHeaderReader";
|
||||||
|
|
||||||
|
|
@ -158,6 +158,10 @@ import java.io.IOException;
|
||||||
wavHeader.setDataBounds(input.getPosition(), chunkHeader.size);
|
wavHeader.setDataBounds(input.getPosition(), chunkHeader.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WavHeaderReader() {
|
||||||
|
// Prevent instantiation.
|
||||||
|
}
|
||||||
|
|
||||||
/** Container for a WAV chunk header. */
|
/** Container for a WAV chunk header. */
|
||||||
private static final class ChunkHeader {
|
private static final class ChunkHeader {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -262,12 +262,23 @@ public final class DownloadManager {
|
||||||
return task.id;
|
return task.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the current number of tasks. */
|
/** Returns the number of tasks. */
|
||||||
public int getTaskCount() {
|
public int getTaskCount() {
|
||||||
Assertions.checkState(!released);
|
Assertions.checkState(!released);
|
||||||
return tasks.size();
|
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 */
|
/** Returns the state of a task, or null if no such task exists */
|
||||||
public @Nullable TaskState getTaskState(int taskId) {
|
public @Nullable TaskState getTaskState(int taskId) {
|
||||||
Assertions.checkState(!released);
|
Assertions.checkState(!released);
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,9 @@ public abstract class DownloadService extends Service {
|
||||||
* Starts the service, adding an action to be executed.
|
* Starts the service, adding an action to be executed.
|
||||||
*
|
*
|
||||||
* @param context A {@link Context}.
|
* @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 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(
|
public static void startWithAction(
|
||||||
Context context,
|
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
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
logd("onCreate");
|
logd("onCreate");
|
||||||
|
|
@ -187,17 +214,6 @@ public abstract class DownloadService extends Service {
|
||||||
downloadManager = getDownloadManager();
|
downloadManager = getDownloadManager();
|
||||||
downloadManagerListener = new DownloadManagerListener();
|
downloadManagerListener = new DownloadManagerListener();
|
||||||
downloadManager.addListener(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
|
@Override
|
||||||
|
|
@ -237,6 +253,7 @@ public abstract class DownloadService extends Service {
|
||||||
Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
|
Log.e(TAG, "Ignoring unrecognized action: " + intentAction);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
maybeStartWatchingRequirements();
|
||||||
if (downloadManager.isIdle()) {
|
if (downloadManager.isIdle()) {
|
||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
|
|
@ -248,14 +265,7 @@ public abstract class DownloadService extends Service {
|
||||||
logd("onDestroy");
|
logd("onDestroy");
|
||||||
foregroundNotificationUpdater.stopPeriodicUpdates();
|
foregroundNotificationUpdater.stopPeriodicUpdates();
|
||||||
downloadManager.removeListener(downloadManagerListener);
|
downloadManager.removeListener(downloadManagerListener);
|
||||||
if (downloadManager.getTaskCount() == 0) {
|
maybeStopWatchingRequirements();
|
||||||
synchronized (requirementsHelpers) {
|
|
||||||
RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass());
|
|
||||||
if (requirementsHelper != null) {
|
|
||||||
requirementsHelper.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
@ -312,6 +322,31 @@ public abstract class DownloadService extends Service {
|
||||||
// Do nothing.
|
// 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() {
|
private void stop() {
|
||||||
foregroundNotificationUpdater.stopPeriodicUpdates();
|
foregroundNotificationUpdater.stopPeriodicUpdates();
|
||||||
// Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260].
|
// 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 {
|
private final class DownloadManagerListener implements DownloadManager.Listener {
|
||||||
@Override
|
@Override
|
||||||
public void onInitialized(DownloadManager downloadManager) {
|
public void onInitialized(DownloadManager downloadManager) {
|
||||||
// Do nothing.
|
maybeStartWatchingRequirements();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.offline;
|
package com.google.android.exoplayer2.offline;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,8 @@ public abstract class SegmentDownloader<M extends FilterableManifest<M, K>, K>
|
||||||
throws InterruptedException, IOException;
|
throws InterruptedException, IOException;
|
||||||
|
|
||||||
/** Initializes the download, returning a list of {@link Segment}s that need to be downloaded. */
|
/** 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 {
|
private List<Segment> initDownload() throws IOException, InterruptedException {
|
||||||
M manifest = getManifest(dataSource, manifestUri);
|
M manifest = getManifest(dataSource, manifestUri);
|
||||||
if (!streamKeys.isEmpty()) {
|
if (!streamKeys.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.SparseIntArray;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
|
|
@ -34,6 +33,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -656,7 +656,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||||
/* package */ static final class MediaSourceHolder implements Comparable<MediaSourceHolder> {
|
/* package */ static final class MediaSourceHolder implements Comparable<MediaSourceHolder> {
|
||||||
|
|
||||||
public final MediaSource mediaSource;
|
public final MediaSource mediaSource;
|
||||||
public final int uid;
|
public final Object uid;
|
||||||
|
|
||||||
public DeferredTimeline timeline;
|
public DeferredTimeline timeline;
|
||||||
public int childIndex;
|
public int childIndex;
|
||||||
|
|
@ -668,9 +668,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||||
|
|
||||||
public MediaSourceHolder(MediaSource mediaSource) {
|
public MediaSourceHolder(MediaSource mediaSource) {
|
||||||
this.mediaSource = mediaSource;
|
this.mediaSource = mediaSource;
|
||||||
this.uid = System.identityHashCode(this);
|
|
||||||
this.timeline = new DeferredTimeline();
|
this.timeline = new DeferredTimeline();
|
||||||
this.activeMediaPeriods = new ArrayList<>();
|
this.activeMediaPeriods = new ArrayList<>();
|
||||||
|
this.uid = new Object();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) {
|
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[] firstPeriodInChildIndices;
|
||||||
private final int[] firstWindowInChildIndices;
|
private final int[] firstWindowInChildIndices;
|
||||||
private final Timeline[] timelines;
|
private final Timeline[] timelines;
|
||||||
private final int[] uids;
|
private final Object[] uids;
|
||||||
private final SparseIntArray childIndexByUid;
|
private final HashMap<Object, Integer> childIndexByUid;
|
||||||
|
|
||||||
public ConcatenatedTimeline(
|
public ConcatenatedTimeline(
|
||||||
Collection<MediaSourceHolder> mediaSourceHolders,
|
Collection<MediaSourceHolder> mediaSourceHolders,
|
||||||
|
|
@ -744,8 +744,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||||
firstPeriodInChildIndices = new int[childCount];
|
firstPeriodInChildIndices = new int[childCount];
|
||||||
firstWindowInChildIndices = new int[childCount];
|
firstWindowInChildIndices = new int[childCount];
|
||||||
timelines = new Timeline[childCount];
|
timelines = new Timeline[childCount];
|
||||||
uids = new int[childCount];
|
uids = new Object[childCount];
|
||||||
childIndexByUid = new SparseIntArray();
|
childIndexByUid = new HashMap<>();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
|
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
|
||||||
timelines[index] = mediaSourceHolder.timeline;
|
timelines[index] = mediaSourceHolder.timeline;
|
||||||
|
|
@ -768,11 +768,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getChildIndexByChildUid(Object childUid) {
|
protected int getChildIndexByChildUid(Object childUid) {
|
||||||
if (!(childUid instanceof Integer)) {
|
Integer index = childIndexByUid.get(childUid);
|
||||||
return C.INDEX_UNSET;
|
return index == null ? C.INDEX_UNSET : index;
|
||||||
}
|
|
||||||
int index = childIndexByUid.get((int) childUid, -1);
|
|
||||||
return index == -1 ? C.INDEX_UNSET : index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -804,7 +801,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
||||||
public int getPeriodCount() {
|
public int getPeriodCount() {
|
||||||
return periodCount;
|
return periodCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,25 @@ package com.google.android.exoplayer2.source;
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource {
|
public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource {
|
||||||
|
|
||||||
/** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource()} instead. */
|
/**
|
||||||
|
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(MediaSource...)}
|
||||||
|
* instead.
|
||||||
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public DynamicConcatenatingMediaSource() {}
|
public DynamicConcatenatingMediaSource() {}
|
||||||
|
|
||||||
/** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean)} instead. */
|
/**
|
||||||
|
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean,
|
||||||
|
* MediaSource...)} instead.
|
||||||
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public DynamicConcatenatingMediaSource(boolean isAtomic) {
|
public DynamicConcatenatingMediaSource(boolean isAtomic) {
|
||||||
super(isAtomic);
|
super(isAtomic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean,
|
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, ShuffleOrder,
|
||||||
* ShuffleOrder)} instead.
|
* MediaSource...)} instead.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) {
|
public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) {
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ import java.util.Arrays;
|
||||||
private final Runnable onContinueLoadingRequestedRunnable;
|
private final Runnable onContinueLoadingRequestedRunnable;
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
|
|
||||||
private Callback callback;
|
private @Nullable Callback callback;
|
||||||
private SeekMap seekMap;
|
private SeekMap seekMap;
|
||||||
private SampleQueue[] sampleQueues;
|
private SampleQueue[] sampleQueues;
|
||||||
private int[] sampleQueueTrackIds;
|
private int[] sampleQueueTrackIds;
|
||||||
|
|
@ -190,6 +190,7 @@ import java.util.Arrays;
|
||||||
}
|
}
|
||||||
loader.release(this);
|
loader.release(this);
|
||||||
handler.removeCallbacksAndMessages(null);
|
handler.removeCallbacksAndMessages(null);
|
||||||
|
callback = null;
|
||||||
released = true;
|
released = true;
|
||||||
eventDispatcher.mediaPeriodReleased();
|
eventDispatcher.mediaPeriodReleased();
|
||||||
}
|
}
|
||||||
|
|
@ -832,11 +833,6 @@ import java.util.Arrays;
|
||||||
loadCanceled = true;
|
loadCanceled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoadCanceled() {
|
|
||||||
return loadCanceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() throws IOException, InterruptedException {
|
public void load() throws IOException, InterruptedException {
|
||||||
int result = Extractor.RESULT_CONTINUE;
|
int result = Extractor.RESULT_CONTINUE;
|
||||||
|
|
|
||||||
|
|
@ -348,11 +348,6 @@ import java.util.Arrays;
|
||||||
// Never happens.
|
// Never happens.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoadCanceled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() throws IOException, InterruptedException {
|
public void load() throws IOException, InterruptedException {
|
||||||
// We always load from the beginning, so reset the sampleSize to 0.
|
// 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.
|
* @param format The format.
|
||||||
* @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.
|
* @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("ReferenceEquality")
|
||||||
public int indexOf(Format format) {
|
public int indexOf(Format format) {
|
||||||
for (int i = 0; i < formats.length; i++) {
|
for (int i = 0; i < formats.length; i++) {
|
||||||
if (format == formats[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 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
|
* @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the
|
||||||
* whole chunk should be output.
|
* 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(
|
public BaseMediaChunk(
|
||||||
DataSource dataSource,
|
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.SampleQueue;
|
||||||
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
|
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider;
|
||||||
|
|
||||||
/**
|
/** An output for {@link BaseMediaChunk}s. */
|
||||||
* An output for {@link BaseMediaChunk}s.
|
public final class BaseMediaChunkOutput implements TrackOutputProvider {
|
||||||
*/
|
|
||||||
/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider {
|
|
||||||
|
|
||||||
private static final String TAG = "BaseMediaChunkOutput";
|
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 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
|
* @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the
|
||||||
* whole chunk should be output.
|
* 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
|
* @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
|
* 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.
|
* underlying media are being merged into a single load.
|
||||||
|
|
@ -106,11 +106,6 @@ public class ContainerMediaChunk extends BaseMediaChunk {
|
||||||
loadCanceled = true;
|
loadCanceled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public final boolean isLoadCanceled() {
|
|
||||||
return loadCanceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("NonAtomicVolatileUpdate")
|
@SuppressWarnings("NonAtomicVolatileUpdate")
|
||||||
@Override
|
@Override
|
||||||
public final void load() throws IOException, InterruptedException {
|
public final void load() throws IOException, InterruptedException {
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,6 @@ public abstract class DataChunk extends Chunk {
|
||||||
loadCanceled = true;
|
loadCanceled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public final boolean isLoadCanceled() {
|
|
||||||
return loadCanceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void load() throws IOException, InterruptedException {
|
public final void load() throws IOException, InterruptedException {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,6 @@ public final class InitializationChunk extends Chunk {
|
||||||
loadCanceled = true;
|
loadCanceled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoadCanceled() {
|
|
||||||
return loadCanceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("NonAtomicVolatileUpdate")
|
@SuppressWarnings("NonAtomicVolatileUpdate")
|
||||||
@Override
|
@Override
|
||||||
public void load() throws IOException, InterruptedException {
|
public void load() throws IOException, InterruptedException {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||||
*/
|
*/
|
||||||
public abstract class MediaChunk extends Chunk {
|
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;
|
public final long chunkIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -37,7 +37,7 @@ public abstract class MediaChunk extends Chunk {
|
||||||
* @param trackSelectionData See {@link #trackSelectionData}.
|
* @param trackSelectionData See {@link #trackSelectionData}.
|
||||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
* @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 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(
|
public MediaChunk(
|
||||||
DataSource dataSource,
|
DataSource dataSource,
|
||||||
|
|
@ -54,9 +54,9 @@ public abstract class MediaChunk extends Chunk {
|
||||||
this.chunkIndex = chunkIndex;
|
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() {
|
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 final Format sampleFormat;
|
||||||
|
|
||||||
private volatile int bytesLoaded;
|
private volatile int bytesLoaded;
|
||||||
private volatile boolean loadCanceled;
|
|
||||||
private volatile boolean loadCompleted;
|
private volatile boolean loadCompleted;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,7 +44,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
|
||||||
* @param trackSelectionData See {@link #trackSelectionData}.
|
* @param trackSelectionData See {@link #trackSelectionData}.
|
||||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
* @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 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_*}
|
* @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*}
|
||||||
* constants.
|
* constants.
|
||||||
* @param sampleFormat The {@link Format} of the sample in the chunk.
|
* @param sampleFormat The {@link Format} of the sample in the chunk.
|
||||||
|
|
@ -90,12 +89,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cancelLoad() {
|
public void cancelLoad() {
|
||||||
loadCanceled = true;
|
// Do nothing.
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoadCanceled() {
|
|
||||||
return loadCanceled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("NonAtomicVolatileUpdate")
|
@SuppressWarnings("NonAtomicVolatileUpdate")
|
||||||
|
|
|
||||||
|
|
@ -374,6 +374,9 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||||
private void handleMidrowCtrl(byte cc2) {
|
private void handleMidrowCtrl(byte cc2) {
|
||||||
// TODO: support the extended styles (i.e. backgrounds and transparencies)
|
// 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
|
// cc2 - 0|0|1|0|ATRBT|U
|
||||||
// ATRBT is the 3-byte encoded attribute, and U is the underline toggle
|
// ATRBT is the 3-byte encoded attribute, and U is the underline toggle
|
||||||
boolean isUnderlined = (cc2 & 0x01) == 0x01;
|
boolean isUnderlined = (cc2 & 0x01) == 0x01;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -196,7 +195,10 @@ public final class Cea708Decoder extends CeaDecoder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void decode(SubtitleInputBuffer inputBuffer) {
|
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) {
|
while (ccData.bytesLeft() >= 3) {
|
||||||
int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07);
|
int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07);
|
||||||
|
|
||||||
|
|
@ -879,7 +881,7 @@ public final class Cea708Decoder extends CeaDecoder {
|
||||||
private int row;
|
private int row;
|
||||||
|
|
||||||
public CueBuilder() {
|
public CueBuilder() {
|
||||||
rolledUpCaptions = new LinkedList<>();
|
rolledUpCaptions = new ArrayList<>();
|
||||||
captionStringBuilder = new SpannableStringBuilder();
|
captionStringBuilder = new SpannableStringBuilder();
|
||||||
reset();
|
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.SubtitleInputBuffer;
|
||||||
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
|
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import java.util.LinkedList;
|
import java.util.ArrayDeque;
|
||||||
import java.util.PriorityQueue;
|
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_INPUT_BUFFERS = 10;
|
||||||
private static final int NUM_OUTPUT_BUFFERS = 2;
|
private static final int NUM_OUTPUT_BUFFERS = 2;
|
||||||
|
|
||||||
private final LinkedList<CeaInputBuffer> availableInputBuffers;
|
private final ArrayDeque<CeaInputBuffer> availableInputBuffers;
|
||||||
private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
|
private final ArrayDeque<SubtitleOutputBuffer> availableOutputBuffers;
|
||||||
private final PriorityQueue<CeaInputBuffer> queuedInputBuffers;
|
private final PriorityQueue<CeaInputBuffer> queuedInputBuffers;
|
||||||
|
|
||||||
private CeaInputBuffer dequeuedInputBuffer;
|
private CeaInputBuffer dequeuedInputBuffer;
|
||||||
|
|
@ -44,11 +44,11 @@ import java.util.PriorityQueue;
|
||||||
private long queuedInputBufferCount;
|
private long queuedInputBufferCount;
|
||||||
|
|
||||||
public CeaDecoder() {
|
public CeaDecoder() {
|
||||||
availableInputBuffers = new LinkedList<>();
|
availableInputBuffers = new ArrayDeque<>();
|
||||||
for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
|
for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
|
||||||
availableInputBuffers.add(new CeaInputBuffer());
|
availableInputBuffers.add(new CeaInputBuffer());
|
||||||
}
|
}
|
||||||
availableOutputBuffers = new LinkedList<>();
|
availableOutputBuffers = new ArrayDeque<>();
|
||||||
for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
|
for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
|
||||||
availableOutputBuffers.add(new CeaOutputBuffer());
|
availableOutputBuffers.add(new CeaOutputBuffer());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
||||||
super("SsaDecoder");
|
super("SsaDecoder");
|
||||||
if (initializationData != null && !initializationData.isEmpty()) {
|
if (initializationData != null && !initializationData.isEmpty()) {
|
||||||
haveInitializationData = true;
|
haveInitializationData = true;
|
||||||
String formatLine = new String(initializationData.get(0));
|
String formatLine = Util.fromUtf8Bytes(initializationData.get(0));
|
||||||
Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));
|
Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));
|
||||||
parseFormatLine(formatLine);
|
parseFormatLine(formatLine);
|
||||||
parseHeader(new ParsableByteArray(initializationData.get(1)));
|
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 com.google.android.exoplayer2.util.XmlPullParserUtil;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
@ -109,13 +109,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
|
||||||
xmlParser.setInput(inputStream, null);
|
xmlParser.setInput(inputStream, null);
|
||||||
TtmlSubtitle ttmlSubtitle = null;
|
TtmlSubtitle ttmlSubtitle = null;
|
||||||
LinkedList<TtmlNode> nodeStack = new LinkedList<>();
|
ArrayDeque<TtmlNode> nodeStack = new ArrayDeque<>();
|
||||||
int unsupportedNodeDepth = 0;
|
int unsupportedNodeDepth = 0;
|
||||||
int eventType = xmlParser.getEventType();
|
int eventType = xmlParser.getEventType();
|
||||||
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
|
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
|
||||||
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
|
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
|
||||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||||
TtmlNode parent = nodeStack.peekLast();
|
TtmlNode parent = nodeStack.peek();
|
||||||
if (unsupportedNodeDepth == 0) {
|
if (unsupportedNodeDepth == 0) {
|
||||||
String name = xmlParser.getName();
|
String name = xmlParser.getName();
|
||||||
if (eventType == XmlPullParser.START_TAG) {
|
if (eventType == XmlPullParser.START_TAG) {
|
||||||
|
|
@ -131,7 +131,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
|
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
|
||||||
nodeStack.addLast(node);
|
nodeStack.push(node);
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
parent.addChild(node);
|
parent.addChild(node);
|
||||||
}
|
}
|
||||||
|
|
@ -145,9 +145,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||||
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
|
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
|
||||||
} else if (eventType == XmlPullParser.END_TAG) {
|
} else if (eventType == XmlPullParser.END_TAG) {
|
||||||
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
|
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 {
|
} else {
|
||||||
if (eventType == XmlPullParser.START_TAG) {
|
if (eventType == XmlPullParser.START_TAG) {
|
||||||
|
|
@ -178,7 +178,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||||
float frameRateMultiplier = 1;
|
float frameRateMultiplier = 1;
|
||||||
String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier");
|
String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier");
|
||||||
if (frameRateMultiplierString != null) {
|
if (frameRateMultiplierString != null) {
|
||||||
String[] parts = frameRateMultiplierString.split(" ");
|
String[] parts = Util.split(frameRateMultiplierString, " ");
|
||||||
if (parts.length != 2) {
|
if (parts.length != 2) {
|
||||||
throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts");
|
throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts");
|
||||||
}
|
}
|
||||||
|
|
@ -354,7 +354,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] parseStyleIds(String parentStyleIds) {
|
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) {
|
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
|
private static void parseFontSize(String expression, TtmlStyle out) throws
|
||||||
SubtitleDecoderException {
|
SubtitleDecoderException {
|
||||||
String[] expressions = expression.split("\\s+");
|
String[] expressions = Util.split(expression, "\\s+");
|
||||||
Matcher matcher;
|
Matcher matcher;
|
||||||
if (expressions.length == 1) {
|
if (expressions.length == 1) {
|
||||||
matcher = FONT_SIZE.matcher(expression);
|
matcher = FONT_SIZE.matcher(expression);
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||||
| ((initializationBytes[27] & 0xFF) << 16)
|
| ((initializationBytes[27] & 0xFF) << 16)
|
||||||
| ((initializationBytes[28] & 0xFF) << 8)
|
| ((initializationBytes[28] & 0xFF) << 8)
|
||||||
| (initializationBytes[29] & 0xFF);
|
| (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;
|
defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME;
|
||||||
//font size (initializationBytes[25]) is 5% of video height
|
//font size (initializationBytes[25]) is 5% of video height
|
||||||
calculatedVideoTrackHeight = 20 * initializationBytes[25];
|
calculatedVideoTrackHeight = 20 * initializationBytes[25];
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.webvtt;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import com.google.android.exoplayer2.util.ColorParser;
|
import com.google.android.exoplayer2.util.ColorParser;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
@ -314,7 +315,7 @@ import java.util.regex.Pattern;
|
||||||
}
|
}
|
||||||
selector = selector.substring(0, voiceStartIndex);
|
selector = selector.substring(0, voiceStartIndex);
|
||||||
}
|
}
|
||||||
String[] classDivision = selector.split("\\.");
|
String[] classDivision = Util.split(selector, "\\.");
|
||||||
String tagAndIdDivision = classDivision[0];
|
String tagAndIdDivision = classDivision[0];
|
||||||
int idPrefixIndex = tagAndIdDivision.indexOf('#');
|
int idPrefixIndex = tagAndIdDivision.indexOf('#');
|
||||||
if (idPrefixIndex != -1) {
|
if (idPrefixIndex != -1) {
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,8 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
|
||||||
int boxType = sampleData.readInt();
|
int boxType = sampleData.readInt();
|
||||||
remainingCueBoxBytes -= BOX_HEADER_SIZE;
|
remainingCueBoxBytes -= BOX_HEADER_SIZE;
|
||||||
int payloadLength = boxSize - 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);
|
sampleData.skipBytes(payloadLength);
|
||||||
remainingCueBoxBytes -= payloadLength;
|
remainingCueBoxBytes -= payloadLength;
|
||||||
if (boxType == TYPE_sttg) {
|
if (boxType == TYPE_sttg) {
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,12 @@ import android.text.style.UnderlineSpan;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
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.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Stack;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
|
@ -157,7 +158,7 @@ public final class WebvttCueParser {
|
||||||
/* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,
|
/* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,
|
||||||
List<WebvttCssStyle> styles) {
|
List<WebvttCssStyle> styles) {
|
||||||
SpannableStringBuilder spannedText = new SpannableStringBuilder();
|
SpannableStringBuilder spannedText = new SpannableStringBuilder();
|
||||||
Stack<StartTag> startTagStack = new Stack<>();
|
ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();
|
||||||
List<StyleMatch> scratchStyleMatches = new ArrayList<>();
|
List<StyleMatch> scratchStyleMatches = new ArrayList<>();
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
while (pos < markup.length()) {
|
while (pos < markup.length()) {
|
||||||
|
|
@ -456,7 +457,7 @@ public final class WebvttCueParser {
|
||||||
if (tagExpression.isEmpty()) {
|
if (tagExpression.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return tagExpression.split("[ \\.]")[0];
|
return Util.splitAtFirst(tagExpression, "[ \\.]")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void getApplicableStyles(List<WebvttCssStyle> declaredStyles, String id,
|
private static void getApplicableStyles(List<WebvttCssStyle> declaredStyles, String id,
|
||||||
|
|
@ -518,7 +519,7 @@ public final class WebvttCueParser {
|
||||||
voice = fullTagExpression.substring(voiceStartIndex).trim();
|
voice = fullTagExpression.substring(voiceStartIndex).trim();
|
||||||
fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);
|
fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);
|
||||||
}
|
}
|
||||||
String[] nameAndClasses = fullTagExpression.split("\\.");
|
String[] nameAndClasses = Util.split(fullTagExpression, "\\.");
|
||||||
String name = nameAndClasses[0];
|
String name = nameAndClasses[0];
|
||||||
String[] classes;
|
String[] classes;
|
||||||
if (nameAndClasses.length > 1) {
|
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.text.SubtitleDecoderException;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
|
@ -53,8 +54,8 @@ public final class WebvttParserUtil {
|
||||||
*/
|
*/
|
||||||
public static long parseTimestampUs(String timestamp) throws NumberFormatException {
|
public static long parseTimestampUs(String timestamp) throws NumberFormatException {
|
||||||
long value = 0;
|
long value = 0;
|
||||||
String[] parts = timestamp.split("\\.", 2);
|
String[] parts = Util.splitAtFirst(timestamp, "\\.");
|
||||||
String[] subparts = parts[0].split(":");
|
String[] subparts = Util.split(parts[0], ":");
|
||||||
for (String subpart : subparts) {
|
for (String subpart : subparts) {
|
||||||
value = (value * 60) + Long.parseLong(subpart);
|
value = (value * 60) + Long.parseLong(subpart);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.trackselection;
|
package com.google.android.exoplayer2.trackselection;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
|
|
@ -242,9 +243,11 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||||
this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
|
this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
playbackSpeed = 1f;
|
playbackSpeed = 1f;
|
||||||
selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
|
|
||||||
reason = C.SELECTION_REASON_INITIAL;
|
reason = C.SELECTION_REASON_INITIAL;
|
||||||
lastBufferEvaluationMs = C.TIME_UNSET;
|
lastBufferEvaluationMs = C.TIME_UNSET;
|
||||||
|
@SuppressWarnings("nullness:method.invocation.invalid")
|
||||||
|
int selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
|
||||||
|
this.selectedIndex = selectedIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -301,7 +304,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getSelectionData() {
|
public @Nullable Object getSelectionData() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ public abstract class BaseTrackSelection implements TrackSelection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("ReferenceEquality")
|
||||||
public final int indexOf(Format format) {
|
public final int indexOf(Format format) {
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
if (formats[i] == format) {
|
if (formats[i] == format) {
|
||||||
|
|
@ -183,7 +184,9 @@ public abstract class BaseTrackSelection implements TrackSelection {
|
||||||
return hashCode;
|
return hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track groups are compared by identity not value, as distinct groups may have the same value.
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("ReferenceEquality")
|
||||||
public boolean equals(@Nullable Object obj) {
|
public boolean equals(@Nullable Object obj) {
|
||||||
if (this == obj) {
|
if (this == obj) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import android.content.Context;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
@ -161,8 +160,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
private final SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides;
|
private final SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides;
|
||||||
private final SparseBooleanArray rendererDisabledFlags;
|
private final SparseBooleanArray rendererDisabledFlags;
|
||||||
|
|
||||||
private String preferredAudioLanguage;
|
private @Nullable String preferredAudioLanguage;
|
||||||
private String preferredTextLanguage;
|
private @Nullable String preferredTextLanguage;
|
||||||
private boolean selectUndeterminedTextLanguage;
|
private boolean selectUndeterminedTextLanguage;
|
||||||
private int disabledTextTrackSelectionFlags;
|
private int disabledTextTrackSelectionFlags;
|
||||||
private boolean forceLowestBitrate;
|
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.
|
* 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.
|
* {@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
|
// Text
|
||||||
/**
|
/**
|
||||||
* The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the
|
* 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.
|
* 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
|
* Whether a text track with undetermined language should be selected if no track with
|
||||||
* {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset.
|
* {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset.
|
||||||
|
|
@ -673,8 +672,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
/* package */ Parameters(
|
/* package */ Parameters(
|
||||||
SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides,
|
SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides,
|
||||||
SparseBooleanArray rendererDisabledFlags,
|
SparseBooleanArray rendererDisabledFlags,
|
||||||
String preferredAudioLanguage,
|
@Nullable String preferredAudioLanguage,
|
||||||
String preferredTextLanguage,
|
@Nullable String preferredTextLanguage,
|
||||||
boolean selectUndeterminedTextLanguage,
|
boolean selectUndeterminedTextLanguage,
|
||||||
int disabledTextTrackSelectionFlags,
|
int disabledTextTrackSelectionFlags,
|
||||||
boolean forceLowestBitrate,
|
boolean forceLowestBitrate,
|
||||||
|
|
@ -759,7 +758,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
* @param groups The {@link TrackGroupArray}.
|
* @param groups The {@link TrackGroupArray}.
|
||||||
* @return The override, or null if no override exists.
|
* @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);
|
Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(rendererIndex);
|
||||||
return overrides != null ? overrides.get(groups) : null;
|
return overrides != null ? overrides.get(groups) : null;
|
||||||
}
|
}
|
||||||
|
|
@ -816,8 +816,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
result = 31 * result + viewportHeight;
|
result = 31 * result + viewportHeight;
|
||||||
result = 31 * result + maxVideoBitrate;
|
result = 31 * result + maxVideoBitrate;
|
||||||
result = 31 * result + tunnelingAudioSessionId;
|
result = 31 * result + tunnelingAudioSessionId;
|
||||||
result = 31 * result + preferredAudioLanguage.hashCode();
|
result =
|
||||||
result = 31 * result + preferredTextLanguage.hashCode();
|
31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode());
|
||||||
|
result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1042,7 +1043,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
private static final int[] NO_TRACKS = new int[0];
|
private static final int[] NO_TRACKS = new int[0];
|
||||||
private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
|
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;
|
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
|
* @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null if
|
||||||
* the selector should not support adaptive tracks.
|
* the selector should not support adaptive tracks.
|
||||||
*/
|
*/
|
||||||
public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) {
|
public DefaultTrackSelector(@Nullable TrackSelection.Factory adaptiveTrackSelectionFactory) {
|
||||||
this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
|
this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
|
||||||
parametersReference = new AtomicReference<>(Parameters.DEFAULT);
|
parametersReference = new AtomicReference<>(Parameters.DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
@ -1139,7 +1140,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
|
|
||||||
/** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */
|
/** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {
|
public final @Nullable SelectionOverride getSelectionOverride(
|
||||||
|
int rendererIndex, TrackGroupArray groups) {
|
||||||
return getParameters().getSelectionOverride(rendererIndex, groups);
|
return getParameters().getSelectionOverride(rendererIndex, groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1170,11 +1172,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
// MappingTrackSelector implementation.
|
// MappingTrackSelector implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected final Pair<RendererConfiguration[], TrackSelection[]> selectTracks(
|
protected final Pair<RendererConfiguration[], TrackSelection[]>
|
||||||
MappedTrackInfo mappedTrackInfo,
|
selectTracks(
|
||||||
int[][][] rendererFormatSupports,
|
MappedTrackInfo mappedTrackInfo,
|
||||||
int[] rendererMixedMimeTypeAdaptationSupports)
|
int[][][] rendererFormatSupports,
|
||||||
throws ExoPlaybackException {
|
int[] rendererMixedMimeTypeAdaptationSupports)
|
||||||
|
throws ExoPlaybackException {
|
||||||
Parameters params = parametersReference.get();
|
Parameters params = parametersReference.get();
|
||||||
int rendererCount = mappedTrackInfo.getRendererCount();
|
int rendererCount = mappedTrackInfo.getRendererCount();
|
||||||
TrackSelection[] rendererTrackSelections =
|
TrackSelection[] rendererTrackSelections =
|
||||||
|
|
@ -1200,8 +1203,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
rendererTrackGroups.get(override.groupIndex), override.tracks[0]);
|
rendererTrackGroups.get(override.groupIndex), override.tracks[0]);
|
||||||
} else {
|
} else {
|
||||||
rendererTrackSelections[i] =
|
rendererTrackSelections[i] =
|
||||||
adaptiveTrackSelectionFactory.createTrackSelection(
|
Assertions.checkNotNull(adaptiveTrackSelectionFactory)
|
||||||
rendererTrackGroups.get(override.groupIndex), override.tracks);
|
.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
|
// Initialize the renderer configurations to the default configuration for all renderers with
|
||||||
// selections, and null otherwise.
|
// selections, and null otherwise.
|
||||||
RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount];
|
RendererConfiguration[] rendererConfigurations =
|
||||||
|
new RendererConfiguration[rendererCount];
|
||||||
for (int i = 0; i < rendererCount; i++) {
|
for (int i = 0; i < rendererCount; i++) {
|
||||||
boolean forceRendererDisabled = params.getRendererDisabled(i);
|
boolean forceRendererDisabled = params.getRendererDisabled(i);
|
||||||
boolean rendererEnabled =
|
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.
|
* @return The {@link TrackSelection} for the renderer, or null if no selection was made.
|
||||||
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
||||||
*/
|
*/
|
||||||
protected TrackSelection selectVideoTrack(
|
protected @Nullable TrackSelection selectVideoTrack(
|
||||||
TrackGroupArray groups,
|
TrackGroupArray groups,
|
||||||
int[][] formatSupports,
|
int[][] formatSupports,
|
||||||
int mixedMimeTypeAdaptationSupports,
|
int mixedMimeTypeAdaptationSupports,
|
||||||
Parameters params,
|
Parameters params,
|
||||||
TrackSelection.Factory adaptiveTrackSelectionFactory)
|
@Nullable TrackSelection.Factory adaptiveTrackSelectionFactory)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
TrackSelection selection = null;
|
TrackSelection selection = null;
|
||||||
if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) {
|
if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) {
|
||||||
|
|
@ -1354,7 +1359,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
return selection;
|
return selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TrackSelection selectAdaptiveVideoTrack(
|
private static @Nullable TrackSelection selectAdaptiveVideoTrack(
|
||||||
TrackGroupArray groups,
|
TrackGroupArray groups,
|
||||||
int[][] formatSupport,
|
int[][] formatSupport,
|
||||||
int mixedMimeTypeAdaptationSupports,
|
int mixedMimeTypeAdaptationSupports,
|
||||||
|
|
@ -1374,7 +1379,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
params.maxVideoBitrate, params.viewportWidth, params.viewportHeight,
|
params.maxVideoBitrate, params.viewportWidth, params.viewportHeight,
|
||||||
params.viewportOrientationMayChange);
|
params.viewportOrientationMayChange);
|
||||||
if (adaptiveTracks.length > 0) {
|
if (adaptiveTracks.length > 0) {
|
||||||
return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks);
|
return Assertions.checkNotNull(adaptiveTrackSelectionFactory)
|
||||||
|
.createTrackSelection(group, adaptiveTracks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1421,9 +1427,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices);
|
return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getAdaptiveVideoTrackCountForMimeType(TrackGroup group, int[] formatSupport,
|
private static int getAdaptiveVideoTrackCountForMimeType(
|
||||||
int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight,
|
TrackGroup group,
|
||||||
int maxVideoBitrate, List<Integer> selectedTrackIndices) {
|
int[] formatSupport,
|
||||||
|
int requiredAdaptiveSupport,
|
||||||
|
@Nullable String mimeType,
|
||||||
|
int maxVideoWidth,
|
||||||
|
int maxVideoHeight,
|
||||||
|
int maxVideoBitrate,
|
||||||
|
List<Integer> selectedTrackIndices) {
|
||||||
int adaptiveTrackCount = 0;
|
int adaptiveTrackCount = 0;
|
||||||
for (int i = 0; i < selectedTrackIndices.size(); i++) {
|
for (int i = 0; i < selectedTrackIndices.size(); i++) {
|
||||||
int trackIndex = selectedTrackIndices.get(i);
|
int trackIndex = selectedTrackIndices.get(i);
|
||||||
|
|
@ -1436,9 +1448,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
return adaptiveTrackCount;
|
return adaptiveTrackCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void filterAdaptiveVideoTrackCountForMimeType(TrackGroup group,
|
private static void filterAdaptiveVideoTrackCountForMimeType(
|
||||||
int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth,
|
TrackGroup group,
|
||||||
int maxVideoHeight, int maxVideoBitrate, List<Integer> selectedTrackIndices) {
|
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--) {
|
for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {
|
||||||
int trackIndex = selectedTrackIndices.get(i);
|
int trackIndex = selectedTrackIndices.get(i);
|
||||||
if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType,
|
if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType,
|
||||||
|
|
@ -1449,8 +1467,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType,
|
private static boolean isSupportedAdaptiveVideoTrack(
|
||||||
int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight,
|
Format format,
|
||||||
|
@Nullable String mimeType,
|
||||||
|
int formatSupport,
|
||||||
|
int requiredAdaptiveSupport,
|
||||||
|
int maxVideoWidth,
|
||||||
|
int maxVideoHeight,
|
||||||
int maxVideoBitrate) {
|
int maxVideoBitrate) {
|
||||||
return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0)
|
return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0)
|
||||||
&& (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType))
|
&& (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType))
|
||||||
|
|
@ -1459,7 +1482,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
&& (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate);
|
&& (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TrackSelection selectFixedVideoTrack(
|
private static @Nullable TrackSelection selectFixedVideoTrack(
|
||||||
TrackGroupArray groups, int[][] formatSupports, Parameters params) {
|
TrackGroupArray groups, int[][] formatSupports, Parameters params) {
|
||||||
TrackGroup selectedGroup = null;
|
TrackGroup selectedGroup = null;
|
||||||
int selectedTrackIndex = 0;
|
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.
|
* @return The {@link TrackSelection} for the renderer, or null if no selection was made.
|
||||||
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
||||||
*/
|
*/
|
||||||
protected TrackSelection selectAudioTrack(
|
protected @Nullable TrackSelection selectAudioTrack(
|
||||||
TrackGroupArray groups,
|
TrackGroupArray groups,
|
||||||
int[][] formatSupports,
|
int[][] formatSupports,
|
||||||
int mixedMimeTypeAdaptationSupports,
|
int mixedMimeTypeAdaptationSupports,
|
||||||
Parameters params,
|
Parameters params,
|
||||||
TrackSelection.Factory adaptiveTrackSelectionFactory)
|
@Nullable TrackSelection.Factory adaptiveTrackSelectionFactory)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
int selectedTrackIndex = C.INDEX_UNSET;
|
int selectedTrackIndex = C.INDEX_UNSET;
|
||||||
int selectedGroupIndex = C.INDEX_UNSET;
|
int selectedGroupIndex = C.INDEX_UNSET;
|
||||||
|
|
@ -1606,8 +1629,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
|
int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (int i = 0; i < group.length; i++) {
|
for (int i = 0; i < group.length; i++) {
|
||||||
if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i],
|
if (isSupportedAdaptiveAudioTrack(
|
||||||
selectedConfiguration)) {
|
group.getFormat(i), formatSupport[i], Assertions.checkNotNull(selectedConfiguration))) {
|
||||||
adaptiveIndices[index++] = i;
|
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.
|
* @return The {@link TrackSelection} for the renderer, or null if no selection was made.
|
||||||
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
||||||
*/
|
*/
|
||||||
protected TrackSelection selectTextTrack(
|
protected @Nullable TrackSelection selectTextTrack(
|
||||||
TrackGroupArray groups, int[][] formatSupport, Parameters params)
|
TrackGroupArray groups, int[][] formatSupport, Parameters params)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
TrackGroup selectedGroup = null;
|
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.
|
* @return The {@link TrackSelection} for the renderer, or null if no selection was made.
|
||||||
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
* @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)
|
int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
TrackGroup selectedGroup = null;
|
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
|
* Returns whether a {@link Format} specifies a particular language, or {@code false} if {@code
|
||||||
* {@code language} is null.
|
* language} is null.
|
||||||
*
|
*
|
||||||
* @param format The {@link Format}.
|
* @param format The {@link Format}.
|
||||||
* @param language The language.
|
* @param language The language.
|
||||||
* @return Whether the format specifies the language, or {@code false} if {@code language} is
|
* @return Whether the format specifies the language, or {@code false} if {@code language} is
|
||||||
* null.
|
* null.
|
||||||
*/
|
*/
|
||||||
protected static boolean formatHasLanguage(Format format, String language) {
|
protected static boolean formatHasLanguage(Format format, @Nullable String language) {
|
||||||
return language != null
|
return language != null
|
||||||
&& TextUtils.equals(language, Util.normalizeLanguageCode(format.language));
|
&& 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.
|
* negative integer if this score is worse than the other.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(@NonNull AudioTrackScore other) {
|
public int compareTo(AudioTrackScore other) {
|
||||||
if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) {
|
if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) {
|
||||||
return compareInts(this.withinRendererCapabilitiesScore,
|
return compareInts(this.withinRendererCapabilitiesScore,
|
||||||
other.withinRendererCapabilitiesScore);
|
other.withinRendererCapabilitiesScore);
|
||||||
|
|
@ -2066,9 +2089,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||||
|
|
||||||
public final int channelCount;
|
public final int channelCount;
|
||||||
public final int sampleRate;
|
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.channelCount = channelCount;
|
||||||
this.sampleRate = sampleRate;
|
this.sampleRate = sampleRate;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.trackselection;
|
package com.google.android.exoplayer2.trackselection;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
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 {
|
public static final class Factory implements TrackSelection.Factory {
|
||||||
|
|
||||||
private final int reason;
|
private final int reason;
|
||||||
private final Object data;
|
private final @Nullable Object data;
|
||||||
|
|
||||||
public Factory() {
|
public Factory() {
|
||||||
this.reason = C.SELECTION_REASON_UNKNOWN;
|
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 reason A reason for the track selection.
|
||||||
* @param data Optional data associated with 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.reason = reason;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
@ -51,11 +52,10 @@ public final class FixedTrackSelection extends BaseTrackSelection {
|
||||||
Assertions.checkArgument(tracks.length == 1);
|
Assertions.checkArgument(tracks.length == 1);
|
||||||
return new FixedTrackSelection(group, tracks[0], reason, data);
|
return new FixedTrackSelection(group, tracks[0], reason, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final int reason;
|
private final int reason;
|
||||||
private final Object data;
|
private final @Nullable Object data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param group The {@link TrackGroup}. Must not be null.
|
* @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 reason A reason for the track selection.
|
||||||
* @param data Optional data associated with 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);
|
super(group, track);
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
|
@ -94,7 +94,7 @@ public final class FixedTrackSelection extends BaseTrackSelection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getSelectionData() {
|
public @Nullable Object getSelectionData() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer2.trackselection;
|
package com.google.android.exoplayer2.trackselection;
|
||||||
|
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
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
|
* Returns the mapping information for the currently active track selection, or null if no
|
||||||
* selection is currently active.
|
* selection is currently active.
|
||||||
*/
|
*/
|
||||||
public final MappedTrackInfo getCurrentMappedTrackInfo() {
|
public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() {
|
||||||
return currentMappedTrackInfo;
|
return currentMappedTrackInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,9 +358,11 @@ public abstract class MappingTrackSelector extends TrackSelector {
|
||||||
int[] rendererTrackTypes = new int[rendererCapabilities.length];
|
int[] rendererTrackTypes = new int[rendererCapabilities.length];
|
||||||
for (int i = 0; i < rendererCapabilities.length; i++) {
|
for (int i = 0; i < rendererCapabilities.length; i++) {
|
||||||
int rendererTrackGroupCount = rendererTrackGroupCounts[i];
|
int rendererTrackGroupCount = rendererTrackGroupCounts[i];
|
||||||
rendererTrackGroupArrays[i] = new TrackGroupArray(
|
rendererTrackGroupArrays[i] =
|
||||||
Arrays.copyOf(rendererTrackGroups[i], rendererTrackGroupCount));
|
new TrackGroupArray(
|
||||||
rendererFormatSupports[i] = Arrays.copyOf(rendererFormatSupports[i], rendererTrackGroupCount);
|
Util.nullSafeArrayCopy(rendererTrackGroups[i], rendererTrackGroupCount));
|
||||||
|
rendererFormatSupports[i] =
|
||||||
|
Util.nullSafeArrayCopy(rendererFormatSupports[i], rendererTrackGroupCount);
|
||||||
rendererTrackTypes[i] = rendererCapabilities[i].getTrackType();
|
rendererTrackTypes[i] = rendererCapabilities[i].getTrackType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,7 +370,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
|
||||||
int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length];
|
int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length];
|
||||||
TrackGroupArray unmappedTrackGroupArray =
|
TrackGroupArray unmappedTrackGroupArray =
|
||||||
new TrackGroupArray(
|
new TrackGroupArray(
|
||||||
Arrays.copyOf(
|
Util.nullSafeArrayCopy(
|
||||||
rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount));
|
rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount));
|
||||||
|
|
||||||
// Package up the track information and selections.
|
// 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}.
|
* RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}.
|
||||||
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
|
||||||
*/
|
*/
|
||||||
protected abstract Pair<RendererConfiguration[], TrackSelection[]> selectTracks(
|
protected abstract Pair<RendererConfiguration[], TrackSelection[]>
|
||||||
MappedTrackInfo mappedTrackInfo,
|
selectTracks(
|
||||||
int[][][] rendererFormatSupports,
|
MappedTrackInfo mappedTrackInfo,
|
||||||
int[] rendererMixedMimeTypeAdaptationSupport)
|
int[][][] rendererFormatSupports,
|
||||||
throws ExoPlaybackException;
|
int[] rendererMixedMimeTypeAdaptationSupport)
|
||||||
|
throws ExoPlaybackException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the renderer to which the provided {@link TrackGroup} should be mapped.
|
* Finds the renderer to which the provided {@link TrackGroup} should be mapped.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer2.trackselection;
|
package com.google.android.exoplayer2.trackselection;
|
||||||
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
@ -47,7 +48,6 @@ public final class RandomTrackSelection extends BaseTrackSelection {
|
||||||
public RandomTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
|
public RandomTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
|
||||||
return new RandomTrackSelection(group, tracks, random);
|
return new RandomTrackSelection(group, tracks, random);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Random random;
|
private final Random random;
|
||||||
|
|
@ -123,7 +123,7 @@ public final class RandomTrackSelection extends BaseTrackSelection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getSelectionData() {
|
public @Nullable Object getSelectionData() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.trackselection;
|
package com.google.android.exoplayer2.trackselection;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
|
|
@ -90,7 +91,9 @@ public interface TrackSelection {
|
||||||
int getIndexInTrackGroup(int index);
|
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.
|
* @param format The format.
|
||||||
* @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
|
* @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();
|
int getSelectionReason();
|
||||||
|
|
||||||
/**
|
/** Returns optional data associated with the current track selection. */
|
||||||
* Returns optional data associated with the current track selection.
|
@Nullable Object getSelectionData();
|
||||||
*/
|
|
||||||
Object getSelectionData();
|
|
||||||
|
|
||||||
// Adaptation.
|
// Adaptation.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,7 @@ public final class TrackSelectionArray {
|
||||||
// Lazily initialized hashcode.
|
// Lazily initialized hashcode.
|
||||||
private int 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) {
|
public TrackSelectionArray(TrackSelection... trackSelections) {
|
||||||
this.trackSelections = trackSelections;
|
this.trackSelections = trackSelections;
|
||||||
this.length = trackSelections.length;
|
this.length = trackSelections.length;
|
||||||
|
|
@ -43,13 +41,11 @@ public final class TrackSelectionArray {
|
||||||
* @param index The index of the selection.
|
* @param index The index of the selection.
|
||||||
* @return The selection.
|
* @return The selection.
|
||||||
*/
|
*/
|
||||||
public TrackSelection get(int index) {
|
public @Nullable TrackSelection get(int index) {
|
||||||
return trackSelections[index];
|
return trackSelections[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the selections in a newly allocated array. */
|
||||||
* Returns the selections in a newly allocated array.
|
|
||||||
*/
|
|
||||||
public TrackSelection[] getAll() {
|
public TrackSelection[] getAll() {
|
||||||
return trackSelections.clone();
|
return trackSelections.clone();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.trackselection;
|
package com.google.android.exoplayer2.trackselection;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Renderer;
|
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.
|
* Called by the player to initialize the selector.
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,9 @@ public final class TrackSelectorResult {
|
||||||
* TrackSelector#onSelectionActivated(Object)} should the selection be activated.
|
* TrackSelector#onSelectionActivated(Object)} should the selection be activated.
|
||||||
*/
|
*/
|
||||||
public TrackSelectorResult(
|
public TrackSelectorResult(
|
||||||
RendererConfiguration[] rendererConfigurations, TrackSelection[] selections, Object info) {
|
RendererConfiguration[] rendererConfigurations,
|
||||||
|
TrackSelection[] selections,
|
||||||
|
Object info) {
|
||||||
this.rendererConfigurations = rendererConfigurations;
|
this.rendererConfigurations = rendererConfigurations;
|
||||||
this.selections = new TrackSelectionArray(selections);
|
this.selections = new TrackSelectionArray(selections);
|
||||||
this.info = info;
|
this.info = info;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import android.net.Uri;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
|
|
||||||
|
|
@ -41,8 +42,8 @@ public final class DataSchemeDataSource implements DataSource {
|
||||||
if (!SCHEME_DATA.equals(scheme)) {
|
if (!SCHEME_DATA.equals(scheme)) {
|
||||||
throw new ParserException("Unsupported scheme: " + scheme);
|
throw new ParserException("Unsupported scheme: " + scheme);
|
||||||
}
|
}
|
||||||
String[] uriParts = uri.getSchemeSpecificPart().split(",");
|
String[] uriParts = Util.split(uri.getSchemeSpecificPart(), ",");
|
||||||
if (uriParts.length > 2) {
|
if (uriParts.length != 2) {
|
||||||
throw new ParserException("Unexpected URI format: " + uri);
|
throw new ParserException("Unexpected URI format: " + uri);
|
||||||
}
|
}
|
||||||
String dataString = uriParts[1];
|
String dataString = uriParts[1];
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,6 @@ public final class Loader implements LoaderErrorThrower {
|
||||||
*/
|
*/
|
||||||
void cancelLoad();
|
void cancelLoad();
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the load has been canceled.
|
|
||||||
*/
|
|
||||||
boolean isLoadCanceled();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs the load, returning on completion or cancellation.
|
* 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_IO_EXCEPTION = 3;
|
||||||
private static final int MSG_FATAL_ERROR = 4;
|
private static final int MSG_FATAL_ERROR = 4;
|
||||||
|
|
||||||
private final T loadable;
|
|
||||||
private final Loader.Callback<T> callback;
|
|
||||||
public final int defaultMinRetryCount;
|
public final int defaultMinRetryCount;
|
||||||
|
|
||||||
|
private final T loadable;
|
||||||
private final long startTimeMs;
|
private final long startTimeMs;
|
||||||
|
|
||||||
|
private @Nullable Loader.Callback<T> callback;
|
||||||
private IOException currentError;
|
private IOException currentError;
|
||||||
private int errorCount;
|
private int errorCount;
|
||||||
|
|
||||||
private volatile Thread executorThread;
|
private volatile Thread executorThread;
|
||||||
|
private volatile boolean canceled;
|
||||||
private volatile boolean released;
|
private volatile boolean released;
|
||||||
|
|
||||||
public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
|
public LoadTask(Looper looper, T loadable, Loader.Callback<T> callback,
|
||||||
|
|
@ -295,6 +292,7 @@ public final class Loader implements LoaderErrorThrower {
|
||||||
sendEmptyMessage(MSG_CANCEL);
|
sendEmptyMessage(MSG_CANCEL);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
canceled = true;
|
||||||
loadable.cancelLoad();
|
loadable.cancelLoad();
|
||||||
if (executorThread != null) {
|
if (executorThread != null) {
|
||||||
executorThread.interrupt();
|
executorThread.interrupt();
|
||||||
|
|
@ -304,6 +302,11 @@ public final class Loader implements LoaderErrorThrower {
|
||||||
finish();
|
finish();
|
||||||
long nowMs = SystemClock.elapsedRealtime();
|
long nowMs = SystemClock.elapsedRealtime();
|
||||||
callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true);
|
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() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
executorThread = Thread.currentThread();
|
executorThread = Thread.currentThread();
|
||||||
if (!loadable.isLoadCanceled()) {
|
if (!canceled) {
|
||||||
TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
|
TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
|
||||||
try {
|
try {
|
||||||
loadable.load();
|
loadable.load();
|
||||||
|
|
@ -328,7 +331,7 @@ public final class Loader implements LoaderErrorThrower {
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// The load was canceled.
|
// The load was canceled.
|
||||||
Assertions.checkState(loadable.isLoadCanceled());
|
Assertions.checkState(canceled);
|
||||||
if (!released) {
|
if (!released) {
|
||||||
sendEmptyMessage(MSG_END_OF_SOURCE);
|
sendEmptyMessage(MSG_END_OF_SOURCE);
|
||||||
}
|
}
|
||||||
|
|
@ -373,7 +376,7 @@ public final class Loader implements LoaderErrorThrower {
|
||||||
finish();
|
finish();
|
||||||
long nowMs = SystemClock.elapsedRealtime();
|
long nowMs = SystemClock.elapsedRealtime();
|
||||||
long durationMs = nowMs - startTimeMs;
|
long durationMs = nowMs - startTimeMs;
|
||||||
if (loadable.isLoadCanceled()) {
|
if (canceled) {
|
||||||
callback.onLoadCanceled(loadable, nowMs, durationMs, false);
|
callback.onLoadCanceled(loadable, nowMs, durationMs, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ public final class ParsingLoadable<T> implements Loadable {
|
||||||
private final Parser<? extends T> parser;
|
private final Parser<? extends T> parser;
|
||||||
|
|
||||||
private volatile T result;
|
private volatile T result;
|
||||||
private volatile boolean isCanceled;
|
|
||||||
private volatile long bytesLoaded;
|
private volatile long bytesLoaded;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -128,14 +127,7 @@ public final class ParsingLoadable<T> implements Loadable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void cancelLoad() {
|
public final void cancelLoad() {
|
||||||
// We don't actually cancel anything, but we need to record the cancellation so that
|
// Do nothing.
|
||||||
// isLoadCanceled can return the correct value.
|
|
||||||
isCanceled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final boolean isLoadCanceled() {
|
|
||||||
return isCanceled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.upstream.DataSink;
|
import com.google.android.exoplayer2.upstream.DataSink;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
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;
|
public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
|
||||||
|
|
||||||
private static final String TAG = "CacheDataSource";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags controlling the cache's behavior.
|
* Flags controlling the cache's behavior.
|
||||||
*/
|
*/
|
||||||
|
|
@ -221,7 +218,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
try {
|
try {
|
||||||
key = CacheUtil.getKey(dataSpec);
|
key = CacheUtil.getKey(dataSpec);
|
||||||
uri = dataSpec.uri;
|
uri = dataSpec.uri;
|
||||||
actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri);
|
actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri);
|
||||||
flags = dataSpec.flags;
|
flags = dataSpec.flags;
|
||||||
readPosition = dataSpec.position;
|
readPosition = dataSpec.position;
|
||||||
|
|
||||||
|
|
@ -272,7 +269,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
bytesRemaining -= bytesRead;
|
bytesRemaining -= bytesRead;
|
||||||
}
|
}
|
||||||
} else if (currentDataSpecLengthUnset) {
|
} else if (currentDataSpecLengthUnset) {
|
||||||
setBytesRemainingAndMaybeStoreLength(0);
|
setNoBytesRemainingAndMaybeStoreLength();
|
||||||
} else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
|
} else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
|
||||||
closeCurrentSource();
|
closeCurrentSource();
|
||||||
openNextSource(false);
|
openNextSource(false);
|
||||||
|
|
@ -281,7 +278,7 @@ public final class CacheDataSource implements DataSource {
|
||||||
return bytesRead;
|
return bytesRead;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
|
if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) {
|
||||||
setBytesRemainingAndMaybeStoreLength(0);
|
setNoBytesRemainingAndMaybeStoreLength();
|
||||||
return C.RESULT_END_OF_INPUT;
|
return C.RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
handleBeforeThrow(e);
|
handleBeforeThrow(e);
|
||||||
|
|
@ -402,46 +399,38 @@ public final class CacheDataSource implements DataSource {
|
||||||
currentDataSource = nextDataSource;
|
currentDataSource = nextDataSource;
|
||||||
currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;
|
currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET;
|
||||||
long resolvedLength = nextDataSource.open(nextDataSpec);
|
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() {
|
// Update bytesRemaining, actualUri and (if writing to cache) the cache metadata.
|
||||||
if (!isReadingFromUpstream()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
actualUri = currentDataSource.getUri();
|
|
||||||
maybeUpdateRedirectedUriMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeUpdateRedirectedUriMetadata() {
|
|
||||||
if (!isWritingToCache()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ContentMetadataMutations mutations = new ContentMetadataMutations();
|
ContentMetadataMutations mutations = new ContentMetadataMutations();
|
||||||
boolean isRedirected = !uri.equals(actualUri);
|
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
|
||||||
if (isRedirected) {
|
bytesRemaining = resolvedLength;
|
||||||
ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
|
ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining);
|
||||||
} else {
|
|
||||||
ContentMetadataInternal.removeRedirectedUri(mutations);
|
|
||||||
}
|
}
|
||||||
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);
|
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);
|
ContentMetadata contentMetadata = cache.getContentMetadata(key);
|
||||||
Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata);
|
Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata);
|
||||||
return redirectedUri == null ? uri : redirectedUri;
|
return redirectedUri == null ? defaultUri : redirectedUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isCausedByPositionOutOfRange(IOException e) {
|
private static boolean isCausedByPositionOutOfRange(IOException e) {
|
||||||
|
|
@ -458,13 +447,6 @@ public final class CacheDataSource implements DataSource {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException {
|
|
||||||
this.bytesRemaining = bytesRemaining;
|
|
||||||
if (isWritingToCache()) {
|
|
||||||
cache.setContentLength(key, readPosition + bytesRemaining);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isReadingFromUpstream() {
|
private boolean isReadingFromUpstream() {
|
||||||
return !isReadingFromCache();
|
return !isReadingFromCache();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,11 +129,11 @@ public final class CacheUtil {
|
||||||
cache,
|
cache,
|
||||||
new CacheDataSource(cache, upstream),
|
new CacheDataSource(cache, upstream),
|
||||||
new byte[DEFAULT_BUFFER_SIZE_BYTES],
|
new byte[DEFAULT_BUFFER_SIZE_BYTES],
|
||||||
null,
|
/* priorityTaskManager= */ null,
|
||||||
0,
|
/* priority= */ 0,
|
||||||
counters,
|
counters,
|
||||||
null,
|
isCanceled,
|
||||||
false);
|
/* enableEOFException= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
|
||||||
/** Helper classes to easily access and modify internal metadata values. */
|
/** 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 PREFIX = ContentMetadata.INTERNAL_METADATA_NAME_PREFIX;
|
||||||
private static final String METADATA_NAME_REDIRECTED_URI = PREFIX + "redir";
|
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) {
|
public static void removeRedirectedUri(ContentMetadataMutations mutations) {
|
||||||
mutations.remove(METADATA_NAME_REDIRECTED_URI);
|
mutations.remove(METADATA_NAME_REDIRECTED_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ContentMetadataInternal() {
|
||||||
|
// Prevent instantiation.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer2.upstream.crypto;
|
package com.google.android.exoplayer2.upstream.crypto;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
|
|
@ -49,7 +50,9 @@ public final class AesFlushingCipher {
|
||||||
flushedBlock = new byte[blockSize];
|
flushedBlock = new byte[blockSize];
|
||||||
long counter = offset / blockSize;
|
long counter = offset / blockSize;
|
||||||
int startPadding = (int) (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)));
|
new IvParameterSpec(getInitializationVector(nonce, counter)));
|
||||||
if (startPadding != 0) {
|
if (startPadding != 0) {
|
||||||
updateInPlace(new byte[startPadding], 0, startPadding);
|
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://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>
|
* @see <a href="https://www.w3.org/TR/ttml2/">Timed Text Markup Language 2 (TTML2) - 10.3.5</a>
|
||||||
**/
|
*/
|
||||||
public final class ColorParser {
|
public final class ColorParser {
|
||||||
|
|
||||||
private static final String RGB = "rgb";
|
private static final String RGB = "rgb";
|
||||||
|
|
@ -271,4 +271,7 @@ public final class ColorParser {
|
||||||
COLOR_MAP.put("yellowgreen", 0xFF9ACD32);
|
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);
|
GLES20.glDeleteTextures(1, textureIdHolder, 0);
|
||||||
}
|
}
|
||||||
} finally {
|
} 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)) {
|
if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) {
|
||||||
EGL14.eglDestroySurface(display, surface);
|
EGL14.eglDestroySurface(display, surface);
|
||||||
}
|
}
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
EGL14.eglDestroyContext(display, context);
|
EGL14.eglDestroyContext(display, context);
|
||||||
}
|
}
|
||||||
|
// EGL14.eglReleaseThread could crash before Android K (see [internal: b/11327779]).
|
||||||
|
if (Util.SDK_INT >= 19) {
|
||||||
|
EGL14.eglReleaseThread();
|
||||||
|
}
|
||||||
display = null;
|
display = null;
|
||||||
context = null;
|
context = null;
|
||||||
surface = null;
|
surface = null;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines common MIME types and helper methods.
|
* 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_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs";
|
||||||
public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif";
|
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.
|
* Whether the top-level type of {@code mimeType} is audio.
|
||||||
|
|
@ -144,7 +167,7 @@ public final class MimeTypes {
|
||||||
if (codecs == null) {
|
if (codecs == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String[] codecList = codecs.split(",");
|
String[] codecList = Util.split(codecs, ",");
|
||||||
for (String codec : codecList) {
|
for (String codec : codecList) {
|
||||||
String mimeType = getMediaMimeType(codec);
|
String mimeType = getMediaMimeType(codec);
|
||||||
if (mimeType != null && isVideo(mimeType)) {
|
if (mimeType != null && isVideo(mimeType)) {
|
||||||
|
|
@ -164,7 +187,7 @@ public final class MimeTypes {
|
||||||
if (codecs == null) {
|
if (codecs == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String[] codecList = codecs.split(",");
|
String[] codecList = Util.split(codecs, ",");
|
||||||
for (String codec : codecList) {
|
for (String codec : codecList) {
|
||||||
String mimeType = getMediaMimeType(codec);
|
String mimeType = getMediaMimeType(codec);
|
||||||
if (mimeType != null && isAudio(mimeType)) {
|
if (mimeType != null && isAudio(mimeType)) {
|
||||||
|
|
@ -222,8 +245,9 @@ public final class MimeTypes {
|
||||||
return MimeTypes.AUDIO_OPUS;
|
return MimeTypes.AUDIO_OPUS;
|
||||||
} else if (codec.startsWith("vorbis")) {
|
} else if (codec.startsWith("vorbis")) {
|
||||||
return MimeTypes.AUDIO_VORBIS;
|
return MimeTypes.AUDIO_VORBIS;
|
||||||
|
} else {
|
||||||
|
return getCustomMimeTypeForCodec(codec);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -236,18 +260,28 @@ public final class MimeTypes {
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getMimeTypeFromMp4ObjectType(int objectType) {
|
public static String getMimeTypeFromMp4ObjectType(int objectType) {
|
||||||
switch (objectType) {
|
switch (objectType) {
|
||||||
case 0x60:
|
|
||||||
case 0x61:
|
|
||||||
return MimeTypes.VIDEO_MPEG2;
|
|
||||||
case 0x20:
|
case 0x20:
|
||||||
return MimeTypes.VIDEO_MP4V;
|
return MimeTypes.VIDEO_MP4V;
|
||||||
case 0x21:
|
case 0x21:
|
||||||
return MimeTypes.VIDEO_H264;
|
return MimeTypes.VIDEO_H264;
|
||||||
case 0x23:
|
case 0x23:
|
||||||
return MimeTypes.VIDEO_H265;
|
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 0x69:
|
||||||
case 0x6B:
|
case 0x6B:
|
||||||
return MimeTypes.AUDIO_MPEG;
|
return MimeTypes.AUDIO_MPEG;
|
||||||
|
case 0xA3:
|
||||||
|
return MimeTypes.VIDEO_VC1;
|
||||||
|
case 0xB1:
|
||||||
|
return MimeTypes.VIDEO_VP9;
|
||||||
case 0x40:
|
case 0x40:
|
||||||
case 0x66:
|
case 0x66:
|
||||||
case 0x67:
|
case 0x67:
|
||||||
|
|
@ -298,7 +332,7 @@ public final class MimeTypes {
|
||||||
|| APPLICATION_CAMERA_MOTION.equals(mimeType)) {
|
|| APPLICATION_CAMERA_MOTION.equals(mimeType)) {
|
||||||
return C.TRACK_TYPE_METADATA;
|
return C.TRACK_TYPE_METADATA;
|
||||||
} else {
|
} else {
|
||||||
return C.TRACK_TYPE_UNKNOWN;
|
return getTrackTypeForCustomMimeType(mimeType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,4 +389,41 @@ public final class MimeTypes {
|
||||||
return mimeType.substring(0, indexOfSlash);
|
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;
|
bitOffset -= 8;
|
||||||
returnValue |= (data[byteOffset++] & 0xFF) << bitOffset;
|
returnValue |= (data[byteOffset++] & 0xFF) << bitOffset;
|
||||||
}
|
}
|
||||||
returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset;
|
returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
|
||||||
returnValue &= 0xFFFFFFFF >>> (32 - numBits);
|
returnValue &= 0xFFFFFFFF >>> (32 - numBits);
|
||||||
if (bitOffset == 8) {
|
if (bitOffset == 8) {
|
||||||
bitOffset = 0;
|
bitOffset = 0;
|
||||||
|
|
@ -199,17 +199,18 @@ public final class ParsableBitArray {
|
||||||
int to = offset + (numBits >> 3) /* numBits / 8 */;
|
int to = offset + (numBits >> 3) /* numBits / 8 */;
|
||||||
for (int i = offset; i < to; i++) {
|
for (int i = offset; i < to; i++) {
|
||||||
buffer[i] = (byte) (data[byteOffset++] << bitOffset);
|
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.
|
// Trailing bits.
|
||||||
int bitsLeft = numBits & 7 /* numBits % 8 */;
|
int bitsLeft = numBits & 7 /* numBits % 8 */;
|
||||||
if (bitsLeft == 0) {
|
if (bitsLeft == 0) {
|
||||||
return;
|
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) {
|
if (bitOffset + bitsLeft > 8) {
|
||||||
// We read the rest of data[byteOffset] and increase byteOffset.
|
// 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 -= 8;
|
||||||
}
|
}
|
||||||
bitOffset += bitsLeft;
|
bitOffset += bitsLeft;
|
||||||
|
|
@ -280,9 +281,10 @@ public final class ParsableBitArray {
|
||||||
int firstByteReadSize = Math.min(8 - bitOffset, numBits);
|
int firstByteReadSize = Math.min(8 - bitOffset, numBits);
|
||||||
int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize;
|
int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize;
|
||||||
int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1);
|
int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1);
|
||||||
data[byteOffset] &= firstByteBitmask;
|
data[byteOffset] = (byte) (data[byteOffset] & firstByteBitmask);
|
||||||
int firstByteInputBits = value >>> (numBits - firstByteReadSize);
|
int firstByteInputBits = value >>> (numBits - firstByteReadSize);
|
||||||
data[byteOffset] |= firstByteInputBits << firstByteRightPaddingSize;
|
data[byteOffset] =
|
||||||
|
(byte) (data[byteOffset] | (firstByteInputBits << firstByteRightPaddingSize));
|
||||||
remainingBitsToRead -= firstByteReadSize;
|
remainingBitsToRead -= firstByteReadSize;
|
||||||
int currentByteIndex = byteOffset + 1;
|
int currentByteIndex = byteOffset + 1;
|
||||||
while (remainingBitsToRead > 8) {
|
while (remainingBitsToRead > 8) {
|
||||||
|
|
@ -290,9 +292,11 @@ public final class ParsableBitArray {
|
||||||
remainingBitsToRead -= 8;
|
remainingBitsToRead -= 8;
|
||||||
}
|
}
|
||||||
int lastByteRightPaddingSize = 8 - remainingBitsToRead;
|
int lastByteRightPaddingSize = 8 - remainingBitsToRead;
|
||||||
data[currentByteIndex] &= (1 << lastByteRightPaddingSize) - 1;
|
data[currentByteIndex] =
|
||||||
|
(byte) (data[currentByteIndex] & ((1 << lastByteRightPaddingSize) - 1));
|
||||||
int lastByteInput = value & ((1 << remainingBitsToRead) - 1);
|
int lastByteInput = value & ((1 << remainingBitsToRead) - 1);
|
||||||
data[currentByteIndex] |= lastByteInput << lastByteRightPaddingSize;
|
data[currentByteIndex] =
|
||||||
|
(byte) (data[currentByteIndex] | (lastByteInput << lastByteRightPaddingSize));
|
||||||
skipBits(numBits);
|
skipBits(numBits);
|
||||||
assertValidOffset();
|
assertValidOffset();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -470,7 +470,7 @@ public final class ParsableByteArray {
|
||||||
if (lastIndex < limit && data[lastIndex] == 0) {
|
if (lastIndex < limit && data[lastIndex] == 0) {
|
||||||
stringLength--;
|
stringLength--;
|
||||||
}
|
}
|
||||||
String result = new String(data, position, stringLength);
|
String result = Util.fromUtf8Bytes(data, position, stringLength);
|
||||||
position += length;
|
position += length;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -489,7 +489,7 @@ public final class ParsableByteArray {
|
||||||
while (stringLimit < limit && data[stringLimit] != 0) {
|
while (stringLimit < limit && data[stringLimit] != 0) {
|
||||||
stringLimit++;
|
stringLimit++;
|
||||||
}
|
}
|
||||||
String string = new String(data, position, stringLimit - position);
|
String string = Util.fromUtf8Bytes(data, position, stringLimit - position);
|
||||||
position = stringLimit;
|
position = stringLimit;
|
||||||
if (position < limit) {
|
if (position < limit) {
|
||||||
position++;
|
position++;
|
||||||
|
|
@ -520,7 +520,7 @@ public final class ParsableByteArray {
|
||||||
// There's a byte order mark at the start of the line. Discard it.
|
// There's a byte order mark at the start of the line. Discard it.
|
||||||
position += 3;
|
position += 3;
|
||||||
}
|
}
|
||||||
String line = new String(data, position, lineLimit - position);
|
String line = Util.fromUtf8Bytes(data, position, lineLimit - position);
|
||||||
position = lineLimit;
|
position = lineLimit;
|
||||||
if (position == limit) {
|
if (position == limit) {
|
||||||
return line;
|
return line;
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ public final class ParsableNalUnitBitArray {
|
||||||
returnValue |= (data[byteOffset] & 0xFF) << bitOffset;
|
returnValue |= (data[byteOffset] & 0xFF) << bitOffset;
|
||||||
byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
|
byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
|
||||||
}
|
}
|
||||||
returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset;
|
returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
|
||||||
returnValue &= 0xFFFFFFFF >>> (32 - numBits);
|
returnValue &= 0xFFFFFFFF >>> (32 - numBits);
|
||||||
if (bitOffset == 8) {
|
if (bitOffset == 8) {
|
||||||
bitOffset = 0;
|
bitOffset = 0;
|
||||||
|
|
|
||||||
|
|
@ -311,10 +311,10 @@ public final class Util {
|
||||||
* Returns a normalized RFC 639-2/T code for {@code language}.
|
* 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.
|
* @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
|
* @return The all-lowercase normalized code, or null if the input was null, or {@code
|
||||||
* {@code language.toLowerCase()} if the language could not be normalized.
|
* language.toLowerCase()} if the language could not be normalized.
|
||||||
*/
|
*/
|
||||||
public static String normalizeLanguageCode(String language) {
|
public static @Nullable String normalizeLanguageCode(@Nullable String language) {
|
||||||
try {
|
try {
|
||||||
return language == null ? null : new Locale(language).getISO3Language();
|
return language == null ? null : new Locale(language).getISO3Language();
|
||||||
} catch (MissingResourceException e) {
|
} catch (MissingResourceException e) {
|
||||||
|
|
@ -332,6 +332,18 @@ public final class Util {
|
||||||
return new String(bytes, Charset.forName(C.UTF8_NAME));
|
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.
|
* 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));
|
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').
|
* 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)) {
|
if (TextUtils.isEmpty(codecs)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String[] codecArray = codecs.trim().split("(\\s*,\\s*)");
|
String[] codecArray = split(codecs.trim(), "(\\s*,\\s*)");
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
for (String codec : codecArray) {
|
for (String codec : codecArray) {
|
||||||
if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
|
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 we managed to read sys.display-size, attempt to parse it.
|
||||||
if (!TextUtils.isEmpty(sysDisplaySize)) {
|
if (!TextUtils.isEmpty(sysDisplaySize)) {
|
||||||
try {
|
try {
|
||||||
String[] sysDisplaySizeParts = sysDisplaySize.trim().split("x");
|
String[] sysDisplaySizeParts = split(sysDisplaySize.trim(), "x");
|
||||||
if (sysDisplaySizeParts.length == 2) {
|
if (sysDisplaySizeParts.length == 2) {
|
||||||
int width = Integer.parseInt(sysDisplaySizeParts[0]);
|
int width = Integer.parseInt(sysDisplaySizeParts[0]);
|
||||||
int height = Integer.parseInt(sysDisplaySizeParts[1]);
|
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_INIT = 1;
|
||||||
private static final int MSG_RELEASE = 2;
|
private static final int MSG_RELEASE = 2;
|
||||||
|
|
||||||
private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure;
|
private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture;
|
||||||
private @MonotonicNonNull Handler handler;
|
private @MonotonicNonNull Handler handler;
|
||||||
private @Nullable Error initError;
|
private @Nullable Error initError;
|
||||||
private @Nullable RuntimeException initException;
|
private @Nullable RuntimeException initException;
|
||||||
|
|
@ -169,7 +169,7 @@ public final class DummySurface extends Surface {
|
||||||
public DummySurface init(@SecureMode int secureMode) {
|
public DummySurface init(@SecureMode int secureMode) {
|
||||||
start();
|
start();
|
||||||
handler = new Handler(getLooper(), /* callback= */ this);
|
handler = new Handler(getLooper(), /* callback= */ this);
|
||||||
eglSurfaceTexure = new EGLSurfaceTexture(handler);
|
eglSurfaceTexture = new EGLSurfaceTexture(handler);
|
||||||
boolean wasInterrupted = false;
|
boolean wasInterrupted = false;
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget();
|
handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget();
|
||||||
|
|
@ -232,16 +232,16 @@ public final class DummySurface extends Surface {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initInternal(@SecureMode int secureMode) {
|
private void initInternal(@SecureMode int secureMode) {
|
||||||
Assertions.checkNotNull(eglSurfaceTexure);
|
Assertions.checkNotNull(eglSurfaceTexture);
|
||||||
eglSurfaceTexure.init(secureMode);
|
eglSurfaceTexture.init(secureMode);
|
||||||
this.surface =
|
this.surface =
|
||||||
new DummySurface(
|
new DummySurface(
|
||||||
this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);
|
this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseInternal() {
|
private void releaseInternal() {
|
||||||
Assertions.checkNotNull(eglSurfaceTexure);
|
Assertions.checkNotNull(eglSurfaceTexture);
|
||||||
eglSurfaceTexure.release();
|
eglSurfaceTexture.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||||
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
|
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
|
||||||
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context);
|
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context);
|
||||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||||
deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
|
deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
|
||||||
pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
|
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/3835,
|
||||||
// https://github.com/google/ExoPlayer/issues/4006,
|
// https://github.com/google/ExoPlayer/issues/4006,
|
||||||
// https://github.com/google/ExoPlayer/issues/4084,
|
// https://github.com/google/ExoPlayer/issues/4084,
|
||||||
// https://github.com/google/ExoPlayer/issues/4104.
|
// https://github.com/google/ExoPlayer/issues/4104,
|
||||||
// https://github.com/google/ExoPlayer/issues/4134.
|
// https://github.com/google/ExoPlayer/issues/4134,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4315.
|
||||||
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013)
|
return (("deb".equals(Util.DEVICE) // Nexus 7 (2013)
|
||||||
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013)
|
|| "flo".equals(Util.DEVICE) // Nexus 7 (2013)
|
||||||
|| "mido".equals(Util.DEVICE) // Redmi Note 4
|
|| "mido".equals(Util.DEVICE) // Redmi Note 4
|
||||||
|
|
@ -1192,7 +1193,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||||
|| "M5c".equals(Util.DEVICE) // Meizu M5C
|
|| "M5c".equals(Util.DEVICE) // Meizu M5C
|
||||||
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E
|
|| "QM16XE_U".equals(Util.DEVICE) // Philips QM163E
|
||||||
|| "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note
|
|| "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))
|
&& "OMX.MTK.VIDEO.DECODER.AVC".equals(name))
|
||||||
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite
|
|| (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite
|
||||||
|| "CAM-L21".equals(Util.MODEL)) // Huawei Y6II
|
|| "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.
|
* @param context A context from which information about the default display can be retrieved.
|
||||||
*/
|
*/
|
||||||
public VideoFrameReleaseTimeHelper(@Nullable Context context) {
|
public VideoFrameReleaseTimeHelper(@Nullable Context context) {
|
||||||
windowManager = context == null ? null
|
if (context != null) {
|
||||||
: (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
context = context.getApplicationContext();
|
||||||
|
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||||
|
} else {
|
||||||
|
windowManager = null;
|
||||||
|
}
|
||||||
if (windowManager != null) {
|
if (windowManager != null) {
|
||||||
displayListener = Util.SDK_INT >= 17 ? maybeBuildDefaultDisplayListenerV17(context) : null;
|
displayListener = Util.SDK_INT >= 17 ? maybeBuildDefaultDisplayListenerV17(context) : null;
|
||||||
vsyncSampler = VSyncSampler.getInstance();
|
vsyncSampler = VSyncSampler.getInstance();
|
||||||
|
|
|
||||||
|
|
@ -1980,6 +1980,105 @@ public final class ExoPlayerTest {
|
||||||
.inOrder();
|
.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.
|
// Internal methods.
|
||||||
|
|
||||||
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
|
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
|
||||||
|
|
|
||||||
|
|
@ -391,11 +391,6 @@ public final class AdaptiveTrackSelectionTest {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoadCanceled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() throws IOException, InterruptedException {
|
public void load() throws IOException, InterruptedException {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer2.source.dash;
|
package com.google.android.exoplayer2.source.dash;
|
||||||
|
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.util.SparseIntArray;
|
import android.util.SparseIntArray;
|
||||||
|
|
@ -72,7 +73,7 @@ import java.util.List;
|
||||||
private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>
|
private final IdentityHashMap<ChunkSampleStream<DashChunkSource>, PlayerTrackEmsgHandler>
|
||||||
trackEmsgHandlerBySampleStream;
|
trackEmsgHandlerBySampleStream;
|
||||||
|
|
||||||
private Callback callback;
|
private @Nullable Callback callback;
|
||||||
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
|
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
|
||||||
private EventSampleStream[] eventSampleStreams;
|
private EventSampleStream[] eventSampleStreams;
|
||||||
private SequenceableLoader compositeSequenceableLoader;
|
private SequenceableLoader compositeSequenceableLoader;
|
||||||
|
|
@ -150,6 +151,7 @@ import java.util.List;
|
||||||
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
|
||||||
sampleStream.release(this);
|
sampleStream.release(this);
|
||||||
}
|
}
|
||||||
|
callback = null;
|
||||||
eventDispatcher.mediaPeriodReleased();
|
eventDispatcher.mediaPeriodReleased();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,15 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
|
||||||
public final class DashWrappingSegmentIndex implements DashSegmentIndex {
|
public final class DashWrappingSegmentIndex implements DashSegmentIndex {
|
||||||
|
|
||||||
private final ChunkIndex chunkIndex;
|
private final ChunkIndex chunkIndex;
|
||||||
|
private final long timeOffsetUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param chunkIndex The {@link ChunkIndex} to wrap.
|
* @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.chunkIndex = chunkIndex;
|
||||||
|
this.timeOffsetUs = timeOffsetUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -45,7 +48,7 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getTimeUs(long segmentNum) {
|
public long getTimeUs(long segmentNum) {
|
||||||
return chunkIndex.timesUs[(int) segmentNum];
|
return chunkIndex.timesUs[(int) segmentNum] - timeOffsetUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -61,7 +64,7 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getSegmentNum(long timeUs, long periodDurationUs) {
|
public long getSegmentNum(long timeUs, long periodDurationUs) {
|
||||||
return chunkIndex.getChunkIndex(timeUs);
|
return chunkIndex.getChunkIndex(timeUs + timeOffsetUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||||
if (representationHolder.segmentIndex == null) {
|
if (representationHolder.segmentIndex == null) {
|
||||||
SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap();
|
SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap();
|
||||||
if (seekMap != null) {
|
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;
|
return index;
|
||||||
}
|
}
|
||||||
ChunkIndex seekMap = DashUtil.loadChunkIndex(dataSource, trackType, representation);
|
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
|
// the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods
|
||||||
// in TrackSelection to avoid unexpected behavior.
|
// in TrackSelection to avoid unexpected behavior.
|
||||||
private TrackSelection trackSelection;
|
private TrackSelection trackSelection;
|
||||||
private long liveEdgeTimeUs;
|
private long liveEdgeInPeriodTimeUs;
|
||||||
private boolean seenExpectedPlaylistError;
|
private boolean seenExpectedPlaylistError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -128,7 +128,7 @@ import java.util.List;
|
||||||
this.variants = variants;
|
this.variants = variants;
|
||||||
this.timestampAdjusterProvider = timestampAdjusterProvider;
|
this.timestampAdjusterProvider = timestampAdjusterProvider;
|
||||||
this.muxedCaptionFormats = muxedCaptionFormats;
|
this.muxedCaptionFormats = muxedCaptionFormats;
|
||||||
liveEdgeTimeUs = C.TIME_UNSET;
|
liveEdgeInPeriodTimeUs = C.TIME_UNSET;
|
||||||
Format[] variantFormats = new Format[variants.length];
|
Format[] variantFormats = new Format[variants.length];
|
||||||
int[] initialTrackSelection = new int[variants.length];
|
int[] initialTrackSelection = new int[variants.length];
|
||||||
for (int i = 0; i < variants.length; i++) {
|
for (int i = 0; i < variants.length; i++) {
|
||||||
|
|
@ -254,16 +254,17 @@ import java.util.List;
|
||||||
|
|
||||||
// Select the chunk.
|
// Select the chunk.
|
||||||
long chunkMediaSequence;
|
long chunkMediaSequence;
|
||||||
|
long startOfPlaylistInPeriodUs =
|
||||||
|
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||||
if (previous == null || switchingVariant) {
|
if (previous == null || switchingVariant) {
|
||||||
long targetPositionUs = (previous == null || independentSegments) ? loadPositionUs
|
long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs;
|
||||||
: previous.startTimeUs;
|
long targetPositionInPeriodUs =
|
||||||
if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) {
|
(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.
|
// If the playlist is too old to contain the chunk, we need to refresh it.
|
||||||
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
|
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
|
||||||
} else {
|
} else {
|
||||||
long positionOfPlaylistInPeriodUs =
|
long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs;
|
||||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
|
||||||
long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs;
|
|
||||||
chunkMediaSequence =
|
chunkMediaSequence =
|
||||||
Util.binarySearchFloor(
|
Util.binarySearchFloor(
|
||||||
mediaPlaylist.segments,
|
mediaPlaylist.segments,
|
||||||
|
|
@ -277,6 +278,8 @@ import java.util.List;
|
||||||
selectedVariantIndex = oldVariantIndex;
|
selectedVariantIndex = oldVariantIndex;
|
||||||
selectedUrl = variants[selectedVariantIndex];
|
selectedUrl = variants[selectedVariantIndex];
|
||||||
mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
|
mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
|
||||||
|
startOfPlaylistInPeriodUs =
|
||||||
|
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||||
chunkMediaSequence = previous.getNextChunkIndex();
|
chunkMediaSequence = previous.getNextChunkIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,9 +334,7 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute start time of the next chunk.
|
// Compute start time of the next chunk.
|
||||||
long positionOfPlaylistInPeriodUs =
|
long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
|
||||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
|
||||||
long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
|
|
||||||
int discontinuitySequence = mediaPlaylist.discontinuitySequence
|
int discontinuitySequence = mediaPlaylist.discontinuitySequence
|
||||||
+ segment.relativeDiscontinuitySequence;
|
+ segment.relativeDiscontinuitySequence;
|
||||||
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
||||||
|
|
@ -420,12 +421,17 @@ import java.util.List;
|
||||||
// Private methods.
|
// Private methods.
|
||||||
|
|
||||||
private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {
|
private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {
|
||||||
final boolean resolveTimeToLiveEdgePossible = liveEdgeTimeUs != C.TIME_UNSET;
|
final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET;
|
||||||
return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET;
|
return resolveTimeToLiveEdgePossible
|
||||||
|
? liveEdgeInPeriodTimeUs - playbackPositionUs
|
||||||
|
: C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) {
|
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,
|
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex,
|
||||||
|
|
|
||||||
|
|
@ -206,11 +206,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
loadCanceled = true;
|
loadCanceled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoadCanceled() {
|
|
||||||
return loadCanceled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() throws IOException, InterruptedException {
|
public void load() throws IOException, InterruptedException {
|
||||||
maybeLoadInitData();
|
maybeLoadInitData();
|
||||||
|
|
@ -242,7 +237,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition);
|
initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
Util.closeQuietly(dataSource);
|
Util.closeQuietly(initDataSource);
|
||||||
}
|
}
|
||||||
initLoadCompleted = true;
|
initLoadCompleted = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source.hls;
|
package com.google.android.exoplayer2.source.hls;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
|
|
@ -57,7 +58,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
private final boolean allowChunklessPreparation;
|
private final boolean allowChunklessPreparation;
|
||||||
|
|
||||||
private Callback callback;
|
private @Nullable Callback callback;
|
||||||
private int pendingPrepareCount;
|
private int pendingPrepareCount;
|
||||||
private TrackGroupArray trackGroups;
|
private TrackGroupArray trackGroups;
|
||||||
private HlsSampleStreamWrapper[] sampleStreamWrappers;
|
private HlsSampleStreamWrapper[] sampleStreamWrappers;
|
||||||
|
|
@ -96,6 +97,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||||
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
|
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
|
||||||
sampleStreamWrapper.release();
|
sampleStreamWrapper.release();
|
||||||
}
|
}
|
||||||
|
callback = null;
|
||||||
eventDispatcher.mediaPeriodReleased();
|
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.SequenceableLoader;
|
||||||
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
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.HlsMediaPlaylist;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
|
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
||||||
|
|
@ -58,6 +59,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
|
|
||||||
private HlsExtractorFactory extractorFactory;
|
private HlsExtractorFactory extractorFactory;
|
||||||
private @Nullable ParsingLoadable.Parser<HlsPlaylist> playlistParser;
|
private @Nullable ParsingLoadable.Parser<HlsPlaylist> playlistParser;
|
||||||
|
private @Nullable HlsPlaylistTracker playlistTracker;
|
||||||
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
private int minLoadableRetryCount;
|
private int minLoadableRetryCount;
|
||||||
private boolean allowChunklessPreparation;
|
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
|
* Sets the parser to parse HLS playlists. The default is an instance of {@link
|
||||||
* HlsPlaylistParser}.
|
* HlsPlaylistParser}.
|
||||||
*
|
*
|
||||||
|
* <p>Must not be called after calling {@link #setPlaylistTracker} on the same builder.
|
||||||
|
*
|
||||||
* @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists.
|
* @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists.
|
||||||
* @return This factory, for convenience.
|
* @return This factory, for convenience.
|
||||||
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||||
*/
|
*/
|
||||||
public Factory setPlaylistParser(ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
|
public Factory setPlaylistParser(ParsingLoadable.Parser<HlsPlaylist> playlistParser) {
|
||||||
Assertions.checkState(!isCreateCalled);
|
Assertions.checkState(!isCreateCalled);
|
||||||
|
Assertions.checkState(playlistTracker == null, "A playlist tracker has already been set.");
|
||||||
this.playlistParser = Assertions.checkNotNull(playlistParser);
|
this.playlistParser = Assertions.checkNotNull(playlistParser);
|
||||||
return this;
|
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
|
* 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
|
* 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
|
@Override
|
||||||
public HlsMediaSource createMediaSource(Uri playlistUri) {
|
public HlsMediaSource createMediaSource(Uri playlistUri) {
|
||||||
isCreateCalled = true;
|
isCreateCalled = true;
|
||||||
if (playlistParser == null) {
|
if (playlistTracker == null) {
|
||||||
playlistParser = new HlsPlaylistParser();
|
playlistTracker =
|
||||||
|
new DefaultHlsPlaylistTracker(
|
||||||
|
hlsDataSourceFactory,
|
||||||
|
minLoadableRetryCount,
|
||||||
|
playlistParser != null ? playlistParser : new HlsPlaylistParser());
|
||||||
}
|
}
|
||||||
return new HlsMediaSource(
|
return new HlsMediaSource(
|
||||||
playlistUri,
|
playlistUri,
|
||||||
|
|
@ -196,7 +223,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
extractorFactory,
|
extractorFactory,
|
||||||
compositeSequenceableLoaderFactory,
|
compositeSequenceableLoaderFactory,
|
||||||
minLoadableRetryCount,
|
minLoadableRetryCount,
|
||||||
playlistParser,
|
playlistTracker,
|
||||||
allowChunklessPreparation,
|
allowChunklessPreparation,
|
||||||
tag);
|
tag);
|
||||||
}
|
}
|
||||||
|
|
@ -233,12 +260,10 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
private final HlsDataSourceFactory dataSourceFactory;
|
private final HlsDataSourceFactory dataSourceFactory;
|
||||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
private final int minLoadableRetryCount;
|
private final int minLoadableRetryCount;
|
||||||
private final ParsingLoadable.Parser<HlsPlaylist> playlistParser;
|
|
||||||
private final boolean allowChunklessPreparation;
|
private final boolean allowChunklessPreparation;
|
||||||
|
private final HlsPlaylistTracker playlistTracker;
|
||||||
private final @Nullable Object tag;
|
private final @Nullable Object tag;
|
||||||
|
|
||||||
private HlsPlaylistTracker playlistTracker;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param manifestUri The {@link Uri} of the HLS manifest.
|
* @param manifestUri The {@link Uri} of the HLS manifest.
|
||||||
* @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests,
|
* @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests,
|
||||||
|
|
@ -276,8 +301,13 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
int minLoadableRetryCount,
|
int minLoadableRetryCount,
|
||||||
Handler eventHandler,
|
Handler eventHandler,
|
||||||
MediaSourceEventListener eventListener) {
|
MediaSourceEventListener eventListener) {
|
||||||
this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory),
|
this(
|
||||||
HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener,
|
manifestUri,
|
||||||
|
new DefaultHlsDataSourceFactory(dataSourceFactory),
|
||||||
|
HlsExtractorFactory.DEFAULT,
|
||||||
|
minLoadableRetryCount,
|
||||||
|
eventHandler,
|
||||||
|
eventListener,
|
||||||
new HlsPlaylistParser());
|
new HlsPlaylistParser());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,7 +339,8 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
extractorFactory,
|
extractorFactory,
|
||||||
new DefaultCompositeSequenceableLoaderFactory(),
|
new DefaultCompositeSequenceableLoaderFactory(),
|
||||||
minLoadableRetryCount,
|
minLoadableRetryCount,
|
||||||
playlistParser,
|
new DefaultHlsPlaylistTracker(
|
||||||
|
dataSourceFactory, minLoadableRetryCount, new HlsPlaylistParser()),
|
||||||
/* allowChunklessPreparation= */ false,
|
/* allowChunklessPreparation= */ false,
|
||||||
/* tag= */ null);
|
/* tag= */ null);
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
|
|
@ -323,7 +354,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
HlsExtractorFactory extractorFactory,
|
HlsExtractorFactory extractorFactory,
|
||||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
||||||
int minLoadableRetryCount,
|
int minLoadableRetryCount,
|
||||||
ParsingLoadable.Parser<HlsPlaylist> playlistParser,
|
HlsPlaylistTracker playlistTracker,
|
||||||
boolean allowChunklessPreparation,
|
boolean allowChunklessPreparation,
|
||||||
@Nullable Object tag) {
|
@Nullable Object tag) {
|
||||||
this.manifestUri = manifestUri;
|
this.manifestUri = manifestUri;
|
||||||
|
|
@ -331,7 +362,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
this.extractorFactory = extractorFactory;
|
this.extractorFactory = extractorFactory;
|
||||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||||
this.playlistParser = playlistParser;
|
this.playlistTracker = playlistTracker;
|
||||||
this.allowChunklessPreparation = allowChunklessPreparation;
|
this.allowChunklessPreparation = allowChunklessPreparation;
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
}
|
}
|
||||||
|
|
@ -339,9 +370,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
@Override
|
@Override
|
||||||
public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
|
public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
|
||||||
EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
|
EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
|
||||||
playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher,
|
playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this);
|
||||||
minLoadableRetryCount, this, playlistParser);
|
|
||||||
playlistTracker.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -373,7 +402,6 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||||
public void releaseSourceInternal() {
|
public void releaseSourceInternal() {
|
||||||
if (playlistTracker != null) {
|
if (playlistTracker != null) {
|
||||||
playlistTracker.release();
|
playlistTracker.release();
|
||||||
playlistTracker = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ public final class HlsDownloadHelper extends DownloadHelper {
|
||||||
public TrackGroupArray getTrackGroups(int periodIndex) {
|
public TrackGroupArray getTrackGroups(int periodIndex) {
|
||||||
Assertions.checkNotNull(playlist);
|
Assertions.checkNotNull(playlist);
|
||||||
if (playlist instanceof HlsMediaPlaylist) {
|
if (playlist instanceof HlsMediaPlaylist) {
|
||||||
|
renditionTypes = new int[0];
|
||||||
return TrackGroupArray.EMPTY;
|
return TrackGroupArray.EMPTY;
|
||||||
}
|
}
|
||||||
// TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction.
|
// 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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;
|
package com.google.android.exoplayer2.source.hls.playlist;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.support.annotation.Nullable;
|
||||||
import android.os.SystemClock;
|
|
||||||
import com.google.android.exoplayer2.C;
|
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.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.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.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
|
* Tracks playlists associated to an HLS stream and provides snapshots.
|
||||||
* master playlist or a media playlist.
|
*
|
||||||
|
* <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 {
|
||||||
|
|
||||||
/**
|
/** Listener for primary playlist changes. */
|
||||||
* Thrown when a playlist is considered to be stuck due to a server side error.
|
interface PrimaryPlaylistListener {
|
||||||
*/
|
|
||||||
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 {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the primary playlist changes.
|
* Called when the primary playlist changes.
|
||||||
|
|
@ -85,10 +47,8 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
||||||
void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
|
void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Called on playlist loading events. */
|
||||||
* Called on playlist loading events.
|
interface PlaylistEventListener {
|
||||||
*/
|
|
||||||
public interface PlaylistEventListener {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called a playlist changes.
|
* Called a playlist changes.
|
||||||
|
|
@ -105,141 +65,107 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
|
||||||
boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist);
|
boolean onPlaylistError(HlsUrl url, boolean shouldBlacklist);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Thrown when a playlist is considered to be stuck due to a server side error. */
|
||||||
* Coefficient applied on the target duration of a playlist to determine the amount of time after
|
final class PlaylistStuckException extends IOException {
|
||||||
* which an unchanging playlist is considered stuck.
|
|
||||||
*/
|
|
||||||
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5;
|
|
||||||
|
|
||||||
private final Uri initialPlaylistUri;
|
/** The url of the stuck playlist. */
|
||||||
private final HlsDataSourceFactory dataSourceFactory;
|
public final String url;
|
||||||
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;
|
|
||||||
|
|
||||||
private HlsMasterPlaylist masterPlaylist;
|
/**
|
||||||
private HlsUrl primaryHlsUrl;
|
* Creates an instance.
|
||||||
private HlsMediaPlaylist primaryUrlSnapshot;
|
*
|
||||||
private boolean isLive;
|
* @param url See {@link #url}.
|
||||||
private long initialStartTimeUs;
|
*/
|
||||||
|
public PlaylistStuckException(String url) {
|
||||||
/**
|
this.url = url;
|
||||||
* @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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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.
|
* Registers a listener to receive events from the playlist tracker.
|
||||||
*
|
*
|
||||||
* @param listener The listener.
|
* @param listener The listener.
|
||||||
*/
|
*/
|
||||||
public void addListener(PlaylistEventListener listener) {
|
void addListener(PlaylistEventListener listener);
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregisters a listener.
|
* Unregisters a listener.
|
||||||
*
|
*
|
||||||
* @param listener The listener to unregister.
|
* @param listener The listener to unregister.
|
||||||
*/
|
*/
|
||||||
public void removeListener(PlaylistEventListener listener) {
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the master playlist.
|
* 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.
|
* @return The master playlist. Null if the initial playlist has yet to be loaded.
|
||||||
*/
|
*/
|
||||||
public HlsMasterPlaylist getMasterPlaylist() {
|
@Nullable
|
||||||
return masterPlaylist;
|
HlsMasterPlaylist getMasterPlaylist();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the most recent snapshot available of the playlist referenced by the provided
|
* Returns the most recent snapshot available of the playlist referenced by the provided {@link
|
||||||
* {@link HlsUrl}.
|
* HlsUrl}.
|
||||||
*
|
*
|
||||||
* @param url The {@link HlsUrl} corresponding to the requested media playlist.
|
* @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
|
* @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May
|
||||||
* be null if no snapshot has been loaded yet.
|
* be null if no snapshot has been loaded yet.
|
||||||
*/
|
*/
|
||||||
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
|
@Nullable
|
||||||
HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
|
HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url);
|
||||||
if (snapshot != null) {
|
|
||||||
maybeSetPrimaryUrl(url);
|
|
||||||
}
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
|
* Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no
|
||||||
* media playlist has been loaded.
|
* media playlist has been loaded.
|
||||||
*/
|
*/
|
||||||
public long getInitialStartTimeUs() {
|
long getInitialStartTimeUs();
|
||||||
return initialStartTimeUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
|
* 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
|
* 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.
|
* playlist is not valid then some of the segments may no longer be available.
|
||||||
|
*
|
||||||
* @param url The {@link HlsUrl}.
|
* @param url The {@link HlsUrl}.
|
||||||
* @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
|
* @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
|
||||||
* valid.
|
* valid.
|
||||||
*/
|
*/
|
||||||
public boolean isSnapshotValid(HlsUrl url) {
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the tracker is having trouble refreshing the master playlist or the primary playlist, this
|
* 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.
|
* @throws IOException The underlying error.
|
||||||
*/
|
*/
|
||||||
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
|
void maybeThrowPrimaryPlaylistRefreshError() throws IOException;
|
||||||
initialPlaylistLoader.maybeThrowError();
|
|
||||||
if (primaryHlsUrl != null) {
|
|
||||||
maybeThrowPlaylistRefreshError(primaryHlsUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the playlist is having trouble refreshing the playlist referenced by the given
|
* If the playlist is having trouble refreshing the playlist referenced by the given {@link
|
||||||
* {@link HlsUrl}, this method throws the underlying error.
|
* HlsUrl}, this method throws the underlying error.
|
||||||
*
|
*
|
||||||
* @param url The {@link HlsUrl}.
|
* @param url The {@link HlsUrl}.
|
||||||
* @throws IOException The underyling error.
|
* @throws IOException The underyling error.
|
||||||
*/
|
*/
|
||||||
public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException {
|
void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException;
|
||||||
playlistBundles.get(url).maybeThrowPlaylistRefreshError();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* @param url The {@link HlsUrl} of the playlist to be refreshed.
|
||||||
*/
|
*/
|
||||||
public void refreshPlaylist(HlsUrl url) {
|
void refreshPlaylist(HlsUrl url);
|
||||||
playlistBundles.get(url).loadPlaylist();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether this is live content.
|
* Returns whether the tracked playlists describe a live stream.
|
||||||
*
|
*
|
||||||
* @return True if the content is live. False otherwise.
|
* @return True if the content is live. False otherwise.
|
||||||
*/
|
*/
|
||||||
public boolean isLive() {
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source.smoothstreaming;
|
package com.google.android.exoplayer2.source.smoothstreaming;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
|
|
@ -52,7 +53,7 @@ import java.util.ArrayList;
|
||||||
private final TrackEncryptionBox[] trackEncryptionBoxes;
|
private final TrackEncryptionBox[] trackEncryptionBoxes;
|
||||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
|
|
||||||
private Callback callback;
|
private @Nullable Callback callback;
|
||||||
private SsManifest manifest;
|
private SsManifest manifest;
|
||||||
private ChunkSampleStream<SsChunkSource>[] sampleStreams;
|
private ChunkSampleStream<SsChunkSource>[] sampleStreams;
|
||||||
private SequenceableLoader compositeSequenceableLoader;
|
private SequenceableLoader compositeSequenceableLoader;
|
||||||
|
|
@ -98,6 +99,7 @@ import java.util.ArrayList;
|
||||||
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
|
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
|
||||||
sampleStream.release();
|
sampleStream.release();
|
||||||
}
|
}
|
||||||
|
callback = null;
|
||||||
eventDispatcher.mediaPeriodReleased();
|
eventDispatcher.mediaPeriodReleased();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ public final class SubtitleView extends View implements TextOutput {
|
||||||
// Calculate the bounds after padding is taken into account.
|
// Calculate the bounds after padding is taken into account.
|
||||||
int left = getLeft() + getPaddingLeft();
|
int left = getLeft() + getPaddingLeft();
|
||||||
int top = rawTop + getPaddingTop();
|
int top = rawTop + getPaddingTop();
|
||||||
int right = getRight() + getPaddingRight();
|
int right = getRight() - getPaddingRight();
|
||||||
int bottom = rawBottom - getPaddingBottom();
|
int bottom = rawBottom - getPaddingBottom();
|
||||||
if (bottom <= top || right <= left) {
|
if (bottom <= top || right <= left) {
|
||||||
// No space to draw subtitles.
|
// No space to draw subtitles.
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,9 @@ public class TrackSelectionView extends LinearLayout {
|
||||||
removeViewAt(i);
|
removeViewAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackSelector == null) {
|
MappingTrackSelector.MappedTrackInfo trackInfo =
|
||||||
|
trackSelector == null ? null : trackSelector.getCurrentMappedTrackInfo();
|
||||||
|
if (trackSelector == null || trackInfo == null) {
|
||||||
// The view is not initialized.
|
// The view is not initialized.
|
||||||
disableView.setEnabled(false);
|
disableView.setEnabled(false);
|
||||||
defaultView.setEnabled(false);
|
defaultView.setEnabled(false);
|
||||||
|
|
@ -212,7 +214,6 @@ public class TrackSelectionView extends LinearLayout {
|
||||||
disableView.setEnabled(true);
|
disableView.setEnabled(true);
|
||||||
defaultView.setEnabled(true);
|
defaultView.setEnabled(true);
|
||||||
|
|
||||||
MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo();
|
|
||||||
trackGroups = trackInfo.getTrackGroups(rendererIndex);
|
trackGroups = trackInfo.getTrackGroups(rendererIndex);
|
||||||
|
|
||||||
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
|
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue