Commit 49a99bea by Michał Seroczyński Committed by GitHub

Merge branch 'dev-v2' into dev-v2

parents 3390b216 254589cb
Showing with 1557 additions and 718 deletions
...@@ -5,15 +5,37 @@ ...@@ -5,15 +5,37 @@
* Support for playing spherical videos on Daydream. * Support for playing spherical videos on Daydream.
* Improve decoder re-use between playbacks. TODO: Write and link a blog post * Improve decoder re-use between playbacks. TODO: Write and link a blog post
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). here ([#2826](https://github.com/google/ExoPlayer/issues/2826)).
* Add options for controlling audio track selections to `DefaultTrackSelector` * Track selection:
([#3314](https://github.com/google/ExoPlayer/issues/3314)). * Add options for controlling audio track selections to `DefaultTrackSelector`
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
* Update `TrackSelection.Factory` interface to support creating all track
selections together.
* Captions:
* Support PNG subtitles in SMPTE-TT
([#1583](https://github.com/google/ExoPlayer/issues/1583)).
* Do not retry failed loads whose error is `FileNotFoundException`. * Do not retry failed loads whose error is `FileNotFoundException`.
* Prevent Cea608Decoder from generating Subtitles with null Cues list * Prevent Cea608Decoder from generating Subtitles with null Cues list.
* Caching: Cache data with unknown length by default. The previous flag to opt in * Offline:
to this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been * Speed up removal of segmented downloads
replaced with an opt out flag (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). ([#5136](https://github.com/google/ExoPlayer/issues/5136)).
* Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS
media sources to simplify filtering by downloaded streams.
* Caching:
* Improve performance of `SimpleCache`.
* Cache data with unknown length by default. The previous flag to opt in to
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
replaced with an opt out flag
(`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
* Disable post processing on Nvidia devices, as it breaks decode-only frame
skippping.
* Workaround for MiTV (dangal) issue when swapping output surface
([#5169](https://github.com/google/ExoPlayer/issues/5169)).
* DownloadManager:
* Create only one task for all DownloadActions for the same content.
* Rename TaskState to DownloadState.
* MP3: Fix issue where streams would play twice on Samsung devices * MP3: Fix issue where streams would play twice on Samsung devices
([#4519](https://github.com/google/ExoPlayer/issues/4519)). ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
### 2.9.2 ### ### 2.9.2 ###
* HLS: * HLS:
...@@ -61,10 +83,10 @@ ...@@ -61,10 +83,10 @@
* DASH: Parse ProgramInformation element if present in the manifest. * DASH: Parse ProgramInformation element if present in the manifest.
* HLS: * HLS:
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload * Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
reader factory flags. reader factory flags
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
* Fix bug in segment sniffing * Fix bug in segment sniffing
([#5039](https://github.com/google/ExoPlayer/issues/5039)). ([#5039](https://github.com/google/ExoPlayer/issues/5039)).
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
* SubRip: Add support for alignment tags, and remove tags from the displayed * SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)). captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
* Fix issue with blind seeking to windows with non-zero offset in a * Fix issue with blind seeking to windows with non-zero offset in a
......
...@@ -49,6 +49,16 @@ android { ...@@ -49,6 +49,16 @@ android {
disable 'MissingTranslation' disable 'MissingTranslation'
} }
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
} }
dependencies { dependencies {
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" /> android:value="${castOptionsProvider}" />
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity" <activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
......
...@@ -268,7 +268,7 @@ import java.util.ArrayList; ...@@ -268,7 +268,7 @@ import java.util.ArrayList;
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex(); updateCurrentItemIndex();
if (timeline.isEmpty()) { if (currentPlayer == castPlayer && timeline.isEmpty()) {
castMediaQueueCreationPending = true; castMediaQueueCreationPending = true;
} }
} }
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.castdemo; package com.google.android.exoplayer2.castdemo;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
...@@ -24,50 +23,64 @@ import java.util.List; ...@@ -24,50 +23,64 @@ import java.util.List;
/** Utility methods and constants for the Cast demo application. */ /** Utility methods and constants for the Cast demo application. */
/* package */ final class DemoUtil { /* package */ final class DemoUtil {
/** Represents a media sample. */
public static final class Sample {
/** The uri of the media content. */
public final String uri;
/** The name of the sample. */
public final String name;
/** The mime type of the sample media content. */
public final String mimeType;
/**
* @param uri See {@link #uri}.
* @param name See {@link #name}.
* @param mimeType See {@link #mimeType}.
*/
public Sample(String uri, String name, String mimeType) {
this.uri = uri;
this.name = name;
this.mimeType = mimeType;
}
@Override
public String toString() {
return name;
}
}
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/** The list of samples available in the cast demo app. */ /** The list of samples available in the cast demo app. */
public static final List<MediaItem> SAMPLES; public static final List<Sample> SAMPLES;
static { static {
// App samples. // App samples.
ArrayList<MediaItem> samples = new ArrayList<>(); ArrayList<Sample> samples = new ArrayList<>();
MediaItem.Builder sampleBuilder = new MediaItem.Builder();
samples.add( samples.add(
sampleBuilder new Sample(
.setTitle("DASH (clear,MP4,H264)") "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
.setMimeType(MIME_TYPE_DASH) "DASH (clear,MP4,H264)",
.setMedia("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd") MIME_TYPE_DASH));
.buildAndClear());
samples.add( samples.add(
sampleBuilder new Sample(
.setTitle("Tears of Steel (HLS)") "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
.setMimeType(MIME_TYPE_HLS) + "hls/TearsOfSteel.m3u8",
.setMedia( "Tears of Steel (HLS)",
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" MIME_TYPE_HLS));
+ "hls/TearsOfSteel.m3u8")
.buildAndClear());
samples.add( samples.add(
sampleBuilder new Sample(
.setTitle("HLS Basic (TS)") "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3"
.setMimeType(MIME_TYPE_HLS) + "/bipbop_4x3_variant.m3u8",
.setMedia( "HLS Basic (TS)",
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3" MIME_TYPE_HLS));
+ "/bipbop_4x3_variant.m3u8")
.buildAndClear());
samples.add( samples.add(
sampleBuilder new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", MIME_TYPE_VIDEO_MP4));
.setTitle("Dizzy (MP4)")
.setMimeType(MIME_TYPE_VIDEO_MP4)
.setMedia("https://html5demos.com/assets/dizzy.mp4")
.buildAndClear());
SAMPLES = Collections.unmodifiableList(samples); SAMPLES = Collections.unmodifiableList(samples);
} }
......
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.castdemo; ...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.graphics.ColorUtils; import android.support.v4.graphics.ColorUtils;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
...@@ -50,6 +49,8 @@ import com.google.android.gms.cast.framework.CastContext; ...@@ -50,6 +49,8 @@ import com.google.android.gms.cast.framework.CastContext;
public class MainActivity extends AppCompatActivity public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.QueuePositionListener { implements OnClickListener, PlayerManager.QueuePositionListener {
private final MediaItem.Builder mediaItemBuilder;
private PlayerView localPlayerView; private PlayerView localPlayerView;
private PlayerControlView castControlView; private PlayerControlView castControlView;
private PlayerManager playerManager; private PlayerManager playerManager;
...@@ -57,6 +58,10 @@ public class MainActivity extends AppCompatActivity ...@@ -57,6 +58,10 @@ public class MainActivity extends AppCompatActivity
private MediaQueueListAdapter mediaQueueListAdapter; private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext; private CastContext castContext;
public MainActivity() {
mediaItemBuilder = new MediaItem.Builder();
}
// Activity lifecycle methods. // Activity lifecycle methods.
@Override @Override
...@@ -154,7 +159,14 @@ public class MainActivity extends AppCompatActivity ...@@ -154,7 +159,14 @@ public class MainActivity extends AppCompatActivity
sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener( sampleList.setOnItemClickListener(
(parent, view, position, id) -> { (parent, view, position, id) -> {
playerManager.addItem(DemoUtil.SAMPLES.get(position)); DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position);
playerManager.addItem(
mediaItemBuilder
.clear()
.setMedia(sample.uri)
.setTitle(sample.name)
.setMimeType(sample.mimeType)
.build());
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
}); });
return dialogList; return dialogList;
...@@ -254,19 +266,11 @@ public class MainActivity extends AppCompatActivity ...@@ -254,19 +266,11 @@ public class MainActivity extends AppCompatActivity
} }
private static final class SampleListAdapter extends ArrayAdapter<MediaItem> { private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.Sample> {
public SampleListAdapter(Context context) { public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
} }
@Override
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
TextView view = (TextView) super.getView(position, convertView, parent);
MediaItem sample = DemoUtil.SAMPLES.get(position);
view.setText(sample.title);
return view;
}
} }
} }
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Application; import android.app.Application;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
...@@ -72,6 +74,17 @@ public class DemoApplication extends Application { ...@@ -72,6 +74,17 @@ public class DemoApplication extends Application {
return "withExtensions".equals(BuildConfig.FLAVOR); return "withExtensions".equals(BuildConfig.FLAVOR);
} }
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
@DefaultRenderersFactory.ExtensionRendererMode
int extensionRendererMode =
useExtensionRenderers()
? (preferExtensionRenderer
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
return new DefaultRenderersFactory(this, extensionRendererMode);
}
public DownloadManager getDownloadManager() { public DownloadManager getDownloadManager() {
initDownloadManager(); initDownloadManager();
return downloadManager; return downloadManager;
......
...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.demo; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.demo;
import android.app.Notification; import android.app.Notification;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.scheduler.PlatformScheduler; import com.google.android.exoplayer2.scheduler.PlatformScheduler;
import com.google.android.exoplayer2.ui.DownloadNotificationUtil; import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
...@@ -31,12 +31,15 @@ public class DemoDownloadService extends DownloadService { ...@@ -31,12 +31,15 @@ public class DemoDownloadService extends DownloadService {
private static final int JOB_ID = 1; private static final int JOB_ID = 1;
private static final int FOREGROUND_NOTIFICATION_ID = 1; private static final int FOREGROUND_NOTIFICATION_ID = 1;
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
public DemoDownloadService() { public DemoDownloadService() {
super( super(
FOREGROUND_NOTIFICATION_ID, FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID, CHANNEL_ID,
R.string.exo_download_notification_channel_name); R.string.exo_download_notification_channel_name);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
} }
@Override @Override
...@@ -50,40 +53,41 @@ public class DemoDownloadService extends DownloadService { ...@@ -50,40 +53,41 @@ public class DemoDownloadService extends DownloadService {
} }
@Override @Override
protected Notification getForegroundNotification(TaskState[] taskStates) { protected Notification getForegroundNotification(DownloadState[] downloadStates) {
return DownloadNotificationUtil.buildProgressNotification( return DownloadNotificationUtil.buildProgressNotification(
/* context= */ this, /* context= */ this,
R.drawable.exo_controls_play, R.drawable.ic_download,
CHANNEL_ID, CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
/* message= */ null, /* message= */ null,
taskStates); downloadStates);
} }
@Override @Override
protected void onTaskStateChanged(TaskState taskState) { protected void onDownloadStateChanged(DownloadState downloadState) {
if (taskState.action.isRemoveAction) { if (downloadState.action.isRemoveAction) {
return; return;
} }
Notification notification = null; Notification notification = null;
if (taskState.state == TaskState.STATE_COMPLETED) { if (downloadState.state == DownloadState.STATE_COMPLETED) {
notification = notification =
DownloadNotificationUtil.buildDownloadCompletedNotification( DownloadNotificationUtil.buildDownloadCompletedNotification(
/* context= */ this, /* context= */ this,
R.drawable.exo_controls_play, R.drawable.ic_download_done,
CHANNEL_ID, CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(taskState.action.data)); Util.fromUtf8Bytes(downloadState.action.data));
} else if (taskState.state == TaskState.STATE_FAILED) { } else if (downloadState.state == DownloadState.STATE_FAILED) {
notification = notification =
DownloadNotificationUtil.buildDownloadFailedNotification( DownloadNotificationUtil.buildDownloadFailedNotification(
/* context= */ this, /* context= */ this,
R.drawable.exo_controls_play, R.drawable.ic_download_done,
CHANNEL_ID, CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(taskState.action.data)); Util.fromUtf8Bytes(downloadState.action.data));
} else {
return;
} }
int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId; NotificationUtil.setNotification(this, nextNotificationId++, notification);
NotificationUtil.setNotification(this, notificationId, notification);
} }
} }
...@@ -35,11 +35,11 @@ import android.widget.TextView; ...@@ -35,11 +35,11 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
...@@ -48,7 +48,6 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; ...@@ -48,7 +48,6 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
...@@ -58,11 +57,8 @@ import com.google.android.exoplayer2.source.TrackGroupArray; ...@@ -58,11 +57,8 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
...@@ -416,13 +412,8 @@ public class PlayerActivity extends Activity ...@@ -416,13 +412,8 @@ public class PlayerActivity extends Activity
boolean preferExtensionDecoders = boolean preferExtensionDecoders =
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false); intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = RenderersFactory renderersFactory =
((DemoApplication) getApplication()).useExtensionRenderers() ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(this, extensionRendererMode);
trackSelector = new DefaultTrackSelector(trackSelectionFactory); trackSelector = new DefaultTrackSelector(trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters); trackSelector.setParameters(trackSelectorParameters);
...@@ -477,21 +468,19 @@ public class PlayerActivity extends Activity ...@@ -477,21 +468,19 @@ public class PlayerActivity extends Activity
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
@ContentType int type = Util.inferContentType(uri, overrideExtension); @ContentType int type = Util.inferContentType(uri, overrideExtension);
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory) return new DashMediaSource.Factory(dataSourceFactory)
.setManifestParser( .setStreamKeys(offlineStreamKeys)
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_SS: case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory) return new SsMediaSource.Factory(dataSourceFactory)
.setManifestParser( .setStreamKeys(offlineStreamKeys)
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory) return new HlsMediaSource.Factory(dataSourceFactory)
.setPlaylistParserFactory( .setStreamKeys(offlineStreamKeys)
new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
......
...@@ -37,6 +37,7 @@ import android.widget.ImageButton; ...@@ -37,6 +37,7 @@ import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSourceInputStream;
...@@ -177,7 +178,11 @@ public class SampleChooserActivity extends Activity ...@@ -177,7 +178,11 @@ public class SampleChooserActivity extends Activity
.show(); .show();
} else { } else {
UriSample uriSample = (UriSample) sample; UriSample uriSample = (UriSample) sample;
downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension); RenderersFactory renderersFactory =
((DemoApplication) getApplication())
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(
this, sample.name, uriSample.uri, uriSample.extension, renderersFactory);
} }
} }
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/track_title"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="4dp"/>
<TextView
android:id="@+id/track_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="4dp"/>
</LinearLayout>
<ImageButton
android:id="@+id/edit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:contentDescription="@string/download_edit_track"
android:src="@drawable/ic_edit"/>
</LinearLayout>
...@@ -13,7 +13,8 @@ ...@@ -13,7 +13,8 @@
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.
--> -->
<ListView xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/representation_list" android:id="@+id/selection_list"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
...@@ -51,6 +51,10 @@ ...@@ -51,6 +51,10 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string> <string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="download_edit_track">Edit selection</string>
<string name="download_preparing">Preparing download…</string>
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string> <string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
......
...@@ -66,13 +66,6 @@ public final class TimelineQueueEditor ...@@ -66,13 +66,6 @@ public final class TimelineQueueEditor
*/ */
public interface QueueDataAdapter { public interface QueueDataAdapter {
/** /**
* Gets the {@link MediaDescriptionCompat} for a {@code position}.
*
* @param position The position in the queue for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
*/
MediaDescriptionCompat getMediaDescription(int position);
/**
* Adds a {@link MediaDescriptionCompat} at the given {@code position}. * Adds a {@link MediaDescriptionCompat} at the given {@code position}.
* *
* @param position The position at which to add. * @param position The position at which to add.
......
...@@ -1693,7 +1693,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1693,7 +1693,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/ */
private static boolean codecNeedsEosFlushWorkaround(String name) { private static boolean codecNeedsEosFlushWorkaround(String name) {
return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name)) return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name))
|| (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE) || (Util.SDK_INT <= 19
&& ("hb2000".equals(Util.DEVICE) || "stvm8".equals(Util.DEVICE))
&& ("OMX.amlogic.avc.decoder.awesome".equals(name) && ("OMX.amlogic.avc.decoder.awesome".equals(name)
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); || "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
} }
......
...@@ -77,7 +77,7 @@ public final class DownloadAction { ...@@ -77,7 +77,7 @@ public final class DownloadAction {
* *
* @param type The type of the action. * @param type The type of the action.
* @param uri The URI of the media to be downloaded. * @param uri The URI of the media to be downloaded.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. * @param keys Keys of streams to be downloaded. If empty, all streams will be downloaded.
* @param customCacheKey A custom key for cache indexing, or null. * @param customCacheKey A custom key for cache indexing, or null.
* @param data Optional custom data for this action. If {@code null} an empty array will be used. * @param data Optional custom data for this action. If {@code null} an empty array will be used.
*/ */
...@@ -108,6 +108,8 @@ public final class DownloadAction { ...@@ -108,6 +108,8 @@ public final class DownloadAction {
/* data= */ null); /* data= */ null);
} }
/** The unique content id. */
public final String id;
/** The type of the action. */ /** The type of the action. */
public final String type; public final String type;
/** The uri being downloaded or removed. */ /** The uri being downloaded or removed. */
...@@ -115,8 +117,8 @@ public final class DownloadAction { ...@@ -115,8 +117,8 @@ public final class DownloadAction {
/** Whether this is a remove action. If false, this is a download action. */ /** Whether this is a remove action. If false, this is a download action. */
public final boolean isRemoveAction; public final boolean isRemoveAction;
/** /**
* Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if this action * Keys of streams to be downloaded. If empty, all streams will be downloaded. Empty if this
* is a remove action. * action is a remove action.
*/ */
public final List<StreamKey> keys; public final List<StreamKey> keys;
/** A custom key for cache indexing, or null. */ /** A custom key for cache indexing, or null. */
...@@ -128,8 +130,8 @@ public final class DownloadAction { ...@@ -128,8 +130,8 @@ public final class DownloadAction {
* @param type The type of the action. * @param type The type of the action.
* @param uri The uri being downloaded or removed. * @param uri The uri being downloaded or removed.
* @param isRemoveAction Whether this is a remove action. If false, this is a download action. * @param isRemoveAction Whether this is a remove action. If false, this is a download action.
* @param keys Keys of tracks to be downloaded. If empty, all tracks will be downloaded. Empty if * @param keys Keys of streams to be downloaded. If empty, all streams will be downloaded. Empty
* this action is a remove action. * if this action is a remove action.
* @param customCacheKey A custom key for cache indexing, or null. * @param customCacheKey A custom key for cache indexing, or null.
* @param data Custom data for this action. Null if this action is a remove action. * @param data Custom data for this action. Null if this action is a remove action.
*/ */
...@@ -140,6 +142,7 @@ public final class DownloadAction { ...@@ -140,6 +142,7 @@ public final class DownloadAction {
List<StreamKey> keys, List<StreamKey> keys,
@Nullable String customCacheKey, @Nullable String customCacheKey,
@Nullable byte[] data) { @Nullable byte[] data) {
this.id = customCacheKey != null ? customCacheKey : uri.toString();
this.type = type; this.type = type;
this.uri = uri; this.uri = uri;
this.isRemoveAction = isRemoveAction; this.isRemoveAction = isRemoveAction;
...@@ -171,12 +174,10 @@ public final class DownloadAction { ...@@ -171,12 +174,10 @@ public final class DownloadAction {
/** Returns whether this is an action for the same media as the {@code other}. */ /** Returns whether this is an action for the same media as the {@code other}. */
public boolean isSameMedia(DownloadAction other) { public boolean isSameMedia(DownloadAction other) {
return customCacheKey == null return id.equals(other.id);
? other.customCacheKey == null && uri.equals(other.uri)
: customCacheKey.equals(other.customCacheKey);
} }
/** Returns keys of tracks to be downloaded. */ /** Returns keys of streams to be downloaded. */
public List<StreamKey> getKeys() { public List<StreamKey> getKeys() {
return keys; return keys;
} }
...@@ -187,7 +188,8 @@ public final class DownloadAction { ...@@ -187,7 +188,8 @@ public final class DownloadAction {
return false; return false;
} }
DownloadAction that = (DownloadAction) o; DownloadAction that = (DownloadAction) o;
return type.equals(that.type) return id.equals(that.id)
&& type.equals(that.type)
&& uri.equals(that.uri) && uri.equals(that.uri)
&& isRemoveAction == that.isRemoveAction && isRemoveAction == that.isRemoveAction
&& keys.equals(that.keys) && keys.equals(that.keys)
...@@ -198,6 +200,7 @@ public final class DownloadAction { ...@@ -198,6 +200,7 @@ public final class DownloadAction {
@Override @Override
public final int hashCode() { public final int hashCode() {
int result = type.hashCode(); int result = type.hashCode();
result = 31 * result + id.hashCode();
result = 31 * result + uri.hashCode(); result = 31 * result + uri.hashCode();
result = 31 * result + (isRemoveAction ? 1 : 0); result = 31 * result + (isRemoveAction ? 1 : 0);
result = 31 * result + keys.hashCode(); result = 31 * result + keys.hashCode();
......
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
import com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
/** {@link DownloadAction} related utility methods. */
public class DownloadActionUtil {
private DownloadActionUtil() {}
/**
* Merge {@link DownloadAction}s in {@code actionQueue} to minimum number of actions.
*
* <p>All actions must have the same type and must be for the same media.
*
* @param actionQueue Queue of actions. Must not be empty.
* @return The first action in the queue.
*/
public static DownloadAction mergeActions(ArrayDeque<DownloadAction> actionQueue) {
DownloadAction removeAction = null;
DownloadAction downloadAction = null;
HashSet<StreamKey> keys = new HashSet<>();
boolean downloadAllTracks = false;
DownloadAction firstAction = Assertions.checkNotNull(actionQueue.peek());
while (!actionQueue.isEmpty()) {
DownloadAction action = actionQueue.remove();
Assertions.checkState(action.type.equals(firstAction.type));
Assertions.checkState(action.isSameMedia(firstAction));
if (action.isRemoveAction) {
removeAction = action;
downloadAction = null;
keys.clear();
downloadAllTracks = false;
} else {
if (!downloadAllTracks) {
if (action.keys.isEmpty()) {
downloadAllTracks = true;
keys.clear();
} else {
keys.addAll(action.keys);
}
}
downloadAction = action;
}
}
if (removeAction != null) {
actionQueue.add(removeAction);
}
if (downloadAction != null) {
actionQueue.add(
DownloadAction.createDownloadAction(
downloadAction.type,
downloadAction.uri,
new ArrayList<>(keys),
downloadAction.customCacheKey,
downloadAction.data));
}
return Assertions.checkNotNull(actionQueue.peek());
}
}
...@@ -24,7 +24,7 @@ import android.os.IBinder; ...@@ -24,7 +24,7 @@ import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.RequirementsWatcher; import com.google.android.exoplayer2.scheduler.RequirementsWatcher;
import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.scheduler.Scheduler;
...@@ -71,9 +71,9 @@ public abstract class DownloadService extends Service { ...@@ -71,9 +71,9 @@ public abstract class DownloadService extends Service {
private static final String TAG = "DownloadService"; private static final String TAG = "DownloadService";
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
// Keep the requirements helper for each DownloadService as long as there are tasks (and the // Keep the requirements helper for each DownloadService as long as there are downloads (and the
// process is running). This allows tasks to resume when there's no scheduler. It may also allow // process is running). This allows downloads to resume when there's no scheduler. It may also
// tasks the resume more quickly than when relying on the scheduler alone. // allow downloads the resume more quickly than when relying on the scheduler alone.
private static final HashMap<Class<? extends DownloadService>, RequirementsHelper> private static final HashMap<Class<? extends DownloadService>, RequirementsHelper>
requirementsHelpers = new HashMap<>(); requirementsHelpers = new HashMap<>();
private static final Requirements DEFAULT_REQUIREMENTS = private static final Requirements DEFAULT_REQUIREMENTS =
...@@ -99,7 +99,7 @@ public abstract class DownloadService extends Service { ...@@ -99,7 +99,7 @@ public abstract class DownloadService extends Service {
* <p>If {@code foregroundNotificationId} isn't {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value * <p>If {@code foregroundNotificationId} isn't {@link #FOREGROUND_NOTIFICATION_ID_NONE} (value
* {@value #FOREGROUND_NOTIFICATION_ID_NONE}) the service runs in the foreground with {@link * {@value #FOREGROUND_NOTIFICATION_ID_NONE}) the service runs in the foreground with {@link
* #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. In that case {@link * #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. In that case {@link
* #getForegroundNotification(TaskState[])} should be overridden in the subclass. * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
* *
* @param foregroundNotificationId The notification id for the foreground notification, or {@link * @param foregroundNotificationId The notification id for the foreground notification, or {@link
* #FOREGROUND_NOTIFICATION_ID_NONE} (value {@value #FOREGROUND_NOTIFICATION_ID_NONE}) * #FOREGROUND_NOTIFICATION_ID_NONE} (value {@value #FOREGROUND_NOTIFICATION_ID_NONE})
...@@ -110,7 +110,7 @@ public abstract class DownloadService extends Service { ...@@ -110,7 +110,7 @@ public abstract class DownloadService extends Service {
/** /**
* Creates a DownloadService which will run in the foreground. {@link * Creates a DownloadService which will run in the foreground. {@link
* #getForegroundNotification(TaskState[])} should be overridden in the subclass. * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
* *
* @param foregroundNotificationId The notification id for the foreground notification, must not * @param foregroundNotificationId The notification id for the foreground notification, must not
* be 0. * be 0.
...@@ -128,7 +128,7 @@ public abstract class DownloadService extends Service { ...@@ -128,7 +128,7 @@ public abstract class DownloadService extends Service {
/** /**
* Creates a DownloadService which will run in the foreground. {@link * Creates a DownloadService which will run in the foreground. {@link
* #getForegroundNotification(TaskState[])} should be overridden in the subclass. * #getForegroundNotification(DownloadState[])} should be overridden in the subclass.
* *
* @param foregroundNotificationId The notification id for the foreground notification. Must not * @param foregroundNotificationId The notification id for the foreground notification. Must not
* be 0. * be 0.
...@@ -338,29 +338,29 @@ public abstract class DownloadService extends Service { ...@@ -338,29 +338,29 @@ public abstract class DownloadService extends Service {
* *
* <p>Returns a notification to be displayed when this service running in the foreground. * <p>Returns a notification to be displayed when this service running in the foreground.
* *
* <p>This method is called when there is a task state change and periodically while there are * <p>This method is called when there is a download state change and periodically while there are
* active tasks. The periodic update interval can be set using {@link #DownloadService(int, * active downloads. The periodic update interval can be set using {@link #DownloadService(int,
* long)}. * long)}.
* *
* <p>On API level 26 and above, this method may also be called just before the service stops, * <p>On API level 26 and above, this method may also be called just before the service stops,
* with an empty {@code taskStates} array. The returned notification is used to satisfy system * with an empty {@code downloadStates} array. The returned notification is used to satisfy system
* requirements for foreground services. * requirements for foreground services.
* *
* @param taskStates The states of all current tasks. * @param downloadStates The states of all current downloads.
* @return The foreground notification to display. * @return The foreground notification to display.
*/ */
protected Notification getForegroundNotification(TaskState[] taskStates) { protected Notification getForegroundNotification(DownloadState[] downloadStates) {
throw new IllegalStateException( throw new IllegalStateException(
getClass().getName() getClass().getName()
+ " is started in the foreground but getForegroundNotification() is not implemented."); + " is started in the foreground but getForegroundNotification() is not implemented.");
} }
/** /**
* Called when the state of a task changes. * Called when the state of a download changes.
* *
* @param taskState The state of the task. * @param downloadState The state of the download.
*/ */
protected void onTaskStateChanged(TaskState taskState) { protected void onDownloadStateChanged(DownloadState downloadState) {
// Do nothing. // Do nothing.
} }
...@@ -428,10 +428,11 @@ public abstract class DownloadService extends Service { ...@@ -428,10 +428,11 @@ public abstract class DownloadService extends Service {
} }
@Override @Override
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) { public void onDownloadStateChanged(
DownloadService.this.onTaskStateChanged(taskState); DownloadManager downloadManager, DownloadState downloadState) {
DownloadService.this.onDownloadStateChanged(downloadState);
if (foregroundNotificationUpdater != null) { if (foregroundNotificationUpdater != null) {
if (taskState.state == TaskState.STATE_STARTED) { if (downloadState.state == DownloadState.STATE_STARTED) {
foregroundNotificationUpdater.startPeriodicUpdates(); foregroundNotificationUpdater.startPeriodicUpdates();
} else { } else {
foregroundNotificationUpdater.update(); foregroundNotificationUpdater.update();
...@@ -471,8 +472,8 @@ public abstract class DownloadService extends Service { ...@@ -471,8 +472,8 @@ public abstract class DownloadService extends Service {
} }
public void update() { public void update() {
TaskState[] taskStates = downloadManager.getAllTaskStates(); DownloadState[] downloadStates = downloadManager.getAllDownloadStates();
startForeground(notificationId, getForegroundNotification(taskStates)); startForeground(notificationId, getForegroundNotification(downloadStates));
notificationDisplayed = true; notificationDisplayed = true;
if (periodicUpdatesStarted) { if (periodicUpdatesStarted) {
handler.removeCallbacks(this); handler.removeCallbacks(this);
......
...@@ -16,22 +16,27 @@ ...@@ -16,22 +16,27 @@
package com.google.android.exoplayer2.offline; package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser; import com.google.android.exoplayer2.upstream.ParsingLoadable.Parser;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
/** A manifest parser that includes only the streams identified by the given stream keys. */ /**
* A manifest parser that includes only the streams identified by the given stream keys.
*
* @param <T> The {@link FilterableManifest} type.
*/
public final class FilteringManifestParser<T extends FilterableManifest<T>> implements Parser<T> { public final class FilteringManifestParser<T extends FilterableManifest<T>> implements Parser<T> {
private final Parser<T> parser; private final Parser<? extends T> parser;
private final List<StreamKey> streamKeys; @Nullable private final List<StreamKey> streamKeys;
/** /**
* @param parser A parser for the manifest that will be filtered. * @param parser A parser for the manifest that will be filtered.
* @param streamKeys The stream keys. If null or empty then filtering will not occur. * @param streamKeys The stream keys. If null or empty then filtering will not occur.
*/ */
public FilteringManifestParser(Parser<T> parser, List<StreamKey> streamKeys) { public FilteringManifestParser(Parser<? extends T> parser, @Nullable List<StreamKey> streamKeys) {
this.parser = parser; this.parser = parser;
this.streamKeys = streamKeys; this.streamKeys = streamKeys;
} }
......
...@@ -17,19 +17,35 @@ package com.google.android.exoplayer2.offline; ...@@ -17,19 +17,35 @@ package com.google.android.exoplayer2.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import java.util.Collections;
import java.util.List;
/** A {@link DownloadHelper} for progressive streams. */ /** A {@link DownloadHelper} for progressive streams. */
public final class ProgressiveDownloadHelper extends DownloadHelper<Void> { public final class ProgressiveDownloadHelper extends DownloadHelper<Void> {
/**
* Creates download helper for progressive streams.
*
* @param uri The stream {@link Uri}.
*/
public ProgressiveDownloadHelper(Uri uri) { public ProgressiveDownloadHelper(Uri uri) {
this(uri, null); this(uri, /* cacheKey= */ null);
} }
public ProgressiveDownloadHelper(Uri uri, @Nullable String customCacheKey) { /**
super(DownloadAction.TYPE_PROGRESSIVE, uri, customCacheKey); * Creates download helper for progressive streams.
*
* @param uri The stream {@link Uri}.
* @param cacheKey An optional cache key.
*/
public ProgressiveDownloadHelper(Uri uri, @Nullable String cacheKey) {
super(
DownloadAction.TYPE_PROGRESSIVE,
uri,
cacheKey,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
(handler, videoListener, audioListener, metadata, text, drm) -> new Renderer[0],
/* drmSessionManager= */ null);
} }
@Override @Override
...@@ -43,7 +59,8 @@ public final class ProgressiveDownloadHelper extends DownloadHelper<Void> { ...@@ -43,7 +59,8 @@ public final class ProgressiveDownloadHelper extends DownloadHelper<Void> {
} }
@Override @Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { protected StreamKey toStreamKey(
return Collections.emptyList(); int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
} }
} }
...@@ -19,8 +19,11 @@ import android.support.annotation.NonNull; ...@@ -19,8 +19,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
/** /**
* Identifies a given track by the index of the containing period, the index of the containing group * A key for a subset of media which can be separately loaded (a "stream").
* within the period, and the index of the track within the group. *
* <p>The stream key consists of a period index, a group index within the period and a track index
* within the group. The interpretation of these indices depends on the type of media for which the
* stream key is used.
*/ */
public final class StreamKey implements Comparable<StreamKey> { public final class StreamKey implements Comparable<StreamKey> {
......
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.offline;
/**
* Identifies a given track by the index of the containing period, the index of the containing group
* within the period, and the index of the track within the group.
*/
public final class TrackKey {
/** The period index. */
public final int periodIndex;
/** The group index. */
public final int groupIndex;
/** The track index. */
public final int trackIndex;
/**
* @param periodIndex The period index.
* @param groupIndex The group index.
* @param trackIndex The track index.
*/
public TrackKey(int periodIndex, int groupIndex, int trackIndex) {
this.periodIndex = periodIndex;
this.groupIndex = groupIndex;
this.trackIndex = trackIndex;
}
}
...@@ -826,7 +826,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -826,7 +826,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public MediaSourceHolder(MediaSource mediaSource) { public MediaSourceHolder(MediaSource mediaSource) {
this.mediaSource = mediaSource; this.mediaSource = mediaSource;
this.timeline = new DeferredTimeline(); this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag());
this.activeMediaPeriods = new ArrayList<>(); this.activeMediaPeriods = new ArrayList<>();
this.uid = new Object(); this.uid = new Object();
} }
...@@ -951,11 +951,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -951,11 +951,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private static final class DeferredTimeline extends ForwardingTimeline { private static final class DeferredTimeline extends ForwardingTimeline {
private static final Object DUMMY_ID = new Object(); private static final Object DUMMY_ID = new Object();
private static final DummyTimeline DUMMY_TIMELINE = new DummyTimeline();
private final Object replacedId; private final Object replacedId;
/** /**
* Returns an instance with a dummy timeline using the provided window tag.
*
* @param windowTag A window tag.
*/
public static DeferredTimeline createWithDummyTimeline(@Nullable Object windowTag) {
return new DeferredTimeline(new DummyTimeline(windowTag), DUMMY_ID);
}
/**
* Returns an instance with a real timeline, replacing the provided period ID with the already * Returns an instance with a real timeline, replacing the provided period ID with the already
* assigned dummy period ID. * assigned dummy period ID.
* *
...@@ -968,11 +976,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -968,11 +976,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
return new DeferredTimeline(timeline, firstPeriodUid); return new DeferredTimeline(timeline, firstPeriodUid);
} }
/** Creates deferred timeline exposing a {@link DummyTimeline}. */
public DeferredTimeline() {
this(DUMMY_TIMELINE, DUMMY_ID);
}
private DeferredTimeline(Timeline timeline, Object replacedId) { private DeferredTimeline(Timeline timeline, Object replacedId) {
super(timeline); super(timeline);
this.replacedId = replacedId; this.replacedId = replacedId;
...@@ -1016,6 +1019,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -1016,6 +1019,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
private static final class DummyTimeline extends Timeline { private static final class DummyTimeline extends Timeline {
@Nullable private final Object tag;
public DummyTimeline(@Nullable Object tag) {
this.tag = tag;
}
@Override @Override
public int getWindowCount() { public int getWindowCount() {
return 1; return 1;
...@@ -1025,7 +1034,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -1025,7 +1034,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public Window getWindow( public Window getWindow(
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
return window.set( return window.set(
/* tag= */ null, tag,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ false, /* isSeekable= */ false,
......
...@@ -19,8 +19,11 @@ import com.google.android.exoplayer2.C; ...@@ -19,8 +19,11 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
...@@ -84,6 +87,22 @@ public interface MediaPeriod extends SequenceableLoader { ...@@ -84,6 +87,22 @@ public interface MediaPeriod extends SequenceableLoader {
TrackGroupArray getTrackGroups(); TrackGroupArray getTrackGroups();
/** /**
* Returns a list of {@link StreamKey stream keys} which allow to filter the media in this period
* to load only the parts needed to play the provided {@link TrackSelection}.
*
* <p>This method is only called after the period has been prepared.
*
* @param trackSelection The {@link TrackSelection} describing the tracks for which stream keys
* are requested.
* @return The corresponding {@link StreamKey stream keys} for the selected tracks, or an empty
* list if filtering is not possible and the entire media needs to be loaded to play the
* selected tracks.
*/
default List<StreamKey> getStreamKeys(TrackSelection trackSelection) {
return Collections.emptyList();
}
/**
* Performs a track selection. * Performs a track selection.
* *
* <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} * <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
......
...@@ -15,7 +15,12 @@ ...@@ -15,7 +15,12 @@
*/ */
package com.google.android.exoplayer2.text.ttml; package com.google.android.exoplayer2.text.ttml;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.Base64;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -44,9 +49,9 @@ import java.util.TreeSet; ...@@ -44,9 +49,9 @@ import java.util.TreeSet;
public static final String TAG_LAYOUT = "layout"; public static final String TAG_LAYOUT = "layout";
public static final String TAG_REGION = "region"; public static final String TAG_REGION = "region";
public static final String TAG_METADATA = "metadata"; public static final String TAG_METADATA = "metadata";
public static final String TAG_SMPTE_IMAGE = "smpte:image"; public static final String TAG_IMAGE = "image";
public static final String TAG_SMPTE_DATA = "smpte:data"; public static final String TAG_DATA = "data";
public static final String TAG_SMPTE_INFORMATION = "smpte:information"; public static final String TAG_INFORMATION = "information";
public static final String ANONYMOUS_REGION_ID = ""; public static final String ANONYMOUS_REGION_ID = "";
public static final String ATTR_ID = "id"; public static final String ATTR_ID = "id";
...@@ -75,34 +80,57 @@ import java.util.TreeSet; ...@@ -75,34 +80,57 @@ import java.util.TreeSet;
public static final String START = "start"; public static final String START = "start";
public static final String END = "end"; public static final String END = "end";
public final String tag; @Nullable public final String tag;
public final String text; @Nullable public final String text;
public final boolean isTextNode; public final boolean isTextNode;
public final long startTimeUs; public final long startTimeUs;
public final long endTimeUs; public final long endTimeUs;
public final TtmlStyle style; @Nullable public final TtmlStyle style;
@Nullable private final String[] styleIds;
public final String regionId; public final String regionId;
@Nullable public final String imageId;
private final String[] styleIds;
private final HashMap<String, Integer> nodeStartsByRegion; private final HashMap<String, Integer> nodeStartsByRegion;
private final HashMap<String, Integer> nodeEndsByRegion; private final HashMap<String, Integer> nodeEndsByRegion;
private List<TtmlNode> children; private List<TtmlNode> children;
public static TtmlNode buildTextNode(String text) { public static TtmlNode buildTextNode(String text) {
return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET, return new TtmlNode(
C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID); /* tag= */ null,
TtmlRenderUtil.applyTextElementSpacePolicy(text),
/* startTimeUs= */ C.TIME_UNSET,
/* endTimeUs= */ C.TIME_UNSET,
/* style= */ null,
/* styleIds= */ null,
ANONYMOUS_REGION_ID,
/* imageId= */ null);
} }
public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs, public static TtmlNode buildNode(
TtmlStyle style, String[] styleIds, String regionId) { @Nullable String tag,
return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId); long startTimeUs,
long endTimeUs,
@Nullable TtmlStyle style,
@Nullable String[] styleIds,
String regionId,
@Nullable String imageId) {
return new TtmlNode(
tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId);
} }
private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs, private TtmlNode(
TtmlStyle style, String[] styleIds, String regionId) { @Nullable String tag,
@Nullable String text,
long startTimeUs,
long endTimeUs,
@Nullable TtmlStyle style,
@Nullable String[] styleIds,
String regionId,
@Nullable String imageId) {
this.tag = tag; this.tag = tag;
this.text = text; this.text = text;
this.imageId = imageId;
this.style = style; this.style = style;
this.styleIds = styleIds; this.styleIds = styleIds;
this.isTextNode = text != null; this.isTextNode = text != null;
...@@ -151,7 +179,8 @@ import java.util.TreeSet; ...@@ -151,7 +179,8 @@ import java.util.TreeSet;
private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) { private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
boolean isPNode = TAG_P.equals(tag); boolean isPNode = TAG_P.equals(tag);
if (descendsPNode || isPNode) { boolean isDivNode = TAG_DIV.equals(tag);
if (descendsPNode || isPNode || (isDivNode && imageId != null)) {
if (startTimeUs != C.TIME_UNSET) { if (startTimeUs != C.TIME_UNSET) {
out.add(startTimeUs); out.add(startTimeUs);
} }
...@@ -171,13 +200,46 @@ import java.util.TreeSet; ...@@ -171,13 +200,46 @@ import java.util.TreeSet;
return styleIds; return styleIds;
} }
public List<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles, public List<Cue> getCues(
Map<String, TtmlRegion> regionMap) { long timeUs,
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>(); Map<String, TtmlStyle> globalStyles,
traverseForText(timeUs, false, regionId, regionOutputs); Map<String, TtmlRegion> regionMap,
traverseForStyle(timeUs, globalStyles, regionOutputs); Map<String, String> imageMap) {
List<Pair<String, String>> regionImageOutputs = new ArrayList<>();
traverseForImage(timeUs, regionId, regionImageOutputs);
TreeMap<String, SpannableStringBuilder> regionTextOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionTextOutputs);
traverseForStyle(timeUs, globalStyles, regionTextOutputs);
List<Cue> cues = new ArrayList<>(); List<Cue> cues = new ArrayList<>();
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
// Create image based cues.
for (Pair<String, String> regionImagePair : regionImageOutputs) {
String encodedBitmapData = imageMap.get(regionImagePair.second);
if (encodedBitmapData == null) {
// Image reference points to an invalid image. Do nothing.
continue;
}
byte[] bitmapData = Base64.decode(encodedBitmapData, Base64.DEFAULT);
Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, /* offset= */ 0, bitmapData.length);
TtmlRegion region = regionMap.get(regionImagePair.first);
cues.add(
new Cue(
bitmap,
region.position,
Cue.ANCHOR_TYPE_MIDDLE,
region.line,
region.lineAnchor,
region.width,
/* height= */ Cue.DIMEN_UNSET));
}
// Create text based cues.
for (Entry<String, SpannableStringBuilder> entry : regionTextOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey()); TtmlRegion region = regionMap.get(entry.getKey());
cues.add( cues.add(
new Cue( new Cue(
...@@ -192,9 +254,22 @@ import java.util.TreeSet; ...@@ -192,9 +254,22 @@ import java.util.TreeSet;
region.textSizeType, region.textSizeType,
region.textSize)); region.textSize));
} }
return cues; return cues;
} }
private void traverseForImage(
long timeUs, String inheritedRegion, List<Pair<String, String>> regionImageList) {
String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
if (isActive(timeUs) && TAG_DIV.equals(tag) && imageId != null) {
regionImageList.add(new Pair<>(resolvedRegionId, imageId));
return;
}
for (int i = 0; i < getChildCount(); ++i) {
getChild(i).traverseForImage(timeUs, resolvedRegionId, regionImageList);
}
}
private void traverseForText( private void traverseForText(
long timeUs, long timeUs,
boolean descendsPNode, boolean descendsPNode,
......
...@@ -33,11 +33,16 @@ import java.util.Map; ...@@ -33,11 +33,16 @@ import java.util.Map;
private final long[] eventTimesUs; private final long[] eventTimesUs;
private final Map<String, TtmlStyle> globalStyles; private final Map<String, TtmlStyle> globalStyles;
private final Map<String, TtmlRegion> regionMap; private final Map<String, TtmlRegion> regionMap;
private final Map<String, String> imageMap;
public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles, public TtmlSubtitle(
Map<String, TtmlRegion> regionMap) { TtmlNode root,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap,
Map<String, String> imageMap) {
this.root = root; this.root = root;
this.regionMap = regionMap; this.regionMap = regionMap;
this.imageMap = imageMap;
this.globalStyles = this.globalStyles =
globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap();
this.eventTimesUs = root.getEventTimesUs(); this.eventTimesUs = root.getEventTimesUs();
...@@ -66,7 +71,7 @@ import java.util.Map; ...@@ -66,7 +71,7 @@ import java.util.Map;
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
return root.getCues(timeUs, globalStyles, regionMap); return root.getCues(timeUs, globalStyles, regionMap, imageMap);
} }
@VisibleForTesting @VisibleForTesting
......
...@@ -227,27 +227,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { ...@@ -227,27 +227,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
} }
@Override @Override
public AdaptiveTrackSelection createTrackSelection(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {
if (this.bandwidthMeter != null) {
bandwidthMeter = this.bandwidthMeter;
}
AdaptiveTrackSelection adaptiveTrackSelection =
new AdaptiveTrackSelection(
group,
tracks,
new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction),
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
bufferedFractionToLiveEdgeForQualityIncrease,
minTimeBetweenBufferReevaluationMs,
clock);
adaptiveTrackSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator);
return adaptiveTrackSelection;
}
@Override
public @NullableType TrackSelection[] createTrackSelections( public @NullableType TrackSelection[] createTrackSelections(
@NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
TrackSelection[] selections = new TrackSelection[definitions.length]; TrackSelection[] selections = new TrackSelection[definitions.length];
...@@ -259,8 +238,9 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { ...@@ -259,8 +238,9 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
continue; continue;
} }
if (definition.tracks.length > 1) { if (definition.tracks.length > 1) {
selections[i] = createTrackSelection(definition.group, bandwidthMeter, definition.tracks); adaptiveSelection =
adaptiveSelection = (AdaptiveTrackSelection) selections[i]; createAdaptiveTrackSelection(definition.group, bandwidthMeter, definition.tracks);
selections[i] = adaptiveSelection;
} else { } else {
selections[i] = new FixedTrackSelection(definition.group, definition.tracks[0]); selections[i] = new FixedTrackSelection(definition.group, definition.tracks[0]);
int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate; int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate;
...@@ -274,6 +254,26 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { ...@@ -274,6 +254,26 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
} }
return selections; return selections;
} }
private AdaptiveTrackSelection createAdaptiveTrackSelection(
TrackGroup group, BandwidthMeter bandwidthMeter, int[] tracks) {
if (this.bandwidthMeter != null) {
bandwidthMeter = this.bandwidthMeter;
}
AdaptiveTrackSelection adaptiveTrackSelection =
new AdaptiveTrackSelection(
group,
tracks,
new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction),
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
bufferedFractionToLiveEdgeForQualityIncrease,
minTimeBetweenBufferReevaluationMs,
clock);
adaptiveTrackSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator);
return adaptiveTrackSelection;
}
} }
public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
......
...@@ -24,12 +24,14 @@ import com.google.android.exoplayer2.LoadControl; ...@@ -24,12 +24,14 @@ import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultAllocator;
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.PriorityTaskManager; import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size
...@@ -273,19 +275,22 @@ public final class BufferSizeAdaptationBuilder { ...@@ -273,19 +275,22 @@ public final class BufferSizeAdaptationBuilder {
TrackSelection.Factory trackSelectionFactory = TrackSelection.Factory trackSelectionFactory =
new TrackSelection.Factory() { new TrackSelection.Factory() {
@Override @Override
public TrackSelection createTrackSelection( public @NullableType TrackSelection[] createTrackSelections(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
return new BufferSizeAdaptiveTrackSelection( return TrackSelectionUtil.createTrackSelectionsForDefinitions(
group, definitions,
tracks, definition ->
bandwidthMeter, new BufferSizeAdaptiveTrackSelection(
minBufferMs, definition.group,
maxBufferMs, definition.tracks,
hysteresisBufferMs, bandwidthMeter,
startUpBandwidthFraction, minBufferMs,
startUpMinBufferForQualityIncreaseMs, maxBufferMs,
dynamicFormatFilter, hysteresisBufferMs,
clock); startUpBandwidthFraction,
startUpMinBufferForQualityIncreaseMs,
dynamicFormatFilter,
clock));
} }
}; };
......
...@@ -21,8 +21,8 @@ import com.google.android.exoplayer2.source.TrackGroup; ...@@ -21,8 +21,8 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* A {@link TrackSelection} consisting of a single track. * A {@link TrackSelection} consisting of a single track.
...@@ -56,10 +56,12 @@ public final class FixedTrackSelection extends BaseTrackSelection { ...@@ -56,10 +56,12 @@ public final class FixedTrackSelection extends BaseTrackSelection {
} }
@Override @Override
public FixedTrackSelection createTrackSelection( public @NullableType TrackSelection[] createTrackSelections(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
Assertions.checkArgument(tracks.length == 1); return TrackSelectionUtil.createTrackSelectionsForDefinitions(
return new FixedTrackSelection(group, tracks[0], reason, data); definitions,
definition ->
new FixedTrackSelection(definition.group, definition.tracks[0], reason, data));
} }
} }
......
...@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; ...@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* A {@link TrackSelection} whose selected track is updated randomly. * A {@link TrackSelection} whose selected track is updated randomly.
...@@ -49,9 +50,11 @@ public final class RandomTrackSelection extends BaseTrackSelection { ...@@ -49,9 +50,11 @@ public final class RandomTrackSelection extends BaseTrackSelection {
} }
@Override @Override
public RandomTrackSelection createTrackSelection( public @NullableType TrackSelection[] createTrackSelections(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
return new RandomTrackSelection(group, tracks, random); return TrackSelectionUtil.createTrackSelectionsForDefinitions(
definitions,
definition -> new RandomTrackSelection(definition.group, definition.tracks, random));
} }
} }
......
...@@ -21,8 +21,8 @@ import com.google.android.exoplayer2.Format; ...@@ -21,8 +21,8 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.trackselection.TrackSelectionUtil.AdaptiveTrackSelectionFactory;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
...@@ -61,42 +61,31 @@ public interface TrackSelection { ...@@ -61,42 +61,31 @@ public interface TrackSelection {
interface Factory { interface Factory {
/** /**
* Creates a new selection. * @deprecated Implement {@link #createTrackSelections(Definition[], BandwidthMeter)} instead.
* * Calling {@link TrackSelectionUtil#createTrackSelectionsForDefinitions(Definition[],
* @param group The {@link TrackGroup}. Must not be null. * AdaptiveTrackSelectionFactory)} helps to create a single adaptive track selection in the
* @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * same way as using this deprecated method.
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
* null or empty. May be in any order.
* @return The created selection.
*/ */
TrackSelection createTrackSelection( @Deprecated
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks); default TrackSelection createTrackSelection(
TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) {
throw new UnsupportedOperationException();
}
/** /**
* Creates a new selection for each {@link Definition}. * Creates a new selection for each {@link Definition}.
* *
* @param definitions A {@link Definition} array. May include null values. * @param definitions A {@link Definition} array. May include null values.
* @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.
* @return The created selections. For null entries in {@code definitions} returns null values. * @return The created selections. Must have the same length as {@code definitions} and may
* include null values.
*/ */
@SuppressWarnings("deprecation")
default @NullableType TrackSelection[] createTrackSelections( default @NullableType TrackSelection[] createTrackSelections(
@NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
TrackSelection[] selections = new TrackSelection[definitions.length]; return TrackSelectionUtil.createTrackSelectionsForDefinitions(
boolean createdAdaptiveTrackSelection = false; definitions,
for (int i = 0; i < definitions.length; i++) { definition -> createTrackSelection(definition.group, bandwidthMeter, definition.tracks));
Definition definition = definitions[i];
if (definition == null) {
continue;
}
if (definition.tracks.length > 1) {
Assertions.checkState(!createdAdaptiveTrackSelection);
createdAdaptiveTrackSelection = true;
selections[i] = createTrackSelection(definition.group, bandwidthMeter, definition.tracks);
} else {
selections[i] = new FixedTrackSelection(definition.group, definition.tracks[0]);
}
}
return selections;
} }
} }
......
...@@ -22,15 +22,59 @@ import com.google.android.exoplayer2.Format; ...@@ -22,15 +22,59 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.source.chunk.MediaChunkListIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkListIterator;
import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Track selection related utility methods. */ /** Track selection related utility methods. */
public final class TrackSelectionUtil { public final class TrackSelectionUtil {
private TrackSelectionUtil() {} private TrackSelectionUtil() {}
/** Functional interface to create a single adaptive track selection. */
public interface AdaptiveTrackSelectionFactory {
/**
* Creates an adaptive track selection for the provided track selection definition.
*
* @param trackSelectionDefinition A {@link Definition} for the track selection.
* @return The created track selection.
*/
TrackSelection createAdaptiveTrackSelection(Definition trackSelectionDefinition);
}
/**
* Creates track selections for an array of track selection definitions, with at most one
* multi-track adaptive selection.
*
* @param definitions The list of track selection {@link Definition definitions}. May include null
* values.
* @param adaptiveTrackSelectionFactory A factory for the multi-track adaptive track selection.
* @return The array of created track selection. For null entries in {@code definitions} returns
* null values.
*/
public static @NullableType TrackSelection[] createTrackSelectionsForDefinitions(
@NullableType Definition[] definitions,
AdaptiveTrackSelectionFactory adaptiveTrackSelectionFactory) {
TrackSelection[] selections = new TrackSelection[definitions.length];
boolean createdAdaptiveTrackSelection = false;
for (int i = 0; i < definitions.length; i++) {
Definition definition = definitions[i];
if (definition == null) {
continue;
}
if (definition.tracks.length > 1 && !createdAdaptiveTrackSelection) {
createdAdaptiveTrackSelection = true;
selections[i] = adaptiveTrackSelectionFactory.createAdaptiveTrackSelection(definition);
} else {
selections[i] = new FixedTrackSelection(definition.group, definition.tracks[0]);
}
}
return selections;
}
/** /**
* Returns average bitrate for chunks in bits per second. Chunks are included in average until * Returns average bitrate for chunks in bits per second. Chunks are included in average until
* {@code maxDurationMs} or the first unknown length chunk. * {@code maxDurationMs} or the first unknown length chunk.
......
...@@ -108,10 +108,7 @@ public final class DataSpec { ...@@ -108,10 +108,7 @@ public final class DataSpec {
* {@link DataSpec} is not intended to be used in conjunction with a cache. * {@link DataSpec} is not intended to be used in conjunction with a cache.
*/ */
public final @Nullable String key; public final @Nullable String key;
/** /** Request {@link Flags flags}. */
* Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
* {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
*/
public final @Flags int flags; public final @Flags int flags;
/** /**
......
...@@ -62,7 +62,7 @@ public interface Cache { ...@@ -62,7 +62,7 @@ public interface Cache {
void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan); void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan);
} }
/** /**
* Thrown when an error is encountered when writing data. * Thrown when an error is encountered when writing data.
*/ */
...@@ -82,7 +82,7 @@ public interface Cache { ...@@ -82,7 +82,7 @@ public interface Cache {
* Releases the cache. This method must be called when the cache is no longer required. The cache * Releases the cache. This method must be called when the cache is no longer required. The cache
* must not be used after calling this method. * must not be used after calling this method.
*/ */
void release() throws CacheException; void release();
/** /**
* Registers a listener to listen for changes to a given key. * Registers a listener to listen for changes to a given key.
...@@ -224,25 +224,6 @@ public interface Cache { ...@@ -224,25 +224,6 @@ public interface Cache {
long getCachedLength(String key, long position, long length); long getCachedLength(String key, long position, long length);
/** /**
* Sets the content length for the given key.
*
* @param key The cache key for the data.
* @param length The length of the data.
* @throws CacheException If an error is encountered.
*/
void setContentLength(String key, long length) throws CacheException;
/**
* Returns the content length for the given key if one set, or {@link
* com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
*
* @param key The cache key for the data.
* @return The content length for the given key if one set, or {@link
* com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
*/
long getContentLength(String key);
/**
* Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link
* CachedContent} is added if there isn't one already with the given key. * CachedContent} is added if there isn't one already with the given key.
* *
......
...@@ -42,10 +42,6 @@ import java.util.Map; ...@@ -42,10 +42,6 @@ import java.util.Map;
* A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache
* when possible. When data is not cached it is requested from an upstream {@link DataSource} and * when possible. When data is not cached it is requested from an upstream {@link DataSource} and
* written into the cache. * written into the cache.
*
* <p>By default requests whose length can not be resolved are not cached. This is to prevent
* caching of progressive live streams, which should usually not be cached. Caching of this kind of
* requests can be enabled per request with {@link DataSpec#FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}.
*/ */
public final class CacheDataSource implements DataSource { public final class CacheDataSource implements DataSource {
...@@ -303,7 +299,7 @@ public final class CacheDataSource implements DataSource { ...@@ -303,7 +299,7 @@ public final class CacheDataSource implements DataSource {
if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) { if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
bytesRemaining = dataSpec.length; bytesRemaining = dataSpec.length;
} else { } else {
bytesRemaining = cache.getContentLength(key); bytesRemaining = ContentMetadata.getContentLength(cache.getContentMetadata(key));
if (bytesRemaining != C.LENGTH_UNSET) { if (bytesRemaining != C.LENGTH_UNSET) {
bytesRemaining -= dataSpec.position; bytesRemaining -= dataSpec.position;
if (bytesRemaining <= 0) { if (bytesRemaining <= 0) {
...@@ -488,16 +484,12 @@ public final class CacheDataSource implements DataSource { ...@@ -488,16 +484,12 @@ public final class CacheDataSource implements DataSource {
ContentMetadataMutations mutations = new ContentMetadataMutations(); ContentMetadataMutations mutations = new ContentMetadataMutations();
if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) {
bytesRemaining = resolvedLength; bytesRemaining = resolvedLength;
ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining); ContentMetadataMutations.setContentLength(mutations, readPosition + bytesRemaining);
} }
if (isReadingFromUpstream()) { if (isReadingFromUpstream()) {
actualUri = currentDataSource.getUri(); actualUri = currentDataSource.getUri();
boolean isRedirected = !uri.equals(actualUri); boolean isRedirected = !uri.equals(actualUri);
if (isRedirected) { ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null);
ContentMetadataInternal.setRedirectedUri(mutations, actualUri);
} else {
ContentMetadataInternal.removeRedirectedUri(mutations);
}
} }
if (isWritingToCache()) { if (isWritingToCache()) {
cache.applyContentMetadataMutations(key, mutations); cache.applyContentMetadataMutations(key, mutations);
...@@ -507,14 +499,15 @@ public final class CacheDataSource implements DataSource { ...@@ -507,14 +499,15 @@ public final class CacheDataSource implements DataSource {
private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { private void setNoBytesRemainingAndMaybeStoreLength() throws IOException {
bytesRemaining = 0; bytesRemaining = 0;
if (isWritingToCache()) { if (isWritingToCache()) {
cache.setContentLength(key, readPosition); ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, readPosition);
cache.applyContentMetadataMutations(key, mutations);
} }
} }
private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {
ContentMetadata contentMetadata = cache.getContentMetadata(key); Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key));
Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata); return redirectedUri != null ? redirectedUri : defaultUri;
return redirectedUri == null ? defaultUri : redirectedUri;
} }
private static boolean isCausedByPositionOutOfRange(IOException e) { private static boolean isCausedByPositionOutOfRange(IOException e) {
......
...@@ -84,7 +84,10 @@ public final class CacheUtil { ...@@ -84,7 +84,10 @@ public final class CacheUtil {
CachingCounters counters) { CachingCounters counters) {
String key = buildCacheKey(dataSpec, cacheKeyFactory); String key = buildCacheKey(dataSpec, cacheKeyFactory);
long start = dataSpec.absoluteStreamPosition; long start = dataSpec.absoluteStreamPosition;
long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key); long left =
dataSpec.length != C.LENGTH_UNSET
? dataSpec.length
: ContentMetadata.getContentLength(cache.getContentMetadata(key));
counters.contentLength = left; counters.contentLength = left;
counters.alreadyCachedBytes = 0; counters.alreadyCachedBytes = 0;
counters.newlyCachedBytes = 0; counters.newlyCachedBytes = 0;
...@@ -188,7 +191,10 @@ public final class CacheUtil { ...@@ -188,7 +191,10 @@ public final class CacheUtil {
String key = buildCacheKey(dataSpec, cacheKeyFactory); String key = buildCacheKey(dataSpec, cacheKeyFactory);
long start = dataSpec.absoluteStreamPosition; long start = dataSpec.absoluteStreamPosition;
long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key); long left =
dataSpec.length != C.LENGTH_UNSET
? dataSpec.length
: ContentMetadata.getContentLength(cache.getContentMetadata(key));
while (left != 0) { while (left != 0) {
throwExceptionIfInterruptedOrCancelled(isCanceled); throwExceptionIfInterruptedOrCancelled(isCanceled);
long blockLength = long blockLength =
......
...@@ -55,7 +55,7 @@ import java.util.TreeSet; ...@@ -55,7 +55,7 @@ import java.util.TreeSet;
if (version < VERSION_METADATA_INTRODUCED) { if (version < VERSION_METADATA_INTRODUCED) {
long length = input.readLong(); long length = input.readLong();
ContentMetadataMutations mutations = new ContentMetadataMutations(); ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataInternal.setContentLength(mutations, length); ContentMetadataMutations.setContentLength(mutations, length);
cachedContent.applyMetadataMutations(mutations); cachedContent.applyMetadataMutations(mutations);
} else { } else {
cachedContent.metadata = DefaultContentMetadata.readFromStream(input); cachedContent.metadata = DefaultContentMetadata.readFromStream(input);
...@@ -216,7 +216,7 @@ import java.util.TreeSet; ...@@ -216,7 +216,7 @@ import java.util.TreeSet;
int result = id; int result = id;
result = 31 * result + key.hashCode(); result = 31 * result + key.hashCode();
if (version < VERSION_METADATA_INTRODUCED) { if (version < VERSION_METADATA_INTRODUCED) {
long length = ContentMetadataInternal.getContentLength(metadata); long length = ContentMetadata.getContentLength(metadata);
result = 31 * result + (int) (length ^ (length >>> 32)); result = 31 * result + (int) (length ^ (length >>> 32));
} else { } else {
result = 31 * result + metadata.hashCode(); result = 31 * result + metadata.hashCode();
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseBooleanArray;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.AtomicFile; import com.google.android.exoplayer2.util.AtomicFile;
...@@ -42,6 +43,7 @@ import javax.crypto.CipherOutputStream; ...@@ -42,6 +43,7 @@ import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Maintains the index of cached content. */ /** Maintains the index of cached content. */
/* package */ class CachedContentIndex { /* package */ class CachedContentIndex {
...@@ -53,7 +55,30 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -53,7 +55,30 @@ import javax.crypto.spec.SecretKeySpec;
private static final int FLAG_ENCRYPTED_INDEX = 1; private static final int FLAG_ENCRYPTED_INDEX = 1;
private final HashMap<String, CachedContent> keyToContent; private final HashMap<String, CachedContent> keyToContent;
private final SparseArray<String> idToKey; /**
* Maps assigned ids to their corresponding keys. Also contains (id -> null) entries for ids that
* have been removed from the index since it was last stored. This prevents reuse of these ids,
* which is necessary to avoid clashes that could otherwise occur as a result of the sequence:
*
* <p>[1] (key1, id1) is removed from the in-memory index ... the index is not stored to disk ...
* [2] id1 is reused for a different key2 ... the index is not stored to disk ... [3] A file for
* key2 is partially written using a path corresponding to id1 ... the process is killed before
* the index is stored to disk ... [4] The index is read from disk, causing the partially written
* file to be incorrectly associated to key1
*
* <p>By avoiding id reuse in step [2], a new id2 will be used instead. Step [4] will then delete
* the partially written file because the index does not contain an entry for id2.
*
* <p>When the index is next stored (id -> null) entries are removed, making the ids eligible for
* reuse.
*/
private final SparseArray<@NullableType String> idToKey;
/**
* Tracks ids for which (id -> null) entries are present in idToKey, so that they can be removed
* efficiently when the index is next stored.
*/
private final SparseBooleanArray removedIds;
private final AtomicFile atomicFile; private final AtomicFile atomicFile;
private final Cipher cipher; private final Cipher cipher;
private final SecretKeySpec secretKeySpec; private final SecretKeySpec secretKeySpec;
...@@ -105,6 +130,7 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -105,6 +130,7 @@ import javax.crypto.spec.SecretKeySpec;
} }
keyToContent = new HashMap<>(); keyToContent = new HashMap<>();
idToKey = new SparseArray<>(); idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray();
atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
} }
...@@ -125,6 +151,12 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -125,6 +151,12 @@ import javax.crypto.spec.SecretKeySpec;
} }
writeFile(); writeFile();
changed = false; changed = false;
// Make ids that were removed since the index was last stored eligible for re-use.
int removedIdCount = removedIds.size();
for (int i = 0; i < removedIdCount; i++) {
idToKey.remove(removedIds.keyAt(i));
}
removedIds.clear();
} }
/** /**
...@@ -169,8 +201,11 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -169,8 +201,11 @@ import javax.crypto.spec.SecretKeySpec;
CachedContent cachedContent = keyToContent.get(key); CachedContent cachedContent = keyToContent.get(key);
if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {
keyToContent.remove(key); keyToContent.remove(key);
idToKey.remove(cachedContent.id);
changed = true; changed = true;
// Keep an entry in idToKey to stop the id from being reused until the index is next stored.
idToKey.put(cachedContent.id, /* value= */ null);
// Track that the entry should be removed from idToKey when the index is next stored.
removedIds.put(cachedContent.id, /* value= */ true);
} }
} }
......
...@@ -15,44 +15,73 @@ ...@@ -15,44 +15,73 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
/** /**
* Interface for an immutable snapshot of keyed metadata. * Interface for an immutable snapshot of keyed metadata.
*
* <p>Internal metadata names are prefixed with {@value #INTERNAL_METADATA_NAME_PREFIX}. Custom
* metadata names should avoid this prefix to prevent clashes.
*/ */
public interface ContentMetadata { public interface ContentMetadata {
/** Prefix of internal metadata names. */ /**
String INTERNAL_METADATA_NAME_PREFIX = "exo_"; * Prefix for custom metadata keys. Applications can use keys starting with this prefix without
* any risk of their keys colliding with ones defined by the ExoPlayer library.
*/
@SuppressWarnings("unused")
String KEY_CUSTOM_PREFIX = "custom_";
/** Key for redirected uri (type: String). */
String KEY_REDIRECTED_URI = "exo_redir";
/** Key for content length in bytes (type: long). */
String KEY_CONTENT_LENGTH = "exo_len";
/** /**
* Returns a metadata value. * Returns a metadata value.
* *
* @param name Name of the metadata to be returned. * @param key Key of the metadata to be returned.
* @param defaultValue Value to return if the metadata doesn't exist. * @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value. * @return The metadata value.
*/ */
byte[] get(String name, byte[] defaultValue); @Nullable
byte[] get(String key, @Nullable byte[] defaultValue);
/** /**
* Returns a metadata value. * Returns a metadata value.
* *
* @param name Name of the metadata to be returned. * @param key Key of the metadata to be returned.
* @param defaultValue Value to return if the metadata doesn't exist. * @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value. * @return The metadata value.
*/ */
String get(String name, String defaultValue); @Nullable
String get(String key, @Nullable String defaultValue);
/** /**
* Returns a metadata value. * Returns a metadata value.
* *
* @param name Name of the metadata to be returned. * @param key Key of the metadata to be returned.
* @param defaultValue Value to return if the metadata doesn't exist. * @param defaultValue Value to return if the metadata doesn't exist.
* @return The metadata value. * @return The metadata value.
*/ */
long get(String name, long defaultValue); long get(String key, long defaultValue);
/** Returns whether the metadata is available. */ /** Returns whether the metadata is available. */
boolean contains(String name); boolean contains(String key);
/**
* Returns the value stored under {@link #KEY_CONTENT_LENGTH}, or {@link C#LENGTH_UNSET} if not
* set.
*/
static long getContentLength(ContentMetadata contentMetadata) {
return contentMetadata.get(KEY_CONTENT_LENGTH, C.LENGTH_UNSET);
}
/**
* Returns the value stored under {@link #KEY_REDIRECTED_URI} as a {@link Uri}, or {code null} if
* not set.
*/
@Nullable
static Uri getRedirectedUri(ContentMetadata contentMetadata) {
String redirectedUri = contentMetadata.get(KEY_REDIRECTED_URI, (String) null);
return redirectedUri == null ? null : Uri.parse(redirectedUri);
}
} }
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
/** Helper classes to easily access and modify internal metadata values. */
/* package */ final class ContentMetadataInternal {
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_CONTENT_LENGTH = PREFIX + "len";
/** Returns the content length metadata, or {@link C#LENGTH_UNSET} if not set. */
public static long getContentLength(ContentMetadata contentMetadata) {
return contentMetadata.get(METADATA_NAME_CONTENT_LENGTH, C.LENGTH_UNSET);
}
/** Adds a mutation to set content length metadata value. */
public static void setContentLength(ContentMetadataMutations mutations, long length) {
mutations.set(METADATA_NAME_CONTENT_LENGTH, length);
}
/** Adds a mutation to remove content length metadata value. */
public static void removeContentLength(ContentMetadataMutations mutations) {
mutations.remove(METADATA_NAME_CONTENT_LENGTH);
}
/** Returns the redirected uri metadata, or {@code null} if not set. */
public @Nullable static Uri getRedirectedUri(ContentMetadata contentMetadata) {
String redirectedUri = contentMetadata.get(METADATA_NAME_REDIRECTED_URI, (String) null);
return redirectedUri == null ? null : Uri.parse(redirectedUri);
}
/**
* Adds a mutation to set redirected uri metadata value. Passing {@code null} as {@code uri} isn't
* allowed.
*/
public static void setRedirectedUri(ContentMetadataMutations mutations, Uri uri) {
mutations.set(METADATA_NAME_REDIRECTED_URI, uri.toString());
}
/** Adds a mutation to remove redirected uri metadata value. */
public static void removeRedirectedUri(ContentMetadataMutations mutations) {
mutations.remove(METADATA_NAME_REDIRECTED_URI);
}
private ContentMetadataInternal() {
// Prevent instantiation.
}
}
...@@ -15,6 +15,9 @@ ...@@ -15,6 +15,9 @@
*/ */
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
...@@ -30,6 +33,36 @@ import java.util.Map.Entry; ...@@ -30,6 +33,36 @@ import java.util.Map.Entry;
*/ */
public class ContentMetadataMutations { public class ContentMetadataMutations {
/**
* Adds a mutation to set the {@link ContentMetadata#KEY_CONTENT_LENGTH} value, or to remove any
* existing value if {@link C#LENGTH_UNSET} is passed.
*
* @param mutations The mutations to modify.
* @param length The length value, or {@link C#LENGTH_UNSET} to remove any existing entry.
* @return The mutations instance, for convenience.
*/
public static ContentMetadataMutations setContentLength(
ContentMetadataMutations mutations, long length) {
return mutations.set(ContentMetadata.KEY_CONTENT_LENGTH, length);
}
/**
* Adds a mutation to set the {@link ContentMetadata#KEY_REDIRECTED_URI} value, or to remove any
* existing entry if {@code null} is passed.
*
* @param mutations The mutations to modify.
* @param uri The {@link Uri} value, or {@code null} to remove any existing entry.
* @return The mutations instance, for convenience.
*/
public static ContentMetadataMutations setRedirectedUri(
ContentMetadataMutations mutations, @Nullable Uri uri) {
if (uri == null) {
return mutations.remove(ContentMetadata.KEY_REDIRECTED_URI);
} else {
return mutations.set(ContentMetadata.KEY_REDIRECTED_URI, uri.toString());
}
}
private final Map<String, Object> editedValues; private final Map<String, Object> editedValues;
private final List<String> removedValues; private final List<String> removedValues;
...@@ -45,7 +78,7 @@ public class ContentMetadataMutations { ...@@ -45,7 +78,7 @@ public class ContentMetadataMutations {
* *
* @param name The name of the metadata value. * @param name The name of the metadata value.
* @param value The value to be set. * @param value The value to be set.
* @return This Editor instance, for convenience. * @return This instance, for convenience.
*/ */
public ContentMetadataMutations set(String name, String value) { public ContentMetadataMutations set(String name, String value) {
return checkAndSet(name, value); return checkAndSet(name, value);
...@@ -56,7 +89,7 @@ public class ContentMetadataMutations { ...@@ -56,7 +89,7 @@ public class ContentMetadataMutations {
* *
* @param name The name of the metadata value. * @param name The name of the metadata value.
* @param value The value to be set. * @param value The value to be set.
* @return This Editor instance, for convenience. * @return This instance, for convenience.
*/ */
public ContentMetadataMutations set(String name, long value) { public ContentMetadataMutations set(String name, long value) {
return checkAndSet(name, value); return checkAndSet(name, value);
...@@ -68,7 +101,7 @@ public class ContentMetadataMutations { ...@@ -68,7 +101,7 @@ public class ContentMetadataMutations {
* *
* @param name The name of the metadata value. * @param name The name of the metadata value.
* @param value The value to be set. * @param value The value to be set.
* @return This Editor instance, for convenience. * @return This instance, for convenience.
*/ */
public ContentMetadataMutations set(String name, byte[] value) { public ContentMetadataMutations set(String name, byte[] value) {
return checkAndSet(name, Arrays.copyOf(value, value.length)); return checkAndSet(name, Arrays.copyOf(value, value.length));
...@@ -78,7 +111,7 @@ public class ContentMetadataMutations { ...@@ -78,7 +111,7 @@ public class ContentMetadataMutations {
* Adds a mutation to remove a metadata value. * Adds a mutation to remove a metadata value.
* *
* @param name The name of the metadata value. * @param name The name of the metadata value.
* @return This Editor instance, for convenience. * @return This instance, for convenience.
*/ */
public ContentMetadataMutations remove(String name) { public ContentMetadataMutations remove(String name) {
removedValues.add(name); removedValues.add(name);
......
...@@ -64,6 +64,10 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -64,6 +64,10 @@ public final class DefaultContentMetadata implements ContentMetadata {
private final Map<String, byte[]> metadata; private final Map<String, byte[]> metadata;
public DefaultContentMetadata() {
this(Collections.emptyMap());
}
private DefaultContentMetadata(Map<String, byte[]> metadata) { private DefaultContentMetadata(Map<String, byte[]> metadata) {
this.metadata = Collections.unmodifiableMap(metadata); this.metadata = Collections.unmodifiableMap(metadata);
} }
...@@ -74,7 +78,7 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -74,7 +78,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
*/ */
public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) { public DefaultContentMetadata copyWithMutationsApplied(ContentMetadataMutations mutations) {
Map<String, byte[]> mutatedMetadata = applyMutations(metadata, mutations); Map<String, byte[]> mutatedMetadata = applyMutations(metadata, mutations);
if (isMetadataEqual(mutatedMetadata)) { if (isMetadataEqual(metadata, mutatedMetadata)) {
return this; return this;
} }
return new DefaultContentMetadata(mutatedMetadata); return new DefaultContentMetadata(mutatedMetadata);
...@@ -97,7 +101,8 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -97,7 +101,8 @@ public final class DefaultContentMetadata implements ContentMetadata {
} }
@Override @Override
public final byte[] get(String name, byte[] defaultValue) { @Nullable
public final byte[] get(String name, @Nullable byte[] defaultValue) {
if (metadata.containsKey(name)) { if (metadata.containsKey(name)) {
byte[] bytes = metadata.get(name); byte[] bytes = metadata.get(name);
return Arrays.copyOf(bytes, bytes.length); return Arrays.copyOf(bytes, bytes.length);
...@@ -107,7 +112,8 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -107,7 +112,8 @@ public final class DefaultContentMetadata implements ContentMetadata {
} }
@Override @Override
public final String get(String name, String defaultValue) { @Nullable
public final String get(String name, @Nullable String defaultValue) {
if (metadata.containsKey(name)) { if (metadata.containsKey(name)) {
byte[] bytes = metadata.get(name); byte[] bytes = metadata.get(name);
return new String(bytes, Charset.forName(C.UTF8_NAME)); return new String(bytes, Charset.forName(C.UTF8_NAME));
...@@ -139,21 +145,7 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -139,21 +145,7 @@ public final class DefaultContentMetadata implements ContentMetadata {
if (o == null || getClass() != o.getClass()) { if (o == null || getClass() != o.getClass()) {
return false; return false;
} }
return isMetadataEqual(((DefaultContentMetadata) o).metadata); return isMetadataEqual(metadata, ((DefaultContentMetadata) o).metadata);
}
private boolean isMetadataEqual(Map<String, byte[]> otherMetadata) {
if (metadata.size() != otherMetadata.size()) {
return false;
}
for (Entry<String, byte[]> entry : metadata.entrySet()) {
byte[] value = entry.getValue();
byte[] otherValue = otherMetadata.get(entry.getKey());
if (!Arrays.equals(value, otherValue)) {
return false;
}
}
return true;
} }
@Override @Override
...@@ -168,6 +160,20 @@ public final class DefaultContentMetadata implements ContentMetadata { ...@@ -168,6 +160,20 @@ public final class DefaultContentMetadata implements ContentMetadata {
return hashCode; return hashCode;
} }
private static boolean isMetadataEqual(Map<String, byte[]> first, Map<String, byte[]> second) {
if (first.size() != second.size()) {
return false;
}
for (Entry<String, byte[]> entry : first.entrySet()) {
byte[] value = entry.getValue();
byte[] otherValue = second.get(entry.getKey());
if (!Arrays.equals(value, otherValue)) {
return false;
}
}
return true;
}
private static Map<String, byte[]> applyMutations( private static Map<String, byte[]> applyMutations(
Map<String, byte[]> otherMetadata, ContentMetadataMutations mutations) { Map<String, byte[]> otherMetadata, ContentMetadataMutations mutations) {
HashMap<String, byte[]> metadata = new HashMap<>(otherMetadata); HashMap<String, byte[]> metadata = new HashMap<>(otherMetadata);
......
...@@ -146,13 +146,16 @@ public final class SimpleCache implements Cache { ...@@ -146,13 +146,16 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void release() throws CacheException { public synchronized void release() {
if (released) { if (released) {
return; return;
} }
listeners.clear(); listeners.clear();
removeStaleSpans();
try { try {
removeStaleSpansAndCachedContents(); index.store();
} catch (CacheException e) {
Log.e(TAG, "Storing index file failed", e);
} finally { } finally {
unlockFolder(cacheDir); unlockFolder(cacheDir);
released = true; released = true;
...@@ -265,7 +268,7 @@ public final class SimpleCache implements Cache { ...@@ -265,7 +268,7 @@ public final class SimpleCache implements Cache {
if (!cacheDir.exists()) { if (!cacheDir.exists()) {
// For some reason the cache directory doesn't exist. Make a best effort to create it. // For some reason the cache directory doesn't exist. Make a best effort to create it.
cacheDir.mkdirs(); cacheDir.mkdirs();
removeStaleSpansAndCachedContents(); removeStaleSpans();
} }
evictor.onStartFile(this, key, position, maxLength); evictor.onStartFile(this, key, position, maxLength);
return SimpleCacheSpan.getCacheFile( return SimpleCacheSpan.getCacheFile(
...@@ -290,7 +293,7 @@ public final class SimpleCache implements Cache { ...@@ -290,7 +293,7 @@ public final class SimpleCache implements Cache {
return; return;
} }
// Check if the span conflicts with the set content length // Check if the span conflicts with the set content length
long length = ContentMetadataInternal.getContentLength(cachedContent.getMetadata()); long length = ContentMetadata.getContentLength(cachedContent.getMetadata());
if (length != C.LENGTH_UNSET) { if (length != C.LENGTH_UNSET) {
Assertions.checkState((span.position + span.length) <= length); Assertions.checkState((span.position + span.length) <= length);
} }
...@@ -311,9 +314,9 @@ public final class SimpleCache implements Cache { ...@@ -311,9 +314,9 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void removeSpan(CacheSpan span) throws CacheException { public synchronized void removeSpan(CacheSpan span) {
Assertions.checkState(!released); Assertions.checkState(!released);
removeSpan(span, true); removeSpanInternal(span);
} }
@Override @Override
...@@ -331,18 +334,6 @@ public final class SimpleCache implements Cache { ...@@ -331,18 +334,6 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void setContentLength(String key, long length) throws CacheException {
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataInternal.setContentLength(mutations, length);
applyContentMetadataMutations(key, mutations);
}
@Override
public synchronized long getContentLength(String key) {
return ContentMetadataInternal.getContentLength(getContentMetadata(key));
}
@Override
public synchronized void applyContentMetadataMutations( public synchronized void applyContentMetadataMutations(
String key, ContentMetadataMutations mutations) throws CacheException { String key, ContentMetadataMutations mutations) throws CacheException {
Assertions.checkState(!released); Assertions.checkState(!released);
...@@ -379,7 +370,7 @@ public final class SimpleCache implements Cache { ...@@ -379,7 +370,7 @@ public final class SimpleCache implements Cache {
if (span.isCached && !span.file.exists()) { if (span.isCached && !span.file.exists()) {
// The file has been deleted from under us. It's likely that other files will have been // The file has been deleted from under us. It's likely that other files will have been
// deleted too, so scan the whole in-memory representation. // deleted too, so scan the whole in-memory representation.
removeStaleSpansAndCachedContents(); removeStaleSpans();
continue; continue;
} }
return span; return span;
...@@ -431,27 +422,21 @@ public final class SimpleCache implements Cache { ...@@ -431,27 +422,21 @@ public final class SimpleCache implements Cache {
notifySpanAdded(span); notifySpanAdded(span);
} }
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { private void removeSpanInternal(CacheSpan span) {
CachedContent cachedContent = index.get(span.key); CachedContent cachedContent = index.get(span.key);
if (cachedContent == null || !cachedContent.removeSpan(span)) { if (cachedContent == null || !cachedContent.removeSpan(span)) {
return; return;
} }
totalSpace -= span.length; totalSpace -= span.length;
try { index.maybeRemove(cachedContent.key);
if (removeEmptyCachedContent) { notifySpanRemoved(span);
index.maybeRemove(cachedContent.key);
index.store();
}
} finally {
notifySpanRemoved(span);
}
} }
/** /**
* Scans all of the cached spans in the in-memory representation, removing any for which files no * Scans all of the cached spans in the in-memory representation, removing any for which files no
* longer exist. * longer exist.
*/ */
private void removeStaleSpansAndCachedContents() throws CacheException { private void removeStaleSpans() {
ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>(); ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : index.getAll()) { for (CachedContent cachedContent : index.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) { for (CacheSpan span : cachedContent.getSpans()) {
...@@ -461,11 +446,8 @@ public final class SimpleCache implements Cache { ...@@ -461,11 +446,8 @@ public final class SimpleCache implements Cache {
} }
} }
for (int i = 0; i < spansToBeRemoved.size(); i++) { for (int i = 0; i < spansToBeRemoved.size(); i++) {
// Remove span but not CachedContent to prevent multiple index.store() calls. removeSpanInternal(spansToBeRemoved.get(i));
removeSpan(spansToBeRemoved.get(i), false);
} }
index.removeEmpty();
index.store();
} }
private void notifySpanRemoved(CacheSpan span) { private void notifySpanRemoved(CacheSpan span) {
......
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="51% 12%" tts:origin="24% 78%"/>
<region xml:id="region_1" tts:extent="57% 6%" tts:origin="21% 85%"/>
<region xml:id="region_2" tts:extent="51% 12%" tts:origin="24% 28%"/>
<region xml:id="region_3" tts:extent="57% 6%" tts:origin="21% 35%"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_2" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_3" smpte:backgroundImage="#img_1"/>
<div begin="00:00:07.200" end="00:59:03.000" region="region_2" smpte:backgroundImage="#img_0"/>
</body>
</tt>
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng" tts:extent="1280px 720px">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="653px 86px" tts:origin="307px 562px"/>
<region xml:id="region_1" tts:extent="730px 43px" tts:origin="269px 612px"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_0" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_1" smpte:backgroundImage="#img_1"/>
</body>
</tt>
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="653px 86px" tts:origin="307px 562px"/>
<region xml:id="region_1" tts:extent="730px 43px" tts:origin="269px 612px"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_0" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_1" smpte:backgroundImage="#img_1"/>
</body>
</tt>
...@@ -63,6 +63,9 @@ public final class TtmlDecoderTest { ...@@ -63,6 +63,9 @@ public final class TtmlDecoderTest {
private static final String FONT_SIZE_INVALID_TTML_FILE = "ttml/font_size_invalid.xml"; private static final String FONT_SIZE_INVALID_TTML_FILE = "ttml/font_size_invalid.xml";
private static final String FONT_SIZE_EMPTY_TTML_FILE = "ttml/font_size_empty.xml"; private static final String FONT_SIZE_EMPTY_TTML_FILE = "ttml/font_size_empty.xml";
private static final String FRAME_RATE_TTML_FILE = "ttml/frame_rate.xml"; private static final String FRAME_RATE_TTML_FILE = "ttml/frame_rate.xml";
private static final String BITMAP_REGION_FILE = "ttml/bitmap_percentage_region.xml";
private static final String BITMAP_PIXEL_REGION_FILE = "ttml/bitmap_pixel_region.xml";
private static final String BITMAP_UNSUPPORTED_REGION_FILE = "ttml/bitmap_unsupported_region.xml";
@Test @Test
public void testInlineAttributes() throws IOException, SubtitleDecoderException { public void testInlineAttributes() throws IOException, SubtitleDecoderException {
...@@ -259,56 +262,56 @@ public final class TtmlDecoderTest { ...@@ -259,56 +262,56 @@ public final class TtmlDecoderTest {
@Test @Test
public void testMultipleRegions() throws IOException, SubtitleDecoderException { public void testMultipleRegions() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);
List<Cue> output = subtitle.getCues(1000000); List<Cue> cues = subtitle.getCues(1000000);
assertThat(output).hasSize(2); assertThat(cues).hasSize(2);
Cue ttmlCue = output.get(0); Cue cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("lorem"); assertThat(cue.text.toString()).isEqualTo("lorem");
assertThat(ttmlCue.position).isEqualTo(10f / 100f); assertThat(cue.position).isEqualTo(10f / 100f);
assertThat(ttmlCue.line).isEqualTo(10f / 100f); assertThat(cue.line).isEqualTo(10f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
ttmlCue = output.get(1); cue = cues.get(1);
assertThat(ttmlCue.text.toString()).isEqualTo("amet"); assertThat(cue.text.toString()).isEqualTo("amet");
assertThat(ttmlCue.position).isEqualTo(60f / 100f); assertThat(cue.position).isEqualTo(60f / 100f);
assertThat(ttmlCue.line).isEqualTo(10f / 100f); assertThat(cue.line).isEqualTo(10f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
output = subtitle.getCues(5000000); cues = subtitle.getCues(5000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("ipsum"); assertThat(cue.text.toString()).isEqualTo("ipsum");
assertThat(ttmlCue.position).isEqualTo(40f / 100f); assertThat(cue.position).isEqualTo(40f / 100f);
assertThat(ttmlCue.line).isEqualTo(40f / 100f); assertThat(cue.line).isEqualTo(40f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
output = subtitle.getCues(9000000); cues = subtitle.getCues(9000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("dolor"); assertThat(cue.text.toString()).isEqualTo("dolor");
assertThat(ttmlCue.position).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(ttmlCue.line).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
assertThat(ttmlCue.size).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
// TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed. // TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.
// assertEquals(10f / 100f, ttmlCue.position); // assertEquals(10f / 100f, cue.position);
// assertEquals(80f / 100f, ttmlCue.line); // assertEquals(80f / 100f, cue.line);
// assertEquals(1f, ttmlCue.size); // assertEquals(1f, cue.size);
output = subtitle.getCues(21000000); cues = subtitle.getCues(21000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this"); assertThat(cue.text.toString()).isEqualTo("She first said this");
assertThat(ttmlCue.position).isEqualTo(45f / 100f); assertThat(cue.position).isEqualTo(45f / 100f);
assertThat(ttmlCue.line).isEqualTo(45f / 100f); assertThat(cue.line).isEqualTo(45f / 100f);
assertThat(ttmlCue.size).isEqualTo(35f / 100f); assertThat(cue.size).isEqualTo(35f / 100f);
output = subtitle.getCues(25000000); cues = subtitle.getCues(25000000);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this"); assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this");
output = subtitle.getCues(29000000); cues = subtitle.getCues(29000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this"); assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this");
assertThat(ttmlCue.position).isEqualTo(45f / 100f); assertThat(cue.position).isEqualTo(45f / 100f);
assertThat(ttmlCue.line).isEqualTo(45f / 100f); assertThat(cue.line).isEqualTo(45f / 100f);
} }
@Test @Test
...@@ -499,6 +502,91 @@ public final class TtmlDecoderTest { ...@@ -499,6 +502,91 @@ public final class TtmlDecoderTest {
assertThat((double) subtitle.getEventTime(3)).isWithin(2000).of(2_002_000_000); assertThat((double) subtitle.getEventTime(3)).isWithin(2000).of(2_002_000_000);
} }
@Test
public void testBitmapPercentageRegion() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(BITMAP_REGION_FILE);
List<Cue> cues = subtitle.getCues(1000000);
assertThat(cues).hasSize(1);
Cue cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(24f / 100f);
assertThat(cue.line).isEqualTo(28f / 100f);
assertThat(cue.size).isEqualTo(51f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(4000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(21f / 100f);
assertThat(cue.line).isEqualTo(35f / 100f);
assertThat(cue.size).isEqualTo(57f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(7500000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(24f / 100f);
assertThat(cue.line).isEqualTo(28f / 100f);
assertThat(cue.size).isEqualTo(51f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
}
@Test
public void testBitmapPixelRegion() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(BITMAP_PIXEL_REGION_FILE);
List<Cue> cues = subtitle.getCues(1000000);
assertThat(cues).hasSize(1);
Cue cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(307f / 1280f);
assertThat(cue.line).isEqualTo(562f / 720f);
assertThat(cue.size).isEqualTo(653f / 1280f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(4000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(269f / 1280f);
assertThat(cue.line).isEqualTo(612f / 720f);
assertThat(cue.size).isEqualTo(730f / 1280f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
}
@Test
public void testBitmapUnsupportedRegion() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(BITMAP_UNSUPPORTED_REGION_FILE);
List<Cue> cues = subtitle.getCues(1000000);
assertThat(cues).hasSize(1);
Cue cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(4000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
}
private void assertSpans( private void assertSpans(
TtmlSubtitle subtitle, TtmlSubtitle subtitle,
int second, int second,
......
...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroup; ...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeMediaChunk; import com.google.android.exoplayer2.testutil.FakeMediaChunk;
import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -66,15 +67,20 @@ public final class AdaptiveTrackSelectionTest { ...@@ -66,15 +67,20 @@ public final class AdaptiveTrackSelectionTest {
} }
@Test @Test
@SuppressWarnings("deprecation")
public void testFactoryUsesInitiallyProvidedBandwidthMeter() { public void testFactoryUsesInitiallyProvidedBandwidthMeter() {
BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class); BandwidthMeter initialBandwidthMeter = mock(BandwidthMeter.class);
BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class); BandwidthMeter injectedBandwidthMeter = mock(BandwidthMeter.class);
Format format = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
@SuppressWarnings("deprecation") Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
AdaptiveTrackSelection adaptiveTrackSelection = TrackSelection[] trackSelections =
new AdaptiveTrackSelection.Factory(initialBandwidthMeter) new AdaptiveTrackSelection.Factory(initialBandwidthMeter)
.createTrackSelection(new TrackGroup(format), injectedBandwidthMeter, /* tracks= */ 0); .createTrackSelections(
adaptiveTrackSelection.updateSelectedTrack( new Definition[] {
new Definition(new TrackGroup(format1, format2), /* tracks= */ 0, 1)
},
injectedBandwidthMeter);
trackSelections[0].updateSelectedTrack(
/* playbackPositionUs= */ 0, /* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 0, /* bufferedDurationUs= */ 0,
/* availableDurationUs= */ C.TIME_UNSET, /* availableDurationUs= */ C.TIME_UNSET,
......
...@@ -247,7 +247,8 @@ public final class CacheDataSourceTest { ...@@ -247,7 +247,8 @@ public final class CacheDataSourceTest {
// Read partial at EOS but don't cross it so length is unknown. // Read partial at EOS but don't cross it so length is unknown.
CacheDataSource cacheDataSource = createCacheDataSource(false, true); CacheDataSource cacheDataSource = createCacheDataSource(false, true);
assertReadData(cacheDataSource, dataSpec, true); assertReadData(cacheDataSource, dataSpec, true);
assertThat(cache.getContentLength(defaultCacheKey)).isEqualTo(C.LENGTH_UNSET); assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))
.isEqualTo(C.LENGTH_UNSET);
// Now do an unbounded request for whole data. This will cause a bounded request from upstream. // Now do an unbounded request for whole data. This will cause a bounded request from upstream.
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong. // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
...@@ -285,7 +286,8 @@ public final class CacheDataSourceTest { ...@@ -285,7 +286,8 @@ public final class CacheDataSourceTest {
cacheDataSource.close(); cacheDataSource.close();
assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1); assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1);
assertThat(cache.getContentLength(defaultCacheKey)).isEqualTo(TEST_DATA.length); assertThat(ContentMetadata.getContentLength(cache.getContentMetadata(defaultCacheKey)))
.isEqualTo(TEST_DATA.length);
} }
@Test @Test
...@@ -467,11 +469,7 @@ public final class CacheDataSourceTest { ...@@ -467,11 +469,7 @@ public final class CacheDataSourceTest {
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey); NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey);
for (CacheSpan cachedSpan : cachedSpans) { for (CacheSpan cachedSpan : cachedSpans) {
if (cachedSpan.position >= halfDataLength) { if (cachedSpan.position >= halfDataLength) {
try { cache.removeSpan(cachedSpan);
cache.removeSpan(cachedSpan);
} catch (Cache.CacheException e) {
// do nothing
}
} }
} }
...@@ -516,7 +514,9 @@ public final class CacheDataSourceTest { ...@@ -516,7 +514,9 @@ public final class CacheDataSourceTest {
// If the request was unbounded then the content length should be cached, either because the // If the request was unbounded then the content length should be cached, either because the
// content length was known or because EOS was read. If the request was bounded then the content // content length was known or because EOS was read. If the request was bounded then the content
// length will not have been determined. // length will not have been determined.
assertThat(cache.getContentLength(customCacheKey ? this.customCacheKey : defaultCacheKey)) ContentMetadata metadata =
cache.getContentMetadata(customCacheKey ? this.customCacheKey : defaultCacheKey);
assertThat(ContentMetadata.getContentLength(metadata))
.isEqualTo(dataSpec.length == C.LENGTH_UNSET ? TEST_DATA.length : C.LENGTH_UNSET); .isEqualTo(dataSpec.length == C.LENGTH_UNSET ? TEST_DATA.length : C.LENGTH_UNSET);
} }
......
...@@ -79,8 +79,11 @@ public final class CacheUtilTest { ...@@ -79,8 +79,11 @@ public final class CacheUtilTest {
} }
@Override @Override
public long getContentLength(String key) { public ContentMetadata getContentMetadata(String key) {
return contentLength; DefaultContentMetadata metadata = new DefaultContentMetadata();
ContentMetadataMutations mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, contentLength);
return metadata.copyWithMutationsApplied(mutations);
} }
} }
......
...@@ -154,11 +154,11 @@ public class CachedContentIndexTest { ...@@ -154,11 +154,11 @@ public class CachedContentIndexTest {
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5); assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
ContentMetadata metadata = index.get("ABCDE").getMetadata(); ContentMetadata metadata = index.get("ABCDE").getMetadata();
assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10); assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2); assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
ContentMetadata metadata2 = index.get("KLMNO").getMetadata(); ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
} }
@Test @Test
...@@ -172,12 +172,12 @@ public class CachedContentIndexTest { ...@@ -172,12 +172,12 @@ public class CachedContentIndexTest {
assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5); assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
ContentMetadata metadata = index.get("ABCDE").getMetadata(); ContentMetadata metadata = index.get("ABCDE").getMetadata();
assertThat(ContentMetadataInternal.getContentLength(metadata)).isEqualTo(10); assertThat(ContentMetadata.getContentLength(metadata)).isEqualTo(10);
assertThat(ContentMetadataInternal.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde")); assertThat(ContentMetadata.getRedirectedUri(metadata)).isEqualTo(Uri.parse("abcde"));
assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2); assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
ContentMetadata metadata2 = index.get("KLMNO").getMetadata(); ContentMetadata metadata2 = index.get("KLMNO").getMetadata();
assertThat(ContentMetadataInternal.getContentLength(metadata2)).isEqualTo(2560); assertThat(ContentMetadata.getContentLength(metadata2)).isEqualTo(2560);
} }
@Test @Test
...@@ -297,11 +297,11 @@ public class CachedContentIndexTest { ...@@ -297,11 +297,11 @@ public class CachedContentIndexTest {
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2) private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
throws IOException { throws IOException {
ContentMetadataMutations mutations1 = new ContentMetadataMutations(); ContentMetadataMutations mutations1 = new ContentMetadataMutations();
ContentMetadataInternal.setContentLength(mutations1, 2560); ContentMetadataMutations.setContentLength(mutations1, 2560);
index.getOrAdd("KLMNO").applyMetadataMutations(mutations1); index.getOrAdd("KLMNO").applyMetadataMutations(mutations1);
ContentMetadataMutations mutations2 = new ContentMetadataMutations(); ContentMetadataMutations mutations2 = new ContentMetadataMutations();
ContentMetadataInternal.setContentLength(mutations2, 10); ContentMetadataMutations.setContentLength(mutations2, 10);
ContentMetadataInternal.setRedirectedUri(mutations2, Uri.parse("abcde")); ContentMetadataMutations.setRedirectedUri(mutations2, Uri.parse("abcde"));
index.getOrAdd("ABCDE").applyMetadataMutations(mutations2); index.getOrAdd("ABCDE").applyMetadataMutations(mutations2);
index.store(); index.store();
......
...@@ -47,6 +47,7 @@ import org.robolectric.RuntimeEnvironment; ...@@ -47,6 +47,7 @@ import org.robolectric.RuntimeEnvironment;
public class SimpleCacheTest { public class SimpleCacheTest {
private static final String KEY_1 = "key1"; private static final String KEY_1 = "key1";
private static final String KEY_2 = "key2";
private File cacheDir; private File cacheDir;
...@@ -105,18 +106,26 @@ public class SimpleCacheTest { ...@@ -105,18 +106,26 @@ public class SimpleCacheTest {
} }
@Test @Test
public void testSetGetLength() throws Exception { public void testSetGetContentMetadata() throws Exception {
SimpleCache simpleCache = getSimpleCache(); SimpleCache simpleCache = getSimpleCache();
assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET); assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
.isEqualTo(LENGTH_UNSET);
simpleCache.setContentLength(KEY_1, 15); ContentMetadataMutations mutations = new ContentMetadataMutations();
assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(15); ContentMetadataMutations.setContentLength(mutations, 15);
simpleCache.applyContentMetadataMutations(KEY_1, mutations);
assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
.isEqualTo(15);
simpleCache.startReadWrite(KEY_1, 0); simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15); addCache(simpleCache, KEY_1, 0, 15);
simpleCache.setContentLength(KEY_1, 150);
assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(150); mutations = new ContentMetadataMutations();
ContentMetadataMutations.setContentLength(mutations, 150);
simpleCache.applyContentMetadataMutations(KEY_1, mutations);
assertThat(ContentMetadata.getContentLength(simpleCache.getContentMetadata(KEY_1)))
.isEqualTo(150);
addCache(simpleCache, KEY_1, 140, 10); addCache(simpleCache, KEY_1, 140, 10);
...@@ -124,14 +133,16 @@ public class SimpleCacheTest { ...@@ -124,14 +133,16 @@ public class SimpleCacheTest {
// Check if values are kept after cache is reloaded. // Check if values are kept after cache is reloaded.
SimpleCache simpleCache2 = getSimpleCache(); SimpleCache simpleCache2 = getSimpleCache();
assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150); assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))
.isEqualTo(150);
// Removing the last span shouldn't cause the length be change next time cache loaded // Removing the last span shouldn't cause the length be change next time cache loaded
SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);
simpleCache2.removeSpan(lastSpan); simpleCache2.removeSpan(lastSpan);
simpleCache2.release(); simpleCache2.release();
simpleCache2 = getSimpleCache(); simpleCache2 = getSimpleCache();
assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150); assertThat(ContentMetadata.getContentLength(simpleCache2.getContentMetadata(KEY_1)))
.isEqualTo(150);
} }
@Test @Test
...@@ -153,6 +164,40 @@ public class SimpleCacheTest { ...@@ -153,6 +164,40 @@ public class SimpleCacheTest {
} }
@Test @Test
public void testReloadCacheWithoutRelease() throws Exception {
SimpleCache simpleCache = getSimpleCache();
// Write data for KEY_1.
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Write and remove data for KEY_2.
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_2, 0);
addCache(simpleCache, KEY_2, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan2);
simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first());
// Don't release the cache. This means the index file wont have been written to disk after the
// data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the
// folder locking check.
File cacheDir2 = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
cacheDir2.delete();
cacheDir.renameTo(cacheDir2);
// Reload the cache from its new location.
simpleCache = new SimpleCache(cacheDir2, new NoOpCacheEvictor());
// Read data back for KEY_1.
CacheSpan cacheSpan3 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan3);
// Check the entry for KEY_2 was removed when the cache was reloaded.
assertThat(simpleCache.getCachedSpans(KEY_2)).isEmpty();
Util.recursiveDelete(cacheDir2);
}
@Test
public void testEncryptedIndex() throws Exception { public void testEncryptedIndex() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key); SimpleCache simpleCache = getEncryptedSimpleCache(key);
......
...@@ -26,6 +26,8 @@ import com.google.android.exoplayer2.ExoPlayer; ...@@ -26,6 +26,8 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
...@@ -59,6 +61,7 @@ import java.io.InputStreamReader; ...@@ -59,6 +61,7 @@ import java.io.InputStreamReader;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.regex.Matcher; import java.util.regex.Matcher;
...@@ -75,15 +78,16 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -75,15 +78,16 @@ public final class DashMediaSource extends BaseMediaSource {
public static final class Factory implements AdsMediaSource.MediaSourceFactory { public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private final DashChunkSource.Factory chunkSourceFactory; private final DashChunkSource.Factory chunkSourceFactory;
private final @Nullable DataSource.Factory manifestDataSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory;
private @Nullable ParsingLoadable.Parser<? extends DashManifest> manifestParser; @Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser;
@Nullable private List<StreamKey> streamKeys;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs; private long livePresentationDelayMs;
private boolean livePresentationDelayOverridesManifest; private boolean livePresentationDelayOverridesManifest;
private boolean isCreateCalled; private boolean isCreateCalled;
private @Nullable Object tag; @Nullable private Object tag;
/** /**
* Creates a new factory for {@link DashMediaSource}s. * Creates a new factory for {@link DashMediaSource}s.
...@@ -211,6 +215,19 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -211,6 +215,19 @@ public final class DashMediaSource extends BaseMediaSource {
} }
/** /**
* Sets a list of {@link StreamKey stream keys} by which the manifest is filtered.
*
* @param streamKeys A list of {@link StreamKey stream keys}.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setStreamKeys(List<StreamKey> streamKeys) {
Assertions.checkState(!isCreateCalled);
this.streamKeys = streamKeys;
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
* DefaultCompositeSequenceableLoaderFactory}. * DefaultCompositeSequenceableLoaderFactory}.
...@@ -240,6 +257,9 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -240,6 +257,9 @@ public final class DashMediaSource extends BaseMediaSource {
public DashMediaSource createMediaSource(DashManifest manifest) { public DashMediaSource createMediaSource(DashManifest manifest) {
Assertions.checkArgument(!manifest.dynamic); Assertions.checkArgument(!manifest.dynamic);
isCreateCalled = true; isCreateCalled = true;
if (streamKeys != null && !streamKeys.isEmpty()) {
manifest = manifest.copy(streamKeys);
}
return new DashMediaSource( return new DashMediaSource(
manifest, manifest,
/* manifestUri= */ null, /* manifestUri= */ null,
...@@ -281,6 +301,9 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -281,6 +301,9 @@ public final class DashMediaSource extends BaseMediaSource {
if (manifestParser == null) { if (manifestParser == null) {
manifestParser = new DashManifestParser(); manifestParser = new DashManifestParser();
} }
if (streamKeys != null) {
manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);
}
return new DashMediaSource( return new DashMediaSource(
/* manifest= */ null, /* manifest= */ null,
Assertions.checkNotNull(manifestUri), Assertions.checkNotNull(manifestUri),
......
...@@ -16,22 +16,25 @@ ...@@ -16,22 +16,25 @@
package com.google.android.exoplayer2.source.dash.offline; package com.google.android.exoplayer2.source.dash.offline;
import android.net.Uri; import android.net.Uri;
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.RenderersFactory;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** A {@link DownloadHelper} for DASH streams. */ /** A {@link DownloadHelper} for DASH streams. */
...@@ -39,8 +42,52 @@ public final class DashDownloadHelper extends DownloadHelper<DashManifest> { ...@@ -39,8 +42,52 @@ public final class DashDownloadHelper extends DownloadHelper<DashManifest> {
private final DataSource.Factory manifestDataSourceFactory; private final DataSource.Factory manifestDataSourceFactory;
public DashDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { /**
super(DownloadAction.TYPE_DASH, uri, /* cacheKey= */ null); * Creates a DASH download helper.
*
* <p>The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection
* and does not support drm protected content.
*
* @param uri A manifest {@link Uri}.
* @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
* are selected.
*/
public DashDownloadHelper(
Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) {
this(
uri,
manifestDataSourceFactory,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
renderersFactory,
/* drmSessionManager= */ null);
}
/**
* Creates a DASH download helper.
*
* @param uri A manifest {@link Uri}.
* @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading.
* @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
* are selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
* {@code renderersFactory}.
*/
public DashDownloadHelper(
Uri uri,
DataSource.Factory manifestDataSourceFactory,
DefaultTrackSelector.Parameters trackSelectorParameters,
RenderersFactory renderersFactory,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
super(
DownloadAction.TYPE_DASH,
uri,
/* cacheKey= */ null,
trackSelectorParameters,
renderersFactory,
drmSessionManager);
this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestDataSourceFactory = manifestDataSourceFactory;
} }
...@@ -72,12 +119,8 @@ public final class DashDownloadHelper extends DownloadHelper<DashManifest> { ...@@ -72,12 +119,8 @@ public final class DashDownloadHelper extends DownloadHelper<DashManifest> {
} }
@Override @Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { protected StreamKey toStreamKey(
List<StreamKey> streamKeys = new ArrayList<>(trackKeys.size()); int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
for (int i = 0; i < trackKeys.size(); i++) { return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup);
TrackKey trackKey = trackKeys.get(i);
streamKeys.add(new StreamKey(trackKey.periodIndex, trackKey.groupIndex, trackKey.trackIndex));
}
return streamKeys;
} }
} }
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C; ...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
...@@ -34,6 +35,7 @@ import com.google.android.exoplayer2.source.SinglePeriodTimeline; ...@@ -34,6 +35,7 @@ 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.DefaultHlsPlaylistParserFactory; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
import com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParserFactory;
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;
...@@ -64,12 +66,13 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -64,12 +66,13 @@ public final class HlsMediaSource extends BaseMediaSource
private HlsExtractorFactory extractorFactory; private HlsExtractorFactory extractorFactory;
private HlsPlaylistParserFactory playlistParserFactory; private HlsPlaylistParserFactory playlistParserFactory;
@Nullable private List<StreamKey> streamKeys;
private HlsPlaylistTracker.Factory playlistTrackerFactory; private HlsPlaylistTracker.Factory playlistTrackerFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private boolean allowChunklessPreparation; private boolean allowChunklessPreparation;
private boolean isCreateCalled; private boolean isCreateCalled;
private @Nullable Object tag; @Nullable private Object tag;
/** /**
* Creates a new factory for {@link HlsMediaSource}s. * Creates a new factory for {@link HlsMediaSource}s.
...@@ -164,8 +167,8 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -164,8 +167,8 @@ public final class HlsMediaSource extends BaseMediaSource
} }
/** /**
* Sets the factory from which playlist parsers will be obtained. The default value is created * Sets the factory from which playlist parsers will be obtained. The default value is a {@link
* by calling {@link DefaultHlsPlaylistParserFactory#DefaultHlsPlaylistParserFactory()}. * DefaultHlsPlaylistParserFactory}.
* *
* @param playlistParserFactory An {@link HlsPlaylistParserFactory}. * @param playlistParserFactory An {@link HlsPlaylistParserFactory}.
* @return This factory, for convenience. * @return This factory, for convenience.
...@@ -178,6 +181,19 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -178,6 +181,19 @@ public final class HlsMediaSource extends BaseMediaSource
} }
/** /**
* Sets a list of {@link StreamKey stream keys} by which the playlists are filtered.
*
* @param streamKeys A list of {@link StreamKey stream keys}.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setStreamKeys(List<StreamKey> streamKeys) {
Assertions.checkState(!isCreateCalled);
this.streamKeys = streamKeys;
return this;
}
/**
* Sets the {@link HlsPlaylistTracker} factory. The default value is {@link * Sets the {@link HlsPlaylistTracker} factory. The default value is {@link
* DefaultHlsPlaylistTracker#FACTORY}. * DefaultHlsPlaylistTracker#FACTORY}.
* *
...@@ -232,6 +248,10 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -232,6 +248,10 @@ public final class HlsMediaSource extends BaseMediaSource
@Override @Override
public HlsMediaSource createMediaSource(Uri playlistUri) { public HlsMediaSource createMediaSource(Uri playlistUri) {
isCreateCalled = true; isCreateCalled = true;
if (streamKeys != null) {
playlistParserFactory =
new FilteringHlsPlaylistParserFactory(playlistParserFactory, streamKeys);
}
return new HlsMediaSource( return new HlsMediaSource(
playlistUri, playlistUri,
hlsDataSourceFactory, hlsDataSourceFactory,
......
...@@ -16,23 +16,26 @@ ...@@ -16,23 +16,26 @@
package com.google.android.exoplayer2.source.hls.offline; package com.google.android.exoplayer2.source.hls.offline;
import android.net.Uri; import android.net.Uri;
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.RenderersFactory;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
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;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
...@@ -43,8 +46,52 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> { ...@@ -43,8 +46,52 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> {
private int[] renditionGroups; private int[] renditionGroups;
public HlsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { /**
super(DownloadAction.TYPE_HLS, uri, /* cacheKey= */ null); * Creates a HLS download helper.
*
* <p>The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection
* and does not support drm protected content.
*
* @param uri A manifest {@link Uri}.
* @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
* are selected.
*/
public HlsDownloadHelper(
Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) {
this(
uri,
manifestDataSourceFactory,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
renderersFactory,
/* drmSessionManager= */ null);
}
/**
* Creates a HLS download helper.
*
* @param uri A manifest {@link Uri}.
* @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading.
* @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
* are selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
* {@code renderersFactory}.
*/
public HlsDownloadHelper(
Uri uri,
DataSource.Factory manifestDataSourceFactory,
DefaultTrackSelector.Parameters trackSelectorParameters,
RenderersFactory renderersFactory,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
super(
DownloadAction.TYPE_HLS,
uri,
/* cacheKey= */ null,
trackSelectorParameters,
renderersFactory,
drmSessionManager);
this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestDataSourceFactory = manifestDataSourceFactory;
} }
...@@ -61,7 +108,7 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> { ...@@ -61,7 +108,7 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> {
renditionGroups = new int[0]; renditionGroups = new int[0];
return new TrackGroupArray[] {TrackGroupArray.EMPTY}; return new TrackGroupArray[] {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 toStreamKey.
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
TrackGroup[] trackGroups = new TrackGroup[3]; TrackGroup[] trackGroups = new TrackGroup[3];
renditionGroups = new int[3]; renditionGroups = new int[3];
...@@ -82,14 +129,9 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> { ...@@ -82,14 +129,9 @@ public final class HlsDownloadHelper extends DownloadHelper<HlsPlaylist> {
} }
@Override @Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { protected StreamKey toStreamKey(
List<StreamKey> representationKeys = new ArrayList<>(trackKeys.size()); int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
for (int i = 0; i < trackKeys.size(); i++) { return new StreamKey(renditionGroups[trackGroupIndex], trackIndexInTrackGroup);
TrackKey trackKey = trackKeys.get(i);
representationKeys.add(
new StreamKey(renditionGroups[trackKey.groupIndex], trackKey.trackIndex));
}
return representationKeys;
} }
private static Format[] toFormats(List<HlsMasterPlaylist.HlsUrl> hlsUrls) { private static Format[] toFormats(List<HlsMasterPlaylist.HlsUrl> hlsUrls) {
......
...@@ -15,40 +15,19 @@ ...@@ -15,40 +15,19 @@
*/ */
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.util.Collections;
import java.util.List;
/** Default implementation for {@link HlsPlaylistParserFactory}. */ /** Default implementation for {@link HlsPlaylistParserFactory}. */
public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserFactory { public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
private final List<StreamKey> streamKeys;
/** Creates an instance that does not filter any parsing results. */
public DefaultHlsPlaylistParserFactory() {
this(Collections.emptyList());
}
/**
* Creates an instance that filters the parsing results using the given {@code streamKeys}.
*
* @param streamKeys See {@link
* FilteringManifestParser#FilteringManifestParser(ParsingLoadable.Parser, List)}.
*/
public DefaultHlsPlaylistParserFactory(List<StreamKey> streamKeys) {
this.streamKeys = streamKeys;
}
@Override @Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() { public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
return new FilteringManifestParser<>(new HlsPlaylistParser(), streamKeys); return new HlsPlaylistParser();
} }
@Override @Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser( public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist) { HlsMasterPlaylist masterPlaylist) {
return new FilteringManifestParser<>(new HlsPlaylistParser(masterPlaylist), streamKeys); return new HlsPlaylistParser(masterPlaylist);
} }
} }
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.util.List;
/**
* A {@link HlsPlaylistParserFactory} that includes only the streams identified by the given stream
* keys.
*/
public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
private final HlsPlaylistParserFactory hlsPlaylistParserFactory;
private final List<StreamKey> streamKeys;
/**
* @param hlsPlaylistParserFactory A factory for the parsers of the playlists which will be
* filtered.
* @param streamKeys The stream keys. If null or empty then filtering will not occur.
*/
public FilteringHlsPlaylistParserFactory(
HlsPlaylistParserFactory hlsPlaylistParserFactory, List<StreamKey> streamKeys) {
this.hlsPlaylistParserFactory = hlsPlaylistParserFactory;
this.streamKeys = streamKeys;
}
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
return new FilteringManifestParser<>(
hlsPlaylistParserFactory.createPlaylistParser(), streamKeys);
}
@Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist) {
return new FilteringManifestParser<>(
hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys);
}
}
...@@ -20,6 +20,7 @@ import android.util.Base64; ...@@ -20,6 +20,7 @@ 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;
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
...@@ -37,12 +38,11 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower; ...@@ -37,12 +38,11 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
/** /** A SmoothStreaming {@link MediaPeriod}. */
* A SmoothStreaming {@link MediaPeriod}. /* package */ final class SsMediaPeriod
*/ implements MediaPeriod, SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {
/* package */ final class SsMediaPeriod implements MediaPeriod,
SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {
private static final int INITIALIZATION_VECTOR_SIZE = 8; private static final int INITIALIZATION_VECTOR_SIZE = 8;
...@@ -112,6 +112,8 @@ import java.util.ArrayList; ...@@ -112,6 +112,8 @@ import java.util.ArrayList;
eventDispatcher.mediaPeriodReleased(); eventDispatcher.mediaPeriodReleased();
} }
// MediaPeriod implementation.
@Override @Override
public void prepare(Callback callback, long positionUs) { public void prepare(Callback callback, long positionUs) {
this.callback = callback; this.callback = callback;
...@@ -158,6 +160,16 @@ import java.util.ArrayList; ...@@ -158,6 +160,16 @@ import java.util.ArrayList;
} }
@Override @Override
public List<StreamKey> getStreamKeys(TrackSelection trackSelection) {
List<StreamKey> streamKeys = new ArrayList<>(trackSelection.length());
int streamElementIndex = trackGroups.indexOf(trackSelection.getTrackGroup());
for (int i = 0; i < trackSelection.length(); i++) {
streamKeys.add(new StreamKey(streamElementIndex, trackSelection.getIndexInTrackGroup(i)));
}
return streamKeys;
}
@Override
public void discardBuffer(long positionUs, boolean toKeyframe) { public void discardBuffer(long positionUs, boolean toKeyframe) {
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) { for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
sampleStream.discardBuffer(positionUs, toKeyframe); sampleStream.discardBuffer(positionUs, toKeyframe);
...@@ -211,7 +223,7 @@ import java.util.ArrayList; ...@@ -211,7 +223,7 @@ import java.util.ArrayList;
return positionUs; return positionUs;
} }
// SequenceableLoader.Callback implementation // SequenceableLoader.Callback implementation.
@Override @Override
public void onContinueLoadingRequested(ChunkSampleStream<SsChunkSource> sampleStream) { public void onContinueLoadingRequested(ChunkSampleStream<SsChunkSource> sampleStream) {
...@@ -277,5 +289,4 @@ import java.util.ArrayList; ...@@ -277,5 +289,4 @@ import java.util.ArrayList;
data[firstPosition] = data[secondPosition]; data[firstPosition] = data[secondPosition];
data[secondPosition] = temp; data[secondPosition] = temp;
} }
} }
...@@ -24,6 +24,8 @@ import com.google.android.exoplayer2.ExoPlayer; ...@@ -24,6 +24,8 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
...@@ -50,6 +52,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; ...@@ -50,6 +52,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
/** A SmoothStreaming {@link MediaSource}. */ /** A SmoothStreaming {@link MediaSource}. */
public final class SsMediaSource extends BaseMediaSource public final class SsMediaSource extends BaseMediaSource
...@@ -63,14 +66,15 @@ public final class SsMediaSource extends BaseMediaSource ...@@ -63,14 +66,15 @@ public final class SsMediaSource extends BaseMediaSource
public static final class Factory implements AdsMediaSource.MediaSourceFactory { public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private final SsChunkSource.Factory chunkSourceFactory; private final SsChunkSource.Factory chunkSourceFactory;
private final @Nullable DataSource.Factory manifestDataSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory;
private @Nullable ParsingLoadable.Parser<? extends SsManifest> manifestParser; @Nullable private ParsingLoadable.Parser<? extends SsManifest> manifestParser;
@Nullable private List<StreamKey> streamKeys;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs; private long livePresentationDelayMs;
private boolean isCreateCalled; private boolean isCreateCalled;
private @Nullable Object tag; @Nullable private Object tag;
/** /**
* Creates a new factory for {@link SsMediaSource}s. * Creates a new factory for {@link SsMediaSource}s.
...@@ -179,6 +183,19 @@ public final class SsMediaSource extends BaseMediaSource ...@@ -179,6 +183,19 @@ public final class SsMediaSource extends BaseMediaSource
} }
/** /**
* Sets a list of {@link StreamKey stream keys} by which the manifest is filtered.
*
* @param streamKeys A list of {@link StreamKey stream keys}.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setStreamKeys(List<StreamKey> streamKeys) {
Assertions.checkState(!isCreateCalled);
this.streamKeys = streamKeys;
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
* DefaultCompositeSequenceableLoaderFactory}. * DefaultCompositeSequenceableLoaderFactory}.
...@@ -208,6 +225,9 @@ public final class SsMediaSource extends BaseMediaSource ...@@ -208,6 +225,9 @@ public final class SsMediaSource extends BaseMediaSource
public SsMediaSource createMediaSource(SsManifest manifest) { public SsMediaSource createMediaSource(SsManifest manifest) {
Assertions.checkArgument(!manifest.isLive); Assertions.checkArgument(!manifest.isLive);
isCreateCalled = true; isCreateCalled = true;
if (streamKeys != null && !streamKeys.isEmpty()) {
manifest = manifest.copy(streamKeys);
}
return new SsMediaSource( return new SsMediaSource(
manifest, manifest,
/* manifestUri= */ null, /* manifestUri= */ null,
...@@ -248,6 +268,9 @@ public final class SsMediaSource extends BaseMediaSource ...@@ -248,6 +268,9 @@ public final class SsMediaSource extends BaseMediaSource
if (manifestParser == null) { if (manifestParser == null) {
manifestParser = new SsManifestParser(); manifestParser = new SsManifestParser();
} }
if (streamKeys != null) {
manifestParser = new FilteringManifestParser<>(manifestParser, streamKeys);
}
return new SsMediaSource( return new SsMediaSource(
/* manifest= */ null, /* manifest= */ null,
Assertions.checkNotNull(manifestUri), Assertions.checkNotNull(manifestUri),
......
...@@ -16,35 +16,83 @@ ...@@ -16,35 +16,83 @@
package com.google.android.exoplayer2.source.smoothstreaming.offline; package com.google.android.exoplayer2.source.smoothstreaming.offline;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** A {@link DownloadHelper} for SmoothStreaming streams. */ /** A {@link DownloadHelper} for SmoothStreaming streams. */
public final class SsDownloadHelper extends DownloadHelper<SsManifest> { public final class SsDownloadHelper extends DownloadHelper<SsManifest> {
private final DataSource.Factory manifestDataSourceFactory; private final DataSource.Factory manifestDataSourceFactory;
public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { /**
super(DownloadAction.TYPE_SS, uri, /* cacheKey= */ null); * Creates a SmoothStreaming download helper.
*
* <p>The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection
* and does not support drm protected content.
*
* @param uri A manifest {@link Uri}.
* @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
* are selected.
*/
public SsDownloadHelper(
Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) {
this(
uri,
manifestDataSourceFactory,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
renderersFactory,
/* drmSessionManager= */ null);
}
/**
* Creates a SmoothStreaming download helper.
*
* @param uri A manifest {@link Uri}.
* @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading.
* @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks
* are selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by
* {@code renderersFactory}.
*/
public SsDownloadHelper(
Uri uri,
DataSource.Factory manifestDataSourceFactory,
DefaultTrackSelector.Parameters trackSelectorParameters,
RenderersFactory renderersFactory,
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
super(
DownloadAction.TYPE_SS,
uri,
/* cacheKey= */ null,
trackSelectorParameters,
renderersFactory,
drmSessionManager);
this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestDataSourceFactory = manifestDataSourceFactory;
} }
@Override @Override
protected SsManifest loadManifest(Uri uri) throws IOException { protected SsManifest loadManifest(Uri uri) throws IOException {
DataSource dataSource = manifestDataSourceFactory.createDataSource(); DataSource dataSource = manifestDataSourceFactory.createDataSource();
return ParsingLoadable.load(dataSource, new SsManifestParser(), uri, C.DATA_TYPE_MANIFEST); Uri fixedUri = SsUtil.fixManifestUri(uri);
return ParsingLoadable.load(dataSource, new SsManifestParser(), fixedUri, C.DATA_TYPE_MANIFEST);
} }
@Override @Override
...@@ -58,12 +106,8 @@ public final class SsDownloadHelper extends DownloadHelper<SsManifest> { ...@@ -58,12 +106,8 @@ public final class SsDownloadHelper extends DownloadHelper<SsManifest> {
} }
@Override @Override
protected List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) { protected StreamKey toStreamKey(
List<StreamKey> representationKeys = new ArrayList<>(trackKeys.size()); int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) {
for (int i = 0; i < trackKeys.size(); i++) { return new StreamKey(trackGroupIndex, trackIndexInTrackGroup);
TrackKey trackKey = trackKeys.get(i);
representationKeys.add(new StreamKey(trackKey.groupIndex, trackKey.trackIndex));
}
return representationKeys;
} }
} }
...@@ -23,7 +23,7 @@ import android.support.annotation.Nullable; ...@@ -23,7 +23,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
/** Helper for creating download notifications. */ /** Helper for creating download notifications. */
public final class DownloadNotificationUtil { public final class DownloadNotificationUtil {
...@@ -33,7 +33,7 @@ public final class DownloadNotificationUtil { ...@@ -33,7 +33,7 @@ public final class DownloadNotificationUtil {
private DownloadNotificationUtil() {} private DownloadNotificationUtil() {}
/** /**
* Returns a progress notification for the given task states. * Returns a progress notification for the given download states.
* *
* @param context A context for accessing resources. * @param context A context for accessing resources.
* @param smallIcon A small icon for the notification. * @param smallIcon A small icon for the notification.
...@@ -41,7 +41,7 @@ public final class DownloadNotificationUtil { ...@@ -41,7 +41,7 @@ public final class DownloadNotificationUtil {
* above. * above.
* @param contentIntent An optional content intent to send when the notification is clicked. * @param contentIntent An optional content intent to send when the notification is clicked.
* @param message An optional message to display on the notification. * @param message An optional message to display on the notification.
* @param taskStates The task states. * @param downloadStates The download states.
* @return The notification. * @return The notification.
*/ */
public static Notification buildProgressNotification( public static Notification buildProgressNotification(
...@@ -50,28 +50,28 @@ public final class DownloadNotificationUtil { ...@@ -50,28 +50,28 @@ public final class DownloadNotificationUtil {
String channelId, String channelId,
@Nullable PendingIntent contentIntent, @Nullable PendingIntent contentIntent,
@Nullable String message, @Nullable String message,
TaskState[] taskStates) { DownloadState[] downloadStates) {
float totalPercentage = 0; float totalPercentage = 0;
int downloadTaskCount = 0; int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true; boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false; boolean haveDownloadedBytes = false;
boolean haveDownloadTasks = false; boolean haveDownloadTasks = false;
boolean haveRemoveTasks = false; boolean haveRemoveTasks = false;
for (TaskState taskState : taskStates) { for (DownloadState downloadState : downloadStates) {
if (taskState.state != TaskState.STATE_STARTED if (downloadState.state != DownloadState.STATE_STARTED
&& taskState.state != TaskState.STATE_COMPLETED) { && downloadState.state != DownloadState.STATE_COMPLETED) {
continue; continue;
} }
if (taskState.action.isRemoveAction) { if (downloadState.action.isRemoveAction) {
haveRemoveTasks = true; haveRemoveTasks = true;
continue; continue;
} }
haveDownloadTasks = true; haveDownloadTasks = true;
if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) { if (downloadState.downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false; allDownloadPercentagesUnknown = false;
totalPercentage += taskState.downloadPercentage; totalPercentage += downloadState.downloadPercentage;
} }
haveDownloadedBytes |= taskState.downloadedBytes > 0; haveDownloadedBytes |= downloadState.downloadedBytes > 0;
downloadTaskCount++; downloadTaskCount++;
} }
......
...@@ -29,7 +29,6 @@ import android.graphics.drawable.BitmapDrawable; ...@@ -29,7 +29,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.util.AttributeSet; import android.util.AttributeSet;
...@@ -187,8 +186,9 @@ import java.util.List; ...@@ -187,8 +186,9 @@ import java.util.List;
* <li>Type: {@link AspectRatioFrameLayout} * <li>Type: {@link AspectRatioFrameLayout}
* </ul> * </ul>
* <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This * <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This
* view is typically an opaque view that covers the video surface view, thereby obscuring it * view is typically an opaque view that covers the video surface, thereby obscuring it when
* when visible. * visible. Obscuring the surface in this way also helps to prevent flicker at the start of
* playback when {@code surface_type="surface_view"}.
* <ul> * <ul>
* <li>Type: {@link View} * <li>Type: {@link View}
* </ul> * </ul>
...@@ -271,13 +271,13 @@ public class PlayerView extends FrameLayout { ...@@ -271,13 +271,13 @@ public class PlayerView extends FrameLayout {
private static final int SURFACE_TYPE_MONO360_VIEW = 3; private static final int SURFACE_TYPE_MONO360_VIEW = 3;
// LINT.ThenChange(../../../../../../res/values/attrs.xml) // LINT.ThenChange(../../../../../../res/values/attrs.xml)
private final AspectRatioFrameLayout contentFrame; @Nullable private final AspectRatioFrameLayout contentFrame;
private final View shutterView; private final View shutterView;
private final View surfaceView; @Nullable private final View surfaceView;
private final ImageView artworkView; private final ImageView artworkView;
private final SubtitleView subtitleView; private final SubtitleView subtitleView;
private final @Nullable View bufferingView; @Nullable private final View bufferingView;
private final @Nullable TextView errorMessageView; @Nullable private final TextView errorMessageView;
private final PlayerControlView controller; private final PlayerControlView controller;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private final FrameLayout overlayFrameLayout; private final FrameLayout overlayFrameLayout;
...@@ -285,11 +285,11 @@ public class PlayerView extends FrameLayout { ...@@ -285,11 +285,11 @@ public class PlayerView extends FrameLayout {
private Player player; private Player player;
private boolean useController; private boolean useController;
private boolean useArtwork; private boolean useArtwork;
private @Nullable Drawable defaultArtwork; @Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering; private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset; private boolean keepContentOnPlayerReset;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable CharSequence customErrorMessage; @Nullable private CharSequence customErrorMessage;
private int controllerShowTimeoutMs; private int controllerShowTimeoutMs;
private boolean controllerAutoShow; private boolean controllerAutoShow;
private boolean controllerHideDuringAds; private boolean controllerHideDuringAds;
...@@ -474,9 +474,7 @@ public class PlayerView extends FrameLayout { ...@@ -474,9 +474,7 @@ public class PlayerView extends FrameLayout {
* @param newPlayerView The new view to attach to the player. * @param newPlayerView The new view to attach to the player.
*/ */
public static void switchTargetView( public static void switchTargetView(
@NonNull Player player, Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) {
@Nullable PlayerView oldPlayerView,
@Nullable PlayerView newPlayerView) {
if (oldPlayerView == newPlayerView) { if (oldPlayerView == newPlayerView) {
return; return;
} }
...@@ -1080,6 +1078,26 @@ public class PlayerView extends FrameLayout { ...@@ -1080,6 +1078,26 @@ public class PlayerView extends FrameLayout {
} }
} }
/**
* Called when there's a change in the aspect ratio of the content being displayed. The default
* implementation sets the aspect ratio of the content frame to that of the content, unless the
* content view is a {@link SphericalSurfaceView} in which case the frame's aspect ratio is
* cleared.
*
* @param contentAspectRatio The aspect ratio of the content.
* @param contentFrame The content frame, or {@code null}.
* @param contentView The view that holds the content being displayed, or {@code null}.
*/
protected void onContentAspectRatioChanged(
float contentAspectRatio,
@Nullable AspectRatioFrameLayout contentFrame,
@Nullable View contentView) {
if (contentFrame != null) {
contentFrame.setAspectRatio(
contentView instanceof SphericalSurfaceView ? 0 : contentAspectRatio);
}
}
private boolean toggleControllerVisibility() { private boolean toggleControllerVisibility() {
if (!useController || player == null) { if (!useController || player == null) {
return false; return false;
...@@ -1193,9 +1211,8 @@ public class PlayerView extends FrameLayout { ...@@ -1193,9 +1211,8 @@ public class PlayerView extends FrameLayout {
int drawableWidth = drawable.getIntrinsicWidth(); int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight(); int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth > 0 && drawableHeight > 0) { if (drawableWidth > 0 && drawableHeight > 0) {
if (contentFrame != null) { float artworkAspectRatio = (float) drawableWidth / drawableHeight;
contentFrame.setAspectRatio((float) drawableWidth / drawableHeight); onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView);
}
artworkView.setImageDrawable(drawable); artworkView.setImageDrawable(drawable);
artworkView.setVisibility(VISIBLE); artworkView.setVisibility(VISIBLE);
return true; return true;
...@@ -1328,9 +1345,6 @@ public class PlayerView extends FrameLayout { ...@@ -1328,9 +1345,6 @@ public class PlayerView extends FrameLayout {
@Override @Override
public void onVideoSizeChanged( public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (contentFrame == null) {
return;
}
float videoAspectRatio = float videoAspectRatio =
(height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;
...@@ -1351,11 +1365,9 @@ public class PlayerView extends FrameLayout { ...@@ -1351,11 +1365,9 @@ public class PlayerView extends FrameLayout {
surfaceView.addOnLayoutChangeListener(this); surfaceView.addOnLayoutChangeListener(this);
} }
applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
} else if (surfaceView instanceof SphericalSurfaceView) {
videoAspectRatio = 0;
} }
contentFrame.setAspectRatio(videoAspectRatio); onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView);
} }
@Override @Override
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
...@@ -33,13 +32,25 @@ import com.google.android.exoplayer2.source.TrackGroup; ...@@ -33,13 +32,25 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays; import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A view for making track selections. */ /** A view for making track selections. */
public class TrackSelectionView extends LinearLayout { public class TrackSelectionView extends LinearLayout {
/** Callback which is invoked when a track selection has been made. */
public interface DialogCallback {
/**
* Called when track are selected.
*
* @param parameters The {@link DefaultTrackSelector.Parameters} for the selected tracks.
*/
void onTracksSelected(DefaultTrackSelector.Parameters parameters);
}
private final int selectableItemBackgroundResourceId; private final int selectableItemBackgroundResourceId;
private final LayoutInflater inflater; private final LayoutInflater inflater;
private final CheckedTextView disableView; private final CheckedTextView disableView;
...@@ -51,35 +62,64 @@ public class TrackSelectionView extends LinearLayout { ...@@ -51,35 +62,64 @@ public class TrackSelectionView extends LinearLayout {
private TrackNameProvider trackNameProvider; private TrackNameProvider trackNameProvider;
private CheckedTextView[][] trackViews; private CheckedTextView[][] trackViews;
private DefaultTrackSelector trackSelector; private @MonotonicNonNull MappedTrackInfo mappedTrackInfo;
private int rendererIndex; private int rendererIndex;
private DefaultTrackSelector.Parameters parameters;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private boolean isDisabled; private boolean isDisabled;
private @Nullable SelectionOverride override; @Nullable private SelectionOverride override;
/** /**
* Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it. * Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it.
* *
* @param activity The parent activity. * <p>The dialog shows the current configuration of the provided {@code TrackSelector} and updates
* the parameters when closing the dialog.
*
* @param context The parent context.
* @param title The dialog's title. * @param title The dialog's title.
* @param trackSelector The track selector. * @param trackSelector The track selector.
* @param rendererIndex The index of the renderer. * @param rendererIndex The index of the renderer.
* @return The dialog and the {@link TrackSelectionView} that will be shown by it. * @return The dialog and the {@link TrackSelectionView} that will be shown by it.
*/ */
public static Pair<AlertDialog, TrackSelectionView> getDialog( public static Pair<AlertDialog, TrackSelectionView> getDialog(
Activity activity, Context context, CharSequence title, DefaultTrackSelector trackSelector, int rendererIndex) {
return getDialog(
context,
title,
Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()),
rendererIndex,
trackSelector.getParameters(),
trackSelector::setParameters);
}
/**
* Gets a pair consisting of a dialog and the {@link TrackSelectionView} that will be shown by it.
*
* @param context The parent context.
* @param title The dialog's title.
* @param mappedTrackInfo The {@link MappedTrackInfo}.
* @param rendererIndex The index of the renderer.
* @param parameters The {@link DefaultTrackSelector.Parameters}.
* @param callback The {@link DialogCallback} invoked when the dialog is closed successfully.
* @return The dialog and the {@link TrackSelectionView} that will be shown by it.
*/
public static Pair<AlertDialog, TrackSelectionView> getDialog(
Context context,
CharSequence title, CharSequence title,
DefaultTrackSelector trackSelector, MappedTrackInfo mappedTrackInfo,
int rendererIndex) { int rendererIndex,
AlertDialog.Builder builder = new AlertDialog.Builder(activity); DefaultTrackSelector.Parameters parameters,
DialogCallback callback) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
// Inflate with the builder's context to ensure the correct style is used. // Inflate with the builder's context to ensure the correct style is used.
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
View dialogView = dialogInflater.inflate(R.layout.exo_track_selection_dialog, null); View dialogView = dialogInflater.inflate(R.layout.exo_track_selection_dialog, null);
final TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view); TrackSelectionView selectionView = dialogView.findViewById(R.id.exo_track_selection_view);
selectionView.init(trackSelector, rendererIndex); selectionView.init(mappedTrackInfo, rendererIndex, parameters);
Dialog.OnClickListener okClickListener = (dialog, which) -> selectionView.applySelection(); Dialog.OnClickListener okClickListener =
(dialog, which) -> callback.onTracksSelected(selectionView.getSelectionParameters());
AlertDialog dialog = AlertDialog dialog =
builder builder
...@@ -113,6 +153,8 @@ public class TrackSelectionView extends LinearLayout { ...@@ -113,6 +153,8 @@ public class TrackSelectionView extends LinearLayout {
inflater = LayoutInflater.from(context); inflater = LayoutInflater.from(context);
componentListener = new ComponentListener(); componentListener = new ComponentListener();
trackNameProvider = new DefaultTrackNameProvider(getResources()); trackNameProvider = new DefaultTrackNameProvider(getResources());
parameters = DefaultTrackSelector.Parameters.DEFAULT;
trackGroups = TrackGroupArray.EMPTY;
// View for disabling the renderer. // View for disabling the renderer.
disableView = disableView =
...@@ -176,18 +218,35 @@ public class TrackSelectionView extends LinearLayout { ...@@ -176,18 +218,35 @@ public class TrackSelectionView extends LinearLayout {
} }
/** /**
* Initialize the view to select tracks for a specified renderer using a {@link * Initialize the view to select tracks for a specified renderer using {@link MappedTrackInfo} and
* DefaultTrackSelector}. * a set of {@link DefaultTrackSelector.Parameters}.
* *
* @param trackSelector The {@link DefaultTrackSelector}. * @param mappedTrackInfo The {@link MappedTrackInfo}.
* @param rendererIndex The index of the renderer. * @param rendererIndex The index of the renderer.
* @param parameters The {@link DefaultTrackSelector.Parameters}.
*/ */
public void init(DefaultTrackSelector trackSelector, int rendererIndex) { public void init(
this.trackSelector = trackSelector; MappedTrackInfo mappedTrackInfo,
int rendererIndex,
DefaultTrackSelector.Parameters parameters) {
this.mappedTrackInfo = mappedTrackInfo;
this.rendererIndex = rendererIndex; this.rendererIndex = rendererIndex;
this.parameters = parameters;
updateViews(); updateViews();
} }
/** Returns the {@link DefaultTrackSelector.Parameters} for the current selection. */
public DefaultTrackSelector.Parameters getSelectionParameters() {
DefaultTrackSelector.ParametersBuilder parametersBuilder = parameters.buildUpon();
parametersBuilder.setRendererDisabled(rendererIndex, isDisabled);
if (override != null) {
parametersBuilder.setSelectionOverride(rendererIndex, trackGroups, override);
} else {
parametersBuilder.clearSelectionOverrides(rendererIndex);
}
return parametersBuilder.build();
}
// Private methods. // Private methods.
private void updateViews() { private void updateViews() {
...@@ -196,9 +255,7 @@ public class TrackSelectionView extends LinearLayout { ...@@ -196,9 +255,7 @@ public class TrackSelectionView extends LinearLayout {
removeViewAt(i); removeViewAt(i);
} }
MappingTrackSelector.MappedTrackInfo trackInfo = if (mappedTrackInfo == null) {
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);
...@@ -207,9 +264,8 @@ public class TrackSelectionView extends LinearLayout { ...@@ -207,9 +264,8 @@ public class TrackSelectionView extends LinearLayout {
disableView.setEnabled(true); disableView.setEnabled(true);
defaultView.setEnabled(true); defaultView.setEnabled(true);
trackGroups = trackInfo.getTrackGroups(rendererIndex); trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
isDisabled = parameters.getRendererDisabled(rendererIndex); isDisabled = parameters.getRendererDisabled(rendererIndex);
override = parameters.getSelectionOverride(rendererIndex, trackGroups); override = parameters.getSelectionOverride(rendererIndex, trackGroups);
...@@ -220,7 +276,7 @@ public class TrackSelectionView extends LinearLayout { ...@@ -220,7 +276,7 @@ public class TrackSelectionView extends LinearLayout {
boolean enableAdaptiveSelections = boolean enableAdaptiveSelections =
allowAdaptiveSelections allowAdaptiveSelections
&& trackGroups.get(groupIndex).length > 1 && trackGroups.get(groupIndex).length > 1
&& trackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false) && mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED;
trackViews[groupIndex] = new CheckedTextView[group.length]; trackViews[groupIndex] = new CheckedTextView[group.length];
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
...@@ -235,7 +291,7 @@ public class TrackSelectionView extends LinearLayout { ...@@ -235,7 +291,7 @@ public class TrackSelectionView extends LinearLayout {
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false); (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId); trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex))); trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex)));
if (trackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) { == RendererCapabilities.FORMAT_HANDLED) {
trackView.setFocusable(true); trackView.setFocusable(true);
trackView.setTag(Pair.create(groupIndex, trackIndex)); trackView.setTag(Pair.create(groupIndex, trackIndex));
...@@ -263,17 +319,6 @@ public class TrackSelectionView extends LinearLayout { ...@@ -263,17 +319,6 @@ public class TrackSelectionView extends LinearLayout {
} }
} }
private void applySelection() {
DefaultTrackSelector.ParametersBuilder parametersBuilder = trackSelector.buildUponParameters();
parametersBuilder.setRendererDisabled(rendererIndex, isDisabled);
if (override != null) {
parametersBuilder.setSelectionOverride(rendererIndex, trackGroups, override);
} else {
parametersBuilder.clearSelectionOverrides(rendererIndex);
}
trackSelector.setParameters(parametersBuilder);
}
private void onClick(View view) { private void onClick(View view) {
if (view == disableView) { if (view == disableView) {
onDisableViewClicked(); onDisableViewClicked();
......
...@@ -79,8 +79,19 @@ public class FakeTrackSelector extends DefaultTrackSelector { ...@@ -79,8 +79,19 @@ public class FakeTrackSelector extends DefaultTrackSelector {
} }
@Override @Override
public TrackSelection createTrackSelection( public TrackSelection[] createTrackSelections(
TrackGroup trackGroup, BandwidthMeter bandwidthMeter, int... tracks) { TrackSelection.Definition[] definitions, BandwidthMeter bandwidthMeter) {
TrackSelection[] selections = new TrackSelection[definitions.length];
for (int i = 0; i < definitions.length; i++) {
TrackSelection.Definition definition = definitions[i];
if (definition != null) {
selections[i] = createTrackSelection(definition.group);
}
}
return selections;
}
private TrackSelection createTrackSelection(TrackGroup trackGroup) {
if (mayReuseTrackSelection) { if (mayReuseTrackSelection) {
for (FakeTrackSelection trackSelection : trackSelections) { for (FakeTrackSelection trackSelection : trackSelections) {
if (trackSelection.getTrackGroup().equals(trackGroup)) { if (trackSelection.getTrackGroup().equals(trackGroup)) {
...@@ -92,18 +103,5 @@ public class FakeTrackSelector extends DefaultTrackSelector { ...@@ -92,18 +103,5 @@ public class FakeTrackSelector extends DefaultTrackSelector {
trackSelections.add(trackSelection); trackSelections.add(trackSelection);
return trackSelection; return trackSelection;
} }
@Override
public TrackSelection[] createTrackSelections(
TrackSelection.Definition[] definitions, BandwidthMeter bandwidthMeter) {
TrackSelection[] selections = new TrackSelection[definitions.length];
for (int i = 0; i < definitions.length; i++) {
TrackSelection.Definition definition = definitions[i];
if (definition != null) {
selections[i] = createTrackSelection(definition.group, bandwidthMeter, definition.tracks);
}
}
return selections;
}
} }
} }
...@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.testutil; ...@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.offline.DownloadAction;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadManager.DownloadState;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
...@@ -31,10 +31,10 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen ...@@ -31,10 +31,10 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
private final DownloadManager downloadManager; private final DownloadManager downloadManager;
private final DummyMainThread dummyMainThread; private final DummyMainThread dummyMainThread;
private final HashMap<DownloadAction, ArrayBlockingQueue<Integer>> actionStates; private final HashMap<String, ArrayBlockingQueue<Integer>> actionStates;
private CountDownLatch downloadFinishedCondition; private CountDownLatch downloadFinishedCondition;
private Throwable downloadError; @DownloadState.FailureReason private int failureReason;
public TestDownloadManagerListener( public TestDownloadManagerListener(
DownloadManager downloadManager, DummyMainThread dummyMainThread) { DownloadManager downloadManager, DummyMainThread dummyMainThread) {
...@@ -43,12 +43,12 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen ...@@ -43,12 +43,12 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
actionStates = new HashMap<>(); actionStates = new HashMap<>();
} }
public int pollStateChange(DownloadAction action, long timeoutMs) throws InterruptedException { public Integer pollStateChange(String taskId, long timeoutMs) throws InterruptedException {
return getStateQueue(action).poll(timeoutMs, TimeUnit.MILLISECONDS); return getStateQueue(taskId).poll(timeoutMs, TimeUnit.MILLISECONDS);
} }
public void clearDownloadError() { public void clearDownloadError() {
this.downloadError = null; this.failureReason = DownloadState.FAILURE_REASON_NONE;
} }
@Override @Override
...@@ -57,12 +57,11 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen ...@@ -57,12 +57,11 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
} }
@Override @Override
public void onTaskStateChanged( public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) {
DownloadManager downloadManager, DownloadManager.TaskState taskState) { if (downloadState.state == DownloadState.STATE_FAILED) {
if (taskState.state == DownloadManager.TaskState.STATE_FAILED && downloadError == null) { failureReason = downloadState.failureReason;
downloadError = taskState.error;
} }
getStateQueue(taskState.action).add(taskState.state); getStateQueue(downloadState.id).add(downloadState.state);
} }
@Override @Override
...@@ -77,6 +76,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen ...@@ -77,6 +76,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
* error. * error.
*/ */
public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable {
blockUntilTasksComplete();
if (failureReason != DownloadState.FAILURE_REASON_NONE) {
throw new Exception("Failure reason: " + DownloadState.getFailureString(failureReason));
}
}
/** Blocks until all remove and download tasks are complete. Task errors are ignored. */
public void blockUntilTasksComplete() throws InterruptedException {
synchronized (this) { synchronized (this) {
downloadFinishedCondition = new CountDownLatch(1); downloadFinishedCondition = new CountDownLatch(1);
} }
...@@ -87,17 +94,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen ...@@ -87,17 +94,14 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen
} }
}); });
assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue(); assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue();
if (downloadError != null) {
throw new Exception(downloadError);
}
} }
private ArrayBlockingQueue<Integer> getStateQueue(DownloadAction action) { private ArrayBlockingQueue<Integer> getStateQueue(String taskId) {
synchronized (actionStates) { synchronized (actionStates) {
if (!actionStates.containsKey(action)) { if (!actionStates.containsKey(taskId)) {
actionStates.put(action, new ArrayBlockingQueue<>(10)); actionStates.put(taskId, new ArrayBlockingQueue<>(10));
} }
return actionStates.get(action); return actionStates.get(taskId);
} }
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment