Merge pull request #1935 from google/dev-v2

Update dev-v2-id3 with dev-v2
This commit is contained in:
ojw28 2016-10-12 17:05:09 +01:00 committed by GitHub
commit f4b8d9490a
18 changed files with 534 additions and 462 deletions

View File

@ -38,15 +38,15 @@ android {
} }
productFlavors { productFlavors {
demo noExtensions
demoExt withExtensions
} }
} }
dependencies { dependencies {
compile project(':library') compile project(':library')
demoExtCompile project(path: ':extension-ffmpeg') withExtensionsCompile project(path: ':extension-ffmpeg')
demoExtCompile project(path: ':extension-flac') withExtensionsCompile project(path: ':extension-flac')
demoExtCompile project(path: ':extension-opus') withExtensionsCompile project(path: ':extension-opus')
demoExtCompile project(path: ':extension-vp9') withExtensionsCompile project(path: ':extension-vp9')
} }

View File

@ -98,6 +98,9 @@ import java.util.Locale;
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
if (timeline == null) {
return;
}
int periodCount = timeline.getPeriodCount(); int periodCount = timeline.getPeriodCount();
int windowCount = timeline.getWindowCount(); int windowCount = timeline.getWindowCount();
Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);

View File

@ -101,6 +101,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
private Handler mainHandler; private Handler mainHandler;
private Timeline.Window window;
private EventLogger eventLogger; private EventLogger eventLogger;
private SimpleExoPlayerView simpleExoPlayerView; private SimpleExoPlayerView simpleExoPlayerView;
private LinearLayout debugRootView; private LinearLayout debugRootView;
@ -115,7 +116,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private boolean playerNeedsSource; private boolean playerNeedsSource;
private boolean shouldAutoPlay; private boolean shouldAutoPlay;
private boolean shouldRestorePosition; private boolean isTimelineStatic;
private int playerWindow; private int playerWindow;
private long playerPosition; private long playerPosition;
@ -127,6 +128,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
shouldAutoPlay = true; shouldAutoPlay = true;
mediaDataSourceFactory = buildDataSourceFactory(true); mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler(); mainHandler = new Handler();
window = new Timeline.Window();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
} }
@ -147,7 +149,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
releasePlayer(); releasePlayer();
shouldRestorePosition = false; isTimelineStatic = false;
setIntent(intent); setIntent(intent);
} }
@ -262,7 +264,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.setVideoDebugListener(eventLogger); player.setVideoDebugListener(eventLogger);
player.setId3Output(eventLogger); player.setId3Output(eventLogger);
simpleExoPlayerView.setPlayer(player); simpleExoPlayerView.setPlayer(player);
if (shouldRestorePosition) { if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) { if (playerPosition == C.TIME_UNSET) {
player.seekToDefaultPosition(playerWindow); player.seekToDefaultPosition(playerWindow);
} else { } else {
@ -305,7 +307,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources); : new ConcatenatingMediaSource(mediaSources);
player.prepare(mediaSource, !shouldRestorePosition); player.prepare(mediaSource, !isTimelineStatic, !isTimelineStatic);
playerNeedsSource = false; playerNeedsSource = false;
updateButtonVisibilities(); updateButtonVisibilities();
} }
@ -348,15 +350,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
debugViewHelper.stop(); debugViewHelper.stop();
debugViewHelper = null; debugViewHelper = null;
shouldAutoPlay = player.getPlayWhenReady(); shouldAutoPlay = player.getPlayWhenReady();
shouldRestorePosition = false; playerWindow = player.getCurrentWindowIndex();
playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline(); Timeline timeline = player.getCurrentTimeline();
if (timeline != null) { if (timeline != null && timeline.getWindow(playerWindow, window).isSeekable) {
playerWindow = player.getCurrentWindowIndex(); playerPosition = player.getCurrentPosition();
Timeline.Window window = timeline.getWindow(playerWindow, new Timeline.Window());
if (!window.isDynamic) {
shouldRestorePosition = true;
playerPosition = window.isSeekable ? player.getCurrentPosition() : C.TIME_UNSET;
}
} }
player.release(); player.release();
player = null; player = null;
@ -412,7 +410,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing. isTimelineStatic = timeline != null && timeline.getWindowCount() > 0
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
} }
@Override @Override
@ -501,7 +500,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
button.setText(label); button.setText(label);
button.setTag(i); button.setTag(i);
button.setOnClickListener(this); button.setOnClickListener(this);
debugRootView.addView(button); debugRootView.addView(button, debugRootView.getChildCount() - 1);
} }
} }
} }

View File

