mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
commit
08256eee32
51 changed files with 1077 additions and 819 deletions
|
|
@ -1,5 +1,25 @@
|
||||||
# Release notes #
|
# Release notes #
|
||||||
|
|
||||||
|
### r2.0.3 ###
|
||||||
|
|
||||||
|
This release contains important bug fixes. Users of r2.0.0, r2.0.1 and r2.0.2
|
||||||
|
should proactively update to this version.
|
||||||
|
|
||||||
|
* Fixed NullPointerException in ExtractorMediaSource
|
||||||
|
([#1914](https://github.com/google/ExoPlayer/issues/1914).
|
||||||
|
* Fixed NullPointerException in HlsMediaPeriod
|
||||||
|
([#1907](https://github.com/google/ExoPlayer/issues/1907).
|
||||||
|
* Fixed memory leak in PlaybackControlView
|
||||||
|
([#1908](https://github.com/google/ExoPlayer/issues/1908).
|
||||||
|
* Fixed strict mode violation when using
|
||||||
|
SimpleExoPlayer.setVideoPlayerTextureView().
|
||||||
|
* Fixed L3 Widevine provisioning
|
||||||
|
([#1925](https://github.com/google/ExoPlayer/issues/1925).
|
||||||
|
* Fixed hiding of controls with use_controller="false"
|
||||||
|
([#1919](https://github.com/google/ExoPlayer/issues/1919).
|
||||||
|
* Improvements to Cronet network stack extension.
|
||||||
|
* Misc bug fixes.
|
||||||
|
|
||||||
### r2.0.2 ###
|
### r2.0.2 ###
|
||||||
|
|
||||||
* Fixes for MergingMediaSource and sideloaded subtitles.
|
* Fixes for MergingMediaSource and sideloaded subtitles.
|
||||||
|
|
@ -88,6 +108,13 @@ some of the motivations behind ExoPlayer 2.x
|
||||||
* Suppressed "Sending message to a Handler on a dead thread" warnings
|
* Suppressed "Sending message to a Handler on a dead thread" warnings
|
||||||
([#426](https://github.com/google/ExoPlayer/issues/426)).
|
([#426](https://github.com/google/ExoPlayer/issues/426)).
|
||||||
|
|
||||||
|
### r1.5.12 ###
|
||||||
|
|
||||||
|
* Improvements to Cronet network stack extension.
|
||||||
|
* Fix bug in demo app introduced in r1.5.11 that caused L3 Widevine
|
||||||
|
provisioning requests to fail.
|
||||||
|
* Misc bugfixes.
|
||||||
|
|
||||||
### r1.5.11 ###
|
### r1.5.11 ###
|
||||||
|
|
||||||
* Cronet network stack extension.
|
* Cronet network stack extension.
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ allprojects {
|
||||||
releaseRepoName = 'exoplayer'
|
releaseRepoName = 'exoplayer'
|
||||||
releaseUserOrg = 'google'
|
releaseUserOrg = 'google'
|
||||||
releaseGroupId = 'com.google.android.exoplayer'
|
releaseGroupId = 'com.google.android.exoplayer'
|
||||||
releaseVersion = 'r2.0.2'
|
releaseVersion = 'r2.0.3'
|
||||||
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.google.android.exoplayer2.demo"
|
package="com.google.android.exoplayer2.demo"
|
||||||
android:versionCode="2002"
|
android:versionCode="2003"
|
||||||
android:versionName="2.0.2">
|
android:versionName="2.0.3">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceExcep
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.android.exoplayer2.util.Predicate;
|
import com.google.android.exoplayer2.util.Predicate;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
|
@ -82,7 +81,6 @@ public final class CronetDataSourceTest {
|
||||||
private static final String TEST_CONTENT_TYPE = "test/test";
|
private static final String TEST_CONTENT_TYPE = "test/test";
|
||||||
private static final byte[] TEST_POST_BODY = "test post body".getBytes();
|
private static final byte[] TEST_POST_BODY = "test post body".getBytes();
|
||||||
private static final long TEST_CONTENT_LENGTH = 16000L;
|
private static final long TEST_CONTENT_LENGTH = 16000L;
|
||||||
private static final int TEST_BUFFER_SIZE = 16;
|
|
||||||
private static final int TEST_CONNECTION_STATUS = 5;
|
private static final int TEST_CONNECTION_STATUS = 5;
|
||||||
|
|
||||||
private DataSpec testDataSpec;
|
private DataSpec testDataSpec;
|
||||||
|
|
@ -174,10 +172,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 +200,7 @@ public final class CronetDataSourceTest {
|
||||||
dataSourceUnderTest.onFailed(
|
dataSourceUnderTest.onFailed(
|
||||||
mockUrlRequest,
|
mockUrlRequest,
|
||||||
testUrlResponseInfo,
|
testUrlResponseInfo,
|
||||||
null);
|
mockUrlRequestException);
|
||||||
dataSourceUnderTest.onResponseStarted(
|
dataSourceUnderTest.onResponseStarted(
|
||||||
mockUrlRequest2,
|
mockUrlRequest2,
|
||||||
testUrlResponseInfo);
|
testUrlResponseInfo);
|
||||||
|
|
@ -234,10 +229,8 @@ public final class CronetDataSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestHeadersSet() throws HttpDataSourceException {
|
public void testRequestHeadersSet() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
|
||||||
|
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||||
testResponseHeader.put("Content-Length", Long.toString(5000L));
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
dataSourceUnderTest.setRequestProperty("firstHeader", "firstValue");
|
dataSourceUnderTest.setRequestProperty("firstHeader", "firstValue");
|
||||||
dataSourceUnderTest.setRequestProperty("secondHeader", "secondValue");
|
dataSourceUnderTest.setRequestProperty("secondHeader", "secondValue");
|
||||||
|
|
@ -253,25 +246,19 @@ 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 {
|
||||||
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000, null);
|
||||||
testResponseHeader.put("Content-Encoding", "gzip");
|
testResponseHeader.put("Content-Encoding", "gzip");
|
||||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
testResponseHeader.put("Content-Length", Long.toString(50L));
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
// Data spec's requested length, 5000. Test response's length, 16,000.
|
|
||||||
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 +273,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 +290,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 +306,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 +322,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,7 +364,7 @@ public final class CronetDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testRequestReadTwice() throws HttpDataSourceException {
|
public void testRequestReadTwice() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess();
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -423,28 +386,23 @@ public final class CronetDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testSecondRequestNoContentLength() throws HttpDataSourceException {
|
public void testSecondRequestNoContentLength() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess();
|
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
byte[] returnedBuffer = new byte[8];
|
|
||||||
|
|
||||||
// First request.
|
// First request.
|
||||||
testResponseHeader.put("Content-Length", Long.toString(1L));
|
|
||||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
byte[] returnedBuffer = new byte[8];
|
||||||
dataSourceUnderTest.read(returnedBuffer, 0, 1);
|
dataSourceUnderTest.read(returnedBuffer, 0, 1);
|
||||||
dataSourceUnderTest.close();
|
dataSourceUnderTest.close();
|
||||||
|
|
||||||
// Second request. There's no Content-Length response header.
|
|
||||||
testResponseHeader.remove("Content-Length");
|
testResponseHeader.remove("Content-Length");
|
||||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
// Second request.
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
returnedBuffer = new byte[16];
|
returnedBuffer = new byte[16];
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||||
assertEquals(10, bytesRead);
|
assertEquals(10, bytesRead);
|
||||||
|
|
||||||
mockResponseFinished();
|
|
||||||
|
|
||||||
// Should read whats left in the buffer first.
|
|
||||||
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||||
assertEquals(6, bytesRead);
|
assertEquals(6, bytesRead);
|
||||||
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||||
|
|
@ -454,23 +412,54 @@ public final class CronetDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testReadWithOffset() throws HttpDataSourceException {
|
public void testReadWithOffset() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess();
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
byte[] returnedBuffer = new byte[16];
|
byte[] returnedBuffer = new byte[16];
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
||||||
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
|
|
||||||
assertEquals(8, bytesRead);
|
assertEquals(8, bytesRead);
|
||||||
|
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
|
||||||
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
|
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRangeRequestWith206Response() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(1000, 5000);
|
||||||
|
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
||||||
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
byte[] returnedBuffer = new byte[16];
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||||
|
assertEquals(16, bytesRead);
|
||||||
|
assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer);
|
||||||
|
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRangeRequestWith200Response() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, 7000);
|
||||||
|
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
||||||
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
byte[] returnedBuffer = new byte[16];
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||||
|
assertEquals(16, bytesRead);
|
||||||
|
assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer);
|
||||||
|
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadWithUnsetLength() throws HttpDataSourceException {
|
public void testReadWithUnsetLength() throws HttpDataSourceException {
|
||||||
testResponseHeader.remove("Content-Length");
|
testResponseHeader.remove("Content-Length");
|
||||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess();
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -484,7 +473,7 @@ public final class CronetDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testReadReturnsWhatItCan() throws HttpDataSourceException {
|
public void testReadReturnsWhatItCan() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess();
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -498,7 +487,7 @@ public final class CronetDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testClosedMeansClosed() throws HttpDataSourceException {
|
public void testClosedMeansClosed() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess();
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
int bytesRead = 0;
|
int bytesRead = 0;
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
@ -510,7 +499,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);
|
||||||
|
|
@ -525,32 +513,29 @@ public final class CronetDataSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOverread() throws HttpDataSourceException {
|
public void testOverread() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
|
||||||
mockReadSuccess();
|
|
||||||
|
|
||||||
// Ask for 16 bytes
|
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 10000, 16, null);
|
|
||||||
// Let the response promise to give 16 bytes back.
|
|
||||||
testResponseHeader.put("Content-Length", Long.toString(16L));
|
testResponseHeader.put("Content-Length", Long.toString(16L));
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
byte[] returnedBuffer = new byte[8];
|
byte[] returnedBuffer = new byte[8];
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||||
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
|
|
||||||
assertEquals(8, bytesRead);
|
assertEquals(8, bytesRead);
|
||||||
|
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
|
||||||
|
|
||||||
// The current buffer is kept if not completely consumed by DataSource reader.
|
// The current buffer is kept if not completely consumed by DataSource reader.
|
||||||
returnedBuffer = new byte[8];
|
returnedBuffer = new byte[8];
|
||||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
|
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
|
||||||
assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer);
|
|
||||||
assertEquals(14, bytesRead);
|
assertEquals(14, bytesRead);
|
||||||
|
assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer);
|
||||||
|
|
||||||
// 2 bytes left at this point.
|
// 2 bytes left at this point.
|
||||||
returnedBuffer = new byte[8];
|
returnedBuffer = new byte[8];
|
||||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||||
assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer);
|
|
||||||
assertEquals(16, bytesRead);
|
assertEquals(16, bytesRead);
|
||||||
|
assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer);
|
||||||
|
|
||||||
// Should have only called read on cronet once.
|
// Should have only called read on cronet once.
|
||||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||||
|
|
@ -572,7 +557,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 +587,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 +618,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 +652,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 +665,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 +675,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());
|
||||||
|
|
@ -796,16 +769,24 @@ public final class CronetDataSourceTest {
|
||||||
}).when(mockUrlRequest).start();
|
}).when(mockUrlRequest).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mockReadSuccess() {
|
private void mockReadSuccess(int position, int length) {
|
||||||
|
final int[] positionAndRemaining = new int[] {position, length};
|
||||||
doAnswer(new Answer<Void>() {
|
doAnswer(new Answer<Void>() {
|
||||||
@Override
|
@Override
|
||||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
if (positionAndRemaining[1] == 0) {
|
||||||
inputBuffer.put(buildTestDataBuffer());
|
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
||||||
dataSourceUnderTest.onReadCompleted(
|
} else {
|
||||||
mockUrlRequest,
|
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||||
testUrlResponseInfo,
|
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||||
inputBuffer);
|
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||||
|
positionAndRemaining[0] += readLength;
|
||||||
|
positionAndRemaining[1] -= readLength;
|
||||||
|
dataSourceUnderTest.onReadCompleted(
|
||||||
|
mockUrlRequest,
|
||||||
|
testUrlResponseInfo,
|
||||||
|
inputBuffer);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||||
|
|
@ -818,17 +799,7 @@ public final class CronetDataSourceTest {
|
||||||
dataSourceUnderTest.onFailed(
|
dataSourceUnderTest.onFailed(
|
||||||
mockUrlRequest,
|
mockUrlRequest,
|
||||||
createUrlResponseInfo(500), // statusCode
|
createUrlResponseInfo(500), // statusCode
|
||||||
null);
|
mockUrlRequestException);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mockResponseFinished() {
|
|
||||||
doAnswer(new Answer<Void>() {
|
|
||||||
@Override
|
|
||||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||||
|
|
@ -846,8 +817,8 @@ public final class CronetDataSourceTest {
|
||||||
return startedCondition;
|
return startedCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] buildTestDataArray(int start, int length) {
|
private static byte[] buildTestDataArray(int position, int length) {
|
||||||
return Arrays.copyOfRange(buildTestDataBuffer().array(), start, start + length);
|
return buildTestDataBuffer(position, length).array();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] prefixZeros(byte[] data, int requiredLength) {
|
public static byte[] prefixZeros(byte[] data, int requiredLength) {
|
||||||
|
|
@ -860,17 +831,13 @@ public final class CronetDataSourceTest {
|
||||||
return Arrays.copyOf(data, requiredLength);
|
return Arrays.copyOf(data, requiredLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteBuffer buildTestDataBuffer() {
|
private static ByteBuffer buildTestDataBuffer(int position, int length) {
|
||||||
ByteBuffer testBuffer = ByteBuffer.allocate(TEST_BUFFER_SIZE);
|
ByteBuffer testBuffer = ByteBuffer.allocate(length);
|
||||||
for (byte i = 1; i <= TEST_BUFFER_SIZE; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
testBuffer.put(i);
|
testBuffer.put((byte) (position + i));
|
||||||
}
|
}
|
||||||
testBuffer.flip();
|
testBuffer.flip();
|
||||||
return testBuffer;
|
return testBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertConnectionState(int state) {
|
|
||||||
assertEquals(state, dataSourceUnderTest.connectionState);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,30 @@ 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 bytesToSkip;
|
||||||
|
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 +167,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 +199,124 @@ 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) {
|
// If we requested a range starting from a non-zero position and received a 200 rather than a
|
||||||
// Logic copied from {@code DefaultHttpDataSource}
|
// 206, then the server does not support partial requests. We'll need to manually skip to the
|
||||||
long contentLength = C.LENGTH_UNSET;
|
// requested position.
|
||||||
List<String> contentLengthHeader = headers.get("Content-Length");
|
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||||
if (contentLengthHeader != null
|
|
||||||
&& !contentLengthHeader.isEmpty()
|
// Calculate the content length.
|
||||||
&& !TextUtils.isEmpty(contentLengthHeader.get(0))) {
|
if (!getIsCompressed(responseInfo)) {
|
||||||
try {
|
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||||
contentLength = Long.parseLong(contentLengthHeader.get(0));
|
bytesRemaining = dataSpec.length;
|
||||||
} catch (NumberFormatException e) {
|
} else {
|
||||||
log(Log.ERROR, "Unexpected Content-Length [" + contentLengthHeader + "]");
|
bytesRemaining = getContentLength(responseInfo);
|
||||||
}
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
while (!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();
|
||||||
|
Assertions.checkState(readBuffer.hasRemaining());
|
||||||
|
if (bytesToSkip > 0) {
|
||||||
|
int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
|
||||||
|
readBuffer.position(readBuffer.position() + bytesSkipped);
|
||||||
|
bytesToSkip -= bytesSkipped;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +325,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 +359,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 +371,80 @@ 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 && dataSpec.postBody.length != 0
|
||||||
|
&& !requestProperties.containsKey(CONTENT_TYPE)) {
|
||||||
|
throw new OpenException("POST request with non-empty body 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 method and (if non-empty) the body.
|
||||||
|
if (dataSpec.postBody != null) {
|
||||||
|
requestBuilder.setHttpMethod("POST");
|
||||||
|
if (dataSpec.postBody.length != 0) {
|
||||||
|
requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody),
|
||||||
|
executor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestBuilder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean blockUntilConnectTimeout() {
|
private boolean blockUntilConnectTimeout() {
|
||||||
|
|
@ -558,4 +461,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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -32,21 +32,21 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import okhttp3.CacheControl;
|
import okhttp3.CacheControl;
|
||||||
|
import okhttp3.Call;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link HttpDataSource} that delegates to Square's {@link OkHttpClient}.
|
* An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}.
|
||||||
*/
|
*/
|
||||||
public class OkHttpDataSource implements HttpDataSource {
|
public class OkHttpDataSource implements HttpDataSource {
|
||||||
|
|
||||||
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
|
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
|
||||||
|
|
||||||
private final OkHttpClient okHttpClient;
|
private final Call.Factory callFactory;
|
||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
private final Predicate<String> contentTypePredicate;
|
private final Predicate<String> contentTypePredicate;
|
||||||
private final TransferListener<? super OkHttpDataSource> listener;
|
private final TransferListener<? super OkHttpDataSource> listener;
|
||||||
|
|
@ -65,31 +65,31 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
private long bytesRead;
|
private long bytesRead;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param client An {@link OkHttpClient} for use by the source.
|
* @param callFactory An {@link Call.Factory} for use by the source.
|
||||||
* @param userAgent The User-Agent string that should be used.
|
* @param userAgent The User-Agent string that should be used.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
|
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSource(OkHttpClient client, String userAgent,
|
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
||||||
Predicate<String> contentTypePredicate) {
|
Predicate<String> contentTypePredicate) {
|
||||||
this(client, userAgent, contentTypePredicate, null);
|
this(callFactory, userAgent, contentTypePredicate, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param client An {@link OkHttpClient} for use by the source.
|
* @param callFactory An {@link Call.Factory} for use by the source.
|
||||||
* @param userAgent The User-Agent string that should be used.
|
* @param userAgent The User-Agent string that should be used.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then a {@link InvalidContentTypeException} is thrown from
|
* predicate then a {@link InvalidContentTypeException} is thrown from
|
||||||
* {@link #open(DataSpec)}.
|
* {@link #open(DataSpec)}.
|
||||||
* @param listener An optional listener.
|
* @param listener An optional listener.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSource(OkHttpClient client, String userAgent,
|
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
||||||
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) {
|
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) {
|
||||||
this(client, userAgent, contentTypePredicate, listener, null);
|
this(callFactory, userAgent, contentTypePredicate, listener, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param client An {@link OkHttpClient} for use by the source.
|
* @param callFactory An {@link Call.Factory} for use by the source.
|
||||||
* @param userAgent The User-Agent string that should be used.
|
* @param userAgent The User-Agent string that should be used.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then a {@link InvalidContentTypeException} is thrown from
|
* predicate then a {@link InvalidContentTypeException} is thrown from
|
||||||
|
|
@ -98,10 +98,10 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
* @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control
|
* @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control
|
||||||
* header. For example, you could force the network response for all requests.
|
* header. For example, you could force the network response for all requests.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSource(OkHttpClient client, String userAgent,
|
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
||||||
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
|
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
|
||||||
CacheControl cacheControl) {
|
CacheControl cacheControl) {
|
||||||
this.okHttpClient = Assertions.checkNotNull(client);
|
this.callFactory = Assertions.checkNotNull(callFactory);
|
||||||
this.userAgent = Assertions.checkNotEmpty(userAgent);
|
this.userAgent = Assertions.checkNotEmpty(userAgent);
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
|
|
@ -150,7 +150,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||||
this.bytesSkipped = 0;
|
this.bytesSkipped = 0;
|
||||||
Request request = makeRequest(dataSpec);
|
Request request = makeRequest(dataSpec);
|
||||||
try {
|
try {
|
||||||
response = okHttpClient.newCall(request).execute();
|
response = callFactory.newCall(request).execute();
|
||||||
responseByteStream = response.body().byteStream();
|
responseByteStream = response.body().byteStream();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
|
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -19,26 +19,26 @@ import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import okhttp3.CacheControl;
|
import okhttp3.CacheControl;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.Call;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link Factory} that produces {@link OkHttpDataSource}.
|
* A {@link Factory} that produces {@link OkHttpDataSource}.
|
||||||
*/
|
*/
|
||||||
public final class OkHttpDataSourceFactory implements Factory {
|
public final class OkHttpDataSourceFactory implements Factory {
|
||||||
|
|
||||||
private final OkHttpClient client;
|
private final Call.Factory callFactory;
|
||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
private final TransferListener<? super DataSource> transferListener;
|
private final TransferListener<? super DataSource> transferListener;
|
||||||
private final CacheControl cacheControl;
|
private final CacheControl cacheControl;
|
||||||
|
|
||||||
public OkHttpDataSourceFactory(OkHttpClient client, String userAgent,
|
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
|
||||||
TransferListener<? super DataSource> transferListener) {
|
TransferListener<? super DataSource> transferListener) {
|
||||||
this(client, userAgent, transferListener, null);
|
this(callFactory, userAgent, transferListener, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OkHttpDataSourceFactory(OkHttpClient client, String userAgent,
|
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
|
||||||
TransferListener<? super DataSource> transferListener, CacheControl cacheControl) {
|
TransferListener<? super DataSource> transferListener, CacheControl cacheControl) {
|
||||||
this.client = client;
|
this.callFactory = callFactory;
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.transferListener = transferListener;
|
this.transferListener = transferListener;
|
||||||
this.cacheControl = cacheControl;
|
this.cacheControl = cacheControl;
|
||||||
|
|
@ -46,7 +46,7 @@ public final class OkHttpDataSourceFactory implements Factory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OkHttpDataSource createDataSource() {
|
public OkHttpDataSource createDataSource() {
|
||||||
return new OkHttpDataSource(client, userAgent, null, transferListener, cacheControl);
|
return new OkHttpDataSource(callFactory, userAgent, null, transferListener, cacheControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
package com.google.android.exoplayer2.extractor.ts;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
@ -66,9 +68,12 @@ public class AdtsReaderTest extends TestCase {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
adtsOutput = new FakeTrackOutput();
|
FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
|
||||||
id3Output = new FakeTrackOutput();
|
adtsOutput = fakeExtractorOutput.track(0);
|
||||||
adtsReader = new AdtsReader(adtsOutput, id3Output);
|
id3Output = fakeExtractorOutput.track(1);
|
||||||
|
adtsReader = new AdtsReader(true);
|
||||||
|
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
|
||||||
|
adtsReader.init(fakeExtractorOutput, idGenerator);
|
||||||
data = new ParsableByteArray(TEST_DATA);
|
data = new ParsableByteArray(TEST_DATA);
|
||||||
firstFeed = true;
|
firstFeed = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo;
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
|
|
@ -72,7 +73,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testCustomPesReader() throws Exception {
|
public void testCustomPesReader() throws Exception {
|
||||||
CustomEsReaderFactory factory = new CustomEsReaderFactory();
|
CustomEsReaderFactory factory = new CustomEsReaderFactory();
|
||||||
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory);
|
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
||||||
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
|
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
|
||||||
.setSimulateIOErrors(false)
|
.setSimulateIOErrors(false)
|
||||||
|
|
@ -107,18 +108,25 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
private static final class CustomEsReader extends ElementaryStreamReader {
|
private static final class CustomEsReader extends ElementaryStreamReader {
|
||||||
|
|
||||||
|
private final String language;
|
||||||
|
private TrackOutput output;
|
||||||
public int packetsRead = 0;
|
public int packetsRead = 0;
|
||||||
|
|
||||||
public CustomEsReader(TrackOutput output, String language) {
|
public CustomEsReader(String language) {
|
||||||
super(output);
|
this.language = language;
|
||||||
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
|
|
||||||
language, null, 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seek() {
|
public void seek() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
|
output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
|
||||||
|
language, null, 0));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
}
|
}
|
||||||
|
|
@ -148,16 +156,12 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ElementaryStreamReader onPmtEntry(int pid, int streamType,
|
public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) {
|
||||||
ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) {
|
|
||||||
if (streamType == 3) {
|
if (streamType == 3) {
|
||||||
// We need to manually avoid a duplicate custom reader creation.
|
reader = new CustomEsReader(esInfo.language);
|
||||||
if (reader == null) {
|
|
||||||
reader = new CustomEsReader(output.track(pid), esInfo.language);
|
|
||||||
}
|
|
||||||
return reader;
|
return reader;
|
||||||
} else {
|
} else {
|
||||||
return defaultFactory.onPmtEntry(pid, streamType, esInfo, output);
|
return defaultFactory.createStreamReader(streamType, esInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
|
||||||
/**
|
/**
|
||||||
* The version of the library, expressed as a string.
|
* The version of the library, expressed as a string.
|
||||||
*/
|
*/
|
||||||
String VERSION = "2.0.2";
|
String VERSION = "2.0.3";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library, expressed as an integer.
|
* The version of the library, expressed as an integer.
|
||||||
|
|
@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
|
||||||
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
|
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
|
||||||
* integer version 123045006 (123-045-006).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
int VERSION_INT = 2000002;
|
int VERSION_INT = 2000003;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
private Format audioFormat;
|
private Format audioFormat;
|
||||||
|
|
||||||
private Surface surface;
|
private Surface surface;
|
||||||
|
private boolean ownsSurface;
|
||||||
private SurfaceHolder surfaceHolder;
|
private SurfaceHolder surfaceHolder;
|
||||||
private TextureView textureView;
|
private TextureView textureView;
|
||||||
private TextRenderer.Output textOutput;
|
private TextRenderer.Output textOutput;
|
||||||
|
|
@ -206,7 +207,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
*/
|
*/
|
||||||
public void setVideoSurface(Surface surface) {
|
public void setVideoSurface(Surface surface) {
|
||||||
removeSurfaceCallbacks();
|
removeSurfaceCallbacks();
|
||||||
setVideoSurfaceInternal(surface);
|
setVideoSurfaceInternal(surface, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -219,9 +220,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
removeSurfaceCallbacks();
|
removeSurfaceCallbacks();
|
||||||
this.surfaceHolder = surfaceHolder;
|
this.surfaceHolder = surfaceHolder;
|
||||||
if (surfaceHolder == null) {
|
if (surfaceHolder == null) {
|
||||||
setVideoSurfaceInternal(null);
|
setVideoSurfaceInternal(null, false);
|
||||||
} else {
|
} else {
|
||||||
setVideoSurfaceInternal(surfaceHolder.getSurface());
|
setVideoSurfaceInternal(surfaceHolder.getSurface(), false);
|
||||||
surfaceHolder.addCallback(componentListener);
|
surfaceHolder.addCallback(componentListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,13 +247,13 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
removeSurfaceCallbacks();
|
removeSurfaceCallbacks();
|
||||||
this.textureView = textureView;
|
this.textureView = textureView;
|
||||||
if (textureView == null) {
|
if (textureView == null) {
|
||||||
setVideoSurfaceInternal(null);
|
setVideoSurfaceInternal(null, true);
|
||||||
} else {
|
} else {
|
||||||
if (textureView.getSurfaceTextureListener() != null) {
|
if (textureView.getSurfaceTextureListener() != null) {
|
||||||
Log.w(TAG, "Replacing existing SurfaceTextureListener.");
|
Log.w(TAG, "Replacing existing SurfaceTextureListener.");
|
||||||
}
|
}
|
||||||
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
|
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
|
||||||
setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture));
|
setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
|
||||||
textureView.setSurfaceTextureListener(componentListener);
|
textureView.setSurfaceTextureListener(componentListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -420,8 +421,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
|
||||||
|
|
@ -468,6 +469,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
public void release() {
|
public void release() {
|
||||||
player.release();
|
player.release();
|
||||||
removeSurfaceCallbacks();
|
removeSurfaceCallbacks();
|
||||||
|
if (surface != null) {
|
||||||
|
if (ownsSurface) {
|
||||||
|
surface.release();
|
||||||
|
}
|
||||||
|
surface = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -618,8 +625,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setVideoSurfaceInternal(Surface surface) {
|
private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) {
|
||||||
this.surface = surface;
|
// Note: We don't turn this method into a no-op if the surface is being replaced with itself
|
||||||
|
// so as to ensure onRenderedFirstFrame callbacks are still called in this case.
|
||||||
ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
|
ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (Renderer renderer : renderers) {
|
for (Renderer renderer : renderers) {
|
||||||
|
|
@ -627,12 +635,18 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface);
|
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (surface == null) {
|
if (this.surface != null && this.surface != surface) {
|
||||||
// Block to ensure that the surface is not accessed after the method returns.
|
// If we created this surface, we are responsible for releasing it.
|
||||||
|
if (this.ownsSurface) {
|
||||||
|
this.surface.release();
|
||||||
|
}
|
||||||
|
// We're replacing a surface. Block to ensure that it's not accessed after the method returns.
|
||||||
player.blockingSendMessages(messages);
|
player.blockingSendMessages(messages);
|
||||||
} else {
|
} else {
|
||||||
player.sendMessages(messages);
|
player.sendMessages(messages);
|
||||||
}
|
}
|
||||||
|
this.surface = surface;
|
||||||
|
this.ownsSurface = ownsSurface;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ComponentListener implements VideoRendererEventListener,
|
private final class ComponentListener implements VideoRendererEventListener,
|
||||||
|
|
@ -781,7 +795,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
setVideoSurfaceInternal(holder.getSurface());
|
setVideoSurfaceInternal(holder.getSurface(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -791,14 +805,14 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
setVideoSurfaceInternal(null);
|
setVideoSurfaceInternal(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextureView.SurfaceTextureListener implementation
|
// TextureView.SurfaceTextureListener implementation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
|
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
|
||||||
setVideoSurfaceInternal(new Surface(surfaceTexture));
|
setVideoSurfaceInternal(new Surface(surfaceTexture), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -808,7 +822,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
|
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
|
||||||
setVideoSurface(null);
|
setVideoSurfaceInternal(null, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
||||||
@Override
|
@Override
|
||||||
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
|
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
|
||||||
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
|
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
|
||||||
return executePost(url, null, null);
|
return executePost(url, new byte[0], null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -81,6 +81,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
||||||
url = defaultUrl;
|
url = defaultUrl;
|
||||||
}
|
}
|
||||||
Map<String, String> requestProperties = new HashMap<>();
|
Map<String, String> requestProperties = new HashMap<>();
|
||||||
|
requestProperties.put("Content-Type", "application/octet-stream");
|
||||||
if (C.PLAYREADY_UUID.equals(uuid)) {
|
if (C.PLAYREADY_UUID.equals(uuid)) {
|
||||||
requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
|
requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
|
||||||
}
|
}
|
||||||
|
|
@ -93,8 +94,6 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
||||||
private byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
|
private byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
HttpDataSource dataSource = dataSourceFactory.createDataSource();
|
HttpDataSource dataSource = dataSourceFactory.createDataSource();
|
||||||
// Note: This will be overridden by a Content-Type in requestProperties, if one is set.
|
|
||||||
dataSource.setRequestProperty("Content-Type", "application/octet-stream");
|
|
||||||
if (requestProperties != null) {
|
if (requestProperties != null) {
|
||||||
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
|
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
|
||||||
dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
|
dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ public interface Extractor {
|
||||||
boolean sniff(ExtractorInput input) throws IOException, InterruptedException;
|
boolean sniff(ExtractorInput input) throws IOException, InterruptedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the extractor with an {@link ExtractorOutput}.
|
* Initializes the extractor with an {@link ExtractorOutput}. Called at most once.
|
||||||
*
|
*
|
||||||
* @param output An {@link ExtractorOutput} to receive extracted data.
|
* @param output An {@link ExtractorOutput} to receive extracted data.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -21,18 +21,19 @@ package com.google.android.exoplayer2.extractor;
|
||||||
public interface ExtractorOutput {
|
public interface ExtractorOutput {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the {@link Extractor} identifies the existence of a track in the stream.
|
* Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
|
||||||
* <p>
|
* <p>
|
||||||
* Returns a {@link TrackOutput} that will receive track level data belonging to the track.
|
* The same {@link TrackOutput} is returned if multiple calls are made with the same
|
||||||
|
* {@code trackId}.
|
||||||
*
|
*
|
||||||
* @param trackId A unique track identifier.
|
* @param trackId A track identifier.
|
||||||
* @return The {@link TrackOutput} that should receive track level data belonging to the track.
|
* @return The {@link TrackOutput} for the given track identifier.
|
||||||
*/
|
*/
|
||||||
TrackOutput track(int trackId);
|
TrackOutput track(int trackId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when all tracks have been identified, meaning that {@link #track(int)} will not be
|
* Called when all tracks have been identified, meaning no new {@code trackId} values will be
|
||||||
* called again.
|
* passed to {@link #track(int)}.
|
||||||
*/
|
*/
|
||||||
void endTracks();
|
void endTracks();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,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).
|
||||||
|
|
@ -1004,7 +1005,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) {
|
||||||
|
|
@ -1017,10 +1018,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;
|
||||||
}
|
}
|
||||||
|
|
@ -1032,6 +1033,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) {
|
||||||
|
|
@ -1042,15 +1044,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,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
|
@ -117,7 +118,8 @@ public final class Ac3Extractor implements Extractor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
reader = new Ac3Reader(output.track(0)); // TODO: Add support for embedded ID3.
|
reader = new Ac3Reader(); // TODO: Add support for embedded ID3.
|
||||||
|
reader.init(output, new TrackIdGenerator(0, 1));
|
||||||
output.endTracks();
|
output.endTracks();
|
||||||
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.audio.Ac3Util;
|
import com.google.android.exoplayer2.audio.Ac3Util;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
private final ParsableByteArray headerScratchBytes;
|
private final ParsableByteArray headerScratchBytes;
|
||||||
private final String language;
|
private final String language;
|
||||||
|
|
||||||
|
private TrackOutput output;
|
||||||
|
|
||||||
private int state;
|
private int state;
|
||||||
private int bytesRead;
|
private int bytesRead;
|
||||||
|
|
||||||
|
|
@ -54,21 +57,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new reader for (E-)AC-3 elementary streams.
|
* Constructs a new reader for (E-)AC-3 elementary streams.
|
||||||
*
|
|
||||||
* @param output Track output for extracted samples.
|
|
||||||
*/
|
*/
|
||||||
public Ac3Reader(TrackOutput output) {
|
public Ac3Reader() {
|
||||||
this(output, null);
|
this(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new reader for (E-)AC-3 elementary streams.
|
* Constructs a new reader for (E-)AC-3 elementary streams.
|
||||||
*
|
*
|
||||||
* @param output Track output for extracted samples.
|
|
||||||
* @param language Track language.
|
* @param language Track language.
|
||||||
*/
|
*/
|
||||||
public Ac3Reader(TrackOutput output, String language) {
|
public Ac3Reader(String language) {
|
||||||
super(output);
|
|
||||||
headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
|
headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
|
||||||
headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
|
headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
|
||||||
state = STATE_FINDING_SYNC;
|
state = STATE_FINDING_SYNC;
|
||||||
|
|
@ -82,6 +81,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
lastByteWas0B = false;
|
lastByteWas0B = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
|
||||||
|
output = extractorOutput.track(generator.getNextId());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
|
||||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
@ -126,7 +127,8 @@ public final class AdtsExtractor implements Extractor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
reader = new AdtsReader(output.track(0), output.track(1));
|
reader = new AdtsReader(true);
|
||||||
|
reader.init(output, new TrackIdGenerator(0, 1));
|
||||||
output.endTracks();
|
output.endTracks();
|
||||||
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
|
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
@ -53,11 +55,14 @@ import java.util.Collections;
|
||||||
private static final int ID3_SIZE_OFFSET = 6;
|
private static final int ID3_SIZE_OFFSET = 6;
|
||||||
private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'};
|
private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'};
|
||||||
|
|
||||||
|
private final boolean exposeId3;
|
||||||
private final ParsableBitArray adtsScratch;
|
private final ParsableBitArray adtsScratch;
|
||||||
private final ParsableByteArray id3HeaderBuffer;
|
private final ParsableByteArray id3HeaderBuffer;
|
||||||
private final TrackOutput id3Output;
|
|
||||||
private final String language;
|
private final String language;
|
||||||
|
|
||||||
|
private TrackOutput output;
|
||||||
|
private TrackOutput id3Output;
|
||||||
|
|
||||||
private int state;
|
private int state;
|
||||||
private int bytesRead;
|
private int bytesRead;
|
||||||
|
|
||||||
|
|
@ -77,26 +82,21 @@ import java.util.Collections;
|
||||||
private long currentSampleDuration;
|
private long currentSampleDuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param output A {@link TrackOutput} to which AAC samples should be written.
|
* @param exposeId3 True if the reader should expose ID3 information.
|
||||||
* @param id3Output A {@link TrackOutput} to which ID3 samples should be written.
|
|
||||||
*/
|
*/
|
||||||
public AdtsReader(TrackOutput output, TrackOutput id3Output) {
|
public AdtsReader(boolean exposeId3) {
|
||||||
this(output, id3Output, null);
|
this(exposeId3, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param output A {@link TrackOutput} to which AAC samples should be written.
|
* @param exposeId3 True if the reader should expose ID3 information.
|
||||||
* @param id3Output A {@link TrackOutput} to which ID3 samples should be written.
|
|
||||||
* @param language Track language.
|
* @param language Track language.
|
||||||
*/
|
*/
|
||||||
public AdtsReader(TrackOutput output, TrackOutput id3Output, String language) {
|
public AdtsReader(boolean exposeId3, String language) {
|
||||||
super(output);
|
|
||||||
this.id3Output = id3Output;
|
|
||||||
id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null,
|
|
||||||
Format.NO_VALUE, null));
|
|
||||||
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
|
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
|
||||||
id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
|
id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
|
||||||
setFindingSampleState();
|
setFindingSampleState();
|
||||||
|
this.exposeId3 = exposeId3;
|
||||||
this.language = language;
|
this.language = language;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,6 +105,18 @@ import java.util.Collections;
|
||||||
setFindingSampleState();
|
setFindingSampleState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
|
output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
if (exposeId3) {
|
||||||
|
id3Output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null,
|
||||||
|
Format.NO_VALUE, null));
|
||||||
|
} else {
|
||||||
|
id3Output = new DummyTrackOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@
|
||||||
package com.google.android.exoplayer2.extractor.ts;
|
package com.google.android.exoplayer2.extractor.ts;
|
||||||
|
|
||||||
import android.support.annotation.IntDef;
|
import android.support.annotation.IntDef;
|
||||||
import android.util.SparseBooleanArray;
|
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo;
|
||||||
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
|
@ -28,80 +26,54 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
public final class DefaultStreamReaderFactory implements ElementaryStreamReader.Factory {
|
public final class DefaultStreamReaderFactory implements ElementaryStreamReader.Factory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags controlling what workarounds are enabled for elementary stream readers.
|
* Flags controlling elementary stream readers behaviour.
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef(flag = true, value = {WORKAROUND_ALLOW_NON_IDR_KEYFRAMES, WORKAROUND_IGNORE_AAC_STREAM,
|
@IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM,
|
||||||
WORKAROUND_IGNORE_H264_STREAM, WORKAROUND_DETECT_ACCESS_UNITS, WORKAROUND_MAP_BY_TYPE})
|
FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS})
|
||||||
public @interface WorkaroundFlags {
|
public @interface Flags {
|
||||||
}
|
}
|
||||||
public static final int WORKAROUND_ALLOW_NON_IDR_KEYFRAMES = 1;
|
public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;
|
||||||
public static final int WORKAROUND_IGNORE_AAC_STREAM = 2;
|
public static final int FLAG_IGNORE_AAC_STREAM = 2;
|
||||||
public static final int WORKAROUND_IGNORE_H264_STREAM = 4;
|
public static final int FLAG_IGNORE_H264_STREAM = 4;
|
||||||
public static final int WORKAROUND_DETECT_ACCESS_UNITS = 8;
|
public static final int FLAG_DETECT_ACCESS_UNITS = 8;
|
||||||
public static final int WORKAROUND_MAP_BY_TYPE = 16;
|
|
||||||
|
|
||||||
private static final int BASE_EMBEDDED_TRACK_ID = 0x2000; // 0xFF + 1.
|
@Flags
|
||||||
|
private final int flags;
|
||||||
private final SparseBooleanArray trackIds;
|
|
||||||
@WorkaroundFlags
|
|
||||||
private final int workaroundFlags;
|
|
||||||
private Id3Reader id3Reader;
|
|
||||||
private int nextEmbeddedTrackId = BASE_EMBEDDED_TRACK_ID;
|
|
||||||
|
|
||||||
public DefaultStreamReaderFactory() {
|
public DefaultStreamReaderFactory() {
|
||||||
this(0);
|
this(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefaultStreamReaderFactory(int workaroundFlags) {
|
public DefaultStreamReaderFactory(@Flags int flags) {
|
||||||
trackIds = new SparseBooleanArray();
|
this.flags = flags;
|
||||||
this.workaroundFlags = workaroundFlags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ElementaryStreamReader onPmtEntry(int pid, int streamType,
|
public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) {
|
||||||
ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) {
|
|
||||||
|
|
||||||
if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0 && id3Reader == null) {
|
|
||||||
// Setup an ID3 track regardless of whether there's a corresponding entry, in case one
|
|
||||||
// appears intermittently during playback. See b/20261500.
|
|
||||||
id3Reader = new Id3Reader(output.track(TsExtractor.TS_STREAM_TYPE_ID3));
|
|
||||||
}
|
|
||||||
int trackId = (workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0 ? streamType : pid;
|
|
||||||
if (trackIds.get(trackId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
trackIds.put(trackId, true);
|
|
||||||
switch (streamType) {
|
switch (streamType) {
|
||||||
case TsExtractor.TS_STREAM_TYPE_MPA:
|
case TsExtractor.TS_STREAM_TYPE_MPA:
|
||||||
case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
|
case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
|
||||||
return new MpegAudioReader(output.track(trackId), esInfo.language);
|
return new MpegAudioReader(esInfo.language);
|
||||||
case TsExtractor.TS_STREAM_TYPE_AAC:
|
case TsExtractor.TS_STREAM_TYPE_AAC:
|
||||||
return (workaroundFlags & WORKAROUND_IGNORE_AAC_STREAM) != 0 ? null
|
return (flags & FLAG_IGNORE_AAC_STREAM) != 0 ? null
|
||||||
: new AdtsReader(output.track(trackId), new DummyTrackOutput(), esInfo.language);
|
: new AdtsReader(false, esInfo.language);
|
||||||
case TsExtractor.TS_STREAM_TYPE_AC3:
|
case TsExtractor.TS_STREAM_TYPE_AC3:
|
||||||
case TsExtractor.TS_STREAM_TYPE_E_AC3:
|
case TsExtractor.TS_STREAM_TYPE_E_AC3:
|
||||||
return new Ac3Reader(output.track(trackId), esInfo.language);
|
return new Ac3Reader(esInfo.language);
|
||||||
case TsExtractor.TS_STREAM_TYPE_DTS:
|
case TsExtractor.TS_STREAM_TYPE_DTS:
|
||||||
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
|
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
|
||||||
return new DtsReader(output.track(trackId), esInfo.language);
|
return new DtsReader(esInfo.language);
|
||||||
case TsExtractor.TS_STREAM_TYPE_H262:
|
case TsExtractor.TS_STREAM_TYPE_H262:
|
||||||
return new H262Reader(output.track(trackId));
|
return new H262Reader();
|
||||||
case TsExtractor.TS_STREAM_TYPE_H264:
|
case TsExtractor.TS_STREAM_TYPE_H264:
|
||||||
return (workaroundFlags & WORKAROUND_IGNORE_H264_STREAM) != 0
|
return (flags & FLAG_IGNORE_H264_STREAM) != 0 ? null
|
||||||
? null : new H264Reader(output.track(trackId),
|
: new H264Reader((flags & FLAG_ALLOW_NON_IDR_KEYFRAMES) != 0,
|
||||||
new SeiReader(output.track(nextEmbeddedTrackId++)),
|
(flags & FLAG_DETECT_ACCESS_UNITS) != 0);
|
||||||
(workaroundFlags & WORKAROUND_ALLOW_NON_IDR_KEYFRAMES) != 0,
|
|
||||||
(workaroundFlags & WORKAROUND_DETECT_ACCESS_UNITS) != 0);
|
|
||||||
case TsExtractor.TS_STREAM_TYPE_H265:
|
case TsExtractor.TS_STREAM_TYPE_H265:
|
||||||
return new H265Reader(output.track(trackId),
|
return new H265Reader();
|
||||||
new SeiReader(output.track(nextEmbeddedTrackId++)));
|
|
||||||
case TsExtractor.TS_STREAM_TYPE_ID3:
|
case TsExtractor.TS_STREAM_TYPE_ID3:
|
||||||
if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0) {
|
return new Id3Reader();
|
||||||
return id3Reader;
|
|
||||||
} else {
|
|
||||||
return new Id3Reader(output.track(nextEmbeddedTrackId++));
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.audio.DtsUtil;
|
import com.google.android.exoplayer2.audio.DtsUtil;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
||||||
|
|
@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
private final ParsableByteArray headerScratchBytes;
|
private final ParsableByteArray headerScratchBytes;
|
||||||
private final String language;
|
private final String language;
|
||||||
|
|
||||||
|
private TrackOutput output;
|
||||||
|
|
||||||
private int state;
|
private int state;
|
||||||
private int bytesRead;
|
private int bytesRead;
|
||||||
|
|
||||||
|
|
@ -54,20 +57,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
/**
|
/**
|
||||||
* Constructs a new reader for DTS elementary streams.
|
* Constructs a new reader for DTS elementary streams.
|
||||||
*
|
*
|
||||||
* @param output Track output for extracted samples.
|
|
||||||
*/
|
|
||||||
public DtsReader(TrackOutput output) {
|
|
||||||
this(output, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new reader for DTS elementary streams.
|
|
||||||
*
|
|
||||||
* @param output Track output for extracted samples.
|
|
||||||
* @param language Track language.
|
* @param language Track language.
|
||||||
*/
|
*/
|
||||||
public DtsReader(TrackOutput output, String language) {
|
public DtsReader(String language) {
|
||||||
super(output);
|
|
||||||
headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
|
headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
|
||||||
headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF);
|
headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF);
|
||||||
headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF);
|
headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF);
|
||||||
|
|
@ -84,6 +76,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
syncBytes = 0;
|
syncBytes = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
|
output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,12 @@ public abstract class ElementaryStreamReader {
|
||||||
* Returns an {@link ElementaryStreamReader} for a given PMT entry. May return null if the
|
* Returns an {@link ElementaryStreamReader} for a given PMT entry. May return null if the
|
||||||
* stream type is not supported or if the stream already has a reader assigned to it.
|
* stream type is not supported or if the stream already has a reader assigned to it.
|
||||||
*
|
*
|
||||||
* @param pid The pid for the PMT entry.
|
* @param streamType Stream type value as defined in the PMT entry or associated descriptors.
|
||||||
* @param streamType One of the {@link TsExtractor}{@code .TS_STREAM_TYPE_*} constants defining
|
* @param esInfo Information associated to the elementary stream provided in the PMT.
|
||||||
* the type of the stream.
|
|
||||||
* @param esInfo The descriptor information linked to the elementary stream.
|
|
||||||
* @param output The {@link ExtractorOutput} that provides the {@link TrackOutput}s for the
|
|
||||||
* created readers.
|
|
||||||
* @return An {@link ElementaryStreamReader} for the elementary streams carried by the provided
|
* @return An {@link ElementaryStreamReader} for the elementary streams carried by the provided
|
||||||
* pid. {@code null} if the stream is not supported or if it should be ignored.
|
* pid. {@code null} if the stream is not supported or if it should be ignored.
|
||||||
*/
|
*/
|
||||||
ElementaryStreamReader onPmtEntry(int pid, int streamType, EsInfo esInfo,
|
ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo);
|
||||||
ExtractorOutput output);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,13 +65,24 @@ public abstract class ElementaryStreamReader {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final TrackOutput output;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param output A {@link TrackOutput} to which samples should be written.
|
* Generates track ids for initializing {@link ElementaryStreamReader}s' {@link TrackOutput}s.
|
||||||
*/
|
*/
|
||||||
protected ElementaryStreamReader(TrackOutput output) {
|
public static final class TrackIdGenerator {
|
||||||
this.output = output;
|
|
||||||
|
private final int firstId;
|
||||||
|
private final int idIncrement;
|
||||||
|
private int generatedIdCount;
|
||||||
|
|
||||||
|
public TrackIdGenerator(int firstId, int idIncrement) {
|
||||||
|
this.firstId = firstId;
|
||||||
|
this.idIncrement = idIncrement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNextId() {
|
||||||
|
return firstId + idIncrement * generatedIdCount++;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -84,6 +90,15 @@ public abstract class ElementaryStreamReader {
|
||||||
*/
|
*/
|
||||||
public abstract void seek();
|
public abstract void seek();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the reader by providing outputs and ids for the tracks.
|
||||||
|
*
|
||||||
|
* @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
|
||||||
|
* @param idGenerator A {@link TrackIdGenerator} that generates unique track ids for the
|
||||||
|
* {@link TrackOutput}s.
|
||||||
|
*/
|
||||||
|
public abstract void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a packet starts.
|
* Called when a packet starts.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||||
|
|
@ -35,6 +36,8 @@ import java.util.Collections;
|
||||||
private static final int START_EXTENSION = 0xB5;
|
private static final int START_EXTENSION = 0xB5;
|
||||||
private static final int START_GROUP = 0xB8;
|
private static final int START_GROUP = 0xB8;
|
||||||
|
|
||||||
|
private TrackOutput output;
|
||||||
|
|
||||||
// Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.
|
// Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.
|
||||||
private static final double[] FRAME_RATE_VALUES = new double[] {
|
private static final double[] FRAME_RATE_VALUES = new double[] {
|
||||||
24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};
|
24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};
|
||||||
|
|
@ -58,8 +61,7 @@ import java.util.Collections;
|
||||||
private long framePosition;
|
private long framePosition;
|
||||||
private long frameTimeUs;
|
private long frameTimeUs;
|
||||||
|
|
||||||
public H262Reader(TrackOutput output) {
|
public H262Reader() {
|
||||||
super(output);
|
|
||||||
prefixFlags = new boolean[4];
|
prefixFlags = new boolean[4];
|
||||||
csdBuffer = new CsdBuffer(128);
|
csdBuffer = new CsdBuffer(128);
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +75,11 @@ import java.util.Collections;
|
||||||
totalBytesWritten = 0;
|
totalBytesWritten = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
|
output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET;
|
pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||||
|
|
@ -37,17 +38,20 @@ import java.util.List;
|
||||||
private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
|
private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
|
||||||
private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
|
private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
|
||||||
|
|
||||||
// State that should not be reset on seek.
|
private final boolean allowNonIdrKeyframes;
|
||||||
private boolean hasOutputFormat;
|
private final boolean detectAccessUnits;
|
||||||
|
|
||||||
// State that should be reset on seek.
|
|
||||||
private final SeiReader seiReader;
|
|
||||||
private final boolean[] prefixFlags;
|
|
||||||
private final SampleReader sampleReader;
|
|
||||||
private final NalUnitTargetBuffer sps;
|
private final NalUnitTargetBuffer sps;
|
||||||
private final NalUnitTargetBuffer pps;
|
private final NalUnitTargetBuffer pps;
|
||||||
private final NalUnitTargetBuffer sei;
|
private final NalUnitTargetBuffer sei;
|
||||||
private long totalBytesWritten;
|
private long totalBytesWritten;
|
||||||
|
private final boolean[] prefixFlags;
|
||||||
|
|
||||||
|
private TrackOutput output;
|
||||||
|
private SeiReader seiReader;
|
||||||
|
private SampleReader sampleReader;
|
||||||
|
|
||||||
|
// State that should not be reset on seek.
|
||||||
|
private boolean hasOutputFormat;
|
||||||
|
|
||||||
// Per packet state that gets reset at the start of each packet.
|
// Per packet state that gets reset at the start of each packet.
|
||||||
private long pesTimeUs;
|
private long pesTimeUs;
|
||||||
|
|
@ -56,19 +60,15 @@ import java.util.List;
|
||||||
private final ParsableByteArray seiWrapper;
|
private final ParsableByteArray seiWrapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param output A {@link TrackOutput} to which H.264 samples should be written.
|
|
||||||
* @param seiReader A reader for CEA-608 samples in SEI NAL units.
|
|
||||||
* @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as
|
* @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as
|
||||||
* synchronization samples (key-frames).
|
* synchronization samples (key-frames).
|
||||||
* @param detectAccessUnits Whether to split the input stream into access units (samples) based on
|
* @param detectAccessUnits Whether to split the input stream into access units (samples) based on
|
||||||
* slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs).
|
* slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs).
|
||||||
*/
|
*/
|
||||||
public H264Reader(TrackOutput output, SeiReader seiReader, boolean allowNonIdrKeyframes,
|
public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
|
||||||
boolean detectAccessUnits) {
|
|
||||||
super(output);
|
|
||||||
this.seiReader = seiReader;
|
|
||||||
prefixFlags = new boolean[3];
|
prefixFlags = new boolean[3];
|
||||||
sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);
|
this.allowNonIdrKeyframes = allowNonIdrKeyframes;
|
||||||
|
this.detectAccessUnits = detectAccessUnits;
|
||||||
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
|
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
|
||||||
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
|
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
|
||||||
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
|
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
|
||||||
|
|
@ -85,6 +85,13 @@ import java.util.List;
|
||||||
totalBytesWritten = 0;
|
totalBytesWritten = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
|
output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);
|
||||||
|
seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
this.pesTimeUs = pesTimeUs;
|
this.pesTimeUs = pesTimeUs;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||||
|
|
@ -42,11 +43,13 @@ import java.util.Collections;
|
||||||
private static final int PREFIX_SEI_NUT = 39;
|
private static final int PREFIX_SEI_NUT = 39;
|
||||||
private static final int SUFFIX_SEI_NUT = 40;
|
private static final int SUFFIX_SEI_NUT = 40;
|
||||||
|
|
||||||
|
private TrackOutput output;
|
||||||
|
private SeiReader seiReader;
|
||||||
|
|
||||||
// State that should not be reset on seek.
|
// State that should not be reset on seek.
|
||||||
private boolean hasOutputFormat;
|
private boolean hasOutputFormat;
|
||||||
|
|
||||||
// State that should be reset on seek.
|
// State that should be reset on seek.
|
||||||
private final SeiReader seiReader;
|
|
||||||
private final boolean[] prefixFlags;
|
private final boolean[] prefixFlags;
|
||||||
private final NalUnitTargetBuffer vps;
|
private final NalUnitTargetBuffer vps;
|
||||||
private final NalUnitTargetBuffer sps;
|
private final NalUnitTargetBuffer sps;
|
||||||
|
|
@ -62,13 +65,7 @@ import java.util.Collections;
|
||||||
// Scratch variables to avoid allocations.
|
// Scratch variables to avoid allocations.
|
||||||
private final ParsableByteArray seiWrapper;
|
private final ParsableByteArray seiWrapper;
|
||||||
|
|
||||||
/**
|
public H265Reader() {
|
||||||
* @param output A {@link TrackOutput} to which H.265 samples should be written.
|
|
||||||
* @param seiReader A reader for CEA-608 samples in SEI NAL units.
|
|
||||||
*/
|
|
||||||
public H265Reader(TrackOutput output, SeiReader seiReader) {
|
|
||||||
super(output);
|
|
||||||
this.seiReader = seiReader;
|
|
||||||
prefixFlags = new boolean[3];
|
prefixFlags = new boolean[3];
|
||||||
vps = new NalUnitTargetBuffer(VPS_NUT, 128);
|
vps = new NalUnitTargetBuffer(VPS_NUT, 128);
|
||||||
sps = new NalUnitTargetBuffer(SPS_NUT, 128);
|
sps = new NalUnitTargetBuffer(SPS_NUT, 128);
|
||||||
|
|
@ -91,6 +88,12 @@ import java.util.Collections;
|
||||||
totalBytesWritten = 0;
|
totalBytesWritten = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
|
output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
this.pesTimeUs = pesTimeUs;
|
this.pesTimeUs = pesTimeUs;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
@ -30,6 +31,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
||||||
private final ParsableByteArray id3Header;
|
private final ParsableByteArray id3Header;
|
||||||
|
|
||||||
|
private TrackOutput output;
|
||||||
|
|
||||||
// State that should be reset on seek.
|
// State that should be reset on seek.
|
||||||
private boolean writingSample;
|
private boolean writingSample;
|
||||||
|
|
||||||
|
|
@ -38,10 +41,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
private int sampleSize;
|
private int sampleSize;
|
||||||
private int sampleBytesRead;
|
private int sampleBytesRead;
|
||||||
|
|
||||||
public Id3Reader(TrackOutput output) {
|
public Id3Reader() {
|
||||||
super(output);
|
|
||||||
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE,
|
|
||||||
null));
|
|
||||||
id3Header = new ParsableByteArray(ID3_HEADER_SIZE);
|
id3Header = new ParsableByteArray(ID3_HEADER_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +50,13 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
writingSample = false;
|
writingSample = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
|
output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE,
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
if (!dataAlignmentIndicator) {
|
if (!dataAlignmentIndicator) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
@ -36,6 +37,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
private final MpegAudioHeader header;
|
private final MpegAudioHeader header;
|
||||||
private final String language;
|
private final String language;
|
||||||
|
|
||||||
|
private TrackOutput output;
|
||||||
|
|
||||||
private int state;
|
private int state;
|
||||||
private int frameBytesRead;
|
private int frameBytesRead;
|
||||||
private boolean hasOutputFormat;
|
private boolean hasOutputFormat;
|
||||||
|
|
@ -50,12 +53,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
// The timestamp to attach to the next sample in the current packet.
|
// The timestamp to attach to the next sample in the current packet.
|
||||||
private long timeUs;
|
private long timeUs;
|
||||||
|
|
||||||
public MpegAudioReader(TrackOutput output) {
|
public MpegAudioReader() {
|
||||||
this(output, null);
|
this(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MpegAudioReader(TrackOutput output, String language) {
|
public MpegAudioReader(String language) {
|
||||||
super(output);
|
|
||||||
state = STATE_FINDING_HEADER;
|
state = STATE_FINDING_HEADER;
|
||||||
// The first byte of an MPEG Audio frame header is always 0xFF.
|
// The first byte of an MPEG Audio frame header is always 0xFF.
|
||||||
headerScratch = new ParsableByteArray(4);
|
headerScratch = new ParsableByteArray(4);
|
||||||
|
|
@ -71,6 +73,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
lastByteWasFF = false;
|
lastByteWasFF = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||||
|
output = extractorOutput.track(idGenerator.getNextId());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||||
timeUs = pesTimeUs;
|
timeUs = pesTimeUs;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
|
||||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -49,6 +50,7 @@ public final class PsExtractor implements Extractor {
|
||||||
private static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
|
private static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
|
||||||
private static final int PACKET_START_CODE_PREFIX = 0x000001;
|
private static final int PACKET_START_CODE_PREFIX = 0x000001;
|
||||||
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
|
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
|
||||||
|
private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
|
||||||
private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
|
private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
|
||||||
|
|
||||||
public static final int PRIVATE_STREAM_1 = 0xBD;
|
public static final int PRIVATE_STREAM_1 = 0xBD;
|
||||||
|
|
@ -189,16 +191,18 @@ public final class PsExtractor implements Extractor {
|
||||||
// Private stream, used for AC3 audio.
|
// Private stream, used for AC3 audio.
|
||||||
// NOTE: This may need further parsing to determine if its DTS, but that's likely only
|
// NOTE: This may need further parsing to determine if its DTS, but that's likely only
|
||||||
// valid for DVDs.
|
// valid for DVDs.
|
||||||
elementaryStreamReader = new Ac3Reader(output.track(streamId));
|
elementaryStreamReader = new Ac3Reader();
|
||||||
foundAudioTrack = true;
|
foundAudioTrack = true;
|
||||||
} else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
|
} else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
|
||||||
elementaryStreamReader = new MpegAudioReader(output.track(streamId));
|
elementaryStreamReader = new MpegAudioReader();
|
||||||
foundAudioTrack = true;
|
foundAudioTrack = true;
|
||||||
} else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
|
} else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
|
||||||
elementaryStreamReader = new H262Reader(output.track(streamId));
|
elementaryStreamReader = new H262Reader();
|
||||||
foundVideoTrack = true;
|
foundVideoTrack = true;
|
||||||
}
|
}
|
||||||
if (elementaryStreamReader != null) {
|
if (elementaryStreamReader != null) {
|
||||||
|
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
|
||||||
|
elementaryStreamReader.init(output, idGenerator);
|
||||||
payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster);
|
payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster);
|
||||||
psPayloadReaders.put(streamId, payloadReader);
|
psPayloadReaders.put(streamId, payloadReader);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
|
import android.util.SparseBooleanArray;
|
||||||
import android.util.SparseIntArray;
|
import android.util.SparseIntArray;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
|
|
@ -26,6 +27,9 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||||
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo;
|
||||||
|
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
|
|
@ -50,12 +54,6 @@ public final class TsExtractor implements Extractor {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final String TAG = "TsExtractor";
|
|
||||||
|
|
||||||
private static final int TS_PACKET_SIZE = 188;
|
|
||||||
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
|
|
||||||
private static final int TS_PAT_PID = 0;
|
|
||||||
|
|
||||||
public static final int TS_STREAM_TYPE_MPA = 0x03;
|
public static final int TS_STREAM_TYPE_MPA = 0x03;
|
||||||
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
|
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
|
||||||
public static final int TS_STREAM_TYPE_AAC = 0x0F;
|
public static final int TS_STREAM_TYPE_AAC = 0x0F;
|
||||||
|
|
@ -68,6 +66,12 @@ public final class TsExtractor implements Extractor {
|
||||||
public static final int TS_STREAM_TYPE_H265 = 0x24;
|
public static final int TS_STREAM_TYPE_H265 = 0x24;
|
||||||
public static final int TS_STREAM_TYPE_ID3 = 0x15;
|
public static final int TS_STREAM_TYPE_ID3 = 0x15;
|
||||||
|
|
||||||
|
private static final String TAG = "TsExtractor";
|
||||||
|
|
||||||
|
private static final int TS_PACKET_SIZE = 188;
|
||||||
|
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
|
||||||
|
private static final int TS_PAT_PID = 0;
|
||||||
|
private static final int MAX_PID_PLUS_ONE = 0x2000;
|
||||||
|
|
||||||
private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3");
|
private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3");
|
||||||
private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3");
|
private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3");
|
||||||
|
|
@ -76,15 +80,19 @@ public final class TsExtractor implements Extractor {
|
||||||
private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2
|
private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2
|
||||||
private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
|
private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
|
||||||
|
|
||||||
|
private final boolean mapByType;
|
||||||
private final TimestampAdjuster timestampAdjuster;
|
private final TimestampAdjuster timestampAdjuster;
|
||||||
private final ParsableByteArray tsPacketBuffer;
|
private final ParsableByteArray tsPacketBuffer;
|
||||||
private final ParsableBitArray tsScratch;
|
private final ParsableBitArray tsScratch;
|
||||||
private final SparseIntArray continuityCounters;
|
private final SparseIntArray continuityCounters;
|
||||||
private final ElementaryStreamReader.Factory streamReaderFactory;
|
private final ElementaryStreamReader.Factory streamReaderFactory;
|
||||||
/* package */ final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
|
||||||
|
private final SparseBooleanArray trackIds;
|
||||||
|
|
||||||
// Accessed only by the loading thread.
|
// Accessed only by the loading thread.
|
||||||
private ExtractorOutput output;
|
private ExtractorOutput output;
|
||||||
|
private boolean tracksEnded;
|
||||||
|
private ElementaryStreamReader id3Reader;
|
||||||
|
|
||||||
public TsExtractor() {
|
public TsExtractor() {
|
||||||
this(new TimestampAdjuster(0));
|
this(new TimestampAdjuster(0));
|
||||||
|
|
@ -94,22 +102,26 @@ public final class TsExtractor implements Extractor {
|
||||||
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
|
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
|
||||||
*/
|
*/
|
||||||
public TsExtractor(TimestampAdjuster timestampAdjuster) {
|
public TsExtractor(TimestampAdjuster timestampAdjuster) {
|
||||||
this(timestampAdjuster, new DefaultStreamReaderFactory());
|
this(timestampAdjuster, new DefaultStreamReaderFactory(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
|
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
|
||||||
* @param customReaderFactory Factory for injecting a custom set of elementary stream readers.
|
* @param customReaderFactory Factory for injecting a custom set of elementary stream readers.
|
||||||
|
* @param mapByType True if {@link TrackOutput}s should be mapped by their type, false to map them
|
||||||
|
* by their PID.
|
||||||
*/
|
*/
|
||||||
public TsExtractor(TimestampAdjuster timestampAdjuster,
|
public TsExtractor(TimestampAdjuster timestampAdjuster,
|
||||||
ElementaryStreamReader.Factory customReaderFactory) {
|
ElementaryStreamReader.Factory customReaderFactory, boolean mapByType) {
|
||||||
this.timestampAdjuster = timestampAdjuster;
|
this.timestampAdjuster = timestampAdjuster;
|
||||||
this.streamReaderFactory = Assertions.checkNotNull(customReaderFactory);
|
this.streamReaderFactory = Assertions.checkNotNull(customReaderFactory);
|
||||||
|
this.mapByType = mapByType;
|
||||||
tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
|
tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
|
||||||
tsScratch = new ParsableBitArray(new byte[3]);
|
tsScratch = new ParsableBitArray(new byte[3]);
|
||||||
|
trackIds = new SparseBooleanArray();
|
||||||
tsPayloadReaders = new SparseArray<>();
|
tsPayloadReaders = new SparseArray<>();
|
||||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
|
||||||
continuityCounters = new SparseIntArray();
|
continuityCounters = new SparseIntArray();
|
||||||
|
resetPayloadReaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extractor implementation.
|
// Extractor implementation.
|
||||||
|
|
@ -141,11 +153,10 @@ public final class TsExtractor implements Extractor {
|
||||||
@Override
|
@Override
|
||||||
public void seek(long position) {
|
public void seek(long position) {
|
||||||
timestampAdjuster.reset();
|
timestampAdjuster.reset();
|
||||||
for (int i = 0; i < tsPayloadReaders.size(); i++) {
|
|
||||||
tsPayloadReaders.valueAt(i).seek();
|
|
||||||
}
|
|
||||||
tsPacketBuffer.reset();
|
tsPacketBuffer.reset();
|
||||||
continuityCounters.clear();
|
continuityCounters.clear();
|
||||||
|
// Elementary stream readers' state should be cleared to get consistent behaviours when seeking.
|
||||||
|
resetPayloadReaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -240,6 +251,13 @@ public final class TsExtractor implements Extractor {
|
||||||
|
|
||||||
// Internals.
|
// Internals.
|
||||||
|
|
||||||
|
private void resetPayloadReaders() {
|
||||||
|
trackIds.clear();
|
||||||
|
tsPayloadReaders.clear();
|
||||||
|
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
||||||
|
id3Reader = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses TS packet payload data.
|
* Parses TS packet payload data.
|
||||||
*/
|
*/
|
||||||
|
|
@ -333,7 +351,7 @@ public final class TsExtractor implements Extractor {
|
||||||
patScratch.skipBits(13); // network_PID (13)
|
patScratch.skipBits(13); // network_PID (13)
|
||||||
} else {
|
} else {
|
||||||
int pid = patScratch.readBits(13);
|
int pid = patScratch.readBits(13);
|
||||||
tsPayloadReaders.put(pid, new PmtReader());
|
tsPayloadReaders.put(pid, new PmtReader(pid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -353,14 +371,16 @@ public final class TsExtractor implements Extractor {
|
||||||
|
|
||||||
private final ParsableBitArray pmtScratch;
|
private final ParsableBitArray pmtScratch;
|
||||||
private final ParsableByteArray sectionData;
|
private final ParsableByteArray sectionData;
|
||||||
|
private final int pid;
|
||||||
|
|
||||||
private int sectionLength;
|
private int sectionLength;
|
||||||
private int sectionBytesRead;
|
private int sectionBytesRead;
|
||||||
private int crc;
|
private int crc;
|
||||||
|
|
||||||
public PmtReader() {
|
public PmtReader(int pid) {
|
||||||
pmtScratch = new ParsableBitArray(new byte[5]);
|
pmtScratch = new ParsableBitArray(new byte[5]);
|
||||||
sectionData = new ParsableByteArray();
|
sectionData = new ParsableByteArray();
|
||||||
|
this.pid = pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -413,6 +433,14 @@ public final class TsExtractor implements Extractor {
|
||||||
// Skip the descriptors.
|
// Skip the descriptors.
|
||||||
sectionData.skipBytes(programInfoLength);
|
sectionData.skipBytes(programInfoLength);
|
||||||
|
|
||||||
|
if (mapByType && id3Reader == null) {
|
||||||
|
// Setup an ID3 track regardless of whether there's a corresponding entry, in case one
|
||||||
|
// appears intermittently during playback. See [Internal: b/20261500].
|
||||||
|
EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]);
|
||||||
|
id3Reader = streamReaderFactory.createStreamReader(TS_STREAM_TYPE_ID3, dummyEsInfo);
|
||||||
|
id3Reader.init(output, new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
|
||||||
|
}
|
||||||
|
|
||||||
int remainingEntriesLength = sectionLength - 9 /* Length of fields before descriptors */
|
int remainingEntriesLength = sectionLength - 9 /* Length of fields before descriptors */
|
||||||
- programInfoLength - 4 /* CRC length */;
|
- programInfoLength - 4 /* CRC length */;
|
||||||
while (remainingEntriesLength > 0) {
|
while (remainingEntriesLength > 0) {
|
||||||
|
|
@ -422,21 +450,40 @@ public final class TsExtractor implements Extractor {
|
||||||
int elementaryPid = pmtScratch.readBits(13);
|
int elementaryPid = pmtScratch.readBits(13);
|
||||||
pmtScratch.skipBits(4); // reserved
|
pmtScratch.skipBits(4); // reserved
|
||||||
int esInfoLength = pmtScratch.readBits(12); // ES_info_length.
|
int esInfoLength = pmtScratch.readBits(12); // ES_info_length.
|
||||||
ElementaryStreamReader.EsInfo esInfo = readEsInfo(sectionData, esInfoLength);
|
EsInfo esInfo = readEsInfo(sectionData, esInfoLength);
|
||||||
if (streamType == 0x06) {
|
if (streamType == 0x06) {
|
||||||
streamType = esInfo.streamType;
|
streamType = esInfo.streamType;
|
||||||
}
|
}
|
||||||
remainingEntriesLength -= esInfoLength + 5;
|
remainingEntriesLength -= esInfoLength + 5;
|
||||||
ElementaryStreamReader pesPayloadReader = streamReaderFactory.onPmtEntry(elementaryPid,
|
|
||||||
streamType, esInfo, output);
|
int trackId = mapByType ? streamType : elementaryPid;
|
||||||
|
if (trackIds.get(trackId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
trackIds.put(trackId, true);
|
||||||
|
|
||||||
|
ElementaryStreamReader pesPayloadReader;
|
||||||
|
if (mapByType && streamType == TS_STREAM_TYPE_ID3) {
|
||||||
|
pesPayloadReader = id3Reader;
|
||||||
|
} else {
|
||||||
|
pesPayloadReader = streamReaderFactory.createStreamReader(streamType, esInfo);
|
||||||
|
pesPayloadReader.init(output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE));
|
||||||
|
}
|
||||||
|
|
||||||
if (pesPayloadReader != null) {
|
if (pesPayloadReader != null) {
|
||||||
tsPayloadReaders.put(elementaryPid,
|
tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader, timestampAdjuster));
|
||||||
new PesReader(pesPayloadReader, timestampAdjuster));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (mapByType) {
|
||||||
output.endTracks();
|
if (!tracksEnded) {
|
||||||
|
output.endTracks();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tsPayloadReaders.remove(TS_PAT_PID);
|
||||||
|
tsPayloadReaders.remove(pid);
|
||||||
|
output.endTracks();
|
||||||
|
}
|
||||||
|
tracksEnded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -447,7 +494,7 @@ public final class TsExtractor implements Extractor {
|
||||||
* @param length The length of descriptors to read from the current position in {@code data}.
|
* @param length The length of descriptors to read from the current position in {@code data}.
|
||||||
* @return The stream info read from the available descriptors.
|
* @return The stream info read from the available descriptors.
|
||||||
*/
|
*/
|
||||||
private ElementaryStreamReader.EsInfo readEsInfo(ParsableByteArray data, int length) {
|
private EsInfo readEsInfo(ParsableByteArray data, int length) {
|
||||||
int descriptorsStartPosition = data.getPosition();
|
int descriptorsStartPosition = data.getPosition();
|
||||||
int descriptorsEndPosition = descriptorsStartPosition + length;
|
int descriptorsEndPosition = descriptorsStartPosition + length;
|
||||||
int streamType = -1;
|
int streamType = -1;
|
||||||
|
|
@ -479,7 +526,7 @@ public final class TsExtractor implements Extractor {
|
||||||
data.skipBytes(positionOfNextDescriptor - data.getPosition());
|
data.skipBytes(positionOfNextDescriptor - data.getPosition());
|
||||||
}
|
}
|
||||||
data.setPosition(descriptorsEndPosition);
|
data.setPosition(descriptorsEndPosition);
|
||||||
return new ElementaryStreamReader.EsInfo(streamType, language,
|
return new EsInfo(streamType, language,
|
||||||
Arrays.copyOfRange(sectionData.data, descriptorsStartPosition, descriptorsEndPosition));
|
Arrays.copyOfRange(sectionData.data, descriptorsStartPosition, descriptorsEndPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.util.SparseArray;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.FormatHolder;
|
import com.google.android.exoplayer2.FormatHolder;
|
||||||
|
|
@ -41,7 +42,6 @@ import com.google.android.exoplayer2.util.ConditionVariable;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link MediaPeriod} that extracts data using an {@link Extractor}.
|
* A {@link MediaPeriod} that extracts data using an {@link Extractor}.
|
||||||
|
|
@ -68,6 +68,7 @@ import java.util.Arrays;
|
||||||
private final Runnable maybeFinishPrepareRunnable;
|
private final Runnable maybeFinishPrepareRunnable;
|
||||||
private final Runnable onContinueLoadingRequestedRunnable;
|
private final Runnable onContinueLoadingRequestedRunnable;
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
|
private final SparseArray<DefaultTrackOutput> sampleQueues;
|
||||||
|
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
private SeekMap seekMap;
|
private SeekMap seekMap;
|
||||||
|
|
@ -77,7 +78,6 @@ import java.util.Arrays;
|
||||||
private boolean seenFirstTrackSelection;
|
private boolean seenFirstTrackSelection;
|
||||||
private boolean notifyReset;
|
private boolean notifyReset;
|
||||||
private int enabledTrackCount;
|
private int enabledTrackCount;
|
||||||
private DefaultTrackOutput[] sampleQueues;
|
|
||||||
private TrackGroupArray tracks;
|
private TrackGroupArray tracks;
|
||||||
private long durationUs;
|
private long durationUs;
|
||||||
private boolean[] trackEnabledStates;
|
private boolean[] trackEnabledStates;
|
||||||
|
|
@ -131,7 +131,7 @@ import java.util.Arrays;
|
||||||
handler = new Handler();
|
handler = new Handler();
|
||||||
|
|
||||||
pendingResetPositionUs = C.TIME_UNSET;
|
pendingResetPositionUs = C.TIME_UNSET;
|
||||||
sampleQueues = new DefaultTrackOutput[0];
|
sampleQueues = new SparseArray<>();
|
||||||
length = C.LENGTH_UNSET;
|
length = C.LENGTH_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,8 +141,9 @@ import java.util.Arrays;
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
extractorHolder.release();
|
extractorHolder.release();
|
||||||
for (DefaultTrackOutput sampleQueue : sampleQueues) {
|
int trackCount = sampleQueues.size();
|
||||||
sampleQueue.disable();
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
sampleQueues.valueAt(i).disable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -178,7 +179,7 @@ import java.util.Arrays;
|
||||||
Assertions.checkState(trackEnabledStates[track]);
|
Assertions.checkState(trackEnabledStates[track]);
|
||||||
enabledTrackCount--;
|
enabledTrackCount--;
|
||||||
trackEnabledStates[track] = false;
|
trackEnabledStates[track] = false;
|
||||||
sampleQueues[track].disable();
|
sampleQueues.valueAt(track).disable();
|
||||||
streams[i] = null;
|
streams[i] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -201,9 +202,10 @@ import java.util.Arrays;
|
||||||
if (!seenFirstTrackSelection) {
|
if (!seenFirstTrackSelection) {
|
||||||
// At the time of the first track selection all queues will be enabled, so we need to disable
|
// At the time of the first track selection all queues will be enabled, so we need to disable
|
||||||
// any that are no longer required.
|
// any that are no longer required.
|
||||||
for (int i = 0; i < sampleQueues.length; i++) {
|
int trackCount = sampleQueues.size();
|
||||||
|
for (int i = 0; i < trackCount; i++) {
|
||||||
if (!trackEnabledStates[i]) {
|
if (!trackEnabledStates[i]) {
|
||||||
sampleQueues[i].disable();
|
sampleQueues.valueAt(i).disable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -270,11 +272,12 @@ import java.util.Arrays;
|
||||||
// Treat all seeks into non-seekable media as being to t=0.
|
// Treat all seeks into non-seekable media as being to t=0.
|
||||||
positionUs = seekMap.isSeekable() ? positionUs : 0;
|
positionUs = seekMap.isSeekable() ? positionUs : 0;
|
||||||
lastSeekPositionUs = positionUs;
|
lastSeekPositionUs = positionUs;
|
||||||
|
int trackCount = sampleQueues.size();
|
||||||
// If we're not pending a reset, see if we can seek within the sample queues.
|
// If we're not pending a reset, see if we can seek within the sample queues.
|
||||||
boolean seekInsideBuffer = !isPendingReset();
|
boolean seekInsideBuffer = !isPendingReset();
|
||||||
for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) {
|
for (int i = 0; seekInsideBuffer && i < trackCount; i++) {
|
||||||
if (trackEnabledStates[i]) {
|
if (trackEnabledStates[i]) {
|
||||||
seekInsideBuffer = sampleQueues[i].skipToKeyframeBefore(positionUs);
|
seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we failed to seek within the sample queues, we need to restart.
|
// If we failed to seek within the sample queues, we need to restart.
|
||||||
|
|
@ -284,8 +287,8 @@ import java.util.Arrays;
|
||||||
if (loader.isLoading()) {
|
if (loader.isLoading()) {
|
||||||
loader.cancelLoading();
|
loader.cancelLoading();
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < sampleQueues.length; i++) {
|
for (int i = 0; i < trackCount; i++) {
|
||||||
sampleQueues[i].reset(trackEnabledStates[i]);
|
sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +299,7 @@ import java.util.Arrays;
|
||||||
// SampleStream methods.
|
// SampleStream methods.
|
||||||
|
|
||||||
/* package */ boolean isReady(int track) {
|
/* package */ boolean isReady(int track) {
|
||||||
return loadingFinished || (!isPendingReset() && !sampleQueues[track].isEmpty());
|
return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void maybeThrowError() throws IOException {
|
/* package */ void maybeThrowError() throws IOException {
|
||||||
|
|
@ -308,7 +311,8 @@ import java.util.Arrays;
|
||||||
return C.RESULT_NOTHING_READ;
|
return C.RESULT_NOTHING_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sampleQueues[track].readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs);
|
return sampleQueues.valueAt(track).readData(formatHolder, buffer, loadingFinished,
|
||||||
|
lastSeekPositionUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loader.Callback implementation.
|
// Loader.Callback implementation.
|
||||||
|
|
@ -332,8 +336,9 @@ import java.util.Arrays;
|
||||||
long loadDurationMs, boolean released) {
|
long loadDurationMs, boolean released) {
|
||||||
copyLengthFromLoader(loadable);
|
copyLengthFromLoader(loadable);
|
||||||
if (!released && enabledTrackCount > 0) {
|
if (!released && enabledTrackCount > 0) {
|
||||||
for (int i = 0; i < sampleQueues.length; i++) {
|
int trackCount = sampleQueues.size();
|
||||||
sampleQueues[i].reset(trackEnabledStates[i]);
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
|
||||||
}
|
}
|
||||||
callback.onContinueLoadingRequested(this);
|
callback.onContinueLoadingRequested(this);
|
||||||
}
|
}
|
||||||
|
|
@ -358,11 +363,13 @@ import java.util.Arrays;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TrackOutput track(int id) {
|
public TrackOutput track(int id) {
|
||||||
sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1);
|
DefaultTrackOutput trackOutput = sampleQueues.get(id);
|
||||||
DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator);
|
if (trackOutput == null) {
|
||||||
sampleQueue.setUpstreamFormatChangeListener(this);
|
trackOutput = new DefaultTrackOutput(allocator);
|
||||||
sampleQueues[sampleQueues.length - 1] = sampleQueue;
|
trackOutput.setUpstreamFormatChangeListener(this);
|
||||||
return sampleQueue;
|
sampleQueues.put(id, trackOutput);
|
||||||
|
}
|
||||||
|
return trackOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -390,18 +397,18 @@ import java.util.Arrays;
|
||||||
if (released || prepared || seekMap == null || !tracksBuilt) {
|
if (released || prepared || seekMap == null || !tracksBuilt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (DefaultTrackOutput sampleQueue : sampleQueues) {
|
int trackCount = sampleQueues.size();
|
||||||
if (sampleQueue.getUpstreamFormat() == null) {
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadCondition.close();
|
loadCondition.close();
|
||||||
int trackCount = sampleQueues.length;
|
|
||||||
TrackGroup[] trackArray = new TrackGroup[trackCount];
|
TrackGroup[] trackArray = new TrackGroup[trackCount];
|
||||||
trackEnabledStates = new boolean[trackCount];
|
trackEnabledStates = new boolean[trackCount];
|
||||||
durationUs = seekMap.getDurationUs();
|
durationUs = seekMap.getDurationUs();
|
||||||
for (int i = 0; i < trackCount; i++) {
|
for (int i = 0; i < trackCount; i++) {
|
||||||
trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat());
|
trackArray[i] = new TrackGroup(sampleQueues.valueAt(i).getUpstreamFormat());
|
||||||
}
|
}
|
||||||
tracks = new TrackGroupArray(trackArray);
|
tracks = new TrackGroupArray(trackArray);
|
||||||
prepared = true;
|
prepared = true;
|
||||||
|
|
@ -455,8 +462,9 @@ import java.util.Arrays;
|
||||||
// a new load.
|
// a new load.
|
||||||
lastSeekPositionUs = 0;
|
lastSeekPositionUs = 0;
|
||||||
notifyReset = prepared;
|
notifyReset = prepared;
|
||||||
for (int i = 0; i < sampleQueues.length; i++) {
|
int trackCount = sampleQueues.size();
|
||||||
sampleQueues[i].reset(!prepared || trackEnabledStates[i]);
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]);
|
||||||
}
|
}
|
||||||
loadable.setLoadPosition(0);
|
loadable.setLoadPosition(0);
|
||||||
}
|
}
|
||||||
|
|
@ -464,17 +472,19 @@ import java.util.Arrays;
|
||||||
|
|
||||||
private int getExtractedSamplesCount() {
|
private int getExtractedSamplesCount() {
|
||||||
int extractedSamplesCount = 0;
|
int extractedSamplesCount = 0;
|
||||||
for (DefaultTrackOutput sampleQueue : sampleQueues) {
|
int trackCount = sampleQueues.size();
|
||||||
extractedSamplesCount += sampleQueue.getWriteIndex();
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex();
|
||||||
}
|
}
|
||||||
return extractedSamplesCount;
|
return extractedSamplesCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getLargestQueuedTimestampUs() {
|
private long getLargestQueuedTimestampUs() {
|
||||||
long largestQueuedTimestampUs = Long.MIN_VALUE;
|
long largestQueuedTimestampUs = Long.MIN_VALUE;
|
||||||
for (DefaultTrackOutput sampleQueue : sampleQueues) {
|
int trackCount = sampleQueues.size();
|
||||||
|
for (int i = 0; i < trackCount; i++) {
|
||||||
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
|
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
|
||||||
sampleQueue.getLargestQueuedTimestampUs());
|
sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
|
||||||
}
|
}
|
||||||
return largestQueuedTimestampUs;
|
return largestQueuedTimestampUs;
|
||||||
}
|
}
|
||||||
|
|
@ -523,7 +533,7 @@ import java.util.Arrays;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void skipToKeyframeBefore(long timeUs) {
|
public void skipToKeyframeBefore(long timeUs) {
|
||||||
sampleQueues[track].skipToKeyframeBefore(timeUs);
|
sampleQueues.valueAt(track).skipToKeyframeBefore(timeUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
|
||||||
|
|
||||||
// Accessed only on the loader thread.
|
// Accessed only on the loader thread.
|
||||||
private boolean seenTrack;
|
private boolean seenTrack;
|
||||||
|
private int seenTrackId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param extractor The extractor to wrap.
|
* @param extractor The extractor to wrap.
|
||||||
|
|
@ -116,8 +117,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TrackOutput track(int id) {
|
public TrackOutput track(int id) {
|
||||||
Assertions.checkState(!seenTrack);
|
Assertions.checkState(!seenTrack || seenTrackId == id);
|
||||||
seenTrack = true;
|
seenTrack = true;
|
||||||
|
seenTrackId = id;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -655,7 +655,9 @@ public class DashManifestParser extends DefaultHandler
|
||||||
return MimeTypes.getVideoMediaMimeType(codecs);
|
return MimeTypes.getVideoMediaMimeType(codecs);
|
||||||
} else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
|
} else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
|
||||||
if (codecs != null) {
|
if (codecs != null) {
|
||||||
if (codecs.contains("eia608") || codecs.contains("cea608")) {
|
if (codecs.contains("cea708")) {
|
||||||
|
return MimeTypes.APPLICATION_CEA708;
|
||||||
|
} else if (codecs.contains("eia608") || codecs.contains("cea608")) {
|
||||||
return MimeTypes.APPLICATION_CEA608;
|
return MimeTypes.APPLICATION_CEA608;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -257,8 +257,16 @@ import java.util.Locale;
|
||||||
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex,
|
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex,
|
||||||
newVariantIndex);
|
newVariantIndex);
|
||||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
||||||
fatalError = new BehindLiveWindowException();
|
// We try getting the next chunk without adapting in case that's the reason for falling
|
||||||
return;
|
// behind the live window.
|
||||||
|
newVariantIndex = oldVariantIndex;
|
||||||
|
mediaPlaylist = variantPlaylists[newVariantIndex];
|
||||||
|
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex,
|
||||||
|
newVariantIndex);
|
||||||
|
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
||||||
|
fatalError = new BehindLiveWindowException();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -369,29 +377,29 @@ import java.util.Locale;
|
||||||
}
|
}
|
||||||
} else if (needNewExtractor) {
|
} else if (needNewExtractor) {
|
||||||
// MPEG-2 TS segments, but we need a new extractor.
|
// MPEG-2 TS segments, but we need a new extractor.
|
||||||
// This flag ensures the change of pid between streams does not affect the sample queues.
|
|
||||||
@DefaultStreamReaderFactory.WorkaroundFlags
|
|
||||||
int workaroundFlags = DefaultStreamReaderFactory.WORKAROUND_MAP_BY_TYPE;
|
|
||||||
String codecs = variants[newVariantIndex].format.codecs;
|
|
||||||
if (!TextUtils.isEmpty(codecs)) {
|
|
||||||
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
|
|
||||||
// exist. If we know from the codec attribute that they don't exist, then we can explicitly
|
|
||||||
// ignore them even if they're declared.
|
|
||||||
if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
|
|
||||||
workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_AAC_STREAM;
|
|
||||||
}
|
|
||||||
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
|
|
||||||
workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_H264_STREAM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isTimestampMaster = true;
|
isTimestampMaster = true;
|
||||||
if (useInitializedExtractor) {
|
if (useInitializedExtractor) {
|
||||||
extractor = lastLoadedInitializationChunk.extractor;
|
extractor = lastLoadedInitializationChunk.extractor;
|
||||||
} else {
|
} else {
|
||||||
timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
||||||
segment.discontinuitySequenceNumber, startTimeUs);
|
segment.discontinuitySequenceNumber, startTimeUs);
|
||||||
|
// This flag ensures the change of pid between streams does not affect the sample queues.
|
||||||
|
@DefaultStreamReaderFactory.Flags
|
||||||
|
int esReaderFactoryFlags = 0;
|
||||||
|
String codecs = variants[newVariantIndex].format.codecs;
|
||||||
|
if (!TextUtils.isEmpty(codecs)) {
|
||||||
|
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
|
||||||
|
// exist. If we know from the codec attribute that they don't exist, then we can
|
||||||
|
// explicitly ignore them even if they're declared.
|
||||||
|
if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
|
||||||
|
esReaderFactoryFlags |= DefaultStreamReaderFactory.FLAG_IGNORE_AAC_STREAM;
|
||||||
|
}
|
||||||
|
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
|
||||||
|
esReaderFactoryFlags |= DefaultStreamReaderFactory.FLAG_IGNORE_H264_STREAM;
|
||||||
|
}
|
||||||
|
}
|
||||||
extractor = new TsExtractor(timestampAdjuster,
|
extractor = new TsExtractor(timestampAdjuster,
|
||||||
new DefaultStreamReaderFactory(workaroundFlags));
|
new DefaultStreamReaderFactory(esReaderFactoryFlags), true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// MPEG-2 TS segments, and we need to continue using the same extractor.
|
// MPEG-2 TS segments, and we need to continue using the same extractor.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,7 +328,7 @@ import java.util.List;
|
||||||
sampleStreamWrappers = new HlsSampleStreamWrapper[] {
|
sampleStreamWrappers = new HlsSampleStreamWrapper[] {
|
||||||
buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)};
|
buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)};
|
||||||
pendingPrepareCount = 1;
|
pendingPrepareCount = 1;
|
||||||
sampleStreamWrappers[0].prepare();
|
sampleStreamWrappers[0].continuePreparing();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,16 +369,16 @@ import java.util.List;
|
||||||
selectedVariants.toArray(variants);
|
selectedVariants.toArray(variants);
|
||||||
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
|
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
|
||||||
baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat);
|
baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat);
|
||||||
sampleStreamWrapper.prepare();
|
|
||||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||||
|
sampleStreamWrapper.continuePreparing();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build audio stream wrappers.
|
// Build audio stream wrappers.
|
||||||
for (int i = 0; i < audioVariants.size(); i++) {
|
for (int i = 0; i < audioVariants.size(); i++) {
|
||||||
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO,
|
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO,
|
||||||
baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null);
|
baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null);
|
||||||
sampleStreamWrapper.prepare();
|
|
||||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||||
|
sampleStreamWrapper.continuePreparing();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build subtitle stream wrappers.
|
// Build subtitle stream wrappers.
|
||||||
|
|
|
||||||
|
|
@ -144,8 +144,10 @@ import java.util.LinkedList;
|
||||||
pendingResetPositionUs = positionUs;
|
pendingResetPositionUs = positionUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void prepare() {
|
public void continuePreparing() {
|
||||||
continueLoading(lastSeekPositionUs);
|
if (!prepared) {
|
||||||
|
continueLoading(lastSeekPositionUs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -154,7 +156,8 @@ import java.util.LinkedList;
|
||||||
*/
|
*/
|
||||||
public void prepareSingleTrack(Format format) {
|
public void prepareSingleTrack(Format format) {
|
||||||
track(0).format(format);
|
track(0).format(format);
|
||||||
endTracks();
|
sampleQueuesBuilt = true;
|
||||||
|
maybeFinishPrepare();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void maybeThrowPrepareError() throws IOException {
|
public void maybeThrowPrepareError() throws IOException {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -410,11 +413,16 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
||||||
connection.setInstanceFollowRedirects(followRedirects);
|
connection.setInstanceFollowRedirects(followRedirects);
|
||||||
connection.setDoOutput(postBody != null);
|
connection.setDoOutput(postBody != null);
|
||||||
if (postBody != null) {
|
if (postBody != null) {
|
||||||
connection.setFixedLengthStreamingMode(postBody.length);
|
connection.setRequestMethod("POST");
|
||||||
connection.connect();
|
if (postBody.length == 0) {
|
||||||
OutputStream os = connection.getOutputStream();
|
connection.connect();
|
||||||
os.write(postBody);
|
} else {
|
||||||
os.close();
|
connection.setFixedLengthStreamingMode(postBody.length);
|
||||||
|
connection.connect();
|
||||||
|
OutputStream os = connection.getOutputStream();
|
||||||
|
os.write(postBody);
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
connection.connect();
|
connection.connect();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ public final class MimeTypes {
|
||||||
public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";
|
public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";
|
||||||
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
|
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
|
||||||
public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608";
|
public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608";
|
||||||
|
public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708";
|
||||||
public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip";
|
public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip";
|
||||||
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
|
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
|
||||||
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
|
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.google.android.exoplayer2.playbacktests"
|
package="com.google.android.exoplayer2.playbacktests"
|
||||||
android:versionCode="2002"
|
android:versionCode="2003"
|
||||||
android:versionName="2.0.2">
|
android:versionName="2.0.3">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import junit.framework.Assert;
|
import junit.framework.Assert;
|
||||||
import junit.framework.TestCase;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fake {@link ExtractorOutput}.
|
* A fake {@link ExtractorOutput}.
|
||||||
|
|
@ -37,8 +36,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
|
||||||
*/
|
*/
|
||||||
private static final boolean WRITE_DUMP = false;
|
private static final boolean WRITE_DUMP = false;
|
||||||
|
|
||||||
private final boolean allowDuplicateTrackIds;
|
|
||||||
|
|
||||||
public final SparseArray<FakeTrackOutput> trackOutputs;
|
public final SparseArray<FakeTrackOutput> trackOutputs;
|
||||||
|
|
||||||
public int numberOfTracks;
|
public int numberOfTracks;
|
||||||
|
|
@ -46,11 +43,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
|
||||||
public SeekMap seekMap;
|
public SeekMap seekMap;
|
||||||
|
|
||||||
public FakeExtractorOutput() {
|
public FakeExtractorOutput() {
|
||||||
this(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FakeExtractorOutput(boolean allowDuplicateTrackIds) {
|
|
||||||
this.allowDuplicateTrackIds = allowDuplicateTrackIds;
|
|
||||||
trackOutputs = new SparseArray<>();
|
trackOutputs = new SparseArray<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,11 +50,10 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
|
||||||
public FakeTrackOutput track(int trackId) {
|
public FakeTrackOutput track(int trackId) {
|
||||||
FakeTrackOutput output = trackOutputs.get(trackId);
|
FakeTrackOutput output = trackOutputs.get(trackId);
|
||||||
if (output == null) {
|
if (output == null) {
|
||||||
|
Assert.assertFalse(tracksEnded);
|
||||||
numberOfTracks++;
|
numberOfTracks++;
|
||||||
output = new FakeTrackOutput();
|
output = new FakeTrackOutput();
|
||||||
trackOutputs.put(trackId, output);
|
trackOutputs.put(trackId, output);
|
||||||
} else {
|
|
||||||
TestCase.assertTrue("Duplicate track id: " + trackId, allowDuplicateTrackIds);
|
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -267,8 +267,8 @@ public class TestUtil {
|
||||||
*/
|
*/
|
||||||
public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile,
|
public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile,
|
||||||
byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors,
|
byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors,
|
||||||
boolean simulateUnknownLength,
|
boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException,
|
||||||
boolean simulatePartialReads) throws IOException, InterruptedException {
|
InterruptedException {
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
|
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
|
||||||
.setSimulateIOErrors(simulateIOErrors)
|
.setSimulateIOErrors(simulateIOErrors)
|
||||||
.setSimulateUnknownLength(simulateUnknownLength)
|
.setSimulateUnknownLength(simulateUnknownLength)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue