提交 10f1bd73 编写于 作者: T tonihei 提交者: Oliver Woodman

Add shuffle mode parameter to Timeline interface methods.

This parameter is used by methods such as getNextWindowIndex
and getPreviousWindowIndex to determine the playback order.
Additionally, there are method to query the first and last
window index given the shuffle mode.

None of the timeline implementations nor the ExoPlayer
implementation supports shuffling so far.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=166170229
上级 5214bfa7
......@@ -127,7 +127,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
return;
}
int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(),
player.getRepeatMode());
player.getRepeatMode(), false);
if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| previousWindowIndex == C.INDEX_UNSET) {
player.seekTo(0);
......@@ -155,7 +155,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
return;
}
int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(),
player.getRepeatMode());
player.getRepeatMode(), false);
if (nextWindowIndex != C.INDEX_UNSET) {
player.seekTo(nextWindowIndex, C.TIME_UNSET);
}
......
......@@ -488,7 +488,7 @@ import java.io.IOException;
}
while (true) {
int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex,
period, window, repeatMode);
period, window, repeatMode, false);
while (lastValidPeriodHolder.next != null
&& !lastValidPeriodHolder.info.isLastInTimelinePeriod) {
lastValidPeriodHolder = lastValidPeriodHolder.next;
......@@ -1122,7 +1122,7 @@ import java.io.IOException;
while (periodHolder.next != null) {
MediaPeriodHolder previousPeriodHolder = periodHolder;
periodHolder = periodHolder.next;
periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode);
periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, false);
if (periodIndex != C.INDEX_UNSET
&& periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) {
// The holder is consistent with the new timeline. Update its index and continue.
......@@ -1204,7 +1204,8 @@ import java.io.IOException;
int newPeriodIndex = C.INDEX_UNSET;
int maxIterations = oldTimeline.getPeriodCount();
for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {
oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode);
oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode,
false);
if (oldPeriodIndex == C.INDEX_UNSET) {
// We've reached the end of the old timeline.
break;
......
......@@ -162,7 +162,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
// timeline is updated, to avoid repeatedly checking the same timeline.
if (currentMediaPeriodInfo.isLastInTimelinePeriod) {
int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex,
period, window, repeatMode);
period, window, repeatMode, false);
if (nextPeriodIndex == C.INDEX_UNSET) {
// We can't create a next period yet.
return null;
......@@ -353,7 +353,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex;
return !timeline.getWindow(windowIndex, window).isDynamic
&& timeline.isLastPeriod(id.periodIndex, period, window, repeatMode)
&& timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, false)
&& isLastMediaPeriodInPeriod;
}
......
......@@ -553,20 +553,24 @@ public abstract class Timeline {
/**
* Returns the index of the window after the window at index {@code windowIndex} depending on the
* {@code repeatMode}.
* {@code repeatMode} and whether shuffling is enabled.
*
* @param windowIndex Index of a window in the timeline.
* @param repeatMode A repeat mode.
* @param shuffleModeEnabled Whether shuffling is enabled.
* @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window.
*/
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1;
return windowIndex == getLastWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET
: windowIndex + 1;
case Player.REPEAT_MODE_ONE:
return windowIndex;
case Player.REPEAT_MODE_ALL:
return windowIndex == getWindowCount() - 1 ? 0 : windowIndex + 1;
return windowIndex == getLastWindowIndex(shuffleModeEnabled)
? getFirstWindowIndex(shuffleModeEnabled) : windowIndex + 1;
default:
throw new IllegalStateException();
}
......@@ -574,25 +578,51 @@ public abstract class Timeline {
/**
* Returns the index of the window before the window at index {@code windowIndex} depending on the
* {@code repeatMode}.
* {@code repeatMode} and whether shuffling is enabled.
*
* @param windowIndex Index of a window in the timeline.
* @param repeatMode A repeat mode.
* @param shuffleModeEnabled Whether shuffling is enabled.
* @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window.
*/
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
switch (repeatMode) {
case Player.REPEAT_MODE_OFF:
return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1;
return windowIndex == getFirstWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET
: windowIndex - 1;
case Player.REPEAT_MODE_ONE:
return windowIndex;
case Player.REPEAT_MODE_ALL:
return windowIndex == 0 ? getWindowCount() - 1 : windowIndex - 1;
return windowIndex == getFirstWindowIndex(shuffleModeEnabled)
? getLastWindowIndex(shuffleModeEnabled) : windowIndex - 1;
default:
throw new IllegalStateException();
}
}
/**
* Returns the index of the last window in the playback order depending on whether shuffling is
* enabled.
*
* @param shuffleModeEnabled Whether shuffling is enabled.
* @return The index of the last window in the playback order.
*/
public int getLastWindowIndex(boolean shuffleModeEnabled) {
return getWindowCount() - 1;
}
/**
* Returns the index of the first window in the playback order depending on whether shuffling is
* enabled.
*
* @param shuffleModeEnabled Whether shuffling is enabled.
* @return The index of the first window in the playback order.
*/
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
return 0;
}
/**
* Populates a {@link Window} with data for the window at the specified index. Does not populate
* {@link Window#id}.
......@@ -614,7 +644,7 @@ public abstract class Timeline {
* null. The caller should pass false for efficiency reasons unless the field is required.
* @return The populated {@link Window}, for convenience.
*/
public Window getWindow(int windowIndex, Window window, boolean setIds) {
public final Window getWindow(int windowIndex, Window window, boolean setIds) {
return getWindow(windowIndex, window, setIds, 0);
}
......@@ -639,19 +669,20 @@ public abstract class Timeline {
/**
* Returns the index of the period after the period at index {@code periodIndex} depending on the
* {@code repeatMode}.
* {@code repeatMode} and whether shuffling is enabled.
*
* @param periodIndex Index of a period in the timeline.
* @param period A {@link Period} to be used internally. Must not be null.
* @param window A {@link Window} to be used internally. Must not be null.
* @param repeatMode A repeat mode.
* @param shuffleModeEnabled Whether shuffling is enabled.
* @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period.
*/
public final int getNextPeriodIndex(int periodIndex, Period period, Window window,
@Player.RepeatMode int repeatMode) {
@Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
int windowIndex = getPeriod(periodIndex, period).windowIndex;
if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) {
int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode);
int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
if (nextWindowIndex == C.INDEX_UNSET) {
return C.INDEX_UNSET;
}
......@@ -662,17 +693,19 @@ public abstract class Timeline {
/**
* Returns whether the given period is the last period of the timeline depending on the
* {@code repeatMode}.
* {@code repeatMode} and whether shuffling is enabled.
*
* @param periodIndex A period index.
* @param period A {@link Period} to be used internally. Must not be null.
* @param window A {@link Window} to be used internally. Must not be null.
* @param repeatMode A repeat mode.
* @param shuffleModeEnabled Whether shuffling is enabled.
* @return Whether the period of the given index is the last period of the timeline.
*/
public final boolean isLastPeriod(int periodIndex, Period period, Window window,
@Player.RepeatMode int repeatMode) {
return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET;
@Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled)
== C.INDEX_UNSET;
}
/**
......
/*
* Copyright (C) 2017 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.ClosedSource;
import java.io.IOException;
/** Thrown on an error during downloading. */
@ClosedSource(reason = "Not ready yet")
public final class DownloadException extends IOException {
/** @param message The message for the exception. */
public DownloadException(String message) {
super(message);
}
}
/*
* Copyright (C) 2017 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.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.ClosedSource;
import java.io.IOException;
/**
* An interface for stream downloaders.
*/
@ClosedSource(reason = "Not ready yet")
public interface Downloader {
/**
* Listener notified when download progresses.
* <p>
* No guarantees are made about the thread or threads on which the listener is called, but it is
* guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in
* the same order as events occurred.
*/
interface ProgressListener {
/**
* Called during the download. Calling intervals depend on the {@link Downloader}
* implementation.
*
* @param downloader The reporting instance.
* @param downloadPercentage The download percentage. This value can be an estimation.
* @param downloadedBytes Total number of downloaded bytes.
* @see #download(ProgressListener)
*/
void onDownloadProgress(Downloader downloader, float downloadPercentage, long downloadedBytes);
}
/**
* Initializes the downloader.
*
* @throws DownloadException Thrown if the media cannot be downloaded.
* @throws InterruptedException If the thread has been interrupted.
* @throws IOException Thrown when there is an io error while reading from cache.
* @see #getDownloadedBytes()
* @see #getDownloadPercentage()
*/
void init() throws InterruptedException, IOException;
/**
* Downloads the media.
*
* @param listener If not null, called during download.
* @throws DownloadException Thrown if the media cannot be downloaded.
* @throws InterruptedException If the thread has been interrupted.
* @throws IOException Thrown when there is an io error while downloading.
*/
void download(@Nullable ProgressListener listener)
throws InterruptedException, IOException;
/**
* Removes all of the downloaded data of the media.
*
* @throws InterruptedException Thrown if the thread was interrupted.
*/
void remove() throws InterruptedException;
/**
* Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been
* calculated yet.
*
* @see #init()
*/
long getDownloadedBytes();
/**
* Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This
* value can be an estimation.
*
* @see #init()
*/
float getDownloadPercentage();
}
/*
* Copyright (C) 2017 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.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSource.Factory;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.PriorityDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ClosedSource;
import com.google.android.exoplayer2.util.PriorityTaskManager;
/** A helper class that holds necessary parameters for {@link Downloader} construction. */
@ClosedSource(reason = "Not ready yet")
public final class DownloaderConstructorHelper {
private final Cache cache;
private final Factory upstreamDataSourceFactory;
private final Factory cacheReadDataSourceFactory;
private final DataSink.Factory cacheWriteDataSinkFactory;
private final PriorityTaskManager priorityTaskManager;
/**
* @param cache Cache instance to be used to store downloaded data.
* @param upstreamDataSourceFactory A {@link Factory} for downloading data.
*/
public DownloaderConstructorHelper(Cache cache, Factory upstreamDataSourceFactory) {
this(cache, upstreamDataSourceFactory, null, null, null);
}
/**
* @param cache Cache instance to be used to store downloaded data.
* @param upstreamDataSourceFactory A {@link Factory} for downloading data.
* @param cacheReadDataSourceFactory A {@link Factory} for reading data from the cache.
* If null, null is passed to {@link Downloader} constructor.
* @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for writing data to the cache. If
* null, null is passed to {@link Downloader} constructor.
* @param priorityTaskManager If one is given then the download priority is set lower than
* loading. If null, null is passed to {@link Downloader} constructor.
*/
public DownloaderConstructorHelper(Cache cache, Factory upstreamDataSourceFactory,
@Nullable Factory cacheReadDataSourceFactory,
@Nullable DataSink.Factory cacheWriteDataSinkFactory,
@Nullable PriorityTaskManager priorityTaskManager) {
Assertions.checkNotNull(upstreamDataSourceFactory);
this.cache = cache;
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;
this.priorityTaskManager = priorityTaskManager;
}
/** Returns the {@link Cache} instance. */
public Cache getCache() {
return cache;
}
/** Returns a {@link PriorityTaskManager} instance.*/
public PriorityTaskManager getPriorityTaskManager() {
// Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager
// each time so clients don't affect each other over the dummy PriorityTaskManager instance.
return priorityTaskManager != null ? priorityTaskManager : new PriorityTaskManager();
}
/**
* Returns a new {@link CacheDataSource} instance. If {@code offline} is true, it can only read
* data from the cache.
*/
public CacheDataSource buildCacheDataSource(boolean offline) {
DataSource cacheReadDataSource = cacheReadDataSourceFactory != null
? cacheReadDataSourceFactory.createDataSource() : new FileDataSource();
if (offline) {
return new CacheDataSource(cache, DummyDataSource.INSTANCE,
cacheReadDataSource, null, CacheDataSource.FLAG_BLOCK_ON_CACHE, null);
} else {
DataSink cacheWriteDataSink = cacheWriteDataSinkFactory != null
? cacheWriteDataSinkFactory.createDataSink()
: new CacheDataSink(cache, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
DataSource upstream = upstreamDataSourceFactory.createDataSource();
upstream = priorityTaskManager == null ? upstream
: new PriorityDataSource(upstream, priorityTaskManager, C.PRIORITY_DOWNLOAD);
return new CacheDataSource(cache, upstream, cacheReadDataSource,
cacheWriteDataSink, CacheDataSource.FLAG_BLOCK_ON_CACHE, null);
}
}
}
/*
* Copyright (C) 2017 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.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheUtil;
import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import com.google.android.exoplayer2.util.ClosedSource;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException;
/**
* A downloader for progressive media streams.
*/
@ClosedSource(reason = "Not ready yet")
public final class ProgressiveDownloader implements Downloader {
private static final int BUFFER_SIZE_BYTES = 128 * 1024;
private final DataSpec dataSpec;
private final Cache cache;
private final CacheDataSource dataSource;
private final PriorityTaskManager priorityTaskManager;
private final CacheUtil.CachingCounters cachingCounters;
/**
* @param uri Uri of the data to be downloaded.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
* @param constructorHelper a {@link DownloaderConstructorHelper} instance.
*/
public ProgressiveDownloader(
String uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) {
this.dataSpec = new DataSpec(Uri.parse(uri), 0, C.LENGTH_UNSET, customCacheKey, 0);
this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.buildCacheDataSource(false);
this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
cachingCounters = new CachingCounters();
}
@Override
public void init() {
CacheUtil.getCached(dataSpec, cache, cachingCounters);
}
@Override
public void download(@Nullable ProgressListener listener) throws InterruptedException,
IOException {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
try {
byte[] buffer = new byte[BUFFER_SIZE_BYTES];
CacheUtil.cache(dataSpec, cache, dataSource, buffer, priorityTaskManager, C.PRIORITY_DOWNLOAD,
cachingCounters, true);
// TODO: Work out how to call onDownloadProgress periodically during the download, or else
// get rid of ProgressListener and move to a model where the manager periodically polls
// Downloaders.
if (listener != null) {
listener.onDownloadProgress(this, 100, cachingCounters.contentLength);
}
} finally {
priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
}
}
@Override
public void remove() {
CacheUtil.remove(cache, CacheUtil.getKey(dataSpec));
}
@Override
public long getDownloadedBytes() {
return cachingCounters.totalCachedBytes();
}
@Override
public float getDownloadPercentage() {
long contentLength = cachingCounters.contentLength;
return contentLength == C.LENGTH_UNSET ? Float.NaN
: ((cachingCounters.totalCachedBytes() * 100f) / contentLength);
}
}
/*
* Copyright (C) 2017 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.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheUtil;
import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters;
import com.google.android.exoplayer2.util.ClosedSource;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
/**
* Base class for multi segment stream downloaders.
*
* <p>All of the methods are blocking. Also they are not thread safe, except {@link
* #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link #getDownloadedBytes()}.
*
* @param <M> The type of the manifest object.
* @param <K> The type of the representation key object.
*/
@ClosedSource(reason = "Not ready yet")
public abstract class SegmentDownloader<M, K> implements Downloader {
/** Smallest unit of content to be downloaded. */
protected static class Segment implements Comparable<Segment> {
/** The start time of the segment in microseconds. */
public final long startTimeUs;
/** The {@link DataSpec} of the segment. */
public final DataSpec dataSpec;
/** Constructs a Segment. */
public Segment(long startTimeUs, DataSpec dataSpec) {
this.startTimeUs = startTimeUs;
this.dataSpec = dataSpec;
}
@Override
public int compareTo(@NonNull Segment other) {
long startOffsetDiff = startTimeUs - other.startTimeUs;
return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1);
}
}
private static final int BUFFER_SIZE_BYTES = 128 * 1024;
private final Uri manifestUri;
private final PriorityTaskManager priorityTaskManager;
private final Cache cache;
private final CacheDataSource dataSource;
private final CacheDataSource offlineDataSource;
private M manifest;
private K[] keys;
private volatile int totalSegments;
private volatile int downloadedSegments;
private volatile long downloadedBytes;
/**
* @param manifestUri The {@link Uri} of the manifest to be downloaded.
* @param constructorHelper a {@link DownloaderConstructorHelper} instance.
*/
public SegmentDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) {
this.manifestUri = manifestUri;
this.cache = constructorHelper.getCache();
this.dataSource = constructorHelper.buildCacheDataSource(false);
this.offlineDataSource = constructorHelper.buildCacheDataSource(true);
this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
resetCounters();
}
/**
* Returns the manifest. Downloads and parses it if necessary.
*
* @return The manifest.
* @throws IOException If an error occurs reading data.
*/
public final M getManifest() throws IOException {
return getManifestIfNeeded(false);
}
/**
* Selects multiple representations pointed to by the keys for downloading, checking status. Any
* previous selection is cleared. If keys are null or empty, all representations are downloaded.
*/
public final void selectRepresentations(K[] keys) {
this.keys = keys != null ? keys.clone() : null;
resetCounters();
}
/**
* Initializes the total segments, downloaded segments and downloaded bytes counters for the
* selected representations.
*
* @throws IOException Thrown when there is an io error while reading from cache.
* @throws DownloadException Thrown if the media cannot be downloaded.
* @throws InterruptedException If the thread has been interrupted.
* @see #getTotalSegments()
* @see #getDownloadedSegments()
* @see #getDownloadedBytes()
*/
@Override
public final void init() throws InterruptedException, IOException {
try {
getManifestIfNeeded(true);
} catch (IOException e) {
// Either the manifest file isn't available offline or not parsable.
return;
}
try {
initStatus(true);
} catch (IOException | InterruptedException e) {
resetCounters();
throw e;
}
}
/**
* Downloads the content for the selected representations in sync or resumes a previously stopped
* download.
*
* @param listener If not null, called during download.
* @throws IOException Thrown when there is an io error while downloading.
* @throws DownloadException Thrown if the media cannot be downloaded.
* @throws InterruptedException If the thread has been interrupted.
*/
@Override
public final synchronized void download(@Nullable ProgressListener listener)
throws IOException, InterruptedException {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
try {
getManifestIfNeeded(false);
List<Segment> segments = initStatus(false);
notifyListener(listener); // Initial notification.
Collections.sort(segments);
byte[] buffer = new byte[BUFFER_SIZE_BYTES];
CachingCounters cachingCounters = new CachingCounters();
for (int i = 0; i < segments.size(); i++) {
CacheUtil.cache(segments.get(i).dataSpec, cache, dataSource, buffer,
priorityTaskManager, C.PRIORITY_DOWNLOAD, cachingCounters, true);
downloadedBytes += cachingCounters.newlyCachedBytes;
downloadedSegments++;
notifyListener(listener);
}
} finally {
priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
}
}
/**
* Returns the total number of segments in the representations which are selected, or {@link
* C#LENGTH_UNSET} if it hasn't been calculated yet.
*
* @see #init()
*/
public final int getTotalSegments() {
return totalSegments;
}
/**
* Returns the total number of downloaded segments in the representations which are selected, or
* {@link C#LENGTH_UNSET} if it hasn't been calculated yet.
*
* @see #init()
*/
public final int getDownloadedSegments() {
return downloadedSegments;
}
/**
* Returns the total number of downloaded bytes in the representations which are selected, or
* {@link C#LENGTH_UNSET} if it hasn't been calculated yet.
*
* @see #init()
*/
@Override
public final long getDownloadedBytes() {
return downloadedBytes;
}
@Override
public float getDownloadPercentage() {
// Take local snapshot of the volatile fields
int totalSegments = this.totalSegments;
int downloadedSegments = this.downloadedSegments;
if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) {
return Float.NaN;
}
return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments;
}
@Override
public final void remove() throws InterruptedException {
try {
getManifestIfNeeded(true);
} catch (IOException e) {
// Either the manifest file isn't available offline, or it's not parsable. Continue anyway to
// reset the counters and attempt to remove the manifest file.
}
resetCounters();
if (manifest != null) {
List<Segment> segments = null;
try {
segments = getAllSegments(offlineDataSource, manifest, true);
} catch (IOException e) {
// Ignore exceptions. We do our best with what's available offline.
}
if (segments != null) {
for (int i = 0; i < segments.size(); i++) {
remove(segments.get(i).dataSpec.uri);
}
}
manifest = null;
}
remove(manifestUri);
}
/**
* Loads and parses the manifest.
*
* @param dataSource The {@link DataSource} through which to load.
* @param uri The manifest uri.
* @return The manifest.
* @throws IOException If an error occurs reading data.
*/
protected abstract M getManifest(DataSource dataSource, Uri uri) throws IOException;
/**
* Returns a list of {@link Segment}s for given keys.
*
* @param dataSource The {@link DataSource} through which to load any required data.
* @param manifest The manifest containing the segments.
* @param keys The selected representation keys.
* @param allowIncompleteIndex Whether to continue in the case that a load error prevents all
* segments from being listed. If true then a partial segment list will be returned. If false
* an {@link IOException} will be thrown.
* @throws InterruptedException Thrown if the thread was interrupted.
* @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if
* the media is not in a form that allows for its segments to be listed.
* @return A list of {@link Segment}s for given keys.
*/
protected abstract List<Segment> getSegments(DataSource dataSource, M manifest, K[] keys,
boolean allowIncompleteIndex) throws InterruptedException, IOException;
/**
* Returns a list of all segments.
*
* @see #getSegments(DataSource, M, Object[], boolean)}.
*/
protected abstract List<Segment> getAllSegments(DataSource dataSource, M manifest,
boolean allowPartialIndex) throws InterruptedException, IOException;
private void resetCounters() {
totalSegments = C.LENGTH_UNSET;
downloadedSegments = C.LENGTH_UNSET;
downloadedBytes = C.LENGTH_UNSET;
}
private void remove(Uri uri) {
CacheUtil.remove(cache, CacheUtil.generateKey(uri));
}
private void notifyListener(ProgressListener listener) {
if (listener != null) {
listener.onDownloadProgress(this, getDownloadPercentage(), downloadedBytes);
}
}
/**
* Initializes totalSegments, downloadedSegments and downloadedBytes for selected representations.
* If not offline then downloads missing metadata.
*
* @return A list of not fully downloaded segments.
*/
private synchronized List<Segment> initStatus(boolean offline)
throws IOException, InterruptedException {
DataSource dataSource = getDataSource(offline);
List<Segment> segments = keys != null && keys.length > 0
? getSegments(dataSource, manifest, keys, offline)
: getAllSegments(dataSource, manifest, offline);
CachingCounters cachingCounters = new CachingCounters();
totalSegments = segments.size();
downloadedSegments = 0;
downloadedBytes = 0;
for (int i = segments.size() - 1; i >= 0; i--) {
Segment segment = segments.get(i);
CacheUtil.getCached(segment.dataSpec, cache, cachingCounters);
downloadedBytes += cachingCounters.alreadyCachedBytes;
if (cachingCounters.alreadyCachedBytes == cachingCounters.contentLength) {
// The segment is fully downloaded.
downloadedSegments++;
segments.remove(i);
}
}
return segments;
}
private M getManifestIfNeeded(boolean offline) throws IOException {
if (manifest == null) {
manifest = getManifest(getDataSource(offline), manifestUri);
}
return manifest;
}
private DataSource getDataSource(boolean offline) {
return offline ? offlineDataSource : dataSource;
}
}
......@@ -32,12 +32,14 @@ import com.google.android.exoplayer2.Timeline;
}
@Override
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex(
windowIndex - firstWindowIndexInChild,
repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode);
repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,
shuffleModeEnabled);
if (nextWindowIndexInChild != C.INDEX_UNSET) {
return firstWindowIndexInChild + nextWindowIndexInChild;
} else {
......@@ -53,12 +55,14 @@ import com.google.android.exoplayer2.Timeline;
}
@Override
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex(
windowIndex - firstWindowIndexInChild,
repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode);
repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,
shuffleModeEnabled);
if (previousWindowIndexInChild != C.INDEX_UNSET) {
return firstWindowIndexInChild + previousWindowIndexInChild;
} else {
......
......@@ -198,19 +198,21 @@ public final class ConcatenatingMediaSource implements MediaSource {
}
@Override
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) {
repeatMode = Player.REPEAT_MODE_ALL;
}
return super.getNextWindowIndex(windowIndex, repeatMode);
return super.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
}
@Override
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) {
repeatMode = Player.REPEAT_MODE_ALL;
}
return super.getPreviousWindowIndex(windowIndex, repeatMode);
return super.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
}
@Override
......
......@@ -35,13 +35,25 @@ public abstract class ForwardingTimeline extends Timeline {
}
@Override
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
return timeline.getNextWindowIndex(windowIndex, repeatMode);
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
return timeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
}
@Override
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
return timeline.getPreviousWindowIndex(windowIndex, repeatMode);
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
return timeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
}
@Override
public int getLastWindowIndex(boolean shuffleModeEnabled) {
return timeline.getLastWindowIndex(shuffleModeEnabled);
}
@Override
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
return timeline.getFirstWindowIndex(shuffleModeEnabled);
}
@Override
......
......@@ -167,14 +167,18 @@ public final class LoopingMediaSource implements MediaSource {
}
@Override
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode);
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode,
shuffleModeEnabled);
return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex;
}
@Override
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode);
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode,
shuffleModeEnabled);
return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1
: childPreviousWindowIndex;
}
......
/*
* Copyright (C) 2017 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 com.google.android.exoplayer2.util.ClosedSource;
/**
* Data for HLS downloading tests.
*/
@ClosedSource(reason = "Not ready yet")
/* package */ interface HlsDownloadTestData {
String MASTER_PLAYLIST_URI = "test.m3u8";
String MEDIA_PLAYLIST_0_DIR = "gear0/";
String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8";
String MEDIA_PLAYLIST_1_DIR = "gear1/";
String MEDIA_PLAYLIST_1_URI = MEDIA_PLAYLIST_1_DIR + "prog_index.m3u8";
String MEDIA_PLAYLIST_2_DIR = "gear2/";
String MEDIA_PLAYLIST_2_URI = MEDIA_PLAYLIST_2_DIR + "prog_index.m3u8";
String MEDIA_PLAYLIST_3_DIR = "gear3/";
String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8";
byte[] MASTER_PLAYLIST_DATA =
("#EXTM3U\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n"
+ MEDIA_PLAYLIST_1_URI + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n"
+ MEDIA_PLAYLIST_2_URI + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n"
+ MEDIA_PLAYLIST_3_URI + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n"
+ MEDIA_PLAYLIST_0_URI).getBytes();
byte[] MEDIA_PLAYLIST_DATA =
("#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXTINF:9.97667,\n"
+ "fileSequence0.ts\n"
+ "#EXTINF:9.97667,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:9.97667,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST").getBytes();
String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8";
byte[] ENC_MEDIA_PLAYLIST_DATA =
("#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n"
+ "#EXTINF:9.97667,\n"
+ "fileSequence0.ts\n"
+ "#EXTINF:9.97667,\n"
+ "fileSequence1.ts\n"
+ "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n"
+ "#EXTINF:9.97667,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST").getBytes();
}
/*
* Copyright (C) 2017 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 static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_DATA;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_URI;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_DATA;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_URI;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_DIR;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_URI;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_DIR;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_URI;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_2_DIR;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_2_URI;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_DIR;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_URI;
import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_DATA;
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData;
import android.net.Uri;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.ClosedSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
/** Unit tests for {@link HlsDownloader}. */
@ClosedSource(reason = "Not ready yet")
public class HlsDownloaderTest extends InstrumentationTestCase {
private SimpleCache cache;
private File tempFolder;
private FakeDataSet fakeDataSet;
private HlsDownloader hlsDownloader;
@Override
public void setUp() throws Exception {
super.setUp();
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
fakeDataSet = new FakeDataSet()
.setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA)
.setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA)
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10)
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11)
.setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12)
.setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA)
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13)
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14)
.setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15);
hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI);
}
@Override
public void tearDown() throws Exception {
Util.recursiveDelete(tempFolder);
super.tearDown();
}
public void testDownloadManifest() throws Exception {
HlsMasterPlaylist manifest = hlsDownloader.getManifest();
assertNotNull(manifest);
assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI);
}
public void testSelectRepresentationsClearsPreviousSelection() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_2_URI});
hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_2_URI,
MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts",
MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts");
}
public void testCounterMethods() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.download(null);
assertEquals(4, hlsDownloader.getTotalSegments());
assertEquals(4, hlsDownloader.getDownloadedSegments());
assertEquals(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12, hlsDownloader.getDownloadedBytes());
}
public void testInitStatus() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.download(null);
HlsDownloader newHlsDownloader =
getHlsDownloader(MASTER_PLAYLIST_URI);
newHlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
newHlsDownloader.init();
assertEquals(4, newHlsDownloader.getTotalSegments());
assertEquals(4, newHlsDownloader.getDownloadedSegments());
assertEquals(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12, newHlsDownloader.getDownloadedBytes());
}
public void testDownloadRepresentation() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_1_URI,
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
}
public void testDownloadMultipleRepresentations() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI});
hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet);
}
public void testDownloadAllRepresentations() throws Exception {
// Add data for the rest of the playlists
fakeDataSet.setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA)
.setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence0.ts", 10)
.setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence1.ts", 11)
.setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence2.ts", 12)
.setData(MEDIA_PLAYLIST_3_URI, MEDIA_PLAYLIST_DATA)
.setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence0.ts", 13)
.setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence1.ts", 14)
.setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15);
hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI);
// hlsDownloader.selectRepresentations() isn't called
hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet);
hlsDownloader.remove();
// select something random
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
// clear selection
hlsDownloader.selectRepresentations(null);
hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet);
hlsDownloader.remove();
hlsDownloader.selectRepresentations(new String[0]);
hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet);
hlsDownloader.remove();
}
public void testRemoveAll() throws Exception {
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI});
hlsDownloader.download(null);
hlsDownloader.remove();
assertCacheEmpty(cache);
}
public void testDownloadMediaPlaylist() throws Exception {
hlsDownloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI);
hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI});
hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet, MEDIA_PLAYLIST_1_URI,
MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts",
MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts");
}
public void testDownloadEncMediaPlaylist() throws Exception {
fakeDataSet = new FakeDataSet()
.setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA)
.setRandomData("enc.key", 8)
.setRandomData("enc2.key", 9)
.setRandomData("fileSequence0.ts", 10)
.setRandomData("fileSequence1.ts", 11)
.setRandomData("fileSequence2.ts", 12);
hlsDownloader =
getHlsDownloader(ENC_MEDIA_PLAYLIST_URI);
hlsDownloader.selectRepresentations(new String[] {ENC_MEDIA_PLAYLIST_URI});
hlsDownloader.download(null);
assertCachedData(cache, fakeDataSet);
}
private HlsDownloader getHlsDownloader(String mediaPlaylistUri) {
Factory factory = new Factory(null).setFakeDataSet(fakeDataSet);
return new HlsDownloader(Uri.parse(mediaPlaylistUri),
new DownloaderConstructorHelper(cache, factory));
}
}
/*
* Copyright (C) 2017 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 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.util.ClosedSource;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
/** An action to download or remove downloaded HLS streams. */
@ClosedSource(reason = "Not ready yet")
public final class HlsDownloadAction extends SegmentDownloadAction<String> {
public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer<String>() {
@Override
public String getType() {
return TYPE;
}
@Override
protected String readKey(DataInputStream input) throws IOException {
return input.readUTF();
}
@Override
protected String[] createKeyArray(int keyCount) {
return new String[0];
}
@Override
protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
String[] keys) {
return new HlsDownloadAction(manifestUri, removeAction, keys);
}
};
private static final String TYPE = "HlsDownloadAction";
/** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */
public HlsDownloadAction(Uri manifestUri, boolean removeAction, String... keys) {
super(manifestUri, removeAction, keys);
}
@Override
public String getType() {
return TYPE;
}
@Override
public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper)
throws IOException {
HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper);
if (!isRemoveAction()) {
downloader.selectRepresentations(keys);
}
return downloader;
}
@Override
protected void writeKey(DataOutputStream output, String key) throws IOException {
output.writeUTF(key);
}
}
/*
* Copyright (C) 2017 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 com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader;
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.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.ClosedSource;
import com.google.android.exoplayer2.util.UriUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* Helper class to download HLS streams.
*
* A subset of renditions can be downloaded by selecting them using {@link
* #selectRepresentations(Object[])}. As key, string form of the rendition's url is used. The urls
* can be absolute or relative to the master playlist url.
*/
@ClosedSource(reason = "Not ready yet")
public final class HlsDownloader extends SegmentDownloader<HlsMasterPlaylist, String> {
/**
* @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper)
*/
public HlsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) {
super(manifestUri, constructorHelper);
}
@Override
protected HlsMasterPlaylist getManifest(DataSource dataSource, Uri uri) throws IOException {
HlsPlaylist hlsPlaylist = loadManifest(dataSource, uri);
if (hlsPlaylist instanceof HlsMasterPlaylist) {
return (HlsMasterPlaylist) hlsPlaylist;
} else {
return HlsMasterPlaylist.createSingleVariantMasterPlaylist(hlsPlaylist.baseUri);
}
}
@Override
protected List<Segment> getAllSegments(DataSource dataSource, HlsMasterPlaylist manifest,
boolean allowIndexLoadErrors) throws InterruptedException, IOException {
ArrayList<String> urls = new ArrayList<>();
extractUrls(manifest.variants, urls);
extractUrls(manifest.audios, urls);
extractUrls(manifest.subtitles, urls);
return getSegments(dataSource, manifest, urls.toArray(new String[urls.size()]),
allowIndexLoadErrors);
}
@Override
protected List<Segment> getSegments(DataSource dataSource, HlsMasterPlaylist manifest,
String[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException {
HashSet<Uri> encryptionKeyUris = new HashSet<>();
ArrayList<Segment> segments = new ArrayList<>();
for (String playlistUrl : keys) {
HlsMediaPlaylist mediaPlaylist = null;
Uri uri = UriUtil.resolveToUri(manifest.baseUri, playlistUrl);
try {
mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, uri);
} catch (IOException e) {
if (!allowIndexLoadErrors) {
throw e;
}
}
segments.add(new Segment(mediaPlaylist != null ? mediaPlaylist.startTimeUs : Long.MIN_VALUE,
new DataSpec(uri)));
if (mediaPlaylist == null) {
continue;
}
HlsMediaPlaylist.Segment initSegment = mediaPlaylist.initializationSegment;
if (initSegment != null) {
addSegment(segments, mediaPlaylist, initSegment, encryptionKeyUris);
}
List<HlsMediaPlaylist.Segment> hlsSegments = mediaPlaylist.segments;
for (int i = 0; i < hlsSegments.size(); i++) {
addSegment(segments, mediaPlaylist, hlsSegments.get(i), encryptionKeyUris);
}
}
return segments;
}
private HlsPlaylist loadManifest(DataSource dataSource, Uri uri) throws IOException {
DataSpec dataSpec = new DataSpec(uri,
DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP);
ParsingLoadable<HlsPlaylist> loadable = new ParsingLoadable<>(dataSource, dataSpec,
C.DATA_TYPE_MANIFEST, new HlsPlaylistParser());
loadable.load();
return loadable.getResult();
}
private static void addSegment(ArrayList<Segment> segments, HlsMediaPlaylist mediaPlaylist,
HlsMediaPlaylist.Segment hlsSegment, HashSet<Uri> encryptionKeyUris)
throws IOException, InterruptedException {
long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs;
if (hlsSegment.isEncrypted) {
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.encryptionKeyUri);
if (encryptionKeyUris.add(keyUri)) {
segments.add(new Segment(startTimeUs, new DataSpec(keyUri)));
}
}
Uri resolvedUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.url);
segments.add(new Segment(startTimeUs,
new DataSpec(resolvedUri, hlsSegment.byterangeOffset, hlsSegment.byterangeLength, null)));
}
private static void extractUrls(List<HlsUrl> hlsUrls, ArrayList<String> urls) {
for (int i = 0; i < hlsUrls.size(); i++) {
urls.add(hlsUrls.get(i).url);
}
}
}
/*
* Copyright (C) 2017 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 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.util.ClosedSource;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
/** An action to download or remove downloaded SmoothStreaming streams. */
@ClosedSource(reason = "Not ready yet")
public final class SsDownloadAction extends SegmentDownloadAction<TrackKey> {
public static final Deserializer DESERIALIZER =
new SegmentDownloadActionDeserializer<TrackKey>() {
@Override
public String getType() {
return TYPE;
}
@Override
protected TrackKey readKey(DataInputStream input) throws IOException {
return new TrackKey(input.readInt(), input.readInt());
}
@Override
protected TrackKey[] createKeyArray(int keyCount) {
return new TrackKey[keyCount];
}
@Override
protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction,
TrackKey[] keys) {
return new SsDownloadAction(manifestUri, removeAction, keys);
}
};
private static final String TYPE = "SsDownloadAction";
/** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */
public SsDownloadAction(Uri manifestUri, boolean removeAction, TrackKey... keys) {
super(manifestUri, removeAction, keys);
}
@Override
public String getType() {
return TYPE;
}
@Override
public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper)
throws IOException {
SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper);
if (!isRemoveAction()) {
downloader.selectRepresentations(keys);
}
return downloader;
}
@Override
protected void writeKey(DataOutputStream output, TrackKey key) throws IOException {
output.writeInt(key.streamElementIndex);
output.writeInt(key.trackIndex);
}
}
/*
* Copyright (C) 2017 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 com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader;
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.TrackKey;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.ClosedSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to download SmoothStreaming streams.
*
* <p>Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link
* #getDownloadedBytes()}, this class isn't thread safe.
*
* <p>Example usage:
*
* <pre>
* {@code
* SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
* DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
* DownloaderConstructorHelper constructorHelper =
* new DownloaderConstructorHelper(cache, factory);
* SsDownloader ssDownloader = new SsDownloader(manifestUrl, constructorHelper);
* // Select the first track of the first stream element
* ssDownloader.selectRepresentations(new TrackKey[] {new TrackKey(0, 0)});
* ssDownloader.download(new ProgressListener() {
* @Override
* public void onDownloadProgress(Downloader downloader, float downloadPercentage,
* long downloadedBytes) {
* // Invoked periodically during the download.
* }
* });
* // Access downloaded data using CacheDataSource
* CacheDataSource cacheDataSource =
* new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
* </pre>
*/
@ClosedSource(reason = "Not ready yet")
public final class SsDownloader extends SegmentDownloader<SsManifest, TrackKey> {
/**
* @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper)
*/
public SsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) {
super(manifestUri, constructorHelper);
}
@Override
public SsManifest getManifest(DataSource dataSource, Uri uri) throws IOException {
DataSpec dataSpec = new DataSpec(uri,
DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP);
ParsingLoadable<SsManifest> loadable = new ParsingLoadable<>(dataSource, dataSpec,
C.DATA_TYPE_MANIFEST, new SsManifestParser());
loadable.load();
return loadable.getResult();
}
@Override
protected List<Segment> getAllSegments(DataSource dataSource, SsManifest manifest,
boolean allowIndexLoadErrors) throws InterruptedException, IOException {
ArrayList<Segment> segments = new ArrayList<>();
for (int i = 0; i < manifest.streamElements.length; i++) {
StreamElement streamElement = manifest.streamElements[i];
for (int j = 0; j < streamElement.formats.length; j++) {
segments.addAll(getSegments(dataSource, manifest, new TrackKey[] {new TrackKey(i, j)},
allowIndexLoadErrors));
}
}
return segments;
}
@Override
protected List<Segment> getSegments(DataSource dataSource, SsManifest manifest,
TrackKey[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException {
ArrayList<Segment> segments = new ArrayList<>();
for (TrackKey key : keys) {
StreamElement streamElement = manifest.streamElements[key.streamElementIndex];
for (int i = 0; i < streamElement.chunkCount; i++) {
segments.add(new Segment(streamElement.getStartTimeUs(i),
new DataSpec(streamElement.buildRequestUri(key.trackIndex, i))));
}
}
return segments;
}
}
......@@ -675,9 +675,11 @@ public class PlaybackControlView extends FrameLayout {
timeline.getWindow(windowIndex, window);
isSeekable = window.isSeekable;
enablePrevious = isSeekable || !window.isDynamic
|| timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET;
|| timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), false)
!= C.INDEX_UNSET;
enableNext = window.isDynamic
|| timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET;
|| timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false)
!= C.INDEX_UNSET;
if (player.isPlayingAd()) {
// Always hide player controls during ads.
hide();
......@@ -861,7 +863,8 @@ public class PlaybackControlView extends FrameLayout {
}
int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode());
int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(),
false);
if (previousWindowIndex != C.INDEX_UNSET
&& (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| (window.isDynamic && !window.isSeekable))) {
......@@ -877,7 +880,7 @@ public class PlaybackControlView extends FrameLayout {
return;
}
int windowIndex = player.getCurrentWindowIndex();
int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode());
int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false);
if (nextWindowIndex != C.INDEX_UNSET) {
seekTo(nextWindowIndex, C.TIME_UNSET);
} else if (timeline.getWindow(windowIndex, window, false).isDynamic) {
......
/*
* Copyright (C) 2017 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.playbacktests.gts;
import android.net.Uri;
import android.test.ActivityInstrumentationTestCase2;
import com.google.android.exoplayer2.offline.Downloader;
import com.google.android.exoplayer2.offline.Downloader.ProgressListener;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
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.Representation;
import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey;
import com.google.android.exoplayer2.source.dash.offline.DashDownloader;
import com.google.android.exoplayer2.testutil.HostActivity;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.ClosedSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.List;
/**
* Tests downloaded DASH playbacks.
*/
@ClosedSource(reason = "Not ready yet")
public final class DashDownloadTest extends ActivityInstrumentationTestCase2<HostActivity> {
private static final String TAG = "DashDownloadTest";
private DashTestRunner testRunner;
private File tempFolder;
private SimpleCache cache;
public DashDownloadTest() {
super(HostActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation())
.setManifestUrl(DashTestData.H264_MANIFEST)
.setFullPlaybackNoSeeking(true)
.setCanIncludeAdditionalVideoFormats(false)
.setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID,
DashTestData.H264_CDD_FIXED);
tempFolder = Util.createTempDirectory(getActivity(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
}
@Override
protected void tearDown() throws Exception {
testRunner = null;
Util.recursiveDelete(tempFolder);
cache = null;
super.tearDown();
}
// Download tests
public void testDownload() throws Exception {
if (Util.SDK_INT < 16) {
return; // Pass.
}
// Download manifest only
createDashDownloader(false).getManifest();
long manifestLength = cache.getCacheSpace();
// Download representations
DashDownloader dashDownloader = downloadContent(false, Float.NaN);
assertEquals(cache.getCacheSpace() - manifestLength, dashDownloader.getDownloadedBytes());
testRunner.setStreamName("test_h264_fixed_download").
setDataSourceFactory(newOfflineCacheDataSourceFactory()).run();
dashDownloader.remove();
assertEquals("There should be no content left.", 0, cache.getKeys().size());
assertEquals("There should be no content left.", 0, cache.getCacheSpace());
}
public void testPartialDownload() throws Exception {
if (Util.SDK_INT < 16) {
return; // Pass.
}
// Just download the first half and manifest
downloadContent(false, 0.5f);
// Download the rest
DashDownloader dashDownloader = downloadContent(false, Float.NaN);
long downloadedBytes = dashDownloader.getDownloadedBytes();
// Make sure it doesn't download any data
dashDownloader = downloadContent(true, Float.NaN);
assertEquals(downloadedBytes, dashDownloader.getDownloadedBytes());
testRunner.setStreamName("test_h264_fixed_partial_download")
.setDataSourceFactory(newOfflineCacheDataSourceFactory()).run();
}
private DashDownloader downloadContent(boolean offline, float stopAt) throws Exception {
DashDownloader dashDownloader = createDashDownloader(offline);
DashManifest dashManifest = dashDownloader.getManifest();
try {
ArrayList<RepresentationKey> keys = new ArrayList<>();
for (int pIndex = 0; pIndex < dashManifest.getPeriodCount(); pIndex++) {
List<AdaptationSet> adaptationSets = dashManifest.getPeriod(pIndex).adaptationSets;
for (int aIndex = 0; aIndex < adaptationSets.size(); aIndex++) {
AdaptationSet adaptationSet = adaptationSets.get(aIndex);
List<Representation> representations = adaptationSet.representations;
for (int rIndex = 0; rIndex < representations.size(); rIndex++) {
String id = representations.get(rIndex).format.id;
if (DashTestData.AAC_AUDIO_REPRESENTATION_ID.equals(id)
|| DashTestData.H264_CDD_FIXED.equals(id)) {
keys.add(new RepresentationKey(pIndex, aIndex, rIndex));
}
}
}
dashDownloader.selectRepresentations(keys.toArray(new RepresentationKey[keys.size()]));
TestProgressListener listener = new TestProgressListener(stopAt);
dashDownloader.download(listener);
}
} catch (InterruptedException e) {
// do nothing
} catch (IOException e) {
Throwable exception = e;
while (!(exception instanceof InterruptedIOException)) {
if (exception == null) {
throw e;
}
exception = exception.getCause();
}
// else do nothing
}
return dashDownloader;
}
private DashDownloader createDashDownloader(boolean offline) {
DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper(cache,
offline ? DummyDataSource.FACTORY : new DefaultHttpDataSourceFactory("ExoPlayer", null));
return new DashDownloader(Uri.parse(DashTestData.H264_MANIFEST), constructorHelper);
}
private CacheDataSourceFactory newOfflineCacheDataSourceFactory() {
return new CacheDataSourceFactory(cache, DummyDataSource.FACTORY,
CacheDataSource.FLAG_BLOCK_ON_CACHE);
}
private static class TestProgressListener implements ProgressListener {
private float stopAt;
private TestProgressListener(float stopAt) {
this.stopAt = stopAt;
}
@Override
public void onDownloadProgress(Downloader downloader, float downloadPercentage,
long downloadedBytes) {
System.out.printf("onDownloadProgress downloadPercentage = [%g], downloadedData = [%d]%n",
downloadPercentage, downloadedBytes);
if (downloadPercentage >= stopAt) {
Thread.currentThread().interrupt();
}
}
}
}
......@@ -74,7 +74,7 @@ public final class TimelineAsserts {
@Player.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) {
for (int i = 0; i < timeline.getWindowCount(); i++) {
assertEquals(expectedPreviousWindowIndices[i],
timeline.getPreviousWindowIndex(i, repeatMode));
timeline.getPreviousWindowIndex(i, repeatMode, false));
}
}
......@@ -86,14 +86,14 @@ public final class TimelineAsserts {
int... expectedNextWindowIndices) {
for (int i = 0; i < timeline.getWindowCount(); i++) {
assertEquals(expectedNextWindowIndices[i],
timeline.getNextWindowIndex(i, repeatMode));
timeline.getNextWindowIndex(i, repeatMode, false));
}
}
/**
* Asserts that period counts for each window are set correctly. Also asserts that
* {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it
* asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int)}.
* asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}.
*/
public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) {
int windowCount = timeline.getWindowCount();
......@@ -119,16 +119,19 @@ public final class TimelineAsserts {
}
assertEquals(expectedWindowIndex, period.windowIndex);
if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) {
assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF));
assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE));
assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL));
assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF,
false));
assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE,
false));
assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL,
false));
} else {
int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex,
Player.REPEAT_MODE_OFF);
Player.REPEAT_MODE_OFF, false);
int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex,
Player.REPEAT_MODE_ONE);
Player.REPEAT_MODE_ONE, false);
int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex,
Player.REPEAT_MODE_ALL);
Player.REPEAT_MODE_ALL, false);
int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET
: accumulatedPeriodCounts[nextWindowOff];
int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET
......@@ -136,11 +139,11 @@ public final class TimelineAsserts {
int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET
: accumulatedPeriodCounts[nextWindowAll];
assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window,
Player.REPEAT_MODE_OFF));
Player.REPEAT_MODE_OFF, false));
assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window,
Player.REPEAT_MODE_ONE));
Player.REPEAT_MODE_ONE, false));
assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window,
Player.REPEAT_MODE_ALL));
Player.REPEAT_MODE_ALL, false));
}
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册