Move download helpers into core library

Also convert them to exposing periods and track groups, like
regular MediaSources do. This gets us much closer to being
able to use standard track selection components during offline
initialization. The helper is responsible for reverse mapping
selected tracks onto physical streams when generating the
download action. This is trivial except for the HLS case, which
is a TODO for now.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=195500826
This commit is contained in:
olly 2018-05-04 18:02:27 -07:00 committed by Oliver Woodman
parent 4ee1daef0e
commit 416d6c9eeb
15 changed files with 642 additions and 272 deletions

View file

@ -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<? extends Parcelable> downloadHelper;
private ListView representationList;
private DownloadHelper downloadHelper;
private ListView trackList;
private ArrayAdapter<String> arrayAdapter;
private ArrayList<TrackKey> 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<TrackKey> 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<? extends Parcelable> keys = downloadHelper.getTrackKeys(selectedTrackIndices);
DownloadAction action = downloadHelper.getDownloadAction(null, getSelectedTrackKeys());
List<? extends ParcelableArray> 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<Integer> checkedIndices = new ArrayList<>();
for (int i = 0; i < representationList.getChildCount(); i++) {
if (representationList.isItemChecked(i)) {
checkedIndices.add(i);
private List<TrackKey> getSelectedTrackKeys() {
ArrayList<TrackKey> 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<K extends Parcelable> {
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<Format> trackFormats;
protected final List<K> 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<K> getTrackKeys(int... trackIndices) {
if (trackFormats.size() == 1 && trackFormats.get(0) == DUMMY_FORMAT) {
return Collections.emptyList();
}
List<K> 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<RepresentationKey> {
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<AdaptationSet> adaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
for (int adaptationIndex = 0; adaptationIndex < adaptationSets.size(); adaptationIndex++) {
List<Representation> 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<RenditionKey> {
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<HlsUrl> 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<TrackKey> {
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<Parcelable> {
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;
}
}

View file

@ -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<TrackKey>) manifestFilter))
new SsManifestParser(), (List<StreamKey>) manifestFilter))
.createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory)

View file

@ -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<TrackKey> 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);
}

View file

@ -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<TrackKey> trackKeys) {
return new ProgressiveDownloadAction(uri, false, data, customCacheKey);
}
@Override
public DownloadAction getRemoveAction(@Nullable byte[] data) {
return new ProgressiveDownloadAction(uri, true, data, customCacheKey);
}
}

View file

@ -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;
}
}

View file

@ -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<AdaptationSet> adaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
TrackGroup[] trackGroups = new TrackGroup[adaptationSets.size()];
for (int i = 0; i < trackGroups.length; i++) {
List<Representation> 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<TrackKey> 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.<RepresentationKey>emptyList());
}
private static List<RepresentationKey> toRepresentationKeys(List<TrackKey> trackKeys) {
List<RepresentationKey> 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;
}
}

View file

@ -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')
}

View file

@ -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<TrackKey> 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.<RenditionKey>emptyList());
}
private static Format[] toFormats(List<HlsMasterPlaylist.HlsUrl> 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<RenditionKey> toRenditionKeys(List<TrackKey> trackKeys, int[] groups) {
List<RenditionKey> 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;
}
}

View file

@ -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')
}

View file

@ -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 <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">IIS Smooth
* Streaming Client Manifest Format</a>
*/
public class SsManifest implements FilterableManifest<SsManifest, TrackKey> {
public class SsManifest implements FilterableManifest<SsManifest, StreamKey> {
public static final int UNSET_LOOKAHEAD = -1;
@ -128,15 +127,16 @@ public class SsManifest implements FilterableManifest<SsManifest, TrackKey> {
* @return A copy of this manifest with the selected tracks.
* @throws IndexOutOfBoundsException If a key has an invalid index.
*/
public final SsManifest copy(List<TrackKey> trackKeys) {
LinkedList<TrackKey> sortedKeys = new LinkedList<>(trackKeys);
@Override
public final SsManifest copy(List<StreamKey> trackKeys) {
ArrayList<StreamKey> sortedKeys = new ArrayList<>(trackKeys);
Collections.sort(sortedKeys);
StreamElement currentStreamElement = null;
List<StreamElement> copiedStreamElements = new ArrayList<>();
List<Format> 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.

View file

@ -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<TrackKey> {
/** Uniquely identifies a track in a {@link SsManifest}. */
public final class StreamKey implements Parcelable, Comparable<StreamKey> {
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<TrackKey> {
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<TrackKey> {
// 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<TrackKey> {
dest.writeInt(trackIndex);
}
public static final Creator<TrackKey> CREATOR = new Creator<TrackKey>() {
@Override
public TrackKey createFromParcel(Parcel in) {
return new TrackKey(in.readInt(), in.readInt());
}
public static final Creator<StreamKey> CREATOR =
new Creator<StreamKey>() {
@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];
}
};
}

View file

@ -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<TrackKey> {
public final class SsDownloadAction extends SegmentDownloadAction<StreamKey> {
private static final String TYPE = "ss";
private static final int VERSION = 0;
public static final Deserializer DESERIALIZER =
new SegmentDownloadActionDeserializer<TrackKey>(TYPE, VERSION) {
new SegmentDownloadActionDeserializer<StreamKey>(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<TrackKey> keys) {
Uri uri, boolean isRemoveAction, byte[] data, List<StreamKey> keys) {
return new SsDownloadAction(uri, isRemoveAction, data, keys);
}
};
@ -55,7 +55,7 @@ public final class SsDownloadAction extends SegmentDownloadAction<TrackKey> {
* removeAction} is true, {@code keys} must be empty.
*/
public SsDownloadAction(
Uri uri, boolean isRemoveAction, @Nullable byte[] data, List<TrackKey> keys) {
Uri uri, boolean isRemoveAction, @Nullable byte[] data, List<StreamKey> keys) {
super(TYPE, VERSION, uri, isRemoveAction, data, keys);
}
@ -65,7 +65,7 @@ public final class SsDownloadAction extends SegmentDownloadAction<TrackKey> {
}
@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);
}

View file

@ -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<TrackKey> 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.<StreamKey>emptyList());
}
private static List<StreamKey> toStreamKeys(List<TrackKey> trackKeys) {
List<StreamKey> 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;
}
}

View file

@ -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);
* }</pre>
*/
public final class SsDownloader extends SegmentDownloader<SsManifest, TrackKey> {
public final class SsDownloader extends SegmentDownloader<SsManifest, StreamKey> {
/** @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper, List) */
public SsDownloader(
Uri manifestUri, DownloaderConstructorHelper constructorHelper, List<TrackKey> trackKeys) {
Uri manifestUri, DownloaderConstructorHelper constructorHelper, List<StreamKey> trackKeys) {
super(SsUtil.fixManifestUri(manifestUri), constructorHelper, trackKeys);
}

View file

@ -43,7 +43,8 @@ public class SsManifestTest {
SsManifest sourceManifest =
newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1]));
List<TrackKey> keys = Arrays.asList(new TrackKey(0, 0), new TrackKey(0, 2), new TrackKey(1, 0));
List<StreamKey> 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<TrackKey> keys = Arrays.asList(new TrackKey(1, 0));
List<StreamKey> keys = Arrays.asList(new StreamKey(1, 0));
// Keys don't need to be in any particular order
Collections.shuffle(keys, new Random(0));