mirror of
https://github.com/samsonjs/media.git
synced 2026-03-25 09:25:53 +00:00
Merge pull request #1958 from google/dev-v2
Merge dev-v2 into dev-v2-id3
This commit is contained in:
commit
e2ff401ea1
37 changed files with 532 additions and 346 deletions
|
|
@ -1,5 +1,25 @@
|
|||
# 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 ###
|
||||
|
||||
* 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
|
||||
([#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 ###
|
||||
|
||||
* Cronet network stack extension.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ allprojects {
|
|||
releaseRepoName = 'exoplayer'
|
||||
releaseUserOrg = 'google'
|
||||
releaseGroupId = 'com.google.android.exoplayer'
|
||||
releaseVersion = 'r2.0.2'
|
||||
releaseVersion = 'r2.0.3'
|
||||
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2002"
|
||||
android:versionName="2.0.2">
|
||||
android:versionCode="2003"
|
||||
android:versionName="2.0.3">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceExcep
|
|||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
|
|
@ -82,7 +81,6 @@ public final class CronetDataSourceTest {
|
|||
private static final String TEST_CONTENT_TYPE = "test/test";
|
||||
private static final byte[] TEST_POST_BODY = "test post body".getBytes();
|
||||
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 DataSpec testDataSpec;
|
||||
|
|
@ -231,10 +229,8 @@ public final class CronetDataSourceTest {
|
|||
|
||||
@Test
|
||||
public void testRequestHeadersSet() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
testResponseHeader.put("Content-Length", Long.toString(5000L));
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.setRequestProperty("firstHeader", "firstValue");
|
||||
dataSourceUnderTest.setRequestProperty("secondHeader", "secondValue");
|
||||
|
|
@ -257,13 +253,11 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testRequestOpenGzippedCompressedReturnsDataSpecLength()
|
||||
throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000, null);
|
||||
testResponseHeader.put("Content-Encoding", "gzip");
|
||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
||||
testResponseHeader.put("Content-Length", Long.toString(50L));
|
||||
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));
|
||||
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
|
||||
}
|
||||
|
|
@ -370,7 +364,7 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testRequestReadTwice() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
|
|
@ -392,28 +386,23 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testSecondRequestNoContentLength() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess();
|
||||
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
// First request.
|
||||
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
dataSourceUnderTest.read(returnedBuffer, 0, 1);
|
||||
dataSourceUnderTest.close();
|
||||
|
||||
// Second request. There's no Content-Length response header.
|
||||
testResponseHeader.remove("Content-Length");
|
||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
// Second request.
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
returnedBuffer = new byte[16];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||
assertEquals(10, bytesRead);
|
||||
|
||||
mockResponseFinished();
|
||||
|
||||
// Should read whats left in the buffer first.
|
||||
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||
assertEquals(6, bytesRead);
|
||||
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
|
||||
|
|
@ -423,23 +412,54 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testReadWithOffset() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
byte[] returnedBuffer = new byte[16];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
|
||||
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
|
||||
assertEquals(8, bytesRead);
|
||||
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
|
||||
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
|
||||
public void testReadWithUnsetLength() throws HttpDataSourceException {
|
||||
testResponseHeader.remove("Content-Length");
|
||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
|
|
@ -453,7 +473,7 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testReadReturnsWhatItCan() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
|
|
@ -467,7 +487,7 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testClosedMeansClosed() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
int bytesRead = 0;
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
|
@ -493,32 +513,29 @@ public final class CronetDataSourceTest {
|
|||
|
||||
@Test
|
||||
public void testOverread() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess();
|
||||
|
||||
// Ask for 16 bytes
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 10000, 16, null);
|
||||
// Let the response promise to give 16 bytes back.
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
|
||||
testResponseHeader.put("Content-Length", Long.toString(16L));
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
byte[] returnedBuffer = new byte[8];
|
||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
|
||||
assertEquals(8, bytesRead);
|
||||
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
|
||||
|
||||
// The current buffer is kept if not completely consumed by DataSource reader.
|
||||
returnedBuffer = new byte[8];
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
|
||||
assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer);
|
||||
assertEquals(14, bytesRead);
|
||||
assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer);
|
||||
|
||||
// 2 bytes left at this point.
|
||||
returnedBuffer = new byte[8];
|
||||
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
|
||||
assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer);
|
||||
assertEquals(16, bytesRead);
|
||||
assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer);
|
||||
|
||||
// Should have only called read on cronet once.
|
||||
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
|
||||
|
|
@ -752,16 +769,24 @@ public final class CronetDataSourceTest {
|
|||
}).when(mockUrlRequest).start();
|
||||
}
|
||||
|
||||
private void mockReadSuccess() {
|
||||
private void mockReadSuccess(int position, int length) {
|
||||
final int[] positionAndRemaining = new int[] {position, length};
|
||||
doAnswer(new Answer<Void>() {
|
||||
@Override
|
||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||
inputBuffer.put(buildTestDataBuffer());
|
||||
dataSourceUnderTest.onReadCompleted(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo,
|
||||
inputBuffer);
|
||||
if (positionAndRemaining[1] == 0) {
|
||||
dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo);
|
||||
} else {
|
||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||
positionAndRemaining[0] += readLength;
|
||||
positionAndRemaining[1] -= readLength;
|
||||
dataSourceUnderTest.onReadCompleted(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo,
|
||||
inputBuffer);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||
|
|
@ -780,16 +805,6 @@ public final class CronetDataSourceTest {
|
|||
}).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;
|
||||
}
|
||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||
}
|
||||
|
||||
private ConditionVariable buildUrlRequestStartedCondition() {
|
||||
final ConditionVariable startedCondition = new ConditionVariable();
|
||||
doAnswer(new Answer<Object>() {
|
||||
|
|
@ -802,8 +817,8 @@ public final class CronetDataSourceTest {
|
|||
return startedCondition;
|
||||
}
|
||||
|
||||
private static byte[] buildTestDataArray(int start, int length) {
|
||||
return Arrays.copyOfRange(buildTestDataBuffer().array(), start, start + length);
|
||||
private static byte[] buildTestDataArray(int position, int length) {
|
||||
return buildTestDataBuffer(position, length).array();
|
||||
}
|
||||
|
||||
public static byte[] prefixZeros(byte[] data, int requiredLength) {
|
||||
|
|
@ -816,10 +831,10 @@ public final class CronetDataSourceTest {
|
|||
return Arrays.copyOf(data, requiredLength);
|
||||
}
|
||||
|
||||
private static ByteBuffer buildTestDataBuffer() {
|
||||
ByteBuffer testBuffer = ByteBuffer.allocate(TEST_BUFFER_SIZE);
|
||||
for (byte i = 1; i <= TEST_BUFFER_SIZE; i++) {
|
||||
testBuffer.put(i);
|
||||
private static ByteBuffer buildTestDataBuffer(int position, int length) {
|
||||
ByteBuffer testBuffer = ByteBuffer.allocate(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
testBuffer.put((byte) (position + i));
|
||||
}
|
||||
testBuffer.flip();
|
||||
return testBuffer;
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
// 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
|
||||
|
|
@ -242,9 +243,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Handle the case where we requested a range starting from a non-zero position and
|
||||
// received a 200 rather than a 206. This occurs if the server does not support partial
|
||||
// requests, and requires that the source skips to the requested position.
|
||||
// If we requested a range starting from a non-zero position and received a 200 rather than a
|
||||
// 206, then the server does not support partial requests. We'll need to manually skip to the
|
||||
// requested position.
|
||||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||
|
||||
// Calculate the content length.
|
||||
if (!getIsCompressed(responseInfo)) {
|
||||
|
|
@ -281,7 +283,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
readBuffer.limit(0);
|
||||
}
|
||||
if (!readBuffer.hasRemaining()) {
|
||||
while (!readBuffer.hasRemaining()) {
|
||||
// Fill readBuffer with more data from Cronet.
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
|
|
@ -301,6 +303,12 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -408,8 +416,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
executor, cronetEngine);
|
||||
// Set the headers.
|
||||
synchronized (requestProperties) {
|
||||
if (dataSpec.postBody != null && !requestProperties.containsKey(CONTENT_TYPE)) {
|
||||
throw new OpenException("POST request must set Content-Type", dataSpec, Status.IDLE);
|
||||
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);
|
||||
}
|
||||
for (Entry<String, String> headerEntry : requestProperties.entrySet()) {
|
||||
requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
|
||||
|
|
@ -426,10 +436,13 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
}
|
||||
requestBuilder.addHeader("Range", rangeValue.toString());
|
||||
}
|
||||
// Set the body.
|
||||
// Set the method and (if non-empty) the body.
|
||||
if (dataSpec.postBody != null) {
|
||||
requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody),
|
||||
executor);
|
||||
requestBuilder.setHttpMethod("POST");
|
||||
if (dataSpec.postBody.length != 0) {
|
||||
requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody),
|
||||
executor);
|
||||
}
|
||||
}
|
||||
return requestBuilder.build();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
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.TestUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -66,9 +68,12 @@ public class AdtsReaderTest extends TestCase {
|
|||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
adtsOutput = new FakeTrackOutput();
|
||||
id3Output = new FakeTrackOutput();
|
||||
adtsReader = new AdtsReader(adtsOutput, id3Output);
|
||||
FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
|
||||
adtsOutput = fakeExtractorOutput.track(0);
|
||||
id3Output = fakeExtractorOutput.track(1);
|
||||
adtsReader = new AdtsReader(true);
|
||||
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
|
||||
adtsReader.init(fakeExtractorOutput, idGenerator);
|
||||
data = new ParsableByteArray(TEST_DATA);
|
||||
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.TimestampAdjuster;
|
||||
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.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
|
|
@ -72,7 +73,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||
|
||||
public void testCustomPesReader() throws Exception {
|
||||
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()
|
||||
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
|
||||
.setSimulateIOErrors(false)
|
||||
|
|
@ -107,18 +108,25 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||
|
||||
private static final class CustomEsReader extends ElementaryStreamReader {
|
||||
|
||||
private final String language;
|
||||
private TrackOutput output;
|
||||
public int packetsRead = 0;
|
||||
|
||||
public CustomEsReader(TrackOutput output, String language) {
|
||||
super(output);
|
||||
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
|
||||
language, null, 0));
|
||||
public CustomEsReader(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
}
|
||||
|
|
@ -148,16 +156,12 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ElementaryStreamReader onPmtEntry(int pid, int streamType,
|
||||
ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) {
|
||||
public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) {
|
||||
if (streamType == 3) {
|
||||
// We need to manually avoid a duplicate custom reader creation.
|
||||
if (reader == null) {
|
||||
reader = new CustomEsReader(output.track(pid), esInfo.language);
|
||||
}
|
||||
reader = new CustomEsReader(esInfo.language);
|
||||
return reader;
|
||||
} else {
|
||||
return defaultFactory.onPmtEntry(pid, streamType, esInfo, output);
|
||||
return defaultFactory.createStreamReader(streamType, esInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
|
|||
/**
|
||||
* 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.
|
||||
|
|
@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
|
|||
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
|
||||
* 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}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
private Format audioFormat;
|
||||
|
||||
private Surface surface;
|
||||
private boolean ownsSurface;
|
||||
private SurfaceHolder surfaceHolder;
|
||||
private TextureView textureView;
|
||||
private TextRenderer.Output textOutput;
|
||||
|
|
@ -206,7 +207,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
*/
|
||||
public void setVideoSurface(Surface surface) {
|
||||
removeSurfaceCallbacks();
|
||||
setVideoSurfaceInternal(surface);
|
||||
setVideoSurfaceInternal(surface, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -219,9 +220,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
removeSurfaceCallbacks();
|
||||
this.surfaceHolder = surfaceHolder;
|
||||
if (surfaceHolder == null) {
|
||||
setVideoSurfaceInternal(null);
|
||||
setVideoSurfaceInternal(null, false);
|
||||
} else {
|
||||
setVideoSurfaceInternal(surfaceHolder.getSurface());
|
||||
setVideoSurfaceInternal(surfaceHolder.getSurface(), false);
|
||||
surfaceHolder.addCallback(componentListener);
|
||||
}
|
||||
}
|
||||
|
|
@ -246,13 +247,13 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
removeSurfaceCallbacks();
|
||||
this.textureView = textureView;
|
||||
if (textureView == null) {
|
||||
setVideoSurfaceInternal(null);
|
||||
setVideoSurfaceInternal(null, true);
|
||||
} else {
|
||||
if (textureView.getSurfaceTextureListener() != null) {
|
||||
Log.w(TAG, "Replacing existing SurfaceTextureListener.");
|
||||
}
|
||||
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
|
||||
setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture));
|
||||
setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
|
||||
textureView.setSurfaceTextureListener(componentListener);
|
||||
}
|
||||
}
|
||||
|
|
@ -477,6 +478,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
public void release() {
|
||||
player.release();
|
||||
removeSurfaceCallbacks();
|
||||
if (surface != null) {
|
||||
if (ownsSurface) {
|
||||
surface.release();
|
||||
}
|
||||
surface = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -627,8 +634,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
private void setVideoSurfaceInternal(Surface surface) {
|
||||
this.surface = surface;
|
||||
private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) {
|
||||
// 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];
|
||||
int count = 0;
|
||||
for (Renderer renderer : renderers) {
|
||||
|
|
@ -636,12 +644,18 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface);
|
||||
}
|
||||
}
|
||||
if (surface == null) {
|
||||
// Block to ensure that the surface is not accessed after the method returns.
|
||||
if (this.surface != null && this.surface != surface) {
|
||||
// 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);
|
||||
} else {
|
||||
player.sendMessages(messages);
|
||||
}
|
||||
this.surface = surface;
|
||||
this.ownsSurface = ownsSurface;
|
||||
}
|
||||
|
||||
private final class ComponentListener implements VideoRendererEventListener,
|
||||
|
|
@ -790,7 +804,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
setVideoSurfaceInternal(holder.getSurface());
|
||||
setVideoSurfaceInternal(holder.getSurface(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -800,14 +814,14 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
setVideoSurfaceInternal(null);
|
||||
setVideoSurfaceInternal(null, false);
|
||||
}
|
||||
|
||||
// TextureView.SurfaceTextureListener implementation
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
|
||||
setVideoSurfaceInternal(new Surface(surfaceTexture));
|
||||
setVideoSurfaceInternal(new Surface(surfaceTexture), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -817,7 +831,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
|
|||
|
||||
@Override
|
||||
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
|
||||
setVideoSurface(null);
|
||||
setVideoSurfaceInternal(null, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
|||
@Override
|
||||
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
|
||||
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
|
||||
return executePost(url, null, null);
|
||||
return executePost(url, new byte[0], null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -81,6 +81,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
|||
url = defaultUrl;
|
||||
}
|
||||
Map<String, String> requestProperties = new HashMap<>();
|
||||
requestProperties.put("Content-Type", "application/octet-stream");
|
||||
if (C.PLAYREADY_UUID.equals(uuid)) {
|
||||
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)
|
||||
throws IOException {
|
||||
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) {
|
||||
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
|
||||
dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public interface Extractor {
|
|||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -21,18 +21,19 @@ package com.google.android.exoplayer2.extractor;
|
|||
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>
|
||||
* 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.
|
||||
* @return The {@link TrackOutput} that should receive track level data belonging to the track.
|
||||
* @param trackId A track identifier.
|
||||
* @return The {@link TrackOutput} for the given track identifier.
|
||||
*/
|
||||
TrackOutput track(int trackId);
|
||||
|
||||
/**
|
||||
* Called when all tracks have been identified, meaning that {@link #track(int)} will not be
|
||||
* called again.
|
||||
* Called when all tracks have been identified, meaning no new {@code trackId} values will be
|
||||
* passed to {@link #track(int)}.
|
||||
*/
|
||||
void endTracks();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
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.Util;
|
||||
|
||||
|
|
@ -117,7 +118,8 @@ public final class Ac3Extractor implements Extractor {
|
|||
|
||||
@Override
|
||||
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.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.Format;
|
||||
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.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
private final ParsableByteArray headerScratchBytes;
|
||||
private final String language;
|
||||
|
||||
private TrackOutput output;
|
||||
|
||||
private int state;
|
||||
private int bytesRead;
|
||||
|
||||
|
|
@ -54,21 +57,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
|
||||
/**
|
||||
* Constructs a new reader for (E-)AC-3 elementary streams.
|
||||
*
|
||||
* @param output Track output for extracted samples.
|
||||
*/
|
||||
public Ac3Reader(TrackOutput output) {
|
||||
this(output, null);
|
||||
public Ac3Reader() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new reader for (E-)AC-3 elementary streams.
|
||||
*
|
||||
* @param output Track output for extracted samples.
|
||||
* @param language Track language.
|
||||
*/
|
||||
public Ac3Reader(TrackOutput output, String language) {
|
||||
super(output);
|
||||
public Ac3Reader(String language) {
|
||||
headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
|
||||
headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
|
||||
state = STATE_FINDING_SYNC;
|
||||
|
|
@ -82,6 +81,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
lastByteWas0B = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
|
||||
output = extractorOutput.track(generator.getNextId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
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.PositionHolder;
|
||||
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.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
|
@ -126,7 +127,8 @@ public final class AdtsExtractor implements Extractor {
|
|||
|
||||
@Override
|
||||
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.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import android.util.Log;
|
|||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.util.CodecSpecificDataUtil;
|
||||
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 byte[] ID3_IDENTIFIER = {'I', 'D', '3'};
|
||||
|
||||
private final boolean exposeId3;
|
||||
private final ParsableBitArray adtsScratch;
|
||||
private final ParsableByteArray id3HeaderBuffer;
|
||||
private final TrackOutput id3Output;
|
||||
private final String language;
|
||||
|
||||
private TrackOutput output;
|
||||
private TrackOutput id3Output;
|
||||
|
||||
private int state;
|
||||
private int bytesRead;
|
||||
|
||||
|
|
@ -77,26 +82,21 @@ import java.util.Collections;
|
|||
private long currentSampleDuration;
|
||||
|
||||
/**
|
||||
* @param output A {@link TrackOutput} to which AAC samples should be written.
|
||||
* @param id3Output A {@link TrackOutput} to which ID3 samples should be written.
|
||||
* @param exposeId3 True if the reader should expose ID3 information.
|
||||
*/
|
||||
public AdtsReader(TrackOutput output, TrackOutput id3Output) {
|
||||
this(output, id3Output, null);
|
||||
public AdtsReader(boolean exposeId3) {
|
||||
this(exposeId3, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param output A {@link TrackOutput} to which AAC samples should be written.
|
||||
* @param id3Output A {@link TrackOutput} to which ID3 samples should be written.
|
||||
* @param exposeId3 True if the reader should expose ID3 information.
|
||||
* @param language Track language.
|
||||
*/
|
||||
public AdtsReader(TrackOutput output, TrackOutput id3Output, String language) {
|
||||
super(output);
|
||||
this.id3Output = id3Output;
|
||||
id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null,
|
||||
Format.NO_VALUE, null));
|
||||
public AdtsReader(boolean exposeId3, String language) {
|
||||
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
|
||||
id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
|
||||
setFindingSampleState();
|
||||
this.exposeId3 = exposeId3;
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +105,18 @@ import java.util.Collections;
|
|||
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
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
timeUs = pesTimeUs;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.SparseBooleanArray;
|
||||
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
|
|
@ -28,80 +26,54 @@ import java.lang.annotation.RetentionPolicy;
|
|||
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)
|
||||
@IntDef(flag = true, value = {WORKAROUND_ALLOW_NON_IDR_KEYFRAMES, WORKAROUND_IGNORE_AAC_STREAM,
|
||||
WORKAROUND_IGNORE_H264_STREAM, WORKAROUND_DETECT_ACCESS_UNITS, WORKAROUND_MAP_BY_TYPE})
|
||||
public @interface WorkaroundFlags {
|
||||
@IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM,
|
||||
FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS})
|
||||
public @interface Flags {
|
||||
}
|
||||
public static final int WORKAROUND_ALLOW_NON_IDR_KEYFRAMES = 1;
|
||||
public static final int WORKAROUND_IGNORE_AAC_STREAM = 2;
|
||||
public static final int WORKAROUND_IGNORE_H264_STREAM = 4;
|
||||
public static final int WORKAROUND_DETECT_ACCESS_UNITS = 8;
|
||||
public static final int WORKAROUND_MAP_BY_TYPE = 16;
|
||||
public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;
|
||||
public static final int FLAG_IGNORE_AAC_STREAM = 2;
|
||||
public static final int FLAG_IGNORE_H264_STREAM = 4;
|
||||
public static final int FLAG_DETECT_ACCESS_UNITS = 8;
|
||||
|
||||
private static final int BASE_EMBEDDED_TRACK_ID = 0x2000; // 0xFF + 1.
|
||||
|
||||
private final SparseBooleanArray trackIds;
|
||||
@WorkaroundFlags
|
||||
private final int workaroundFlags;
|
||||
private Id3Reader id3Reader;
|
||||
private int nextEmbeddedTrackId = BASE_EMBEDDED_TRACK_ID;
|
||||
@Flags
|
||||
private final int flags;
|
||||
|
||||
public DefaultStreamReaderFactory() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
public DefaultStreamReaderFactory(int workaroundFlags) {
|
||||
trackIds = new SparseBooleanArray();
|
||||
this.workaroundFlags = workaroundFlags;
|
||||
public DefaultStreamReaderFactory(@Flags int flags) {
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElementaryStreamReader onPmtEntry(int pid, int streamType,
|
||||
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);
|
||||
public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) {
|
||||
switch (streamType) {
|
||||
case TsExtractor.TS_STREAM_TYPE_MPA:
|
||||
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:
|
||||
return (workaroundFlags & WORKAROUND_IGNORE_AAC_STREAM) != 0 ? null
|
||||
: new AdtsReader(output.track(trackId), new DummyTrackOutput(), esInfo.language);
|
||||
return (flags & FLAG_IGNORE_AAC_STREAM) != 0 ? null
|
||||
: new AdtsReader(false, esInfo.language);
|
||||
case TsExtractor.TS_STREAM_TYPE_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_HDMV_DTS:
|
||||
return new DtsReader(output.track(trackId), esInfo.language);
|
||||
return new DtsReader(esInfo.language);
|
||||
case TsExtractor.TS_STREAM_TYPE_H262:
|
||||
return new H262Reader(output.track(trackId));
|
||||
return new H262Reader();
|
||||
case TsExtractor.TS_STREAM_TYPE_H264:
|
||||
return (workaroundFlags & WORKAROUND_IGNORE_H264_STREAM) != 0
|
||||
? null : new H264Reader(output.track(trackId),
|
||||
new SeiReader(output.track(nextEmbeddedTrackId++)),
|
||||
(workaroundFlags & WORKAROUND_ALLOW_NON_IDR_KEYFRAMES) != 0,
|
||||
(workaroundFlags & WORKAROUND_DETECT_ACCESS_UNITS) != 0);
|
||||
return (flags & FLAG_IGNORE_H264_STREAM) != 0 ? null
|
||||
: new H264Reader((flags & FLAG_ALLOW_NON_IDR_KEYFRAMES) != 0,
|
||||
(flags & FLAG_DETECT_ACCESS_UNITS) != 0);
|
||||
case TsExtractor.TS_STREAM_TYPE_H265:
|
||||
return new H265Reader(output.track(trackId),
|
||||
new SeiReader(output.track(nextEmbeddedTrackId++)));
|
||||
return new H265Reader();
|
||||
case TsExtractor.TS_STREAM_TYPE_ID3:
|
||||
if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0) {
|
||||
return id3Reader;
|
||||
} else {
|
||||
return new Id3Reader(output.track(nextEmbeddedTrackId++));
|
||||
}
|
||||
return new Id3Reader();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
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.util.ParsableByteArray;
|
||||
|
||||
|
|
@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
private final ParsableByteArray headerScratchBytes;
|
||||
private final String language;
|
||||
|
||||
private TrackOutput output;
|
||||
|
||||
private int state;
|
||||
private int bytesRead;
|
||||
|
||||
|
|
@ -54,20 +57,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
public DtsReader(TrackOutput output, String language) {
|
||||
super(output);
|
||||
public DtsReader(String language) {
|
||||
headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
|
||||
headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF);
|
||||
headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF);
|
||||
|
|
@ -84,6 +76,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
syncBytes = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
timeUs = pesTimeUs;
|
||||
|
|
|
|||
|
|
@ -33,17 +33,12 @@ public abstract class ElementaryStreamReader {
|
|||
* 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.
|
||||
*
|
||||
* @param pid The pid for the PMT entry.
|
||||
* @param streamType One of the {@link TsExtractor}{@code .TS_STREAM_TYPE_*} constants defining
|
||||
* 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.
|
||||
* @param streamType Stream type value as defined in the PMT entry or associated descriptors.
|
||||
* @param esInfo Information associated to the elementary stream provided in the PMT.
|
||||
* @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.
|
||||
*/
|
||||
ElementaryStreamReader onPmtEntry(int pid, int streamType, EsInfo esInfo,
|
||||
ExtractorOutput output);
|
||||
ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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) {
|
||||
this.output = output;
|
||||
public static final class TrackIdGenerator {
|
||||
|
||||
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();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.util.MimeTypes;
|
||||
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_GROUP = 0xB8;
|
||||
|
||||
private TrackOutput output;
|
||||
|
||||
// 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[] {
|
||||
24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};
|
||||
|
|
@ -58,8 +61,7 @@ import java.util.Collections;
|
|||
private long framePosition;
|
||||
private long frameTimeUs;
|
||||
|
||||
public H262Reader(TrackOutput output) {
|
||||
super(output);
|
||||
public H262Reader() {
|
||||
prefixFlags = new boolean[4];
|
||||
csdBuffer = new CsdBuffer(128);
|
||||
}
|
||||
|
|
@ -73,6 +75,11 @@ import java.util.Collections;
|
|||
totalBytesWritten = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.util.MimeTypes;
|
||||
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_PPS = 8; // Picture parameter set
|
||||
|
||||
// State that should not be reset on seek.
|
||||
private boolean hasOutputFormat;
|
||||
|
||||
// State that should be reset on seek.
|
||||
private final SeiReader seiReader;
|
||||
private final boolean[] prefixFlags;
|
||||
private final SampleReader sampleReader;
|
||||
private final boolean allowNonIdrKeyframes;
|
||||
private final boolean detectAccessUnits;
|
||||
private final NalUnitTargetBuffer sps;
|
||||
private final NalUnitTargetBuffer pps;
|
||||
private final NalUnitTargetBuffer sei;
|
||||
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.
|
||||
private long pesTimeUs;
|
||||
|
|
@ -56,19 +60,15 @@ import java.util.List;
|
|||
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
|
||||
* synchronization samples (key-frames).
|
||||
* @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).
|
||||
*/
|
||||
public H264Reader(TrackOutput output, SeiReader seiReader, boolean allowNonIdrKeyframes,
|
||||
boolean detectAccessUnits) {
|
||||
super(output);
|
||||
this.seiReader = seiReader;
|
||||
public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
|
||||
prefixFlags = new boolean[3];
|
||||
sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);
|
||||
this.allowNonIdrKeyframes = allowNonIdrKeyframes;
|
||||
this.detectAccessUnits = detectAccessUnits;
|
||||
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
|
||||
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
|
||||
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
|
||||
|
|
@ -85,6 +85,13 @@ import java.util.List;
|
|||
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
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
this.pesTimeUs = pesTimeUs;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.util.MimeTypes;
|
||||
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 SUFFIX_SEI_NUT = 40;
|
||||
|
||||
private TrackOutput output;
|
||||
private SeiReader seiReader;
|
||||
|
||||
// State that should not be reset on seek.
|
||||
private boolean hasOutputFormat;
|
||||
|
||||
// State that should be reset on seek.
|
||||
private final SeiReader seiReader;
|
||||
private final boolean[] prefixFlags;
|
||||
private final NalUnitTargetBuffer vps;
|
||||
private final NalUnitTargetBuffer sps;
|
||||
|
|
@ -62,13 +65,7 @@ import java.util.Collections;
|
|||
// Scratch variables to avoid allocations.
|
||||
private final ParsableByteArray seiWrapper;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
public H265Reader() {
|
||||
prefixFlags = new boolean[3];
|
||||
vps = new NalUnitTargetBuffer(VPS_NUT, 128);
|
||||
sps = new NalUnitTargetBuffer(SPS_NUT, 128);
|
||||
|
|
@ -91,6 +88,12 @@ import java.util.Collections;
|
|||
totalBytesWritten = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
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.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -30,6 +31,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
|
||||
private final ParsableByteArray id3Header;
|
||||
|
||||
private TrackOutput output;
|
||||
|
||||
// State that should be reset on seek.
|
||||
private boolean writingSample;
|
||||
|
||||
|
|
@ -38,10 +41,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
private int sampleSize;
|
||||
private int sampleBytesRead;
|
||||
|
||||
public Id3Reader(TrackOutput output) {
|
||||
super(output);
|
||||
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE,
|
||||
null));
|
||||
public Id3Reader() {
|
||||
id3Header = new ParsableByteArray(ID3_HEADER_SIZE);
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +50,13 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
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
|
||||
public void packetStarted(long pesTimeUs, boolean 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.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -36,6 +37,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
private final MpegAudioHeader header;
|
||||
private final String language;
|
||||
|
||||
private TrackOutput output;
|
||||
|
||||
private int state;
|
||||
private int frameBytesRead;
|
||||
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.
|
||||
private long timeUs;
|
||||
|
||||
public MpegAudioReader(TrackOutput output) {
|
||||
this(output, null);
|
||||
public MpegAudioReader() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public MpegAudioReader(TrackOutput output, String language) {
|
||||
super(output);
|
||||
public MpegAudioReader(String language) {
|
||||
state = STATE_FINDING_HEADER;
|
||||
// The first byte of an MPEG Audio frame header is always 0xFF.
|
||||
headerScratch = new ParsableByteArray(4);
|
||||
|
|
@ -71,6 +73,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
lastByteWasFF = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
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.SeekMap;
|
||||
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.ParsableByteArray;
|
||||
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 PACKET_START_CODE_PREFIX = 0x000001;
|
||||
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;
|
||||
|
||||
public static final int PRIVATE_STREAM_1 = 0xBD;
|
||||
|
|
@ -189,16 +191,18 @@ public final class PsExtractor implements Extractor {
|
|||
// Private stream, used for AC3 audio.
|
||||
// NOTE: This may need further parsing to determine if its DTS, but that's likely only
|
||||
// valid for DVDs.
|
||||
elementaryStreamReader = new Ac3Reader(output.track(streamId));
|
||||
elementaryStreamReader = new Ac3Reader();
|
||||
foundAudioTrack = true;
|
||||
} else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
|
||||
elementaryStreamReader = new MpegAudioReader(output.track(streamId));
|
||||
elementaryStreamReader = new MpegAudioReader();
|
||||
foundAudioTrack = true;
|
||||
} else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
|
||||
elementaryStreamReader = new H262Reader(output.track(streamId));
|
||||
elementaryStreamReader = new H262Reader();
|
||||
foundVideoTrack = true;
|
||||
}
|
||||
if (elementaryStreamReader != null) {
|
||||
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
|
||||
elementaryStreamReader.init(output, idGenerator);
|
||||
payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster);
|
||||
psPayloadReaders.put(streamId, payloadReader);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.util.SparseIntArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.SeekMap;
|
||||
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.ParsableBitArray;
|
||||
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_LSF = 0x04;
|
||||
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_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 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_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
|
||||
|
||||
private final boolean mapByType;
|
||||
private final TimestampAdjuster timestampAdjuster;
|
||||
private final ParsableByteArray tsPacketBuffer;
|
||||
private final ParsableBitArray tsScratch;
|
||||
private final SparseIntArray continuityCounters;
|
||||
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.
|
||||
private ExtractorOutput output;
|
||||
private boolean tracksEnded;
|
||||
private ElementaryStreamReader id3Reader;
|
||||
|
||||
public TsExtractor() {
|
||||
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.
|
||||
*/
|
||||
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 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,
|
||||
ElementaryStreamReader.Factory customReaderFactory) {
|
||||
ElementaryStreamReader.Factory customReaderFactory, boolean mapByType) {
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
this.streamReaderFactory = Assertions.checkNotNull(customReaderFactory);
|
||||
this.mapByType = mapByType;
|
||||
tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
|
||||
tsScratch = new ParsableBitArray(new byte[3]);
|
||||
trackIds = new SparseBooleanArray();
|
||||
tsPayloadReaders = new SparseArray<>();
|
||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
||||
continuityCounters = new SparseIntArray();
|
||||
resetPayloadReaders();
|
||||
}
|
||||
|
||||
// Extractor implementation.
|
||||
|
|
@ -141,11 +153,10 @@ public final class TsExtractor implements Extractor {
|
|||
@Override
|
||||
public void seek(long position) {
|
||||
timestampAdjuster.reset();
|
||||
for (int i = 0; i < tsPayloadReaders.size(); i++) {
|
||||
tsPayloadReaders.valueAt(i).seek();
|
||||
}
|
||||
tsPacketBuffer.reset();
|
||||
continuityCounters.clear();
|
||||
// Elementary stream readers' state should be cleared to get consistent behaviours when seeking.
|
||||
resetPayloadReaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -240,6 +251,13 @@ public final class TsExtractor implements Extractor {
|
|||
|
||||
// Internals.
|
||||
|
||||
private void resetPayloadReaders() {
|
||||
trackIds.clear();
|
||||
tsPayloadReaders.clear();
|
||||
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
|
||||
id3Reader = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses TS packet payload data.
|
||||
*/
|
||||
|
|
@ -333,7 +351,7 @@ public final class TsExtractor implements Extractor {
|
|||
patScratch.skipBits(13); // network_PID (13)
|
||||
} else {
|
||||
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 ParsableByteArray sectionData;
|
||||
private final int pid;
|
||||
|
||||
private int sectionLength;
|
||||
private int sectionBytesRead;
|
||||
private int crc;
|
||||
|
||||
public PmtReader() {
|
||||
public PmtReader(int pid) {
|
||||
pmtScratch = new ParsableBitArray(new byte[5]);
|
||||
sectionData = new ParsableByteArray();
|
||||
this.pid = pid;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -413,6 +433,14 @@ public final class TsExtractor implements Extractor {
|
|||
// Skip the descriptors.
|
||||
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 */
|
||||
- programInfoLength - 4 /* CRC length */;
|
||||
while (remainingEntriesLength > 0) {
|
||||
|
|
@ -422,21 +450,40 @@ public final class TsExtractor implements Extractor {
|
|||
int elementaryPid = pmtScratch.readBits(13);
|
||||
pmtScratch.skipBits(4); // reserved
|
||||
int esInfoLength = pmtScratch.readBits(12); // ES_info_length.
|
||||
ElementaryStreamReader.EsInfo esInfo = readEsInfo(sectionData, esInfoLength);
|
||||
EsInfo esInfo = readEsInfo(sectionData, esInfoLength);
|
||||
if (streamType == 0x06) {
|
||||
streamType = esInfo.streamType;
|
||||
}
|
||||
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) {
|
||||
tsPayloadReaders.put(elementaryPid,
|
||||
new PesReader(pesPayloadReader, timestampAdjuster));
|
||||
tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader, timestampAdjuster));
|
||||
}
|
||||
}
|
||||
|
||||
output.endTracks();
|
||||
if (mapByType) {
|
||||
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}.
|
||||
* @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 descriptorsEndPosition = descriptorsStartPosition + length;
|
||||
int streamType = -1;
|
||||
|
|
@ -479,7 +526,7 @@ public final class TsExtractor implements Extractor {
|
|||
data.skipBytes(positionOfNextDescriptor - data.getPosition());
|
||||
}
|
||||
data.setPosition(descriptorsEndPosition);
|
||||
return new ElementaryStreamReader.EsInfo(streamType, language,
|
||||
return new EsInfo(streamType, language,
|
||||
Arrays.copyOfRange(sectionData.data, descriptorsStartPosition, descriptorsEndPosition));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
|
|||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
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 java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 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 onContinueLoadingRequestedRunnable;
|
||||
private final Handler handler;
|
||||
private final SparseArray<DefaultTrackOutput> sampleQueues;
|
||||
|
||||
private Callback callback;
|
||||
private SeekMap seekMap;
|
||||
|
|
@ -77,7 +78,6 @@ import java.util.Arrays;
|
|||
private boolean seenFirstTrackSelection;
|
||||
private boolean notifyReset;
|
||||
private int enabledTrackCount;
|
||||
private DefaultTrackOutput[] sampleQueues;
|
||||
private TrackGroupArray tracks;
|
||||
private long durationUs;
|
||||
private boolean[] trackEnabledStates;
|
||||
|
|
@ -131,7 +131,7 @@ import java.util.Arrays;
|
|||
handler = new Handler();
|
||||
|
||||
pendingResetPositionUs = C.TIME_UNSET;
|
||||
sampleQueues = new DefaultTrackOutput[0];
|
||||
sampleQueues = new SparseArray<>();
|
||||
length = C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
|
|
@ -141,8 +141,9 @@ import java.util.Arrays;
|
|||
@Override
|
||||
public void run() {
|
||||
extractorHolder.release();
|
||||
for (DefaultTrackOutput sampleQueue : sampleQueues) {
|
||||
sampleQueue.disable();
|
||||
int trackCount = sampleQueues.size();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
sampleQueues.valueAt(i).disable();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -178,7 +179,7 @@ import java.util.Arrays;
|
|||
Assertions.checkState(trackEnabledStates[track]);
|
||||
enabledTrackCount--;
|
||||
trackEnabledStates[track] = false;
|
||||
sampleQueues[track].disable();
|
||||
sampleQueues.valueAt(track).disable();
|
||||
streams[i] = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -201,9 +202,10 @@ import java.util.Arrays;
|
|||
if (!seenFirstTrackSelection) {
|
||||
// At the time of the first track selection all queues will be enabled, so we need to disable
|
||||
// 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]) {
|
||||
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.
|
||||
positionUs = seekMap.isSeekable() ? positionUs : 0;
|
||||
lastSeekPositionUs = positionUs;
|
||||
int trackCount = sampleQueues.size();
|
||||
// If we're not pending a reset, see if we can seek within the sample queues.
|
||||
boolean seekInsideBuffer = !isPendingReset();
|
||||
for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) {
|
||||
for (int i = 0; seekInsideBuffer && i < trackCount; 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.
|
||||
|
|
@ -284,8 +287,8 @@ import java.util.Arrays;
|
|||
if (loader.isLoading()) {
|
||||
loader.cancelLoading();
|
||||
} else {
|
||||
for (int i = 0; i < sampleQueues.length; i++) {
|
||||
sampleQueues[i].reset(trackEnabledStates[i]);
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -296,7 +299,7 @@ import java.util.Arrays;
|
|||
// SampleStream methods.
|
||||
|
||||
/* package */ boolean isReady(int track) {
|
||||
return loadingFinished || (!isPendingReset() && !sampleQueues[track].isEmpty());
|
||||
return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty());
|
||||
}
|
||||
|
||||
/* package */ void maybeThrowError() throws IOException {
|
||||
|
|
@ -308,7 +311,8 @@ import java.util.Arrays;
|
|||
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.
|
||||
|
|
@ -332,8 +336,9 @@ import java.util.Arrays;
|
|||
long loadDurationMs, boolean released) {
|
||||
copyLengthFromLoader(loadable);
|
||||
if (!released && enabledTrackCount > 0) {
|
||||
for (int i = 0; i < sampleQueues.length; i++) {
|
||||
sampleQueues[i].reset(trackEnabledStates[i]);
|
||||
int trackCount = sampleQueues.size();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
|
||||
}
|
||||
callback.onContinueLoadingRequested(this);
|
||||
}
|
||||
|
|
@ -358,11 +363,13 @@ import java.util.Arrays;
|
|||
|
||||
@Override
|
||||
public TrackOutput track(int id) {
|
||||
sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1);
|
||||
DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator);
|
||||
sampleQueue.setUpstreamFormatChangeListener(this);
|
||||
sampleQueues[sampleQueues.length - 1] = sampleQueue;
|
||||
return sampleQueue;
|
||||
DefaultTrackOutput trackOutput = sampleQueues.get(id);
|
||||
if (trackOutput == null) {
|
||||
trackOutput = new DefaultTrackOutput(allocator);
|
||||
trackOutput.setUpstreamFormatChangeListener(this);
|
||||
sampleQueues.put(id, trackOutput);
|
||||
}
|
||||
return trackOutput;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -390,18 +397,18 @@ import java.util.Arrays;
|
|||
if (released || prepared || seekMap == null || !tracksBuilt) {
|
||||
return;
|
||||
}
|
||||
for (DefaultTrackOutput sampleQueue : sampleQueues) {
|
||||
if (sampleQueue.getUpstreamFormat() == null) {
|
||||
int trackCount = sampleQueues.size();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
loadCondition.close();
|
||||
int trackCount = sampleQueues.length;
|
||||
TrackGroup[] trackArray = new TrackGroup[trackCount];
|
||||
trackEnabledStates = new boolean[trackCount];
|
||||
durationUs = seekMap.getDurationUs();
|
||||
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);
|
||||
prepared = true;
|
||||
|
|
@ -455,8 +462,9 @@ import java.util.Arrays;
|
|||
// a new load.
|
||||
lastSeekPositionUs = 0;
|
||||
notifyReset = prepared;
|
||||
for (int i = 0; i < sampleQueues.length; i++) {
|
||||
sampleQueues[i].reset(!prepared || trackEnabledStates[i]);
|
||||
int trackCount = sampleQueues.size();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]);
|
||||
}
|
||||
loadable.setLoadPosition(0);
|
||||
}
|
||||
|
|
@ -464,17 +472,19 @@ import java.util.Arrays;
|
|||
|
||||
private int getExtractedSamplesCount() {
|
||||
int extractedSamplesCount = 0;
|
||||
for (DefaultTrackOutput sampleQueue : sampleQueues) {
|
||||
extractedSamplesCount += sampleQueue.getWriteIndex();
|
||||
int trackCount = sampleQueues.size();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex();
|
||||
}
|
||||
return extractedSamplesCount;
|
||||
}
|
||||
|
||||
private long getLargestQueuedTimestampUs() {
|
||||
long largestQueuedTimestampUs = Long.MIN_VALUE;
|
||||
for (DefaultTrackOutput sampleQueue : sampleQueues) {
|
||||
int trackCount = sampleQueues.size();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
|
||||
sampleQueue.getLargestQueuedTimestampUs());
|
||||
sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
|
||||
}
|
||||
return largestQueuedTimestampUs;
|
||||
}
|
||||
|
|
@ -523,7 +533,7 @@ import java.util.Arrays;
|
|||
|
||||
@Override
|
||||
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.
|
||||
private boolean seenTrack;
|
||||
private int seenTrackId;
|
||||
|
||||
/**
|
||||
* @param extractor The extractor to wrap.
|
||||
|
|
@ -116,8 +117,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
|
|||
|
||||
@Override
|
||||
public TrackOutput track(int id) {
|
||||
Assertions.checkState(!seenTrack);
|
||||
Assertions.checkState(!seenTrack || seenTrackId == id);
|
||||
seenTrack = true;
|
||||
seenTrackId = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -655,7 +655,9 @@ public class DashManifestParser extends DefaultHandler
|
|||
return MimeTypes.getVideoMediaMimeType(codecs);
|
||||
} else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,8 +257,16 @@ import java.util.Locale;
|
|||
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex,
|
||||
newVariantIndex);
|
||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
||||
fatalError = new BehindLiveWindowException();
|
||||
return;
|
||||
// We try getting the next chunk without adapting in case that's the reason for falling
|
||||
// behind the live window.
|
||||
newVariantIndex = oldVariantIndex;
|
||||
mediaPlaylist = variantPlaylists[newVariantIndex];
|
||||
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex,
|
||||
newVariantIndex);
|
||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
||||
fatalError = new BehindLiveWindowException();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -369,29 +377,29 @@ import java.util.Locale;
|
|||
}
|
||||
} else if (needNewExtractor) {
|
||||
// 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;
|
||||
if (useInitializedExtractor) {
|
||||
extractor = lastLoadedInitializationChunk.extractor;
|
||||
} else {
|
||||
timestampAdjuster = timestampAdjusterProvider.getAdjuster(
|
||||
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,
|
||||
new DefaultStreamReaderFactory(workaroundFlags));
|
||||
new DefaultStreamReaderFactory(esReaderFactoryFlags), true);
|
||||
}
|
||||
} else {
|
||||
// MPEG-2 TS segments, and we need to continue using the same extractor.
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ import java.util.List;
|
|||
sampleStreamWrappers = new HlsSampleStreamWrapper[] {
|
||||
buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)};
|
||||
pendingPrepareCount = 1;
|
||||
sampleStreamWrappers[0].prepare();
|
||||
sampleStreamWrappers[0].continuePreparing();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -369,16 +369,16 @@ import java.util.List;
|
|||
selectedVariants.toArray(variants);
|
||||
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
|
||||
baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat);
|
||||
sampleStreamWrapper.prepare();
|
||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||
sampleStreamWrapper.continuePreparing();
|
||||
}
|
||||
|
||||
// Build audio stream wrappers.
|
||||
for (int i = 0; i < audioVariants.size(); i++) {
|
||||
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO,
|
||||
baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null);
|
||||
sampleStreamWrapper.prepare();
|
||||
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
|
||||
sampleStreamWrapper.continuePreparing();
|
||||
}
|
||||
|
||||
// Build subtitle stream wrappers.
|
||||
|
|
|
|||
|
|
@ -144,8 +144,10 @@ import java.util.LinkedList;
|
|||
pendingResetPositionUs = positionUs;
|
||||
}
|
||||
|
||||
public void prepare() {
|
||||
continueLoading(lastSeekPositionUs);
|
||||
public void continuePreparing() {
|
||||
if (!prepared) {
|
||||
continueLoading(lastSeekPositionUs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -154,7 +156,8 @@ import java.util.LinkedList;
|
|||
*/
|
||||
public void prepareSingleTrack(Format format) {
|
||||
track(0).format(format);
|
||||
endTracks();
|
||||
sampleQueuesBuilt = true;
|
||||
maybeFinishPrepare();
|
||||
}
|
||||
|
||||
public void maybeThrowPrepareError() throws IOException {
|
||||
|
|
|
|||
|
|
@ -413,11 +413,16 @@ public class DefaultHttpDataSource implements HttpDataSource {
|
|||
connection.setInstanceFollowRedirects(followRedirects);
|
||||
connection.setDoOutput(postBody != null);
|
||||
if (postBody != null) {
|
||||
connection.setFixedLengthStreamingMode(postBody.length);
|
||||
connection.connect();
|
||||
OutputStream os = connection.getOutputStream();
|
||||
os.write(postBody);
|
||||
os.close();
|
||||
connection.setRequestMethod("POST");
|
||||
if (postBody.length == 0) {
|
||||
connection.connect();
|
||||
} else {
|
||||
connection.setFixedLengthStreamingMode(postBody.length);
|
||||
connection.connect();
|
||||
OutputStream os = connection.getOutputStream();
|
||||
os.write(postBody);
|
||||
os.close();
|
||||
}
|
||||
} else {
|
||||
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_ID3 = BASE_TYPE_APPLICATION + "/id3";
|
||||
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_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
|
||||
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.playbacktests"
|
||||
android:versionCode="2002"
|
||||
android:versionName="2.0.2">
|
||||
android:versionCode="2003"
|
||||
android:versionName="2.0.3">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import junit.framework.Assert;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* A fake {@link ExtractorOutput}.
|
||||
|
|
@ -37,8 +36,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
|
|||
*/
|
||||
private static final boolean WRITE_DUMP = false;
|
||||
|
||||
private final boolean allowDuplicateTrackIds;
|
||||
|
||||
public final SparseArray<FakeTrackOutput> trackOutputs;
|
||||
|
||||
public int numberOfTracks;
|
||||
|
|
@ -46,11 +43,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
|
|||
public SeekMap seekMap;
|
||||
|
||||
public FakeExtractorOutput() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public FakeExtractorOutput(boolean allowDuplicateTrackIds) {
|
||||
this.allowDuplicateTrackIds = allowDuplicateTrackIds;
|
||||
trackOutputs = new SparseArray<>();
|
||||
}
|
||||
|
||||
|
|
@ -58,11 +50,10 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
|
|||
public FakeTrackOutput track(int trackId) {
|
||||
FakeTrackOutput output = trackOutputs.get(trackId);
|
||||
if (output == null) {
|
||||
Assert.assertFalse(tracksEnded);
|
||||
numberOfTracks++;
|
||||
output = new FakeTrackOutput();
|
||||
trackOutputs.put(trackId, output);
|
||||
} else {
|
||||
TestCase.assertTrue("Duplicate track id: " + trackId, allowDuplicateTrackIds);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,8 +267,8 @@ public class TestUtil {
|
|||
*/
|
||||
public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile,
|
||||
byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors,
|
||||
boolean simulateUnknownLength,
|
||||
boolean simulatePartialReads) throws IOException, InterruptedException {
|
||||
boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException,
|
||||
InterruptedException {
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
|
||||
.setSimulateIOErrors(simulateIOErrors)
|
||||
.setSimulateUnknownLength(simulateUnknownLength)
|
||||
|
|
|
|||
Loading…
Reference in a new issue