@ -174,10 +174,7 @@ public final class CronetDataSourceTest {
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void testOpeningTwiceThrows() throws HttpDataSourceException { public void testOpeningTwiceThrows() throws HttpDataSourceException {
mockResponseStartSuccess(); mockResponseStartSuccess();
assertConnectionState(CronetDataSource.IDLE_CONNECTION);
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
} }
@ -205,7 +202,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.onFailed( dataSourceUnderTest.onFailed(
mockUrlRequest, mockUrlRequest,
testUrlResponseInfo, testUrlResponseInfo,
null); mockUrlRequestException);
dataSourceUnderTest.onResponseStarted( dataSourceUnderTest.onResponseStarted(
mockUrlRequest2, mockUrlRequest2,
testUrlResponseInfo); testUrlResponseInfo);
@ -253,13 +250,10 @@ public final class CronetDataSourceTest {
@Test @Test
public void testRequestOpen() throws HttpDataSourceException { public void testRequestOpen() throws HttpDataSourceException {
mockResponseStartSuccess(); mockResponseStartSuccess();
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testDataSpec)); assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testDataSpec));
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
} }
@Test @Test
public void testRequestOpenGzippedCompressedReturnsDataSpecLength() public void testRequestOpenGzippedCompressedReturnsDataSpecLength()
throws HttpDataSourceException { throws HttpDataSourceException {
@ -271,7 +265,6 @@ public final class CronetDataSourceTest {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
assertEquals(5000 /* contentLength */, dataSourceUnderTest.open(testDataSpec)); assertEquals(5000 /* contentLength */, dataSourceUnderTest.open(testDataSpec));
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
} }
@ -286,7 +279,6 @@ public final class CronetDataSourceTest {
// Check for connection not automatically closed. // Check for connection not automatically closed.
assertFalse(e.getCause() instanceof UnknownHostException); assertFalse(e.getCause() instanceof UnknownHostException);
verify(mockUrlRequest, never()).cancel(); verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
} }
} }
@ -304,7 +296,6 @@ public final class CronetDataSourceTest {
// Check for connection not automatically closed. // Check for connection not automatically closed.
assertTrue(e.getCause() instanceof UnknownHostException); assertTrue(e.getCause() instanceof UnknownHostException);
verify(mockUrlRequest, never()).cancel(); verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
} }
} }
@ -321,7 +312,6 @@ public final class CronetDataSourceTest {
assertTrue(e instanceof HttpDataSource.InvalidResponseCodeException); assertTrue(e instanceof HttpDataSource.InvalidResponseCodeException);
// Check for connection not automatically closed. // Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel(); verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
} }
} }
@ -338,37 +328,16 @@ public final class CronetDataSourceTest {
assertTrue(e instanceof HttpDataSource.InvalidContentTypeException); assertTrue(e instanceof HttpDataSource.InvalidContentTypeException);
// Check for connection not automatically closed. // Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel(); verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE); verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
} }
} }
@Test
public void testRequestOpenValidatesContentLength() {
mockResponseStartSuccess();
// Data spec's requested length, 5000. Test response's length, 16,000.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
try {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
verify(mockUrlRequest).addHeader("Range", "bytes=1000-5999");
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPENING_CONNECTION);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testPostDataSpec);
}
}
@Test @Test
public void testPostRequestOpen() throws HttpDataSourceException { public void testPostRequestOpen() throws HttpDataSourceException {
mockResponseStartSuccess(); mockResponseStartSuccess();
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testPostDataSpec)); assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testPostDataSpec));
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec); verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
} }
@ -510,7 +479,6 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.close(); dataSourceUnderTest.close();
verify(mockTransferListener).onTransferEnd(dataSourceUnderTest); verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
assertConnectionState(CronetDataSource.IDLE_CONNECTION);
try { try {
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8); bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
@ -572,7 +540,6 @@ public final class CronetDataSourceTest {
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class)); verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
// Check for connection not automatically closed. // Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel(); verify(mockUrlRequest, never()).cancel();
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
assertEquals(16, bytesRead); assertEquals(16, bytesRead);
} }
@ -603,15 +570,12 @@ public final class CronetDataSourceTest {
// We should still be trying to open. // We should still be trying to open.
assertFalse(timedOutCondition.block(50)); assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// We should still be trying to open as we approach the timeout. // We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(timedOutCondition.block(50)); assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// Now we timeout. // Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS); when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS);
timedOutCondition.block(); timedOutCondition.block();
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
} }
@ -637,15 +601,12 @@ public final class CronetDataSourceTest {
// We should still be trying to open. // We should still be trying to open.
assertFalse(openCondition.block(50)); assertFalse(openCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// We should still be trying to open as we approach the timeout. // We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(openCondition.block(50)); assertFalse(openCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// The response arrives just in time. // The response arrives just in time.
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
openCondition.block(); openCondition.block();
assertEquals(CronetDataSource.OPEN_CONNECTION, dataSourceUnderTest.connectionState);
} }
@Test @Test
@ -674,11 +635,9 @@ public final class CronetDataSourceTest {
// We should still be trying to open. // We should still be trying to open.
assertFalse(timedOutCondition.block(50)); assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// We should still be trying to open as we approach the timeout. // We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(timedOutCondition.block(50)); assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// A redirect arrives just in time. // A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo, dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
"RandomRedirectedUrl1"); "RandomRedirectedUrl1");
@ -689,7 +648,6 @@ public final class CronetDataSourceTest {
assertFalse(timedOutCondition.block(newTimeoutMs)); assertFalse(timedOutCondition.block(newTimeoutMs));
// We should still be trying to open as we approach the new timeout. // We should still be trying to open as we approach the new timeout.
assertFalse(timedOutCondition.block(50)); assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// A redirect arrives just in time. // A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo, dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
"RandomRedirectedUrl2"); "RandomRedirectedUrl2");
@ -700,11 +658,9 @@ public final class CronetDataSourceTest {
assertFalse(timedOutCondition.block(newTimeoutMs)); assertFalse(timedOutCondition.block(newTimeoutMs));
// We should still be trying to open as we approach the new timeout. // We should still be trying to open as we approach the new timeout.
assertFalse(timedOutCondition.block(50)); assertFalse(timedOutCondition.block(50));
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
// Now we timeout. // Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs); when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs);
timedOutCondition.block(); timedOutCondition.block();
assertEquals(CronetDataSource.OPENING_CONNECTION, dataSourceUnderTest.connectionState);
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
assertEquals(1, openExceptions.get()); assertEquals(1, openExceptions.get());
@ -818,7 +774,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.onFailed( dataSourceUnderTest.onFailed(
mockUrlRequest, mockUrlRequest,
createUrlResponseInfo(500), // statusCode createUrlResponseInfo(500), // statusCode
null); mockUrlRequestException);
return null; return null;
} }
}).when(mockUrlRequest).read(any(ByteBuffer.class)); }).when(mockUrlRequest).read(any(ByteBuffer.class));
@ -869,8 +825,4 @@ public final class CronetDataSourceTest {
return testBuffer; return testBuffer;
} }
private void assertConnectionState(int state) {
assertEquals(state, dataSourceUnderTest.connectionState);
}
} }

View File

