diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadActivity.java index 285f5f282c..8546cf50e0 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadActivity.java @@ -27,36 +27,24 @@ import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.Toast; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadService; -import com.google.android.exoplayer2.offline.ProgressiveDownloadAction; -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.DashManifestParser; -import com.google.android.exoplayer2.source.dash.manifest.Representation; -import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; -import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction; -import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; -import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; -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.TrackKey; -import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction; +import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper; +import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper; +import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper; import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; import com.google.android.exoplayer2.ui.TrackNameProvider; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.ParcelableArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** An activity for downloading media. */ @@ -69,9 +57,10 @@ public class DownloadActivity extends Activity { private String sampleName; private TrackNameProvider trackNameProvider; - private DownloadHelper downloadHelper; - private ListView representationList; + private DownloadHelper downloadHelper; + private ListView trackList; private ArrayAdapter arrayAdapter; + private ArrayList trackKeys; @Override protected void onCreate(Bundle savedInstanceState) { @@ -86,9 +75,10 @@ public class DownloadActivity extends Activity { getActionBar().setTitle(sampleName); arrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_multiple_choice); - representationList = findViewById(R.id.representation_list); - representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - representationList.setAdapter(arrayAdapter); + trackList = findViewById(R.id.representation_list); + trackList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + trackList.setAdapter(arrayAdapter); + trackKeys = new ArrayList<>(); DemoApplication application = (DemoApplication) getApplication(); DataSource.Factory manifestDataSourceFactory = @@ -112,29 +102,37 @@ public class DownloadActivity extends Activity { throw new IllegalStateException("Unsupported type: " + type); } - new Thread() { - @Override - public void run() { - try { - downloadHelper.init(); - runOnUiThread( - new Runnable() { - @Override - public void run() { - onInitialized(); - } - }); - } catch (IOException e) { - runOnUiThread( - new Runnable() { - @Override - public void run() { - onInitializationError(); - } - }); + downloadHelper.prepare( + new DownloadHelper.Callback() { + @Override + public void onPrepared(DownloadHelper helper) { + DownloadActivity.this.onPrepared(); + } + + @Override + public void onPrepareError(DownloadHelper helper, IOException e) { + DownloadActivity.this.onPrepareError(); + } + }); + } + + private void onPrepared() { + for (int i = 0; i < downloadHelper.getPeriodCount(); i++) { + TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i); + for (int j = 0; j < trackGroups.length; j++) { + TrackGroup trackGroup = trackGroups.get(j); + for (int k = 0; k < trackGroup.length; k++) { + arrayAdapter.add(trackNameProvider.getTrackName(trackGroup.getFormat(k))); + trackKeys.add(new TrackKey(i, j, k)); } } - }.start(); + } + } + + private void onPrepareError() { + Toast.makeText( + getApplicationContext(), R.string.download_manifest_load_error, Toast.LENGTH_LONG) + .show(); } // This method is referenced in the layout file @@ -150,26 +148,13 @@ public class DownloadActivity extends Activity { } } - private void onInitialized() { - for (int i = 0; i < downloadHelper.getTrackCount(); i++) { - arrayAdapter.add(trackNameProvider.getTrackName(downloadHelper.getTrackFormat(i))); - } - } - - private void onInitializationError() { - Toast.makeText( - getApplicationContext(), R.string.download_manifest_load_error, Toast.LENGTH_LONG) - .show(); - } - private void startDownload() { - int[] selectedTrackIndices = getSelectedTrackIndices(); - if (selectedTrackIndices.length > 0) { + List selectedTrackKeys = getSelectedTrackKeys(); + if (trackKeys.isEmpty() || !selectedTrackKeys.isEmpty()) { DownloadService.addDownloadAction( this, DemoDownloadService.class, - downloadHelper.getDownloadAction( - /* isRemoveAction= */ false, sampleName, selectedTrackIndices)); + downloadHelper.getDownloadAction(Util.getUtf8Bytes(sampleName), selectedTrackKeys)); } } @@ -177,189 +162,35 @@ public class DownloadActivity extends Activity { DownloadService.addDownloadAction( this, DemoDownloadService.class, - downloadHelper.getDownloadAction(/* isRemoveAction= */ true, sampleName)); - for (int i = 0; i < representationList.getChildCount(); i++) { - representationList.setItemChecked(i, false); + downloadHelper.getRemoveAction(Util.getUtf8Bytes(sampleName))); + for (int i = 0; i < trackList.getChildCount(); i++) { + trackList.setItemChecked(i, false); } } private void playDownload() { - int[] selectedTrackIndices = getSelectedTrackIndices(); - List keys = downloadHelper.getTrackKeys(selectedTrackIndices); + DownloadAction action = downloadHelper.getDownloadAction(null, getSelectedTrackKeys()); + List keys = null; + if (action instanceof SegmentDownloadAction) { + keys = ((SegmentDownloadAction) action).keys; + } if (keys.isEmpty()) { playerIntent.removeExtra(PlayerActivity.MANIFEST_FILTER_EXTRA); } else { - Parcelable[] keysArray = keys.toArray(new Parcelable[selectedTrackIndices.length]); - playerIntent.putExtra(PlayerActivity.MANIFEST_FILTER_EXTRA, new ParcelableArray<>(keysArray)); + playerIntent.putExtra( + PlayerActivity.MANIFEST_FILTER_EXTRA, + new ParcelableArray(keys.toArray(new Parcelable[0]))); } startActivity(playerIntent); } - private int[] getSelectedTrackIndices() { - ArrayList checkedIndices = new ArrayList<>(); - for (int i = 0; i < representationList.getChildCount(); i++) { - if (representationList.isItemChecked(i)) { - checkedIndices.add(i); + private List getSelectedTrackKeys() { + ArrayList selectedTrackKeys = new ArrayList<>(); + for (int i = 0; i < trackList.getChildCount(); i++) { + if (trackList.isItemChecked(i)) { + selectedTrackKeys.add(trackKeys.get(i)); } } - return Util.toArray(checkedIndices); - } - - private abstract static class DownloadHelper { - - protected static final Format DUMMY_FORMAT = - Format.createContainerFormat(null, null, null, null, Format.NO_VALUE, 0, null); - - protected final Uri uri; - protected final DataSource.Factory dataSourceFactory; - protected final List trackFormats; - protected final List trackKeys; - - public DownloadHelper(Uri uri, DataSource.Factory dataSourceFactory) { - this.uri = uri; - this.dataSourceFactory = dataSourceFactory; - trackFormats = new ArrayList<>(); - trackKeys = new ArrayList<>(); - } - - public abstract void init() throws IOException; - - public int getTrackCount() { - return trackFormats.size(); - } - - public Format getTrackFormat(int trackIndex) { - return trackFormats.get(trackIndex); - } - - public List getTrackKeys(int... trackIndices) { - if (trackFormats.size() == 1 && trackFormats.get(0) == DUMMY_FORMAT) { - return Collections.emptyList(); - } - List keys = new ArrayList<>(trackIndices.length); - for (int trackIndex : trackIndices) { - keys.add(trackKeys.get(trackIndex)); - } - return keys; - } - - public abstract DownloadAction getDownloadAction( - boolean isRemoveAction, String sampleName, int... trackIndices); - } - - private static final class DashDownloadHelper extends DownloadHelper { - - public DashDownloadHelper(Uri uri, DataSource.Factory dataSourceFactory) { - super(uri, dataSourceFactory); - } - - @Override - public void init() throws IOException { - DataSource dataSource = dataSourceFactory.createDataSource(); - DashManifest manifest = ParsingLoadable.load(dataSource, new DashManifestParser(), uri); - - for (int periodIndex = 0; periodIndex < manifest.getPeriodCount(); periodIndex++) { - List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; - for (int adaptationIndex = 0; adaptationIndex < adaptationSets.size(); adaptationIndex++) { - List representations = - adaptationSets.get(adaptationIndex).representations; - int representationsCount = representations.size(); - for (int i = 0; i < representationsCount; i++) { - trackFormats.add(representations.get(i).format); - trackKeys.add(new RepresentationKey(periodIndex, adaptationIndex, i)); - } - } - } - } - - @Override - public DownloadAction getDownloadAction( - boolean isRemoveAction, String sampleName, int... trackIndices) { - return new DashDownloadAction( - uri, isRemoveAction, Util.getUtf8Bytes(sampleName), getTrackKeys(trackIndices)); - } - } - - private static final class HlsDownloadHelper extends DownloadHelper { - - public HlsDownloadHelper(Uri uri, DataSource.Factory dataSourceFactory) { - super(uri, dataSourceFactory); - } - - @Override - public void init() throws IOException { - DataSource dataSource = dataSourceFactory.createDataSource(); - HlsPlaylist playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri); - - if (playlist instanceof HlsMediaPlaylist) { - trackFormats.add(DUMMY_FORMAT); - } else { - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; - addRepresentationItems(masterPlaylist.variants, RenditionKey.TYPE_VARIANT); - addRepresentationItems(masterPlaylist.audios, RenditionKey.TYPE_AUDIO); - addRepresentationItems(masterPlaylist.subtitles, RenditionKey.TYPE_SUBTITLE); - } - } - - private void addRepresentationItems(List renditions, int renditionGroup) { - for (int i = 0; i < renditions.size(); i++) { - trackFormats.add(renditions.get(i).format); - trackKeys.add(new RenditionKey(renditionGroup, i)); - } - } - - @Override - public DownloadAction getDownloadAction( - boolean isRemoveAction, String sampleName, int... trackIndices) { - return new HlsDownloadAction( - uri, isRemoveAction, Util.getUtf8Bytes(sampleName), getTrackKeys(trackIndices)); - } - } - - private static final class SsDownloadHelper extends DownloadHelper { - - public SsDownloadHelper(Uri uri, DataSource.Factory dataSourceFactory) { - super(uri, dataSourceFactory); - } - - @Override - public void init() throws IOException { - DataSource dataSource = dataSourceFactory.createDataSource(); - SsManifest manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri); - - for (int i = 0; i < manifest.streamElements.length; i++) { - SsManifest.StreamElement streamElement = manifest.streamElements[i]; - for (int j = 0; j < streamElement.formats.length; j++) { - trackFormats.add(streamElement.formats[j]); - trackKeys.add(new TrackKey(i, j)); - } - } - } - - @Override - public DownloadAction getDownloadAction( - boolean isRemoveAction, String sampleName, int... trackIndices) { - return new SsDownloadAction( - uri, isRemoveAction, Util.getUtf8Bytes(sampleName), getTrackKeys(trackIndices)); - } - } - - private static final class ProgressiveDownloadHelper extends DownloadHelper { - - public ProgressiveDownloadHelper(Uri uri) { - super(uri, null); - } - - @Override - public void init() { - trackFormats.add(DUMMY_FORMAT); - } - - @Override - public DownloadAction getDownloadAction( - boolean isRemoveAction, String sampleName, int... trackIndices) { - return new ProgressiveDownloadAction( - uri, isRemoveAction, Util.getUtf8Bytes(sampleName), /* customCacheKey= */ null); - } + return selectedTrackKeys; } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 543f7d5e69..0427de05a8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -67,7 +67,7 @@ import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; @@ -467,7 +467,7 @@ public class PlayerActivity extends Activity buildDataSourceFactory(false)) .setManifestParser( new FilteringManifestParser<>( - new SsManifestParser(), (List) manifestFilter)) + new SsManifestParser(), (List) manifestFilter)) .createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java new file mode 100644 index 0000000000..6a5db23b16 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -0,0 +1,122 @@ +/* + * 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 android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.source.TrackGroupArray; +import java.io.IOException; +import java.util.List; + +/** A helper for initializing and removing downloads. */ +public abstract class DownloadHelper { + + /** A callback to be notified when the {@link DownloadHelper} is prepared. */ + public interface Callback { + + /** + * Called when preparation completes. + * + * @param helper The reporting {@link DownloadHelper}. + */ + void onPrepared(DownloadHelper helper); + + /** + * Called when preparation fails. + * + * @param helper The reporting {@link DownloadHelper}. + * @param e The error. + */ + void onPrepareError(DownloadHelper helper, IOException e); + } + + /** + * Initializes the helper for starting a download. + * + * @param callback A callback to be notified when preparation completes or fails. The callback + * will be invoked on the calling thread unless that thread does not have an associated {@link + * Looper}, in which case it will be called on the application's main thread. + */ + public void prepare(final Callback callback) { + final Handler handler = + new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); + new Thread() { + @Override + public void run() { + try { + prepareInternal(); + handler.post( + new Runnable() { + @Override + public void run() { + callback.onPrepared(DownloadHelper.this); + } + }); + } catch (final IOException e) { + handler.post( + new Runnable() { + @Override + public void run() { + callback.onPrepareError(DownloadHelper.this, e); + } + }); + } + } + }.start(); + } + + /** + * Called on a background thread during preparation. + * + * @throws IOException If preparation fails. + */ + protected abstract void prepareInternal() throws IOException; + + /** + * Returns the number of periods for which media is available. Must not be called until after + * preparation completes. + */ + public abstract int getPeriodCount(); + + /** + * Returns the track groups for the given period. Must not be called until after preparation + * completes. + * + * @param periodIndex The period index. + * @return The track groups for the period. May be {@link TrackGroupArray.EMPTY} for single stream + * content. + */ + public abstract TrackGroupArray getTrackGroups(int periodIndex); + + /** + * Builds a {@link DownloadAction} for downloading the specified tracks. Must not be called until + * after preparation completes. + * + * @param data Application provided data to store in {@link DownloadAction#data}. + * @param trackKeys The selected tracks. If empty, all streams will be downloaded. + * @return The built {@link DownloadAction}. + */ + public abstract DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys); + + /** + * Builds a {@link DownloadAction} for removing the media. May be called in any state. + * + * @param data Application provided data to store in {@link DownloadAction#data}. + * @return The built {@link DownloadAction}. + */ + public abstract DownloadAction getRemoveAction(@Nullable byte[] data); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java new file mode 100644 index 0000000000..49b7e36ea6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java @@ -0,0 +1,62 @@ +/* + * 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 android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.source.TrackGroupArray; +import java.util.List; + +/** A {@link DownloadHelper} for progressive streams. */ +public final class ProgressiveDownloadHelper extends DownloadHelper { + + private final Uri uri; + private final @Nullable String customCacheKey; + + public ProgressiveDownloadHelper(Uri uri) { + this(uri, null); + } + + public ProgressiveDownloadHelper(Uri uri, @Nullable String customCacheKey) { + this.uri = uri; + this.customCacheKey = customCacheKey; + } + + @Override + protected void prepareInternal() { + // Do nothing. + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public TrackGroupArray getTrackGroups(int periodIndex) { + return TrackGroupArray.EMPTY; + } + + @Override + public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return new ProgressiveDownloadAction(uri, false, data, customCacheKey); + } + + @Override + public DownloadAction getRemoveAction(@Nullable byte[] data) { + return new ProgressiveDownloadAction(uri, true, data, customCacheKey); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java new file mode 100644 index 0000000000..7c65b3f9b2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/TrackKey.java @@ -0,0 +1,38 @@ +/* + * 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 class TrackKey { + + public final int periodIndex; + public final int groupIndex; + 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; + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java new file mode 100644 index 0000000000..8a6069e477 --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -0,0 +1,103 @@ +/* + * 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.dash.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +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.DashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link DownloadHelper} for DASH streams. */ +public final class DashDownloadHelper extends DownloadHelper { + + private final Uri uri; + private final DataSource.Factory manifestDataSourceFactory; + + private @MonotonicNonNull DashManifest manifest; + + public DashDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { + this.uri = uri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + } + + @Override + protected void prepareInternal() throws IOException { + manifest = + ParsingLoadable.load( + manifestDataSourceFactory.createDataSource(), new DashManifestParser(), uri); + } + + @Override + public int getPeriodCount() { + Assertions.checkNotNull(manifest); + return manifest.getPeriodCount(); + } + + @Override + public TrackGroupArray getTrackGroups(int periodIndex) { + Assertions.checkNotNull(manifest); + List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + TrackGroup[] trackGroups = new TrackGroup[adaptationSets.size()]; + for (int i = 0; i < trackGroups.length; i++) { + List representations = adaptationSets.get(i).representations; + Format[] formats = new Format[representations.size()]; + int representationsCount = representations.size(); + for (int j = 0; j < representationsCount; j++) { + formats[j] = representations.get(j).format; + } + trackGroups[i] = new TrackGroup(formats); + } + return new TrackGroupArray(trackGroups); + } + + @Override + public DashDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return new DashDownloadAction( + uri, /* isRemoveAction= */ false, data, toRepresentationKeys(trackKeys)); + } + + @Override + public DashDownloadAction getRemoveAction(@Nullable byte[] data) { + return new DashDownloadAction( + uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + + private static List toRepresentationKeys(List trackKeys) { + List representationKeys = new ArrayList<>(trackKeys.size()); + for (int i = 0; i < trackKeys.size(); i++) { + TrackKey trackKey = trackKeys.get(i); + representationKeys.add( + new RepresentationKey(trackKey.periodIndex, trackKey.groupIndex, trackKey.trackIndex)); + } + return representationKeys; + } +} diff --git a/library/hls/build.gradle b/library/hls/build.gradle index c2268a3007..87115a0712 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -34,6 +34,7 @@ android { dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java new file mode 100644 index 0000000000..0df1264f90 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -0,0 +1,121 @@ +/* + * 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.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +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.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.RenditionKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link DownloadHelper} for HLS streams. */ +public final class HlsDownloadHelper extends DownloadHelper { + + private final Uri uri; + private final DataSource.Factory manifestDataSourceFactory; + + private @MonotonicNonNull HlsPlaylist playlist; + private int[] renditionTypes; + + public HlsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { + this.uri = uri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + } + + @Override + protected void prepareInternal() throws IOException { + DataSource dataSource = manifestDataSourceFactory.createDataSource(); + playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri); + } + + @Override + public int getPeriodCount() { + Assertions.checkNotNull(playlist); + return 1; + } + + @Override + public TrackGroupArray getTrackGroups(int periodIndex) { + Assertions.checkNotNull(playlist); + if (playlist instanceof HlsMediaPlaylist) { + return TrackGroupArray.EMPTY; + } + // TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction. + HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + TrackGroup[] trackGroups = new TrackGroup[3]; + renditionTypes = new int[3]; + int trackGroupIndex = 0; + if (!masterPlaylist.variants.isEmpty()) { + renditionTypes[trackGroupIndex] = RenditionKey.TYPE_VARIANT; + trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.variants)); + } + if (!masterPlaylist.audios.isEmpty()) { + renditionTypes[trackGroupIndex] = RenditionKey.TYPE_AUDIO; + trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.audios)); + } + if (!masterPlaylist.subtitles.isEmpty()) { + renditionTypes[trackGroupIndex] = RenditionKey.TYPE_SUBTITLE; + trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.subtitles)); + } + return new TrackGroupArray(Arrays.copyOf(trackGroups, trackGroupIndex)); + } + + @Override + public HlsDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + Assertions.checkNotNull(renditionTypes); + return new HlsDownloadAction( + uri, /* isRemoveAction= */ false, data, toRenditionKeys(trackKeys, renditionTypes)); + } + + @Override + public HlsDownloadAction getRemoveAction(@Nullable byte[] data) { + return new HlsDownloadAction( + uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + + private static Format[] toFormats(List hlsUrls) { + Format[] formats = new Format[hlsUrls.size()]; + for (int i = 0; i < hlsUrls.size(); i++) { + formats[i] = hlsUrls.get(i).format; + } + return formats; + } + + private static List toRenditionKeys(List trackKeys, int[] groups) { + List representationKeys = new ArrayList<>(trackKeys.size()); + for (int i = 0; i < trackKeys.size(); i++) { + TrackKey trackKey = trackKeys.get(i); + representationKeys.add(new RenditionKey(groups[trackKey.groupIndex], trackKey.trackIndex)); + } + return representationKeys; + } +} diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index 6ca5570a93..e83a7df81a 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -34,6 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') + implementation 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 256014e112..5ce7b18d61 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -34,7 +33,7 @@ import java.util.UUID; * @see IIS Smooth * Streaming Client Manifest Format */ -public class SsManifest implements FilterableManifest { +public class SsManifest implements FilterableManifest { public static final int UNSET_LOOKAHEAD = -1; @@ -128,15 +127,16 @@ public class SsManifest implements FilterableManifest { * @return A copy of this manifest with the selected tracks. * @throws IndexOutOfBoundsException If a key has an invalid index. */ - public final SsManifest copy(List trackKeys) { - LinkedList sortedKeys = new LinkedList<>(trackKeys); + @Override + public final SsManifest copy(List trackKeys) { + ArrayList sortedKeys = new ArrayList<>(trackKeys); Collections.sort(sortedKeys); StreamElement currentStreamElement = null; List copiedStreamElements = new ArrayList<>(); List copiedFormats = new ArrayList<>(); for (int i = 0; i < sortedKeys.size(); i++) { - TrackKey key = sortedKeys.get(i); + StreamKey key = sortedKeys.get(i); StreamElement streamElement = streamElements[key.streamElementIndex]; if (streamElement != currentStreamElement && currentStreamElement != null) { // We're advancing to a new stream element. Add the current one. diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java similarity index 75% rename from library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java rename to library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java index d529f0b62a..767c2702f8 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/StreamKey.java @@ -20,15 +20,13 @@ import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -/** - * Uniquely identifies a track in a {@link SsManifest}. - */ -public final class TrackKey implements Parcelable, Comparable { +/** Uniquely identifies a track in a {@link SsManifest}. */ +public final class StreamKey implements Parcelable, Comparable { public final int streamElementIndex; public final int trackIndex; - public TrackKey(int streamElementIndex, int trackIndex) { + public StreamKey(int streamElementIndex, int trackIndex) { this.streamElementIndex = streamElementIndex; this.trackIndex = trackIndex; } @@ -47,7 +45,7 @@ public final class TrackKey implements Parcelable, Comparable { return false; } - TrackKey that = (TrackKey) o; + StreamKey that = (StreamKey) o; return streamElementIndex == that.streamElementIndex && trackIndex == that.trackIndex; } @@ -61,7 +59,7 @@ public final class TrackKey implements Parcelable, Comparable { // Comparable implementation. @Override - public int compareTo(@NonNull TrackKey o) { + public int compareTo(@NonNull StreamKey o) { int result = streamElementIndex - o.streamElementIndex; if (result == 0) { result = trackIndex - o.trackIndex; @@ -82,15 +80,16 @@ public final class TrackKey implements Parcelable, Comparable { dest.writeInt(trackIndex); } - public static final Creator CREATOR = new Creator() { - @Override - public TrackKey createFromParcel(Parcel in) { - return new TrackKey(in.readInt(), in.readInt()); - } + public static final Creator CREATOR = + new Creator() { + @Override + public StreamKey createFromParcel(Parcel in) { + return new StreamKey(in.readInt(), in.readInt()); + } - @Override - public TrackKey[] newArray(int size) { - return new TrackKey[size]; - } - }; + @Override + public StreamKey[] newArray(int size) { + return new StreamKey[size]; + } + }; } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java index 2e26915a86..7fa89afef0 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java @@ -20,29 +20,29 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.List; /** An action to download or remove downloaded SmoothStreaming streams. */ -public final class SsDownloadAction extends SegmentDownloadAction { +public final class SsDownloadAction extends SegmentDownloadAction { private static final String TYPE = "ss"; private static final int VERSION = 0; public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer(TYPE, VERSION) { + new SegmentDownloadActionDeserializer(TYPE, VERSION) { @Override - protected TrackKey readKey(DataInputStream input) throws IOException { - return new TrackKey(input.readInt(), input.readInt()); + protected StreamKey readKey(DataInputStream input) throws IOException { + return new StreamKey(input.readInt(), input.readInt()); } @Override protected DownloadAction createDownloadAction( - Uri uri, boolean isRemoveAction, byte[] data, List keys) { + Uri uri, boolean isRemoveAction, byte[] data, List keys) { return new SsDownloadAction(uri, isRemoveAction, data, keys); } }; @@ -55,7 +55,7 @@ public final class SsDownloadAction extends SegmentDownloadAction { * removeAction} is true, {@code keys} must be empty. */ public SsDownloadAction( - Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { + Uri uri, boolean isRemoveAction, @Nullable byte[] data, List keys) { super(TYPE, VERSION, uri, isRemoveAction, data, keys); } @@ -65,7 +65,7 @@ public final class SsDownloadAction extends SegmentDownloadAction { } @Override - protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { + protected void writeKey(DataOutputStream output, StreamKey key) throws IOException { output.writeInt(key.streamElementIndex); output.writeInt(key.trackIndex); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java new file mode 100644 index 0000000000..82464101d6 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -0,0 +1,91 @@ +/* + * 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.smoothstreaming.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.offline.TrackKey; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +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.StreamKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link DownloadHelper} for SmoothStreaming streams. */ +public final class SsDownloadHelper extends DownloadHelper { + + private final Uri uri; + private final DataSource.Factory manifestDataSourceFactory; + + private @MonotonicNonNull SsManifest manifest; + + public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { + this.uri = uri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + } + + @Override + protected void prepareInternal() throws IOException { + DataSource dataSource = manifestDataSourceFactory.createDataSource(); + manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri); + } + + @Override + public int getPeriodCount() { + Assertions.checkNotNull(manifest); + return 1; + } + + @Override + public TrackGroupArray getTrackGroups(int periodIndex) { + Assertions.checkNotNull(manifest); + SsManifest.StreamElement[] streamElements = manifest.streamElements; + TrackGroup[] trackGroups = new TrackGroup[streamElements.length]; + for (int i = 0; i < streamElements.length; i++) { + trackGroups[i] = new TrackGroup(streamElements[i].formats); + } + return new TrackGroupArray(trackGroups); + } + + @Override + public SsDownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return new SsDownloadAction(uri, /* isRemoveAction= */ false, data, toStreamKeys(trackKeys)); + } + + @Override + public SsDownloadAction getRemoveAction(@Nullable byte[] data) { + return new SsDownloadAction( + uri, /* isRemoveAction= */ true, data, Collections.emptyList()); + } + + private static List toStreamKeys(List trackKeys) { + List representationKeys = new ArrayList<>(trackKeys.size()); + for (int i = 0; i < trackKeys.size(); i++) { + TrackKey trackKey = trackKeys.get(i); + representationKeys.add(new StreamKey(trackKey.groupIndex, trackKey.trackIndex)); + } + return representationKeys; + } +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 69f6f9e7cf..cdb971426c 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.StreamKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -46,7 +46,7 @@ import java.util.List; * new SsDownloader( * manifestUrl, * constructorHelper, - * Collections.singletonList(new TrackKey(0, 0))); + * Collections.singletonList(new StreamKey(0, 0))); * // Perform the download. * ssDownloader.download(); * // Access downloaded data using CacheDataSource @@ -54,11 +54,11 @@ import java.util.List; * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); * } */ -public final class SsDownloader extends SegmentDownloader { +public final class SsDownloader extends SegmentDownloader { /** @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper, List) */ public SsDownloader( - Uri manifestUri, DownloaderConstructorHelper constructorHelper, List trackKeys) { + Uri manifestUri, DownloaderConstructorHelper constructorHelper, List trackKeys) { super(SsUtil.fixManifestUri(manifestUri), constructorHelper, trackKeys); } diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java index fbb2c3d4c4..c7c6c6f3fb 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java @@ -43,7 +43,8 @@ public class SsManifestTest { SsManifest sourceManifest = newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1])); - List keys = Arrays.asList(new TrackKey(0, 0), new TrackKey(0, 2), new TrackKey(1, 0)); + List keys = + Arrays.asList(new StreamKey(0, 0), new StreamKey(0, 2), new StreamKey(1, 0)); // Keys don't need to be in any particular order Collections.shuffle(keys, new Random(0)); @@ -62,7 +63,7 @@ public class SsManifestTest { SsManifest sourceManifest = newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1])); - List keys = Arrays.asList(new TrackKey(1, 0)); + List keys = Arrays.asList(new StreamKey(1, 0)); // Keys don't need to be in any particular order Collections.shuffle(keys, new Random(0));