@ -37,12 +37,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.chromium.net.CronetEngine; import org.chromium.net.CronetEngine;
import org.chromium.net.UrlRequest; import org.chromium.net.UrlRequest;
import org.chromium.net.UrlRequest.Status;
import org.chromium.net.UrlRequestException; import org.chromium.net.UrlRequestException;
import org.chromium.net.UrlResponseInfo; import org.chromium.net.UrlResponseInfo;
@ -85,16 +84,12 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;
private static final String TAG = "CronetDataSource"; private static final String TAG = "CronetDataSource";
private static final String CONTENT_TYPE = "Content-Type";
private static final Pattern CONTENT_RANGE_HEADER_PATTERN = private static final Pattern CONTENT_RANGE_HEADER_PATTERN =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
// The size of read buffer passed to cronet UrlRequest.read(). // The size of read buffer passed to cronet UrlRequest.read().
private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024; private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024;
/* package */ static final int IDLE_CONNECTION = 5;
/* package */ static final int OPENING_CONNECTION = 2;
/* package */ static final int CONNECTED_CONNECTION = 3;
/* package */ static final int OPEN_CONNECTION = 4;
private final CronetEngine cronetEngine; private final CronetEngine cronetEngine;
private final Executor executor; private final Executor executor;
private final Predicate<String> contentTypePredicate; private final Predicate<String> contentTypePredicate;
@ -104,21 +99,29 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
private final boolean resetTimeoutOnRedirects; private final boolean resetTimeoutOnRedirects;
private final Map<String, String> requestProperties; private final Map<String, String> requestProperties;
private final ConditionVariable operation; private final ConditionVariable operation;
private final ByteBuffer readBuffer;
private final Clock clock; private final Clock clock;
// Accessed by the calling thread only.
private boolean opened;
private long bytesRemaining;
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
// to reads made by the Cronet thread.
private UrlRequest currentUrlRequest; private UrlRequest currentUrlRequest;
private DataSpec currentDataSpec; private DataSpec currentDataSpec;
private UrlResponseInfo responseInfo;
/* package */ volatile int connectionState; // Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
private volatile String currentUrl; // operation.open() calls ensure writes into the buffer are visible to reads made by the calling
// thread.
private ByteBuffer readBuffer;
// Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
// made by the calling thread.
private UrlResponseInfo responseInfo;
private IOException exception;
private boolean finished;
private volatile long currentConnectTimeoutMs; private volatile long currentConnectTimeoutMs;
private volatile HttpDataSourceException exception;
private volatile long contentLength;
private volatile AtomicLong expectedBytesRemainingToRead;
private volatile boolean hasData;
private volatile boolean responseFinished;
/** /**
* @param cronetEngine A CronetEngine. * @param cronetEngine A CronetEngine.
@ -163,12 +166,12 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
this.readTimeoutMs = readTimeoutMs; this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
this.clock = Assertions.checkNotNull(clock); this.clock = Assertions.checkNotNull(clock);
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
requestProperties = new HashMap<>(); requestProperties = new HashMap<>();
operation = new ConditionVariable(); operation = new ConditionVariable();
connectionState = IDLE_CONNECTION;
} }
// HttpDataSource implementation.
@Override @Override
public void setRequestProperty(String name, String value) { public void setRequestProperty(String name, String value) {
synchronized (requestProperties) { synchronized (requestProperties) {
@ -195,255 +198,117 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
return responseInfo == null ? null : responseInfo.getAllHeaders(); return responseInfo == null ? null : responseInfo.getAllHeaders();
} }
@Override
public Uri getUri() {
return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
}
@Override @Override
public long open(DataSpec dataSpec) throws HttpDataSourceException { public long open(DataSpec dataSpec) throws HttpDataSourceException {
Assertions.checkNotNull(dataSpec); Assertions.checkNotNull(dataSpec);
synchronized (this) { Assertions.checkState(!opened);
Assertions.checkState(connectionState == IDLE_CONNECTION, "Connection already open");
connectionState = OPENING_CONNECTION;
}
operation.close(); operation.close();
resetConnectTimeout(); resetConnectTimeout();
startRequest(dataSpec); currentDataSpec = dataSpec;
currentUrlRequest = buildRequest(dataSpec);
currentUrlRequest.start();
boolean requestStarted = blockUntilConnectTimeout(); boolean requestStarted = blockUntilConnectTimeout();
if (exception != null) { if (exception != null) {
// An error occurred opening the connection. throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
throw exception;
} else if (!requestStarted) { } else if (!requestStarted) {
// The timeout was reached before the connection was opened. // The timeout was reached before the connection was opened.
throw new OpenException(new SocketTimeoutException(), dataSpec, getCurrentRequestStatus()); throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
} }
// Connection was opened.
if (listener != null) {
listener.onTransferStart(this, dataSpec);
}
connectionState = OPEN_CONNECTION;
return contentLength;
}
private void startRequest(DataSpec dataSpec) throws HttpDataSourceException {
currentUrl = dataSpec.uri.toString();
currentDataSpec = dataSpec;
UrlRequest.Builder urlRequestBuilder = new UrlRequest.Builder(currentUrl, this, executor,
cronetEngine);
fillCurrentRequestHeader(urlRequestBuilder);
fillCurrentRequestPostBody(urlRequestBuilder, dataSpec);
currentUrlRequest = urlRequestBuilder.build();
currentUrlRequest.start();
}
private void fillCurrentRequestHeader(UrlRequest.Builder urlRequestBuilder) {
synchronized (requestProperties) {
for (Entry<String, String> headerEntry : requestProperties.entrySet()) {
urlRequestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
}
}
if (currentDataSpec.position == 0 && currentDataSpec.length == C.LENGTH_UNSET) {
// Not required.
return;
}
StringBuilder rangeValue = new StringBuilder();
rangeValue.append("bytes=");
rangeValue.append(currentDataSpec.position);
rangeValue.append("-");
if (currentDataSpec.length != C.LENGTH_UNSET) {
rangeValue.append(currentDataSpec.position + currentDataSpec.length - 1);
}
urlRequestBuilder.addHeader("Range", rangeValue.toString());
}
private void fillCurrentRequestPostBody(UrlRequest.Builder urlRequestBuilder, DataSpec dataSpec)
throws HttpDataSourceException {
if (dataSpec.postBody != null) {
if (!requestProperties.containsKey("Content-Type")) {
throw new OpenException("POST requests must set a Content-Type header", dataSpec,
getCurrentRequestStatus());
}
urlRequestBuilder.setUploadDataProvider(
new ByteArrayUploadDataProvider(dataSpec.postBody), executor);
}
}
@Override
public synchronized void onFailed(UrlRequest request, UrlResponseInfo info,
UrlRequestException error) {
if (request != currentUrlRequest) {
return;
}
if (connectionState == OPENING_CONNECTION) {
IOException cause = error.getErrorCode() == UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED
? new UnknownHostException() : error;
exception = new OpenException(cause, currentDataSpec, getCurrentRequestStatus());
} else if (connectionState == OPEN_CONNECTION) {
exception = new HttpDataSourceException(error, currentDataSpec,
HttpDataSourceException.TYPE_READ);
}
operation.open();
}
@Override
public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
if (request != currentUrlRequest) {
return;
}
try {
validateResponse(info);
responseInfo = info;
if (isCompressed(info)) {
contentLength = currentDataSpec.length;
} else {
// Check content length.
contentLength = getContentLength(info.getAllHeaders());
// If a specific length is requested and a specific length is returned but the 2 don't match
// it's an error.
if (currentDataSpec.length != C.LENGTH_UNSET
&& contentLength != C.LENGTH_UNSET
&& currentDataSpec.length != contentLength) {
throw new OpenException("Content length did not match requested length", currentDataSpec,
getCurrentRequestStatus());
}
}
if (contentLength > 0) {
expectedBytesRemainingToRead = new AtomicLong(contentLength);
}
// Keep track of redirects.
currentUrl = responseInfo.getUrl();
connectionState = CONNECTED_CONNECTION;
} catch (HttpDataSourceException e) {
exception = e;
} finally {
operation.open();
}
}
/**
* Returns {@code true} iff the content is compressed.
*
* <p>If {@code true}, clients cannot use the value of content length from the request headers to
* read the data, since Cronet returns the uncompressed data and this content length reflects the
* compressed content length.
*/
private boolean isCompressed(UrlResponseInfo info) {
for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
return !entry.getValue().equalsIgnoreCase("identity");
}
}
return false;
}
private void validateResponse(UrlResponseInfo info) throws HttpDataSourceException {
// Check for a valid response code. // Check for a valid response code.
int responseCode = info.getHttpStatusCode(); int responseCode = responseInfo.getHttpStatusCode();
if (responseCode < 200 || responseCode > 299) { if (responseCode < 200 || responseCode > 299) {
InvalidResponseCodeException exception = new InvalidResponseCodeException( InvalidResponseCodeException exception = new InvalidResponseCodeException(responseCode,
responseCode, info.getAllHeaders(), currentDataSpec); responseInfo.getAllHeaders(), currentDataSpec);
if (responseCode == 416) { if (responseCode == 416) {
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE)); exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
} }
throw exception; throw exception;
} }
// Check for a valid content type. // Check for a valid content type.
try { if (contentTypePredicate != null) {
String contentType = info.getAllHeaders().get("Content-Type").get(0); List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) { String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
if (!contentTypePredicate.evaluate(contentType)) {
throw new InvalidContentTypeException(contentType, currentDataSpec); throw new InvalidContentTypeException(contentType, currentDataSpec);
} }
} catch (IndexOutOfBoundsException e) {
throw new InvalidContentTypeException(null, currentDataSpec);
} }
}
private long getContentLength(Map<String, List<String>> headers) { // TODO: Handle the case where we requested a range starting from a non-zero position and
// Logic copied from {@code DefaultHttpDataSource} // received a 200 rather than a 206. This occurs if the server does not support partial
long contentLength = C.LENGTH_UNSET; // requests, and requires that the source skips to the requested position.
List<String> contentLengthHeader = headers.get("Content-Length");
if (contentLengthHeader != null // Calculate the content length.
&& !contentLengthHeader.isEmpty() if (!getIsCompressed(responseInfo)) {
&& !TextUtils.isEmpty(contentLengthHeader.get(0))) { if (dataSpec.length != C.LENGTH_UNSET) {
try { bytesRemaining = dataSpec.length;
contentLength = Long.parseLong(contentLengthHeader.get(0)); } else {
} catch (NumberFormatException e) { bytesRemaining = getContentLength(responseInfo);
log(Log.ERROR, "Unexpected Content-Length [" + contentLengthHeader + "]");
} }
} else {
// If the response is compressed then the content length will be that of the compressed data
// which isn't what we want. Always use the dataSpec length in this case.
bytesRemaining = currentDataSpec.length;
} }
List<String> contentRangeHeader = headers.get("Content-Range");
if (contentRangeHeader != null opened = true;
&& !contentRangeHeader.isEmpty() if (listener != null) {
&& !TextUtils.isEmpty(contentRangeHeader.get(0))) { listener.onTransferStart(this, dataSpec);
Matcher matcher = CONTENT_RANGE_HEADER_PATTERN.matcher(contentRangeHeader.get(0));
if (matcher.find()) {
try {
long contentLengthFromRange =
Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
if (contentLength < 0) {
// Some proxy servers strip the Content-Length header. Fall back to the length
// calculated here in this case.
contentLength = contentLengthFromRange;
} else if (contentLength != contentLengthFromRange) {
// If there is a discrepancy between the Content-Length and Content-Range headers,
// assume the one with the larger value is correct. We have seen cases where carrier
// change one of them to reduce the size of a request, but it is unlikely anybody
// would increase it.
log(Log.WARN, "Inconsistent headers [" + contentLengthHeader + "] ["
+ contentRangeHeader + "]");
contentLength = Math.max(contentLength, contentLengthFromRange);
}
} catch (NumberFormatException e) {
log(Log.ERROR, "Unexpected Content-Range [" + contentRangeHeader + "]");
}
}
} }
return contentLength;
return bytesRemaining;
} }
@Override @Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
synchronized (this) { Assertions.checkState(opened);
Assertions.checkState(connectionState == OPEN_CONNECTION);
}
if (readLength == 0) { if (readLength == 0) {
return 0; return 0;
} } else if (bytesRemaining == 0) {
if (expectedBytesRemainingToRead != null && expectedBytesRemainingToRead.get() == 0) {
return C.RESULT_END_OF_INPUT; return C.RESULT_END_OF_INPUT;
} }
if (!hasData) { if (readBuffer == null) {
// Read more data from cronet. readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
readBuffer.limit(0);
}
if (!readBuffer.hasRemaining()) {
// Fill readBuffer with more data from Cronet.
operation.close(); operation.close();
readBuffer.clear(); readBuffer.clear();
currentUrlRequest.read(readBuffer); currentUrlRequest.read(readBuffer);
if (!operation.block(readTimeoutMs)) { if (!operation.block(readTimeoutMs)) {
// We're timing out, but since the operation is still ongoing we'll need to replace
// readBuffer to avoid the possibility of it being written to by this operation during a
// subsequent request.
readBuffer = null;
throw new HttpDataSourceException( throw new HttpDataSourceException(
new SocketTimeoutException(), currentDataSpec, HttpDataSourceException.TYPE_READ); new SocketTimeoutException(), currentDataSpec, HttpDataSourceException.TYPE_READ);
} } else if (exception != null) {
if (exception != null) { throw new HttpDataSourceException(exception, currentDataSpec,
throw exception; HttpDataSourceException.TYPE_READ);
} } else if (finished) {
// The expected response length is unknown, but cronet has indicated that the request
// already finished successfully.
if (responseFinished) {
return C.RESULT_END_OF_INPUT; return C.RESULT_END_OF_INPUT;
} else {
// The operation didn't time out, fail or finish, and therefore data must have been read.
readBuffer.flip();
} }
} }
int bytesRead = Math.min(readBuffer.remaining(), readLength); int bytesRead = Math.min(readBuffer.remaining(), readLength);
readBuffer.get(buffer, offset, bytesRead); readBuffer.get(buffer, offset, bytesRead);
if (!readBuffer.hasRemaining()) {
hasData = false;
}
if (expectedBytesRemainingToRead != null) { if (bytesRemaining != C.LENGTH_UNSET) {
expectedBytesRemainingToRead.addAndGet(-bytesRead); bytesRemaining -= bytesRead;
} }
if (listener != null) { if (listener != null) {
listener.onBytesTransferred(this, bytesRead); listener.onBytesTransferred(this, bytesRead);
@ -452,7 +317,31 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
} }
@Override @Override
public void onRedirectReceived(UrlRequest request, UrlResponseInfo info, String newLocationUrl) { public synchronized void close() {
if (currentUrlRequest != null) {
currentUrlRequest.cancel();
currentUrlRequest = null;
}
if (readBuffer != null) {
readBuffer.limit(0);
}
currentDataSpec = null;
responseInfo = null;
exception = null;
finished = false;
if (opened) {
opened = false;
if (listener != null) {
listener.onTransferEnd(this);
}
}
}
// UrlRequest.Callback implementation
@Override
public synchronized void onRedirectReceived(UrlRequest request, UrlResponseInfo info,
String newLocationUrl) {
if (request != currentUrlRequest) { if (request != currentUrlRequest) {
return; return;
} }
@ -462,8 +351,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
// For other redirect response codes the POST request is converted to a GET request and the // For other redirect response codes the POST request is converted to a GET request and the
// redirect is followed. // redirect is followed.
if (responseCode == 307 || responseCode == 308) { if (responseCode == 307 || responseCode == 308) {
exception = new OpenException("POST request redirected with 307 or 308 response code", exception = new InvalidResponseCodeException(responseCode, info.getAllHeaders(),
currentDataSpec, getCurrentRequestStatus()); currentDataSpec);
operation.open(); operation.open();
return; return;
} }
@ -474,74 +363,75 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
request.followRedirect(); request.followRedirect();
} }
@Override
public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
if (request != currentUrlRequest) {
return;
}
responseInfo = info;
operation.open();
}
@Override @Override
public synchronized void onReadCompleted(UrlRequest request, UrlResponseInfo info, public synchronized void onReadCompleted(UrlRequest request, UrlResponseInfo info,
ByteBuffer buffer) { ByteBuffer buffer) {
if (request != currentUrlRequest) { if (request != currentUrlRequest) {
return; return;
} }
readBuffer.flip();
hasData = true;
operation.open(); operation.open();
} }
@Override @Override
public void onSucceeded(UrlRequest request, UrlResponseInfo info) { public synchronized void onSucceeded(UrlRequest request, UrlResponseInfo info) {
if (request != currentUrlRequest) { if (request != currentUrlRequest) {
return; return;
} }
responseFinished = true; finished = true;
operation.open(); operation.open();
} }
@Override @Override
public synchronized void close() { public synchronized void onFailed(UrlRequest request, UrlResponseInfo info,
if (currentUrlRequest != null) { UrlRequestException error) {
currentUrlRequest.cancel(); if (request != currentUrlRequest) {
currentUrlRequest = null; return;
} }
currentDataSpec = null; exception = error.getErrorCode() == UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED
currentUrl = null; ? new UnknownHostException() : error;
exception = null; operation.open();
contentLength = 0; }
hasData = false;
responseInfo = null; // Internal methods.
expectedBytesRemainingToRead = null;
responseFinished = false; private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException {
try { UrlRequest.Builder requestBuilder = new UrlRequest.Builder(dataSpec.uri.toString(), this,
if (listener != null && connectionState == OPEN_CONNECTION) { executor, cronetEngine);
listener.onTransferEnd(this); // Set the headers.
synchronized (requestProperties) {
if (dataSpec.postBody != null && !requestProperties.containsKey(CONTENT_TYPE)) {
throw new OpenException("POST request must set Content-Type", dataSpec, Status.IDLE);
} }
} finally { for (Entry<String, String> headerEntry : requestProperties.entrySet()) {
connectionState = IDLE_CONNECTION; requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
}
}
@Override
public Uri getUri() {
return Uri.parse(currentUrl);
}
private void log(int priority, String message) {
if (Log.isLoggable(TAG, priority)) {
Log.println(priority, TAG, message);
}
}
private int getCurrentRequestStatus() {
if (currentUrlRequest == null) {
return UrlRequest.Status.IDLE;
}
final ConditionVariable conditionVariable = new ConditionVariable();
final AtomicInteger result = new AtomicInteger();
currentUrlRequest.getStatus(new UrlRequest.StatusListener() {
@Override
public void onStatus(int status) {
result.set(status);
conditionVariable.open();
} }
}); }
return result.get(); // Set the Range header.
if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder();
rangeValue.append("bytes=");
rangeValue.append(currentDataSpec.position);
rangeValue.append("-");
if (currentDataSpec.length != C.LENGTH_UNSET) {
rangeValue.append(currentDataSpec.position + currentDataSpec.length - 1);
}
requestBuilder.addHeader("Range", rangeValue.toString());
}
// Set the body.
if (dataSpec.postBody != null) {
requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody),
executor);
}
return requestBuilder.build();
} }
private boolean blockUntilConnectTimeout() { private boolean blockUntilConnectTimeout() {
@ -558,4 +448,75 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs; currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
} }
private static boolean getIsCompressed(UrlResponseInfo info) {
for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
return !entry.getValue().equalsIgnoreCase("identity");
}
}
return false;
}
private static long getContentLength(UrlResponseInfo info) {
long contentLength = C.LENGTH_UNSET;
Map<String, List<String>> headers = info.getAllHeaders();
List<String> contentLengthHeaders = headers.get("Content-Length");
String contentLengthHeader = null;
if (!isEmpty(contentLengthHeaders)) {
contentLengthHeader = contentLengthHeaders.get(0);
if (!TextUtils.isEmpty(contentLengthHeader)) {
try {
contentLength = Long.parseLong(contentLengthHeader);
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
}
}
}
List<String> contentRangeHeaders = headers.get("Content-Range");
if (!isEmpty(contentRangeHeaders)) {
String contentRangeHeader = contentRangeHeaders.get(0);
Matcher matcher = CONTENT_RANGE_HEADER_PATTERN.matcher(contentRangeHeader);
if (matcher.find()) {
try {
long contentLengthFromRange =
Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
if (contentLength < 0) {
// Some proxy servers strip the Content-Length header. Fall back to the length
// calculated here in this case.
contentLength = contentLengthFromRange;
} else if (contentLength != contentLengthFromRange) {
// If there is a discrepancy between the Content-Length and Content-Range headers,
// assume the one with the larger value is correct. We have seen cases where carrier
// change one of them to reduce the size of a request, but it is unlikely anybody
// would increase it.
Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
+ "]");
contentLength = Math.max(contentLength, contentLengthFromRange);
}
} catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
}
}
}
return contentLength;
}
private static int getStatus(UrlRequest request) {
final ConditionVariable conditionVariable = new ConditionVariable();
final int[] statusHolder = new int[1];
request.getStatus(new UrlRequest.StatusListener() {
@Override
public void onStatus(int status) {
statusHolder[0] = status;
conditionVariable.open();
}
});
conditionVariable.block();
return statusHolder[0];
}
private static boolean isEmpty(List<?> list) {
return list == null || list.isEmpty();
}
} }

View File

@ -41,7 +41,7 @@ public final class CronetDataSourceFactory implements Factory {
private final CronetEngine cronetEngine; private final CronetEngine cronetEngine;
private final Executor executor; private final Executor executor;
private final Predicate<String> contentTypePredicate; private final Predicate<String> contentTypePredicate;
private final TransferListener transferListener; private final TransferListener<? super DataSource> transferListener;
private final int connectTimeoutMs; private final int connectTimeoutMs;
private final int readTimeoutMs; private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects; private final boolean resetTimeoutOnRedirects;

View File

@ -185,9 +185,12 @@ public class OkHttpDataSource implements HttpDataSource {
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping. // Determine the length of the data to be read, after skipping.
long contentLength = response.body().contentLength(); if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length bytesToRead = dataSpec.length;
: (contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET); } else {
long contentLength = response.body().contentLength();
bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
opened = true; opened = true;
if (listener != null) { if (listener != null) {

View File

@ -130,8 +130,8 @@ public interface ExoPlayer {
/** /**
* Called when timeline and/or manifest has been refreshed. * Called when timeline and/or manifest has been refreshed.
* *
* @param timeline The latest timeline. * @param timeline The latest timeline, or null if the timeline is being cleared.
* @param manifest The latest manifest. * @param manifest The latest manifest, or null if the manifest is being cleared.
*/ */
void onTimelineChanged(Timeline timeline, Object manifest); void onTimelineChanged(Timeline timeline, Object manifest);
@ -247,7 +247,7 @@ public interface ExoPlayer {
/** /**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to * Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true)}. * {@code prepare(mediaSource, true, true)}.
*/ */
void prepare(MediaSource mediaSource); void prepare(MediaSource mediaSource);
@ -259,8 +259,11 @@ public interface ExoPlayer {
* @param resetPosition Whether the playback position should be reset to the default position in * @param resetPosition Whether the playback position should be reset to the default position in
* the first {@link Timeline.Window}. If false, playback will start from the position defined * the first {@link Timeline.Window}. If false, playback will start from the position defined
* by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
* @param resetTimeline Whether the timeline and manifest should be reset. Should be true unless
* the player is being prepared to play the same media as it was playing previously (e.g. if
* playback failed and is being retried).
*/ */
void prepare(MediaSource mediaSource, boolean resetPosition); void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline);
/** /**
* Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2;
import android.content.Context; import android.content.Context;
import android.os.Looper; import android.os.Looper;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
/** /**
@ -41,7 +42,7 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl) { LoadControl loadControl) {
return newSimpleInstance(context, trackSelector, loadControl, null); return newSimpleInstance(context, trackSelector, loadControl, null);
} }
@ -56,8 +57,8 @@ public final class ExoPlayerFactory {
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks. * will not be used for DRM protected playbacks.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl, DrmSessionManager drmSessionManager) { LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, false); return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, false);
} }
@ -74,8 +75,8 @@ public final class ExoPlayerFactory {
* available extensions over those defined in the core library. Note that extensions must be * available extensions over those defined in the core library. Note that extensions must be
* included in the application build for setting this flag to have any effect. * included in the application build for setting this flag to have any effect.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl, DrmSessionManager drmSessionManager, LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders) { boolean preferExtensionDecoders) {
return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager,
preferExtensionDecoders, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); preferExtensionDecoders, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
@ -96,8 +97,8 @@ public final class ExoPlayerFactory {
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
* seamlessly join an ongoing playback. * seamlessly join an ongoing playback.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl, DrmSessionManager drmSessionManager, LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) {
return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager, return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager,
preferExtensionDecoders, allowedVideoJoiningTimeMs); preferExtensionDecoders, allowedVideoJoiningTimeMs);
@ -110,7 +111,7 @@ public final class ExoPlayerFactory {
* @param renderers The {@link Renderer}s that will be used by the instance. * @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
*/ */
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) { public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector<?> trackSelector) {
return newInstance(renderers, trackSelector, new DefaultLoadControl()); return newInstance(renderers, trackSelector, new DefaultLoadControl());
} }
@ -122,7 +123,7 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
*/ */
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector, public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector<?> trackSelector,
LoadControl loadControl) { LoadControl loadControl) {
return new ExoPlayerImpl(renderers, trackSelector, loadControl); return new ExoPlayerImpl(renderers, trackSelector, loadControl);
} }

View File

@ -35,7 +35,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private static final String TAG = "ExoPlayerImpl"; private static final String TAG = "ExoPlayerImpl";
private final Handler eventHandler; private final Handler eventHandler;
private final ExoPlayerImplInternal internalPlayer; private final ExoPlayerImplInternal<?> internalPlayer;
private final CopyOnWriteArraySet<EventListener> listeners; private final CopyOnWriteArraySet<EventListener> listeners;
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period; private final Timeline.Period period;
@ -63,7 +63,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
*/ */
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { public ExoPlayerImpl(Renderer[] renderers, TrackSelector<?> trackSelector,
LoadControl loadControl) {
Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION); Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION);
Assertions.checkNotNull(renderers); Assertions.checkNotNull(renderers);
Assertions.checkState(renderers.length > 0); Assertions.checkState(renderers.length > 0);
@ -79,8 +80,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
}; };
playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0); playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, internalPlayer = new ExoPlayerImplInternal<>(renderers, trackSelector, loadControl,
eventHandler, playbackInfo); playWhenReady, eventHandler, playbackInfo);
} }
@Override @Override
@ -100,12 +101,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override @Override
public void prepare(MediaSource mediaSource) { public void prepare(MediaSource mediaSource) {
prepare(mediaSource, true); prepare(mediaSource, true, true);
} }
@Override @Override
public void prepare(MediaSource mediaSource, boolean resetPosition) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) {
timeline = null; if (resetTimeline && (timeline != null || manifest != null)) {
timeline = null;
manifest = null;
for (EventListener listener : listeners) {
listener.onTimelineChanged(null, null);
}
}
internalPlayer.prepare(mediaSource, resetPosition); internalPlayer.prepare(mediaSource, resetPosition);
} }

View File

@ -538,16 +538,23 @@ import java.io.IOException;
periodIndex = C.INDEX_UNSET; periodIndex = C.INDEX_UNSET;
} }
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder<T> periodHolder = playingPeriodHolder;
MediaPeriodHolder<T> newPlayingPeriodHolder = null; MediaPeriodHolder<T> newPlayingPeriodHolder = null;
while (periodHolder != null) { if (playingPeriodHolder == null) {
if (periodHolder.index == periodIndex && periodHolder.prepared) { // We're still waiting for the first period to be prepared.
newPlayingPeriodHolder = periodHolder; if (loadingPeriodHolder != null) {
} else { loadingPeriodHolder.release();
periodHolder.release(); }
} else {
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder<T> periodHolder = playingPeriodHolder;
while (periodHolder != null) {
if (periodHolder.index == periodIndex && periodHolder.prepared) {
newPlayingPeriodHolder = periodHolder;
} else {
periodHolder.release();
}
periodHolder = periodHolder.next;
} }
periodHolder = periodHolder.next;
} }
// Disable all the renderers if the period is changing. // Disable all the renderers if the period is changing.
@ -892,7 +899,8 @@ import java.io.IOException;
} }
// Release all loaded periods. // Release all loaded periods.
releasePeriodHoldersFrom(playingPeriodHolder); releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder
: loadingPeriodHolder);
bufferAheadPeriodCount = 0; bufferAheadPeriodCount = 0;
playingPeriodHolder = null; playingPeriodHolder = null;
readingPeriodHolder = null; readingPeriodHolder = null;

View File

@ -429,8 +429,8 @@ public final class SimpleExoPlayer implements ExoPlayer {
} }
@Override @Override
public void prepare(MediaSource mediaSource, boolean resetPosition) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) {
player.prepare(mediaSource, resetPosition); player.prepare(mediaSource, resetPosition, resetTimeline);
} }
@Override @Override

View File

@ -55,6 +55,7 @@ import java.util.List;
private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl");
private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc");
/** /**
* Parses a trak atom (defined in 14496-12). * Parses a trak atom (defined in 14496-12).
@ -1283,7 +1284,7 @@ import java.util.List;
/** /**
* Parses encryption data from an audio/video sample entry, populating {@code out} and returning * Parses encryption data from an audio/video sample entry, populating {@code out} and returning
* the unencrypted atom type, or 0 if no sinf atom was present. * the unencrypted atom type, or 0 if no common encryption sinf atom was present.
*/ */
private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position, private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position,
int size, StsdData out, int entryIndex) { int size, StsdData out, int entryIndex) {
@ -1296,10 +1297,10 @@ import java.util.List;
if (childAtomType == Atom.TYPE_sinf) { if (childAtomType == Atom.TYPE_sinf) {
Pair<Integer, TrackEncryptionBox> result = parseSinfFromParent(parent, childPosition, Pair<Integer, TrackEncryptionBox> result = parseSinfFromParent(parent, childPosition,
childAtomSize); childAtomSize);
Integer dataFormat = result.first; if (result != null) {
Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); out.trackEncryptionBoxes[entryIndex] = result.second;
out.trackEncryptionBoxes[entryIndex] = result.second; return result.first;
return dataFormat; }
} }
childPosition += childAtomSize; childPosition += childAtomSize;
} }
@ -1311,6 +1312,7 @@ import java.util.List;
int position, int size) { int position, int size) {
int childPosition = position + Atom.HEADER_SIZE; int childPosition = position + Atom.HEADER_SIZE;
boolean isCencScheme = false;
TrackEncryptionBox trackEncryptionBox = null; TrackEncryptionBox trackEncryptionBox = null;
Integer dataFormat = null; Integer dataFormat = null;
while (childPosition - position < size) { while (childPosition - position < size) {
@ -1321,15 +1323,20 @@ import java.util.List;
dataFormat = parent.readInt(); dataFormat = parent.readInt();
} else if (childAtomType == Atom.TYPE_schm) { } else if (childAtomType == Atom.TYPE_schm) {
parent.skipBytes(4); parent.skipBytes(4);
parent.readInt(); // schemeType. Expect cenc isCencScheme = parent.readInt() == TYPE_cenc;
parent.readInt(); // schemeVersion. Expect 0x00010000
} else if (childAtomType == Atom.TYPE_schi) { } else if (childAtomType == Atom.TYPE_schi) {
trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize); trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize);
} }
childPosition += childAtomSize; childPosition += childAtomSize;
} }
return Pair.create(dataFormat, trackEncryptionBox); if (isCencScheme) {
Assertions.checkArgument(dataFormat != null, "frma atom is mandatory");
Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory");
return Pair.create(dataFormat, trackEncryptionBox);
} else {
return null;
}
} }
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,

View File

@ -103,8 +103,10 @@ import java.util.List;
public void release() { public void release() {
continueLoadingHandler.removeCallbacksAndMessages(null); continueLoadingHandler.removeCallbacksAndMessages(null);
manifestFetcher.release(); manifestFetcher.release();
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { if (sampleStreamWrappers != null) {
sampleStreamWrapper.release(); for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.release();
}
} }
} }

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -52,7 +54,7 @@ public class PlaybackControlView extends FrameLayout {
public static final int DEFAULT_FAST_FORWARD_MS = 15000; public static final int DEFAULT_FAST_FORWARD_MS = 15000;
public static final int DEFAULT_REWIND_MS = 5000; public static final int DEFAULT_REWIND_MS = 5000;
public static final int DEFAULT_SHOW_DURATION_MS = 5000; public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;
private static final int PROGRESS_BAR_MAX = 1000; private static final int PROGRESS_BAR_MAX = 1000;
private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;
@ -74,9 +76,10 @@ public class PlaybackControlView extends FrameLayout {
private VisibilityListener visibilityListener; private VisibilityListener visibilityListener;
private boolean dragging; private boolean dragging;
private int rewindMs = DEFAULT_REWIND_MS; private int rewindMs;
private int fastForwardMs = DEFAULT_FAST_FORWARD_MS; private int fastForwardMs;
private int showDurationMs = DEFAULT_SHOW_DURATION_MS; private int showTimeoutMs;
private long hideAtMs;
private final Runnable updateProgressAction = new Runnable() { private final Runnable updateProgressAction = new Runnable() {
@Override @Override
@ -103,6 +106,22 @@ public class PlaybackControlView extends FrameLayout {
public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
rewindMs = DEFAULT_REWIND_MS;
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.PlaybackControlView, 0, 0);
try {
rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs);
fastForwardMs = a.getInt(R.styleable.PlaybackControlView_fastforward_increment,
fastForwardMs);
showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs);
} finally {
a.recycle();
}
}
currentWindow = new Timeline.Window(); currentWindow = new Timeline.Window();
formatBuilder = new StringBuilder(); formatBuilder = new StringBuilder();
formatter = new Formatter(formatBuilder, Locale.getDefault()); formatter = new Formatter(formatBuilder, Locale.getDefault());
@ -124,7 +143,6 @@ public class PlaybackControlView extends FrameLayout {
rewindButton.setOnClickListener(componentListener); rewindButton.setOnClickListener(componentListener);
fastForwardButton = findViewById(R.id.ffwd); fastForwardButton = findViewById(R.id.ffwd);
fastForwardButton.setOnClickListener(componentListener); fastForwardButton.setOnClickListener(componentListener);
updateAll();
} }
/** /**
@ -169,6 +187,7 @@ public class PlaybackControlView extends FrameLayout {
*/ */
public void setRewindIncrementMs(int rewindMs) { public void setRewindIncrementMs(int rewindMs) {
this.rewindMs = rewindMs; this.rewindMs = rewindMs;
updateNavigation();
} }
/** /**
@ -178,51 +197,60 @@ public class PlaybackControlView extends FrameLayout {
*/ */
public void setFastForwardIncrementMs(int fastForwardMs) { public void setFastForwardIncrementMs(int fastForwardMs) {
this.fastForwardMs = fastForwardMs; this.fastForwardMs = fastForwardMs;
updateNavigation();
} }
/** /**
* Sets the duration to show the playback control in milliseconds. * Returns the playback controls timeout. The playback controls are automatically hidden after
* this duration of time has elapsed without user input.
* *
* @param showDurationMs The duration in milliseconds. * @return The duration in milliseconds. A non-positive value indicates that the controls will
* remain visible indefinitely.
*/ */
public void setShowDurationMs(int showDurationMs) { public int getShowTimeoutMs() {
this.showDurationMs = showDurationMs; return showTimeoutMs;
} }
/** /**
* Shows the controller for the duration last passed to {@link #setShowDurationMs(int)}, or for * Sets the playback controls timeout. The playback controls are automatically hidden after this
* {@link #DEFAULT_SHOW_DURATION_MS} if {@link #setShowDurationMs(int)} has not been called. * duration of time has elapsed without user input.
*
* @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls
* to remain visible indefinitely.
*/
public void setShowTimeoutMs(int showTimeoutMs) {
this.showTimeoutMs = showTimeoutMs;
}
/**
* Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
* be automatically hidden after this duration of time has elapsed without user input.
*/ */
public void show() { public void show() {
show(showDurationMs); if (!isVisible()) {
} setVisibility(VISIBLE);
if (visibilityListener != null) {
/** visibilityListener.onVisibilityChange(getVisibility());
* Shows the controller for the {@code durationMs}. If {@code durationMs} is 0 the controller is }
* shown until {@link #hide()} is called. updateAll();
*
* @param durationMs The duration in milliseconds.
*/
public void show(int durationMs) {
setVisibility(VISIBLE);
if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
} }
updateAll(); // Call hideAfterTimeout even if already visible to reset the timeout.
showDurationMs = durationMs; hideAfterTimeout();
hideDeferred();
} }
/** /**
* Hides the controller. * Hides the controller.
*/ */
public void hide() { public void hide() {
setVisibility(GONE); if (isVisible()) {
if (visibilityListener != null) { setVisibility(GONE);
visibilityListener.onVisibilityChange(getVisibility()); if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
}
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
hideAtMs = C.TIME_UNSET;
} }
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
} }
/** /**
@ -232,10 +260,15 @@ public class PlaybackControlView extends FrameLayout {
return getVisibility() == VISIBLE; return getVisibility() == VISIBLE;
} }
private void hideDeferred() { private void hideAfterTimeout() {
removeCallbacks(hideAction); removeCallbacks(hideAction);
if (showDurationMs > 0) { if (showTimeoutMs > 0) {
postDelayed(hideAction, showDurationMs); hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs;
if (isAttachedToWindow()) {
postDelayed(hideAction, showTimeoutMs);
}
} else {
hideAtMs = C.TIME_UNSET;
} }
} }
@ -246,7 +279,7 @@ public class PlaybackControlView extends FrameLayout {
} }
private void updatePlayPauseButton() { private void updatePlayPauseButton() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
boolean playing = player != null && player.getPlayWhenReady(); boolean playing = player != null && player.getPlayWhenReady();
@ -258,7 +291,7 @@ public class PlaybackControlView extends FrameLayout {
} }
private void updateNavigation() { private void updateNavigation() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null;
@ -276,13 +309,13 @@ public class PlaybackControlView extends FrameLayout {
} }
setButtonEnabled(enablePrevious , previousButton); setButtonEnabled(enablePrevious , previousButton);
setButtonEnabled(enableNext, nextButton); setButtonEnabled(enableNext, nextButton);
setButtonEnabled(isSeekable, fastForwardButton); setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton);
setButtonEnabled(isSeekable, rewindButton); setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton);
progressBar.setEnabled(isSeekable); progressBar.setEnabled(isSeekable);
} }
private void updateProgress() { private void updateProgress() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
long duration = player == null ? 0 : player.getDuration(); long duration = player == null ? 0 : player.getDuration();
@ -377,13 +410,40 @@ public class PlaybackControlView extends FrameLayout {
} }
private void rewind() { private void rewind() {
if (rewindMs <= 0) {
return;
}
player.seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); player.seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0));
} }
private void fastForward() { private void fastForward() {
if (fastForwardMs <= 0) {
return;
}
player.seekTo(Math.min(player.getCurrentPosition() + fastForwardMs, player.getDuration())); player.seekTo(Math.min(player.getCurrentPosition() + fastForwardMs, player.getDuration()));
} }
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (hideAtMs != C.TIME_UNSET) {
long delayMs = hideAtMs - SystemClock.uptimeMillis();
if (delayMs <= 0) {
hide();
} else {
postDelayed(hideAction, delayMs);
}
}
updateAll();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
}
@Override @Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
if (player == null || event.getAction() != KeyEvent.ACTION_DOWN) { if (player == null || event.getAction() != KeyEvent.ACTION_DOWN) {
@ -440,7 +500,7 @@ public class PlaybackControlView extends FrameLayout {
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
dragging = false; dragging = false;
player.seekTo(positionValue(seekBar.getProgress())); player.seekTo(positionValue(seekBar.getProgress()));
hideDeferred(); hideAfterTimeout();
} }
@Override @Override
@ -485,7 +545,7 @@ public class PlaybackControlView extends FrameLayout {
} else if (playButton == view) { } else if (playButton == view) {
player.setPlayWhenReady(!player.getPlayWhenReady()); player.setPlayWhenReady(!player.getPlayWhenReady());
} }
hideDeferred(); hideAfterTimeout();
} }
} }

View File

@ -48,8 +48,10 @@ public final class SimpleExoPlayerView extends FrameLayout {
private final AspectRatioFrameLayout layout; private final AspectRatioFrameLayout layout;
private final PlaybackControlView controller; private final PlaybackControlView controller;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private boolean useController = true; private boolean useController = true;
private int controllerShowTimeoutMs;
public SimpleExoPlayerView(Context context) { public SimpleExoPlayerView(Context context) {
this(context, null); this(context, null);
@ -64,6 +66,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
boolean useTextureView = false; boolean useTextureView = false;
int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
int rewindMs = PlaybackControlView.DEFAULT_REWIND_MS;
int fastForwardMs = PlaybackControlView.DEFAULT_FAST_FORWARD_MS;
int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS;
if (attrs != null) { if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.SimpleExoPlayerView, 0, 0); R.styleable.SimpleExoPlayerView, 0, 0);
@ -73,6 +78,11 @@ public final class SimpleExoPlayerView extends FrameLayout {
useTextureView); useTextureView);
resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode,
AspectRatioFrameLayout.RESIZE_MODE_FIT); AspectRatioFrameLayout.RESIZE_MODE_FIT);
rewindMs = a.getInt(R.styleable.SimpleExoPlayerView_rewind_increment, rewindMs);
fastForwardMs = a.getInt(R.styleable.SimpleExoPlayerView_fastforward_increment,
fastForwardMs);
controllerShowTimeoutMs = a.getInt(R.styleable.SimpleExoPlayerView_show_timeout,
controllerShowTimeoutMs);
} finally { } finally {
a.recycle(); a.recycle();
} }
@ -82,12 +92,17 @@ public final class SimpleExoPlayerView extends FrameLayout {
componentListener = new ComponentListener(); componentListener = new ComponentListener();
layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame); layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame);
layout.setResizeMode(resizeMode); layout.setResizeMode(resizeMode);
controller = (PlaybackControlView) findViewById(R.id.control);
shutterView = findViewById(R.id.shutter); shutterView = findViewById(R.id.shutter);
subtitleLayout = (SubtitleView) findViewById(R.id.subtitles); subtitleLayout = (SubtitleView) findViewById(R.id.subtitles);
subtitleLayout.setUserDefaultStyle(); subtitleLayout.setUserDefaultStyle();
subtitleLayout.setUserDefaultTextSize(); subtitleLayout.setUserDefaultTextSize();
controller = (PlaybackControlView) findViewById(R.id.control);
controller.hide();
controller.setRewindIncrementMs(rewindMs);
controller.setFastForwardIncrementMs(fastForwardMs);
this.controllerShowTimeoutMs = controllerShowTimeoutMs;
View view = useTextureView ? new TextureView(context) : new SurfaceView(context); View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@ -122,6 +137,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
this.player.setVideoSurface(null); this.player.setVideoSurface(null);
} }
this.player = player; this.player = player;
if (useController) {
controller.setPlayer(player);
}
if (player != null) { if (player != null) {
if (surfaceView instanceof TextureView) { if (surfaceView instanceof TextureView) {
player.setVideoTextureView((TextureView) surfaceView); player.setVideoTextureView((TextureView) surfaceView);
@ -131,20 +149,36 @@ public final class SimpleExoPlayerView extends FrameLayout {
player.setVideoListener(componentListener); player.setVideoListener(componentListener);
player.addListener(componentListener); player.addListener(componentListener);
player.setTextOutput(componentListener); player.setTextOutput(componentListener);
maybeShowController(false);
} else { } else {
shutterView.setVisibility(VISIBLE); shutterView.setVisibility(VISIBLE);
} controller.hide();
if (useController) {
controller.setPlayer(player);
} }
} }
/** /**
* Set the {@code useController} flag which indicates whether the playback control view should * Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT},
* be used or not. If set to {@code false} the controller is never visible and is disconnected * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or
* from the player. * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}.
* *
* @param useController If {@code false} the playback control is never used. * @param resizeMode The resize mode.
*/
public void setResizeMode(int resizeMode) {
layout.setResizeMode(resizeMode);
}
/**
* Returns whether the playback controls are enabled.
*/
public boolean getUseController() {
return useController;
}
/**
* Sets whether playback controls are enabled. If set to {@code false} the playback controls are
* never visible and are disconnected from the player.
*
* @param useController Whether playback controls should be enabled.
*/ */
public void setUseController(boolean useController) { public void setUseController(boolean useController) {
if (this.useController == useController) { if (this.useController == useController) {
@ -160,14 +194,26 @@ public final class SimpleExoPlayerView extends FrameLayout {
} }
/** /**
* Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT}, * Returns the playback controls timeout. The playback controls are automatically hidden after
* {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or * this duration of time has elapsed without user input and with playback or buffering in
* {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}. * progress.
* *
* @param resizeMode The resize mode. * @return The timeout in milliseconds. A non-positive value will cause the controller to remain
* visible indefinitely.
*/ */
public void setResizeMode(int resizeMode) { public int getControllerShowTimeoutMs() {
layout.setResizeMode(resizeMode); return controllerShowTimeoutMs;
}
/**
* Sets the playback controls timeout. The playback controls are automatically hidden after this
* duration of time has elapsed without user input and with playback or buffering in progress.
*
* @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause
* the controller to remain visible indefinitely.
*/
public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {
this.controllerShowTimeoutMs = controllerShowTimeoutMs;
} }
/** /**
@ -197,15 +243,6 @@ public final class SimpleExoPlayerView extends FrameLayout {
controller.setFastForwardIncrementMs(fastForwardMs); controller.setFastForwardIncrementMs(fastForwardMs);
} }
/**
* Sets the duration to show the playback control in milliseconds.
*
* @param showDurationMs The duration in milliseconds.
*/
public void setControlShowDurationMs(int showDurationMs) {
controller.setShowDurationMs(showDurationMs);
}
/** /**
* Get the view onto which video is rendered. This is either a {@link SurfaceView} (default) * Get the view onto which video is rendered. This is either a {@link SurfaceView} (default)
* or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true. * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true.
@ -218,21 +255,23 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override @Override
public boolean onTouchEvent(MotionEvent ev) { public boolean onTouchEvent(MotionEvent ev) {
if (useController && ev.getActionMasked() == MotionEvent.ACTION_DOWN) { if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
if (controller.isVisible()) { return false;
controller.hide(); }
} else { if (controller.isVisible()) {
controller.show(); controller.hide();
} } else {
maybeShowController(true);
} }
return true; return true;
} }
@Override @Override
public boolean onTrackballEvent(MotionEvent ev) { public boolean onTrackballEvent(MotionEvent ev) {
if (!useController) { if (!useController || player == null) {
return false; return false;
} }
controller.show(); maybeShowController(true);
return true; return true;
} }
@ -241,6 +280,20 @@ public final class SimpleExoPlayerView extends FrameLayout {
return useController ? controller.dispatchKeyEvent(event) : super.dispatchKeyEvent(event); return useController ? controller.dispatchKeyEvent(event) : super.dispatchKeyEvent(event);
} }
private void maybeShowController(boolean isForced) {
if (!useController || player == null) {
return;
}
int playbackState = player.getPlaybackState();
boolean showIndefinitely = playbackState == ExoPlayer.STATE_IDLE
|| playbackState == ExoPlayer.STATE_ENDED || !player.getPlayWhenReady();
boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0;
controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs);
if (isForced || showIndefinitely || wasShowingIndefinitely) {
controller.show();
}
}
private final class ComponentListener implements SimpleExoPlayer.VideoListener, private final class ComponentListener implements SimpleExoPlayer.VideoListener,
TextRenderer.Output, ExoPlayer.EventListener { TextRenderer.Output, ExoPlayer.EventListener {
@ -278,9 +331,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (useController && playbackState == ExoPlayer.STATE_ENDED) { maybeShowController(false);
controller.show(0);
}
} }
@Override @Override

View File

@ -231,10 +231,13 @@ public class DefaultHttpDataSource implements HttpDataSource {
// Determine the length of the data to be read, after skipping. // Determine the length of the data to be read, after skipping.
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) { if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
long contentLength = getContentLength(connection); if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length bytesToRead = dataSpec.length;
: contentLength != C.LENGTH_UNSET ? contentLength - bytesToSkip } else {
: C.LENGTH_UNSET; long contentLength = getContentLength(connection);
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
: C.LENGTH_UNSET;
}
} else { } else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response // Gzip is enabled. If the server opts to use gzip then the content length in the response
// will be that of the compressed data, which isn't what we want. Furthermore, there isn't a // will be that of the compressed data, which isn't what we want. Furthermore, there isn't a

View File

@ -20,10 +20,16 @@
<enum name="fixed_width" value="1"/> <enum name="fixed_width" value="1"/>
<enum name="fixed_height" value="2"/> <enum name="fixed_height" value="2"/>
</attr> </attr>
<attr name="show_timeout" format="integer"/>
<attr name="rewind_increment" format="integer"/>
<attr name="fastforward_increment" format="integer"/>
<declare-styleable name="SimpleExoPlayerView"> <declare-styleable name="SimpleExoPlayerView">
<attr name="use_controller" format="boolean"/> <attr name="use_controller" format="boolean"/>
<attr name="use_texture_view" format="boolean"/> <attr name="use_texture_view" format="boolean"/>
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
<attr name="resize_mode"/> <attr name="resize_mode"/>
</declare-styleable> </declare-styleable>
@ -31,4 +37,10 @@
<attr name="resize_mode"/> <attr name="resize_mode"/>
</declare-styleable> </declare-styleable>
<declare-styleable name="PlaybackControlView">
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
</declare-styleable>
</resources> </resources>