Commit 08256eee by ojw28 Committed by GitHub

Merge pull request #1955 from google/dev-v2

r2.0.3
parents f94218a7 cecb1f5f
Showing with 757 additions and 440 deletions
# Release notes # # Release notes #
### r2.0.3 ###
This release contains important bug fixes. Users of r2.0.0, r2.0.1 and r2.0.2
should proactively update to this version.
* Fixed NullPointerException in ExtractorMediaSource
([#1914](https://github.com/google/ExoPlayer/issues/1914).
* Fixed NullPointerException in HlsMediaPeriod
([#1907](https://github.com/google/ExoPlayer/issues/1907).
* Fixed memory leak in PlaybackControlView
([#1908](https://github.com/google/ExoPlayer/issues/1908).
* Fixed strict mode violation when using
SimpleExoPlayer.setVideoPlayerTextureView().
* Fixed L3 Widevine provisioning
([#1925](https://github.com/google/ExoPlayer/issues/1925).
* Fixed hiding of controls with use_controller="false"
([#1919](https://github.com/google/ExoPlayer/issues/1919).
* Improvements to Cronet network stack extension.
* Misc bug fixes.
### r2.0.2 ### ### r2.0.2 ###
* Fixes for MergingMediaSource and sideloaded subtitles. * Fixes for MergingMediaSource and sideloaded subtitles.
...@@ -88,6 +108,13 @@ some of the motivations behind ExoPlayer 2.x ...@@ -88,6 +108,13 @@ some of the motivations behind ExoPlayer 2.x
* Suppressed "Sending message to a Handler on a dead thread" warnings * Suppressed "Sending message to a Handler on a dead thread" warnings
([#426](https://github.com/google/ExoPlayer/issues/426)). ([#426](https://github.com/google/ExoPlayer/issues/426)).
### r1.5.12 ###
* Improvements to Cronet network stack extension.
* Fix bug in demo app introduced in r1.5.11 that caused L3 Widevine
provisioning requests to fail.
* Misc bugfixes.
### r1.5.11 ### ### r1.5.11 ###
* Cronet network stack extension. * Cronet network stack extension.
......
...@@ -35,7 +35,7 @@ allprojects { ...@@ -35,7 +35,7 @@ allprojects {
releaseRepoName = 'exoplayer' releaseRepoName = 'exoplayer'
releaseUserOrg = 'google' releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer' releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.0.2' releaseVersion = 'r2.0.3'
releaseWebsite = 'https://github.com/google/ExoPlayer' releaseWebsite = 'https://github.com/google/ExoPlayer'
} }
} }
...@@ -38,15 +38,15 @@ android { ...@@ -38,15 +38,15 @@ android {
} }
productFlavors { productFlavors {
demo noExtensions
demoExt withExtensions
} }
} }
dependencies { dependencies {
compile project(':library') compile project(':library')
demoExtCompile project(path: ':extension-ffmpeg') withExtensionsCompile project(path: ':extension-ffmpeg')
demoExtCompile project(path: ':extension-flac') withExtensionsCompile project(path: ':extension-flac')
demoExtCompile project(path: ':extension-opus') withExtensionsCompile project(path: ':extension-opus')
demoExtCompile project(path: ':extension-vp9') withExtensionsCompile project(path: ':extension-vp9')
} }
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo"
android:versionCode="2002" android:versionCode="2003"
android:versionName="2.0.2"> android:versionName="2.0.3">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
......
...@@ -97,6 +97,9 @@ import java.util.Locale; ...@@ -97,6 +97,9 @@ import java.util.Locale;
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
if (timeline == null) {
return;
}
int periodCount = timeline.getPeriodCount(); int periodCount = timeline.getPeriodCount();
int windowCount = timeline.getWindowCount(); int windowCount = timeline.getWindowCount();
Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);
......
...@@ -101,6 +101,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -101,6 +101,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
private Handler mainHandler; private Handler mainHandler;
private Timeline.Window window;
private EventLogger eventLogger; private EventLogger eventLogger;
private SimpleExoPlayerView simpleExoPlayerView; private SimpleExoPlayerView simpleExoPlayerView;
private LinearLayout debugRootView; private LinearLayout debugRootView;
...@@ -115,7 +116,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -115,7 +116,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private boolean playerNeedsSource; private boolean playerNeedsSource;
private boolean shouldAutoPlay; private boolean shouldAutoPlay;
private boolean shouldRestorePosition; private boolean isTimelineStatic;
private int playerWindow; private int playerWindow;
private long playerPosition; private long playerPosition;
...@@ -127,6 +128,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -127,6 +128,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
shouldAutoPlay = true; shouldAutoPlay = true;
mediaDataSourceFactory = buildDataSourceFactory(true); mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler(); mainHandler = new Handler();
window = new Timeline.Window();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
} }
...@@ -147,7 +149,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -147,7 +149,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
releasePlayer(); releasePlayer();
shouldRestorePosition = false; isTimelineStatic = false;
setIntent(intent); setIntent(intent);
} }
...@@ -262,7 +264,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -262,7 +264,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.setVideoDebugListener(eventLogger); player.setVideoDebugListener(eventLogger);
player.setId3Output(eventLogger); player.setId3Output(eventLogger);
simpleExoPlayerView.setPlayer(player); simpleExoPlayerView.setPlayer(player);
if (shouldRestorePosition) { if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) { if (playerPosition == C.TIME_UNSET) {
player.seekToDefaultPosition(playerWindow); player.seekToDefaultPosition(playerWindow);
} else { } else {
...@@ -305,7 +307,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -305,7 +307,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources); : new ConcatenatingMediaSource(mediaSources);
player.prepare(mediaSource, !shouldRestorePosition); player.prepare(mediaSource, !isTimelineStatic, !isTimelineStatic);
playerNeedsSource = false; playerNeedsSource = false;
updateButtonVisibilities(); updateButtonVisibilities();
} }
...@@ -348,15 +350,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -348,15 +350,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
debugViewHelper.stop(); debugViewHelper.stop();
debugViewHelper = null; debugViewHelper = null;
shouldAutoPlay = player.getPlayWhenReady(); shouldAutoPlay = player.getPlayWhenReady();
shouldRestorePosition = false; playerWindow = player.getCurrentWindowIndex();
playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline(); Timeline timeline = player.getCurrentTimeline();
if (timeline != null) { if (timeline != null && timeline.getWindow(playerWindow, window).isSeekable) {
playerWindow = player.getCurrentWindowIndex(); playerPosition = player.getCurrentPosition();
Timeline.Window window = timeline.getWindow(playerWindow, new Timeline.Window());
if (!window.isDynamic) {
shouldRestorePosition = true;
playerPosition = window.isSeekable ? player.getCurrentPosition() : C.TIME_UNSET;
}
} }
player.release(); player.release();
player = null; player = null;
...@@ -412,7 +410,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -412,7 +410,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing. isTimelineStatic = timeline != null && timeline.getWindowCount() > 0
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
} }
@Override @Override
...@@ -501,7 +500,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -501,7 +500,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
button.setText(label); button.setText(label);
button.setTag(i); button.setTag(i);
button.setOnClickListener(this); button.setOnClickListener(this);
debugRootView.addView(button); debugRootView.addView(button, debugRootView.getChildCount() - 1);
} }
} }
} }
......
...@@ -41,7 +41,7 @@ public final class CronetDataSourceFactory implements Factory { ...@@ -41,7 +41,7 @@ public final class CronetDataSourceFactory implements Factory {
private final CronetEngine cronetEngine; private final CronetEngine cronetEngine;
private final Executor executor; private final Executor executor;
private final Predicate<String> contentTypePredicate; private final Predicate<String> contentTypePredicate;
private final TransferListener transferListener; private final TransferListener<? super DataSource> transferListener;
private final int connectTimeoutMs; private final int connectTimeoutMs;
private final int readTimeoutMs; private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects; private final boolean resetTimeoutOnRedirects;
......
...@@ -32,21 +32,21 @@ import java.util.List; ...@@ -32,21 +32,21 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import okhttp3.CacheControl; import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
/** /**
* An {@link HttpDataSource} that delegates to Square's {@link OkHttpClient}. * An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}.
*/ */
public class OkHttpDataSource implements HttpDataSource { public class OkHttpDataSource implements HttpDataSource {
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>(); private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
private final OkHttpClient okHttpClient; private final Call.Factory callFactory;
private final String userAgent; private final String userAgent;
private final Predicate<String> contentTypePredicate; private final Predicate<String> contentTypePredicate;
private final TransferListener<? super OkHttpDataSource> listener; private final TransferListener<? super OkHttpDataSource> listener;
...@@ -65,31 +65,31 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -65,31 +65,31 @@ public class OkHttpDataSource implements HttpDataSource {
private long bytesRead; private long bytesRead;
/** /**
* @param client An {@link OkHttpClient} for use by the source. * @param callFactory An {@link Call.Factory} for use by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
*/ */
public OkHttpDataSource(OkHttpClient client, String userAgent, public OkHttpDataSource(Call.Factory callFactory, String userAgent,
Predicate<String> contentTypePredicate) { Predicate<String> contentTypePredicate) {
this(client, userAgent, contentTypePredicate, null); this(callFactory, userAgent, contentTypePredicate, null);
} }
/** /**
* @param client An {@link OkHttpClient} for use by the source. * @param callFactory An {@link Call.Factory} for use by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from * predicate then a {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
* @param listener An optional listener. * @param listener An optional listener.
*/ */
public OkHttpDataSource(OkHttpClient client, String userAgent, public OkHttpDataSource(Call.Factory callFactory, String userAgent,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) { Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) {
this(client, userAgent, contentTypePredicate, listener, null); this(callFactory, userAgent, contentTypePredicate, listener, null);
} }
/** /**
* @param client An {@link OkHttpClient} for use by the source. * @param callFactory An {@link Call.Factory} for use by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from * predicate then a {@link InvalidContentTypeException} is thrown from
...@@ -98,10 +98,10 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -98,10 +98,10 @@ public class OkHttpDataSource implements HttpDataSource {
* @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control * @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control
* header. For example, you could force the network response for all requests. * header. For example, you could force the network response for all requests.
*/ */
public OkHttpDataSource(OkHttpClient client, String userAgent, public OkHttpDataSource(Call.Factory callFactory, String userAgent,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener, Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
CacheControl cacheControl) { CacheControl cacheControl) {
this.okHttpClient = Assertions.checkNotNull(client); this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = Assertions.checkNotEmpty(userAgent); this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate; this.contentTypePredicate = contentTypePredicate;
this.listener = listener; this.listener = listener;
...@@ -150,7 +150,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -150,7 +150,7 @@ public class OkHttpDataSource implements HttpDataSource {
this.bytesSkipped = 0; this.bytesSkipped = 0;
Request request = makeRequest(dataSpec); Request request = makeRequest(dataSpec);
try { try {
response = okHttpClient.newCall(request).execute(); response = callFactory.newCall(request).execute();
responseByteStream = response.body().byteStream(); responseByteStream = response.body().byteStream();
} catch (IOException e) { } catch (IOException e) {
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
...@@ -185,9 +185,12 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -185,9 +185,12 @@ public class OkHttpDataSource implements HttpDataSource {
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping. // Determine the length of the data to be read, after skipping.
long contentLength = response.body().contentLength(); if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length bytesToRead = dataSpec.length;
: (contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET); } else {
long contentLength = response.body().contentLength();
bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
opened = true; opened = true;
if (listener != null) { if (listener != null) {
......
...@@ -19,26 +19,26 @@ import com.google.android.exoplayer2.upstream.DataSource; ...@@ -19,26 +19,26 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import okhttp3.CacheControl; import okhttp3.CacheControl;
import okhttp3.OkHttpClient; import okhttp3.Call;
/** /**
* A {@link Factory} that produces {@link OkHttpDataSource}. * A {@link Factory} that produces {@link OkHttpDataSource}.
*/ */
public final class OkHttpDataSourceFactory implements Factory { public final class OkHttpDataSourceFactory implements Factory {
private final OkHttpClient client; private final Call.Factory callFactory;
private final String userAgent; private final String userAgent;
private final TransferListener<? super DataSource> transferListener; private final TransferListener<? super DataSource> transferListener;
private final CacheControl cacheControl; private final CacheControl cacheControl;
public OkHttpDataSourceFactory(OkHttpClient client, String userAgent, public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
TransferListener<? super DataSource> transferListener) { TransferListener<? super DataSource> transferListener) {
this(client, userAgent, transferListener, null); this(callFactory, userAgent, transferListener, null);
} }
public OkHttpDataSourceFactory(OkHttpClient client, String userAgent, public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
TransferListener<? super DataSource> transferListener, CacheControl cacheControl) { TransferListener<? super DataSource> transferListener, CacheControl cacheControl) {
this.client = client; this.callFactory = callFactory;
this.userAgent = userAgent; this.userAgent = userAgent;
this.transferListener = transferListener; this.transferListener = transferListener;
this.cacheControl = cacheControl; this.cacheControl = cacheControl;
...@@ -46,7 +46,7 @@ public final class OkHttpDataSourceFactory implements Factory { ...@@ -46,7 +46,7 @@ public final class OkHttpDataSourceFactory implements Factory {
@Override @Override
public OkHttpDataSource createDataSource() { public OkHttpDataSource createDataSource() {
return new OkHttpDataSource(client, userAgent, null, transferListener, cacheControl); return new OkHttpDataSource(callFactory, userAgent, null, transferListener, cacheControl);
} }
} }
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -66,9 +68,12 @@ public class AdtsReaderTest extends TestCase { ...@@ -66,9 +68,12 @@ public class AdtsReaderTest extends TestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
adtsOutput = new FakeTrackOutput(); FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
id3Output = new FakeTrackOutput(); adtsOutput = fakeExtractorOutput.track(0);
adtsReader = new AdtsReader(adtsOutput, id3Output); id3Output = fakeExtractorOutput.track(1);
adtsReader = new AdtsReader(true);
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
adtsReader.init(fakeExtractorOutput, idGenerator);
data = new ParsableByteArray(TEST_DATA); data = new ParsableByteArray(TEST_DATA);
firstFeed = true; firstFeed = true;
} }
......
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; ...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput;
...@@ -72,7 +73,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -72,7 +73,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomPesReader() throws Exception { public void testCustomPesReader() throws Exception {
CustomEsReaderFactory factory = new CustomEsReaderFactory(); CustomEsReaderFactory factory = new CustomEsReaderFactory();
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory); TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
FakeExtractorInput input = new FakeExtractorInput.Builder() FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
.setSimulateIOErrors(false) .setSimulateIOErrors(false)
...@@ -107,12 +108,12 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -107,12 +108,12 @@ public final class TsExtractorTest extends InstrumentationTestCase {
private static final class CustomEsReader extends ElementaryStreamReader { private static final class CustomEsReader extends ElementaryStreamReader {
private final String language;
private TrackOutput output;
public int packetsRead = 0; public int packetsRead = 0;
public CustomEsReader(TrackOutput output, String language) { public CustomEsReader(String language) {
super(output); this.language = language;
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
language, null, 0));
} }
@Override @Override
...@@ -120,6 +121,13 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -120,6 +121,13 @@ public final class TsExtractorTest extends InstrumentationTestCase {
} }
@Override @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) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
} }
...@@ -148,16 +156,12 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -148,16 +156,12 @@ public final class TsExtractorTest extends InstrumentationTestCase {
} }
@Override @Override
public ElementaryStreamReader onPmtEntry(int pid, int streamType, public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) {
ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) {
if (streamType == 3) { if (streamType == 3) {
// We need to manually avoid a duplicate custom reader creation. reader = new CustomEsReader(esInfo.language);
if (reader == null) {
reader = new CustomEsReader(output.track(pid), esInfo.language);
}
return reader; return reader;
} else { } else {
return defaultFactory.onPmtEntry(pid, streamType, esInfo, output); return defaultFactory.createStreamReader(streamType, esInfo);
} }
} }
......
...@@ -130,8 +130,8 @@ public interface ExoPlayer { ...@@ -130,8 +130,8 @@ public interface ExoPlayer {
/** /**
* Called when timeline and/or manifest has been refreshed. * Called when timeline and/or manifest has been refreshed.
* *
* @param timeline The latest timeline. * @param timeline The latest timeline, or null if the timeline is being cleared.
* @param manifest The latest manifest. * @param manifest The latest manifest, or null if the manifest is being cleared.
*/ */
void onTimelineChanged(Timeline timeline, Object manifest); void onTimelineChanged(Timeline timeline, Object manifest);
...@@ -247,7 +247,7 @@ public interface ExoPlayer { ...@@ -247,7 +247,7 @@ public interface ExoPlayer {
/** /**
* Prepares the player to play the provided {@link MediaSource}. Equivalent to * Prepares the player to play the provided {@link MediaSource}. Equivalent to
* {@code prepare(mediaSource, true)}. * {@code prepare(mediaSource, true, true)}.
*/ */
void prepare(MediaSource mediaSource); void prepare(MediaSource mediaSource);
...@@ -259,8 +259,11 @@ public interface ExoPlayer { ...@@ -259,8 +259,11 @@ public interface ExoPlayer {
* @param resetPosition Whether the playback position should be reset to the default position in * @param resetPosition Whether the playback position should be reset to the default position in
* the first {@link Timeline.Window}. If false, playback will start from the position defined * the first {@link Timeline.Window}. If false, playback will start from the position defined
* by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
* @param resetTimeline Whether the timeline and manifest should be reset. Should be true unless
* the player is being prepared to play the same media as it was playing previously (e.g. if
* playback failed and is being retried).
*/ */
void prepare(MediaSource mediaSource, boolean resetPosition); void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline);
/** /**
* Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2;
import android.content.Context; import android.content.Context;
import android.os.Looper; import android.os.Looper;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
/** /**
...@@ -41,7 +42,7 @@ public final class ExoPlayerFactory { ...@@ -41,7 +42,7 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl) { LoadControl loadControl) {
return newSimpleInstance(context, trackSelector, loadControl, null); return newSimpleInstance(context, trackSelector, loadControl, null);
} }
...@@ -56,8 +57,8 @@ public final class ExoPlayerFactory { ...@@ -56,8 +57,8 @@ public final class ExoPlayerFactory {
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks. * will not be used for DRM protected playbacks.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl, DrmSessionManager drmSessionManager) { LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, false); return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, false);
} }
...@@ -74,8 +75,8 @@ public final class ExoPlayerFactory { ...@@ -74,8 +75,8 @@ public final class ExoPlayerFactory {
* available extensions over those defined in the core library. Note that extensions must be * available extensions over those defined in the core library. Note that extensions must be
* included in the application build for setting this flag to have any effect. * included in the application build for setting this flag to have any effect.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl, DrmSessionManager drmSessionManager, LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders) { boolean preferExtensionDecoders) {
return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager,
preferExtensionDecoders, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); preferExtensionDecoders, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
...@@ -96,8 +97,8 @@ public final class ExoPlayerFactory { ...@@ -96,8 +97,8 @@ public final class ExoPlayerFactory {
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
* seamlessly join an ongoing playback. * seamlessly join an ongoing playback.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl, DrmSessionManager drmSessionManager, LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) {
return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager, return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager,
preferExtensionDecoders, allowedVideoJoiningTimeMs); preferExtensionDecoders, allowedVideoJoiningTimeMs);
...@@ -110,7 +111,7 @@ public final class ExoPlayerFactory { ...@@ -110,7 +111,7 @@ public final class ExoPlayerFactory {
* @param renderers The {@link Renderer}s that will be used by the instance. * @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
*/ */
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) { public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector<?> trackSelector) {
return newInstance(renderers, trackSelector, new DefaultLoadControl()); return newInstance(renderers, trackSelector, new DefaultLoadControl());
} }
...@@ -122,7 +123,7 @@ public final class ExoPlayerFactory { ...@@ -122,7 +123,7 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
*/ */
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector, public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector<?> trackSelector,
LoadControl loadControl) { LoadControl loadControl) {
return new ExoPlayerImpl(renderers, trackSelector, loadControl); return new ExoPlayerImpl(renderers, trackSelector, loadControl);
} }
......
...@@ -35,7 +35,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -35,7 +35,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private static final String TAG = "ExoPlayerImpl"; private static final String TAG = "ExoPlayerImpl";
private final Handler eventHandler; private final Handler eventHandler;
private final ExoPlayerImplInternal internalPlayer; private final ExoPlayerImplInternal<?> internalPlayer;
private final CopyOnWriteArraySet<EventListener> listeners; private final CopyOnWriteArraySet<EventListener> listeners;
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period; private final Timeline.Period period;
...@@ -63,7 +63,8 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -63,7 +63,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
*/ */
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { public ExoPlayerImpl(Renderer[] renderers, TrackSelector<?> trackSelector,
LoadControl loadControl) {
Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION); Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION);
Assertions.checkNotNull(renderers); Assertions.checkNotNull(renderers);
Assertions.checkState(renderers.length > 0); Assertions.checkState(renderers.length > 0);
...@@ -79,8 +80,8 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -79,8 +80,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
}; };
playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0); playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, internalPlayer = new ExoPlayerImplInternal<>(renderers, trackSelector, loadControl,
eventHandler, playbackInfo); playWhenReady, eventHandler, playbackInfo);
} }
@Override @Override
...@@ -100,12 +101,18 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -100,12 +101,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override @Override
public void prepare(MediaSource mediaSource) { public void prepare(MediaSource mediaSource) {
prepare(mediaSource, true); prepare(mediaSource, true, true);
} }
@Override @Override
public void prepare(MediaSource mediaSource, boolean resetPosition) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) {
timeline = null; if (resetTimeline && (timeline != null || manifest != null)) {
timeline = null;
manifest = null;
for (EventListener listener : listeners) {
listener.onTimelineChanged(null, null);
}
}
internalPlayer.prepare(mediaSource, resetPosition); internalPlayer.prepare(mediaSource, resetPosition);
} }
......
...@@ -538,16 +538,23 @@ import java.io.IOException; ...@@ -538,16 +538,23 @@ import java.io.IOException;
periodIndex = C.INDEX_UNSET; periodIndex = C.INDEX_UNSET;
} }
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder<T> periodHolder = playingPeriodHolder;
MediaPeriodHolder<T> newPlayingPeriodHolder = null; MediaPeriodHolder<T> newPlayingPeriodHolder = null;
while (periodHolder != null) { if (playingPeriodHolder == null) {
if (periodHolder.index == periodIndex && periodHolder.prepared) { // We're still waiting for the first period to be prepared.
newPlayingPeriodHolder = periodHolder; if (loadingPeriodHolder != null) {
} else { loadingPeriodHolder.release();
periodHolder.release(); }
} else {
// Clear the timeline, but keep the requested period if it is already prepared.
MediaPeriodHolder<T> periodHolder = playingPeriodHolder;
while (periodHolder != null) {
if (periodHolder.index == periodIndex && periodHolder.prepared) {
newPlayingPeriodHolder = periodHolder;
} else {
periodHolder.release();
}
periodHolder = periodHolder.next;
} }
periodHolder = periodHolder.next;
} }
// Disable all the renderers if the period is changing. // Disable all the renderers if the period is changing.
...@@ -892,7 +899,8 @@ import java.io.IOException; ...@@ -892,7 +899,8 @@ import java.io.IOException;
} }
// Release all loaded periods. // Release all loaded periods.
releasePeriodHoldersFrom(playingPeriodHolder); releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder
: loadingPeriodHolder);
bufferAheadPeriodCount = 0; bufferAheadPeriodCount = 0;
playingPeriodHolder = null; playingPeriodHolder = null;
readingPeriodHolder = null; readingPeriodHolder = null;
......
...@@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { ...@@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
/** /**
* The version of the library, expressed as a string. * The version of the library, expressed as a string.
*/ */
String VERSION = "2.0.2"; String VERSION = "2.0.3";
/** /**
* The version of the library, expressed as an integer. * The version of the library, expressed as an integer.
...@@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { ...@@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
int VERSION_INT = 2000002; int VERSION_INT = 2000003;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -108,6 +108,7 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -108,6 +108,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
private Format audioFormat; private Format audioFormat;
private Surface surface; private Surface surface;
private boolean ownsSurface;
private SurfaceHolder surfaceHolder; private SurfaceHolder surfaceHolder;
private TextureView textureView; private TextureView textureView;
private TextRenderer.Output textOutput; private TextRenderer.Output textOutput;
...@@ -206,7 +207,7 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -206,7 +207,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
*/ */
public void setVideoSurface(Surface surface) { public void setVideoSurface(Surface surface) {
removeSurfaceCallbacks(); removeSurfaceCallbacks();
setVideoSurfaceInternal(surface); setVideoSurfaceInternal(surface, false);
} }
/** /**
...@@ -219,9 +220,9 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -219,9 +220,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
removeSurfaceCallbacks(); removeSurfaceCallbacks();
this.surfaceHolder = surfaceHolder; this.surfaceHolder = surfaceHolder;
if (surfaceHolder == null) { if (surfaceHolder == null) {
setVideoSurfaceInternal(null); setVideoSurfaceInternal(null, false);
} else { } else {
setVideoSurfaceInternal(surfaceHolder.getSurface()); setVideoSurfaceInternal(surfaceHolder.getSurface(), false);
surfaceHolder.addCallback(componentListener); surfaceHolder.addCallback(componentListener);
} }
} }
...@@ -246,13 +247,13 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -246,13 +247,13 @@ public final class SimpleExoPlayer implements ExoPlayer {
removeSurfaceCallbacks(); removeSurfaceCallbacks();
this.textureView = textureView; this.textureView = textureView;
if (textureView == null) { if (textureView == null) {
setVideoSurfaceInternal(null); setVideoSurfaceInternal(null, true);
} else { } else {
if (textureView.getSurfaceTextureListener() != null) { if (textureView.getSurfaceTextureListener() != null) {
Log.w(TAG, "Replacing existing SurfaceTextureListener."); Log.w(TAG, "Replacing existing SurfaceTextureListener.");
} }
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture)); setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
textureView.setSurfaceTextureListener(componentListener); textureView.setSurfaceTextureListener(componentListener);
} }
} }
...@@ -420,8 +421,8 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -420,8 +421,8 @@ public final class SimpleExoPlayer implements ExoPlayer {
} }
@Override @Override
public void prepare(MediaSource mediaSource, boolean resetPosition) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) {
player.prepare(mediaSource, resetPosition); player.prepare(mediaSource, resetPosition, resetTimeline);
} }
@Override @Override
...@@ -468,6 +469,12 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -468,6 +469,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
public void release() { public void release() {
player.release(); player.release();
removeSurfaceCallbacks(); removeSurfaceCallbacks();
if (surface != null) {
if (ownsSurface) {
surface.release();
}
surface = null;
}
} }
@Override @Override
...@@ -618,8 +625,9 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -618,8 +625,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
} }
} }
private void setVideoSurfaceInternal(Surface surface) { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) {
this.surface = surface; // Note: We don't turn this method into a no-op if the surface is being replaced with itself
// so as to ensure onRenderedFirstFrame callbacks are still called in this case.
ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
int count = 0; int count = 0;
for (Renderer renderer : renderers) { for (Renderer renderer : renderers) {
...@@ -627,12 +635,18 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -627,12 +635,18 @@ public final class SimpleExoPlayer implements ExoPlayer {
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface);
} }
} }
if (surface == null) { if (this.surface != null && this.surface != surface) {
// Block to ensure that the surface is not accessed after the method returns. // If we created this surface, we are responsible for releasing it.
if (this.ownsSurface) {
this.surface.release();
}
// We're replacing a surface. Block to ensure that it's not accessed after the method returns.
player.blockingSendMessages(messages); player.blockingSendMessages(messages);
} else { } else {
player.sendMessages(messages); player.sendMessages(messages);
} }
this.surface = surface;
this.ownsSurface = ownsSurface;
} }
private final class ComponentListener implements VideoRendererEventListener, private final class ComponentListener implements VideoRendererEventListener,
...@@ -781,7 +795,7 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -781,7 +795,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void surfaceCreated(SurfaceHolder holder) { public void surfaceCreated(SurfaceHolder holder) {
setVideoSurfaceInternal(holder.getSurface()); setVideoSurfaceInternal(holder.getSurface(), false);
} }
@Override @Override
...@@ -791,14 +805,14 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -791,14 +805,14 @@ public final class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
setVideoSurfaceInternal(null); setVideoSurfaceInternal(null, false);
} }
// TextureView.SurfaceTextureListener implementation // TextureView.SurfaceTextureListener implementation
@Override @Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
setVideoSurfaceInternal(new Surface(surfaceTexture)); setVideoSurfaceInternal(new Surface(surfaceTexture), true);
} }
@Override @Override
...@@ -808,7 +822,7 @@ public final class SimpleExoPlayer implements ExoPlayer { ...@@ -808,7 +822,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
setVideoSurface(null); setVideoSurfaceInternal(null, true);
return true; return true;
} }
......
...@@ -71,7 +71,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { ...@@ -71,7 +71,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
@Override @Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
return executePost(url, null, null); return executePost(url, new byte[0], null);
} }
@Override @Override
...@@ -81,6 +81,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { ...@@ -81,6 +81,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
url = defaultUrl; url = defaultUrl;
} }
Map<String, String> requestProperties = new HashMap<>(); Map<String, String> requestProperties = new HashMap<>();
requestProperties.put("Content-Type", "application/octet-stream");
if (C.PLAYREADY_UUID.equals(uuid)) { if (C.PLAYREADY_UUID.equals(uuid)) {
requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
} }
...@@ -93,8 +94,6 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { ...@@ -93,8 +94,6 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
private byte[] executePost(String url, byte[] data, Map<String, String> requestProperties) private byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
throws IOException { throws IOException {
HttpDataSource dataSource = dataSourceFactory.createDataSource(); HttpDataSource dataSource = dataSourceFactory.createDataSource();
// Note: This will be overridden by a Content-Type in requestProperties, if one is set.
dataSource.setRequestProperty("Content-Type", "application/octet-stream");
if (requestProperties != null) { if (requestProperties != null) {
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) { for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
......
...@@ -56,7 +56,7 @@ public interface Extractor { ...@@ -56,7 +56,7 @@ public interface Extractor {
boolean sniff(ExtractorInput input) throws IOException, InterruptedException; boolean sniff(ExtractorInput input) throws IOException, InterruptedException;
/** /**
* Initializes the extractor with an {@link ExtractorOutput}. * Initializes the extractor with an {@link ExtractorOutput}. Called at most once.
* *
* @param output An {@link ExtractorOutput} to receive extracted data. * @param output An {@link ExtractorOutput} to receive extracted data.
*/ */
......
...@@ -21,18 +21,19 @@ package com.google.android.exoplayer2.extractor; ...@@ -21,18 +21,19 @@ package com.google.android.exoplayer2.extractor;
public interface ExtractorOutput { public interface ExtractorOutput {
/** /**
* Called when the {@link Extractor} identifies the existence of a track in the stream. * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
* <p> * <p>
* Returns a {@link TrackOutput} that will receive track level data belonging to the track. * The same {@link TrackOutput} is returned if multiple calls are made with the same
* {@code trackId}.
* *
* @param trackId A unique track identifier. * @param trackId A track identifier.
* @return The {@link TrackOutput} that should receive track level data belonging to the track. * @return The {@link TrackOutput} for the given track identifier.
*/ */
TrackOutput track(int trackId); TrackOutput track(int trackId);
/** /**
* Called when all tracks have been identified, meaning that {@link #track(int)} will not be * Called when all tracks have been identified, meaning no new {@code trackId} values will be
* called again. * passed to {@link #track(int)}.
*/ */
void endTracks(); void endTracks();
......
...@@ -47,6 +47,7 @@ import java.util.List; ...@@ -47,6 +47,7 @@ import java.util.List;
private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl");
private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc");
/** /**
* Parses a trak atom (defined in 14496-12). * Parses a trak atom (defined in 14496-12).
...@@ -1004,7 +1005,7 @@ import java.util.List; ...@@ -1004,7 +1005,7 @@ import java.util.List;
/** /**
* Parses encryption data from an audio/video sample entry, populating {@code out} and returning * Parses encryption data from an audio/video sample entry, populating {@code out} and returning
* the unencrypted atom type, or 0 if no sinf atom was present. * the unencrypted atom type, or 0 if no common encryption sinf atom was present.
*/ */
private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position, private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position,
int size, StsdData out, int entryIndex) { int size, StsdData out, int entryIndex) {
...@@ -1017,10 +1018,10 @@ import java.util.List; ...@@ -1017,10 +1018,10 @@ import java.util.List;
if (childAtomType == Atom.TYPE_sinf) { if (childAtomType == Atom.TYPE_sinf) {
Pair<Integer, TrackEncryptionBox> result = parseSinfFromParent(parent, childPosition, Pair<Integer, TrackEncryptionBox> result = parseSinfFromParent(parent, childPosition,
childAtomSize); childAtomSize);
Integer dataFormat = result.first; if (result != null) {
Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); out.trackEncryptionBoxes[entryIndex] = result.second;
out.trackEncryptionBoxes[entryIndex] = result.second; return result.first;
return dataFormat; }
} }
childPosition += childAtomSize; childPosition += childAtomSize;
} }
...@@ -1032,6 +1033,7 @@ import java.util.List; ...@@ -1032,6 +1033,7 @@ import java.util.List;
int position, int size) { int position, int size) {
int childPosition = position + Atom.HEADER_SIZE; int childPosition = position + Atom.HEADER_SIZE;
boolean isCencScheme = false;
TrackEncryptionBox trackEncryptionBox = null; TrackEncryptionBox trackEncryptionBox = null;
Integer dataFormat = null; Integer dataFormat = null;
while (childPosition - position < size) { while (childPosition - position < size) {
...@@ -1042,15 +1044,20 @@ import java.util.List; ...@@ -1042,15 +1044,20 @@ import java.util.List;
dataFormat = parent.readInt(); dataFormat = parent.readInt();
} else if (childAtomType == Atom.TYPE_schm) { } else if (childAtomType == Atom.TYPE_schm) {
parent.skipBytes(4); parent.skipBytes(4);
parent.readInt(); // schemeType. Expect cenc isCencScheme = parent.readInt() == TYPE_cenc;
parent.readInt(); // schemeVersion. Expect 0x00010000
} else if (childAtomType == Atom.TYPE_schi) { } else if (childAtomType == Atom.TYPE_schi) {
trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize); trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize);
} }
childPosition += childAtomSize; childPosition += childAtomSize;
} }
return Pair.create(dataFormat, trackEncryptionBox); if (isCencScheme) {
Assertions.checkArgument(dataFormat != null, "frma atom is mandatory");
Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory");
return Pair.create(dataFormat, trackEncryptionBox);
} else {
return null;
}
} }
private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position,
......
...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; ...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -117,7 +118,8 @@ public final class Ac3Extractor implements Extractor { ...@@ -117,7 +118,8 @@ public final class Ac3Extractor implements Extractor {
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
reader = new Ac3Reader(output.track(0)); // TODO: Add support for embedded ID3. reader = new Ac3Reader(); // TODO: Add support for embedded ID3.
reader.init(output, new TrackIdGenerator(0, 1));
output.endTracks(); output.endTracks();
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
private final ParsableByteArray headerScratchBytes; private final ParsableByteArray headerScratchBytes;
private final String language; private final String language;
private TrackOutput output;
private int state; private int state;
private int bytesRead; private int bytesRead;
...@@ -54,21 +57,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -54,21 +57,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
/** /**
* Constructs a new reader for (E-)AC-3 elementary streams. * Constructs a new reader for (E-)AC-3 elementary streams.
*
* @param output Track output for extracted samples.
*/ */
public Ac3Reader(TrackOutput output) { public Ac3Reader() {
this(output, null); this(null);
} }
/** /**
* Constructs a new reader for (E-)AC-3 elementary streams. * Constructs a new reader for (E-)AC-3 elementary streams.
* *
* @param output Track output for extracted samples.
* @param language Track language. * @param language Track language.
*/ */
public Ac3Reader(TrackOutput output, String language) { public Ac3Reader(String language) {
super(output);
headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
headerScratchBytes = new ParsableByteArray(headerScratchBits.data); headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
...@@ -83,6 +82,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -83,6 +82,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
} }
@Override @Override
public void init(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
output = extractorOutput.track(generator.getNextId());
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; ...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -126,7 +127,8 @@ public final class AdtsExtractor implements Extractor { ...@@ -126,7 +127,8 @@ public final class AdtsExtractor implements Extractor {
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
reader = new AdtsReader(output.track(0), output.track(1)); reader = new AdtsReader(true);
reader.init(output, new TrackIdGenerator(0, 1));
output.endTracks(); output.endTracks();
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
} }
......
...@@ -19,6 +19,8 @@ import android.util.Log; ...@@ -19,6 +19,8 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -53,11 +55,14 @@ import java.util.Collections; ...@@ -53,11 +55,14 @@ import java.util.Collections;
private static final int ID3_SIZE_OFFSET = 6; private static final int ID3_SIZE_OFFSET = 6;
private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'}; private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'};
private final boolean exposeId3;
private final ParsableBitArray adtsScratch; private final ParsableBitArray adtsScratch;
private final ParsableByteArray id3HeaderBuffer; private final ParsableByteArray id3HeaderBuffer;
private final TrackOutput id3Output;
private final String language; private final String language;
private TrackOutput output;
private TrackOutput id3Output;
private int state; private int state;
private int bytesRead; private int bytesRead;
...@@ -77,26 +82,21 @@ import java.util.Collections; ...@@ -77,26 +82,21 @@ import java.util.Collections;
private long currentSampleDuration; private long currentSampleDuration;
/** /**
* @param output A {@link TrackOutput} to which AAC samples should be written. * @param exposeId3 True if the reader should expose ID3 information.
* @param id3Output A {@link TrackOutput} to which ID3 samples should be written.
*/ */
public AdtsReader(TrackOutput output, TrackOutput id3Output) { public AdtsReader(boolean exposeId3) {
this(output, id3Output, null); this(exposeId3, null);
} }
/** /**
* @param output A {@link TrackOutput} to which AAC samples should be written. * @param exposeId3 True if the reader should expose ID3 information.
* @param id3Output A {@link TrackOutput} to which ID3 samples should be written.
* @param language Track language. * @param language Track language.
*/ */
public AdtsReader(TrackOutput output, TrackOutput id3Output, String language) { public AdtsReader(boolean exposeId3, String language) {
super(output);
this.id3Output = id3Output;
id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null,
Format.NO_VALUE, null));
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
setFindingSampleState(); setFindingSampleState();
this.exposeId3 = exposeId3;
this.language = language; this.language = language;
} }
...@@ -106,6 +106,18 @@ import java.util.Collections; ...@@ -106,6 +106,18 @@ import java.util.Collections;
} }
@Override @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) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -16,9 +16,7 @@ ...@@ -16,9 +16,7 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.util.SparseBooleanArray; import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
...@@ -28,80 +26,54 @@ import java.lang.annotation.RetentionPolicy; ...@@ -28,80 +26,54 @@ import java.lang.annotation.RetentionPolicy;
public final class DefaultStreamReaderFactory implements ElementaryStreamReader.Factory { public final class DefaultStreamReaderFactory implements ElementaryStreamReader.Factory {
/** /**
* Flags controlling what workarounds are enabled for elementary stream readers. * Flags controlling elementary stream readers behaviour.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {WORKAROUND_ALLOW_NON_IDR_KEYFRAMES, WORKAROUND_IGNORE_AAC_STREAM, @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM,
WORKAROUND_IGNORE_H264_STREAM, WORKAROUND_DETECT_ACCESS_UNITS, WORKAROUND_MAP_BY_TYPE}) FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS})
public @interface WorkaroundFlags { public @interface Flags {
} }
public static final int WORKAROUND_ALLOW_NON_IDR_KEYFRAMES = 1; public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;
public static final int WORKAROUND_IGNORE_AAC_STREAM = 2; public static final int FLAG_IGNORE_AAC_STREAM = 2;
public static final int WORKAROUND_IGNORE_H264_STREAM = 4; public static final int FLAG_IGNORE_H264_STREAM = 4;
public static final int WORKAROUND_DETECT_ACCESS_UNITS = 8; public static final int FLAG_DETECT_ACCESS_UNITS = 8;
public static final int WORKAROUND_MAP_BY_TYPE = 16;
private static final int BASE_EMBEDDED_TRACK_ID = 0x2000; // 0xFF + 1. @Flags
private final int flags;
private final SparseBooleanArray trackIds;
@WorkaroundFlags
private final int workaroundFlags;
private Id3Reader id3Reader;
private int nextEmbeddedTrackId = BASE_EMBEDDED_TRACK_ID;
public DefaultStreamReaderFactory() { public DefaultStreamReaderFactory() {
this(0); this(0);
} }
public DefaultStreamReaderFactory(int workaroundFlags) { public DefaultStreamReaderFactory(@Flags int flags) {
trackIds = new SparseBooleanArray(); this.flags = flags;
this.workaroundFlags = workaroundFlags;
} }
@Override @Override
public ElementaryStreamReader onPmtEntry(int pid, int streamType, public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) {
ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) {
if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0 && id3Reader == null) {
// Setup an ID3 track regardless of whether there's a corresponding entry, in case one
// appears intermittently during playback. See b/20261500.
id3Reader = new Id3Reader(output.track(TsExtractor.TS_STREAM_TYPE_ID3));
}
int trackId = (workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0 ? streamType : pid;
if (trackIds.get(trackId)) {
return null;
}
trackIds.put(trackId, true);
switch (streamType) { switch (streamType) {
case TsExtractor.TS_STREAM_TYPE_MPA: case TsExtractor.TS_STREAM_TYPE_MPA:
case TsExtractor.TS_STREAM_TYPE_MPA_LSF: case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
return new MpegAudioReader(output.track(trackId), esInfo.language); return new MpegAudioReader(esInfo.language);
case TsExtractor.TS_STREAM_TYPE_AAC: case TsExtractor.TS_STREAM_TYPE_AAC:
return (workaroundFlags & WORKAROUND_IGNORE_AAC_STREAM) != 0 ? null return (flags & FLAG_IGNORE_AAC_STREAM) != 0 ? null
: new AdtsReader(output.track(trackId), new DummyTrackOutput(), esInfo.language); : new AdtsReader(false, esInfo.language);
case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_AC3:
case TsExtractor.TS_STREAM_TYPE_E_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3:
return new Ac3Reader(output.track(trackId), esInfo.language); return new Ac3Reader(esInfo.language);
case TsExtractor.TS_STREAM_TYPE_DTS: case TsExtractor.TS_STREAM_TYPE_DTS:
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
return new DtsReader(output.track(trackId), esInfo.language); return new DtsReader(esInfo.language);
case TsExtractor.TS_STREAM_TYPE_H262: case TsExtractor.TS_STREAM_TYPE_H262:
return new H262Reader(output.track(trackId)); return new H262Reader();
case TsExtractor.TS_STREAM_TYPE_H264: case TsExtractor.TS_STREAM_TYPE_H264:
return (workaroundFlags & WORKAROUND_IGNORE_H264_STREAM) != 0 return (flags & FLAG_IGNORE_H264_STREAM) != 0 ? null
? null : new H264Reader(output.track(trackId), : new H264Reader((flags & FLAG_ALLOW_NON_IDR_KEYFRAMES) != 0,
new SeiReader(output.track(nextEmbeddedTrackId++)), (flags & FLAG_DETECT_ACCESS_UNITS) != 0);
(workaroundFlags & WORKAROUND_ALLOW_NON_IDR_KEYFRAMES) != 0,
(workaroundFlags & WORKAROUND_DETECT_ACCESS_UNITS) != 0);
case TsExtractor.TS_STREAM_TYPE_H265: case TsExtractor.TS_STREAM_TYPE_H265:
return new H265Reader(output.track(trackId), return new H265Reader();
new SeiReader(output.track(nextEmbeddedTrackId++)));
case TsExtractor.TS_STREAM_TYPE_ID3: case TsExtractor.TS_STREAM_TYPE_ID3:
if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0) { return new Id3Reader();
return id3Reader;
} else {
return new Id3Reader(output.track(nextEmbeddedTrackId++));
}
default: default:
return null; return null;
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.DtsUtil; import com.google.android.exoplayer2.audio.DtsUtil;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
private final ParsableByteArray headerScratchBytes; private final ParsableByteArray headerScratchBytes;
private final String language; private final String language;
private TrackOutput output;
private int state; private int state;
private int bytesRead; private int bytesRead;
...@@ -54,20 +57,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -54,20 +57,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
/** /**
* Constructs a new reader for DTS elementary streams. * Constructs a new reader for DTS elementary streams.
* *
* @param output Track output for extracted samples.
*/
public DtsReader(TrackOutput output) {
this(output, null);
}
/**
* Constructs a new reader for DTS elementary streams.
*
* @param output Track output for extracted samples.
* @param language Track language. * @param language Track language.
*/ */
public DtsReader(TrackOutput output, String language) { public DtsReader(String language) {
super(output);
headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF); headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF);
headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF); headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF);
...@@ -85,6 +77,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -85,6 +77,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
} }
@Override @Override
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
output = extractorOutput.track(idGenerator.getNextId());
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -33,17 +33,12 @@ public abstract class ElementaryStreamReader { ...@@ -33,17 +33,12 @@ public abstract class ElementaryStreamReader {
* Returns an {@link ElementaryStreamReader} for a given PMT entry. May return null if the * Returns an {@link ElementaryStreamReader} for a given PMT entry. May return null if the
* stream type is not supported or if the stream already has a reader assigned to it. * stream type is not supported or if the stream already has a reader assigned to it.
* *
* @param pid The pid for the PMT entry. * @param streamType Stream type value as defined in the PMT entry or associated descriptors.
* @param streamType One of the {@link TsExtractor}{@code .TS_STREAM_TYPE_*} constants defining * @param esInfo Information associated to the elementary stream provided in the PMT.
* the type of the stream.
* @param esInfo The descriptor information linked to the elementary stream.
* @param output The {@link ExtractorOutput} that provides the {@link TrackOutput}s for the
* created readers.
* @return An {@link ElementaryStreamReader} for the elementary streams carried by the provided * @return An {@link ElementaryStreamReader} for the elementary streams carried by the provided
* pid. {@code null} if the stream is not supported or if it should be ignored. * pid. {@code null} if the stream is not supported or if it should be ignored.
*/ */
ElementaryStreamReader onPmtEntry(int pid, int streamType, EsInfo esInfo, ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo);
ExtractorOutput output);
} }
...@@ -70,13 +65,24 @@ public abstract class ElementaryStreamReader { ...@@ -70,13 +65,24 @@ public abstract class ElementaryStreamReader {
} }
protected final TrackOutput output;
/** /**
* @param output A {@link TrackOutput} to which samples should be written. * Generates track ids for initializing {@link ElementaryStreamReader}s' {@link TrackOutput}s.
*/ */
protected ElementaryStreamReader(TrackOutput output) { public static final class TrackIdGenerator {
this.output = output;
private final int firstId;
private final int idIncrement;
private int generatedIdCount;
public TrackIdGenerator(int firstId, int idIncrement) {
this.firstId = firstId;
this.idIncrement = idIncrement;
}
public int getNextId() {
return firstId + idIncrement * generatedIdCount++;
}
} }
/** /**
...@@ -85,6 +91,15 @@ public abstract class ElementaryStreamReader { ...@@ -85,6 +91,15 @@ public abstract class ElementaryStreamReader {
public abstract void seek(); public abstract void seek();
/** /**
* Initializes the reader by providing outputs and ids for the tracks.
*
* @param extractorOutput The {@link ExtractorOutput} that receives the extracted data.
* @param idGenerator A {@link TrackIdGenerator} that generates unique track ids for the
* {@link TrackOutput}s.
*/
public abstract void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator);
/**
* Called when a packet starts. * Called when a packet starts.
* *
* @param pesTimeUs The timestamp associated with the packet. * @param pesTimeUs The timestamp associated with the packet.
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
...@@ -35,6 +36,8 @@ import java.util.Collections; ...@@ -35,6 +36,8 @@ import java.util.Collections;
private static final int START_EXTENSION = 0xB5; private static final int START_EXTENSION = 0xB5;
private static final int START_GROUP = 0xB8; private static final int START_GROUP = 0xB8;
private TrackOutput output;
// Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.
private static final double[] FRAME_RATE_VALUES = new double[] { private static final double[] FRAME_RATE_VALUES = new double[] {
24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};
...@@ -58,8 +61,7 @@ import java.util.Collections; ...@@ -58,8 +61,7 @@ import java.util.Collections;
private long framePosition; private long framePosition;
private long frameTimeUs; private long frameTimeUs;
public H262Reader(TrackOutput output) { public H262Reader() {
super(output);
prefixFlags = new boolean[4]; prefixFlags = new boolean[4];
csdBuffer = new CsdBuffer(128); csdBuffer = new CsdBuffer(128);
} }
...@@ -74,6 +76,11 @@ import java.util.Collections; ...@@ -74,6 +76,11 @@ import java.util.Collections;
} }
@Override @Override
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
output = extractorOutput.track(idGenerator.getNextId());
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET; pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET;
if (pesPtsUsAvailable) { if (pesPtsUsAvailable) {
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
...@@ -37,17 +38,20 @@ import java.util.List; ...@@ -37,17 +38,20 @@ import java.util.List;
private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
// State that should not be reset on seek. private final boolean allowNonIdrKeyframes;
private boolean hasOutputFormat; private final boolean detectAccessUnits;
// State that should be reset on seek.
private final SeiReader seiReader;
private final boolean[] prefixFlags;
private final SampleReader sampleReader;
private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer sps;
private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer pps;
private final NalUnitTargetBuffer sei; private final NalUnitTargetBuffer sei;
private long totalBytesWritten; private long totalBytesWritten;
private final boolean[] prefixFlags;
private TrackOutput output;
private SeiReader seiReader;
private SampleReader sampleReader;
// State that should not be reset on seek.
private boolean hasOutputFormat;
// Per packet state that gets reset at the start of each packet. // Per packet state that gets reset at the start of each packet.
private long pesTimeUs; private long pesTimeUs;
...@@ -56,19 +60,15 @@ import java.util.List; ...@@ -56,19 +60,15 @@ import java.util.List;
private final ParsableByteArray seiWrapper; private final ParsableByteArray seiWrapper;
/** /**
* @param output A {@link TrackOutput} to which H.264 samples should be written.
* @param seiReader A reader for CEA-608 samples in SEI NAL units.
* @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as
* synchronization samples (key-frames). * synchronization samples (key-frames).
* @param detectAccessUnits Whether to split the input stream into access units (samples) based on * @param detectAccessUnits Whether to split the input stream into access units (samples) based on
* slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs).
*/ */
public H264Reader(TrackOutput output, SeiReader seiReader, boolean allowNonIdrKeyframes, public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
boolean detectAccessUnits) {
super(output);
this.seiReader = seiReader;
prefixFlags = new boolean[3]; prefixFlags = new boolean[3];
sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); this.allowNonIdrKeyframes = allowNonIdrKeyframes;
this.detectAccessUnits = detectAccessUnits;
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
...@@ -86,6 +86,13 @@ import java.util.List; ...@@ -86,6 +86,13 @@ import java.util.List;
} }
@Override @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) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
this.pesTimeUs = pesTimeUs; this.pesTimeUs = pesTimeUs;
} }
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
...@@ -42,11 +43,13 @@ import java.util.Collections; ...@@ -42,11 +43,13 @@ import java.util.Collections;
private static final int PREFIX_SEI_NUT = 39; private static final int PREFIX_SEI_NUT = 39;
private static final int SUFFIX_SEI_NUT = 40; private static final int SUFFIX_SEI_NUT = 40;
private TrackOutput output;
private SeiReader seiReader;
// State that should not be reset on seek. // State that should not be reset on seek.
private boolean hasOutputFormat; private boolean hasOutputFormat;
// State that should be reset on seek. // State that should be reset on seek.
private final SeiReader seiReader;
private final boolean[] prefixFlags; private final boolean[] prefixFlags;
private final NalUnitTargetBuffer vps; private final NalUnitTargetBuffer vps;
private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer sps;
...@@ -62,13 +65,7 @@ import java.util.Collections; ...@@ -62,13 +65,7 @@ import java.util.Collections;
// Scratch variables to avoid allocations. // Scratch variables to avoid allocations.
private final ParsableByteArray seiWrapper; private final ParsableByteArray seiWrapper;
/** public H265Reader() {
* @param output A {@link TrackOutput} to which H.265 samples should be written.
* @param seiReader A reader for CEA-608 samples in SEI NAL units.
*/
public H265Reader(TrackOutput output, SeiReader seiReader) {
super(output);
this.seiReader = seiReader;
prefixFlags = new boolean[3]; prefixFlags = new boolean[3];
vps = new NalUnitTargetBuffer(VPS_NUT, 128); vps = new NalUnitTargetBuffer(VPS_NUT, 128);
sps = new NalUnitTargetBuffer(SPS_NUT, 128); sps = new NalUnitTargetBuffer(SPS_NUT, 128);
...@@ -92,6 +89,12 @@ import java.util.Collections; ...@@ -92,6 +89,12 @@ import java.util.Collections;
} }
@Override @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) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
this.pesTimeUs = pesTimeUs; this.pesTimeUs = pesTimeUs;
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -30,6 +31,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -30,6 +31,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
private final ParsableByteArray id3Header; private final ParsableByteArray id3Header;
private TrackOutput output;
// State that should be reset on seek. // State that should be reset on seek.
private boolean writingSample; private boolean writingSample;
...@@ -38,10 +41,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -38,10 +41,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
private int sampleSize; private int sampleSize;
private int sampleBytesRead; private int sampleBytesRead;
public Id3Reader(TrackOutput output) { public Id3Reader() {
super(output);
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE,
null));
id3Header = new ParsableByteArray(ID3_HEADER_SIZE); id3Header = new ParsableByteArray(ID3_HEADER_SIZE);
} }
...@@ -51,6 +51,13 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -51,6 +51,13 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
} }
@Override @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) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (!dataAlignmentIndicator) { if (!dataAlignmentIndicator) {
return; return;
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -36,6 +37,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -36,6 +37,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
private final MpegAudioHeader header; private final MpegAudioHeader header;
private final String language; private final String language;
private TrackOutput output;
private int state; private int state;
private int frameBytesRead; private int frameBytesRead;
private boolean hasOutputFormat; private boolean hasOutputFormat;
...@@ -50,12 +53,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -50,12 +53,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
// The timestamp to attach to the next sample in the current packet. // The timestamp to attach to the next sample in the current packet.
private long timeUs; private long timeUs;
public MpegAudioReader(TrackOutput output) { public MpegAudioReader() {
this(output, null); this(null);
} }
public MpegAudioReader(TrackOutput output, String language) { public MpegAudioReader(String language) {
super(output);
state = STATE_FINDING_HEADER; state = STATE_FINDING_HEADER;
// The first byte of an MPEG Audio frame header is always 0xFF. // The first byte of an MPEG Audio frame header is always 0xFF.
headerScratch = new ParsableByteArray(4); headerScratch = new ParsableByteArray(4);
...@@ -72,6 +74,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -72,6 +74,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
} }
@Override @Override
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
output = extractorOutput.track(idGenerator.getNextId());
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; ...@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
...@@ -49,6 +50,7 @@ public final class PsExtractor implements Extractor { ...@@ -49,6 +50,7 @@ public final class PsExtractor implements Extractor {
private static final int SYSTEM_HEADER_START_CODE = 0x000001BB; private static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
private static final int PACKET_START_CODE_PREFIX = 0x000001; private static final int PACKET_START_CODE_PREFIX = 0x000001;
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
private static final long MAX_SEARCH_LENGTH = 1024 * 1024; private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
public static final int PRIVATE_STREAM_1 = 0xBD; public static final int PRIVATE_STREAM_1 = 0xBD;
...@@ -189,16 +191,18 @@ public final class PsExtractor implements Extractor { ...@@ -189,16 +191,18 @@ public final class PsExtractor implements Extractor {
// Private stream, used for AC3 audio. // Private stream, used for AC3 audio.
// NOTE: This may need further parsing to determine if its DTS, but that's likely only // NOTE: This may need further parsing to determine if its DTS, but that's likely only
// valid for DVDs. // valid for DVDs.
elementaryStreamReader = new Ac3Reader(output.track(streamId)); elementaryStreamReader = new Ac3Reader();
foundAudioTrack = true; foundAudioTrack = true;
} else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
elementaryStreamReader = new MpegAudioReader(output.track(streamId)); elementaryStreamReader = new MpegAudioReader();
foundAudioTrack = true; foundAudioTrack = true;
} else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
elementaryStreamReader = new H262Reader(output.track(streamId)); elementaryStreamReader = new H262Reader();
foundVideoTrack = true; foundVideoTrack = true;
} }
if (elementaryStreamReader != null) { if (elementaryStreamReader != null) {
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
elementaryStreamReader.init(output, idGenerator);
payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster); payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster);
psPayloadReaders.put(streamId, payloadReader); psPayloadReaders.put(streamId, payloadReader);
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
...@@ -26,6 +27,9 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; ...@@ -26,6 +27,9 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -50,12 +54,6 @@ public final class TsExtractor implements Extractor { ...@@ -50,12 +54,6 @@ public final class TsExtractor implements Extractor {
}; };
private static final String TAG = "TsExtractor";
private static final int TS_PACKET_SIZE = 188;
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
private static final int TS_PAT_PID = 0;
public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA = 0x03;
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
public static final int TS_STREAM_TYPE_AAC = 0x0F; public static final int TS_STREAM_TYPE_AAC = 0x0F;
...@@ -68,6 +66,12 @@ public final class TsExtractor implements Extractor { ...@@ -68,6 +66,12 @@ public final class TsExtractor implements Extractor {
public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_H265 = 0x24;
public static final int TS_STREAM_TYPE_ID3 = 0x15; public static final int TS_STREAM_TYPE_ID3 = 0x15;
private static final String TAG = "TsExtractor";
private static final int TS_PACKET_SIZE = 188;
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
private static final int TS_PAT_PID = 0;
private static final int MAX_PID_PLUS_ONE = 0x2000;
private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3"); private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3");
private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3");
...@@ -76,15 +80,19 @@ public final class TsExtractor implements Extractor { ...@@ -76,15 +80,19 @@ public final class TsExtractor implements Extractor {
private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2 private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2
private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
private final boolean mapByType;
private final TimestampAdjuster timestampAdjuster; private final TimestampAdjuster timestampAdjuster;
private final ParsableByteArray tsPacketBuffer; private final ParsableByteArray tsPacketBuffer;
private final ParsableBitArray tsScratch; private final ParsableBitArray tsScratch;
private final SparseIntArray continuityCounters; private final SparseIntArray continuityCounters;
private final ElementaryStreamReader.Factory streamReaderFactory; private final ElementaryStreamReader.Factory streamReaderFactory;
/* package */ final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final SparseBooleanArray trackIds;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private ExtractorOutput output; private ExtractorOutput output;
private boolean tracksEnded;
private ElementaryStreamReader id3Reader;
public TsExtractor() { public TsExtractor() {
this(new TimestampAdjuster(0)); this(new TimestampAdjuster(0));
...@@ -94,22 +102,26 @@ public final class TsExtractor implements Extractor { ...@@ -94,22 +102,26 @@ public final class TsExtractor implements Extractor {
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
*/ */
public TsExtractor(TimestampAdjuster timestampAdjuster) { public TsExtractor(TimestampAdjuster timestampAdjuster) {
this(timestampAdjuster, new DefaultStreamReaderFactory()); this(timestampAdjuster, new DefaultStreamReaderFactory(), false);
} }
/** /**
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
* @param customReaderFactory Factory for injecting a custom set of elementary stream readers. * @param customReaderFactory Factory for injecting a custom set of elementary stream readers.
* @param mapByType True if {@link TrackOutput}s should be mapped by their type, false to map them
* by their PID.
*/ */
public TsExtractor(TimestampAdjuster timestampAdjuster, public TsExtractor(TimestampAdjuster timestampAdjuster,
ElementaryStreamReader.Factory customReaderFactory) { ElementaryStreamReader.Factory customReaderFactory, boolean mapByType) {
this.timestampAdjuster = timestampAdjuster; this.timestampAdjuster = timestampAdjuster;
this.streamReaderFactory = Assertions.checkNotNull(customReaderFactory); this.streamReaderFactory = Assertions.checkNotNull(customReaderFactory);
this.mapByType = mapByType;
tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
tsScratch = new ParsableBitArray(new byte[3]); tsScratch = new ParsableBitArray(new byte[3]);
trackIds = new SparseBooleanArray();
tsPayloadReaders = new SparseArray<>(); tsPayloadReaders = new SparseArray<>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
continuityCounters = new SparseIntArray(); continuityCounters = new SparseIntArray();
resetPayloadReaders();
} }
// Extractor implementation. // Extractor implementation.
...@@ -141,11 +153,10 @@ public final class TsExtractor implements Extractor { ...@@ -141,11 +153,10 @@ public final class TsExtractor implements Extractor {
@Override @Override
public void seek(long position) { public void seek(long position) {
timestampAdjuster.reset(); timestampAdjuster.reset();
for (int i = 0; i < tsPayloadReaders.size(); i++) {
tsPayloadReaders.valueAt(i).seek();
}
tsPacketBuffer.reset(); tsPacketBuffer.reset();
continuityCounters.clear(); continuityCounters.clear();
// Elementary stream readers' state should be cleared to get consistent behaviours when seeking.
resetPayloadReaders();
} }
@Override @Override
...@@ -240,6 +251,13 @@ public final class TsExtractor implements Extractor { ...@@ -240,6 +251,13 @@ public final class TsExtractor implements Extractor {
// Internals. // Internals.
private void resetPayloadReaders() {
trackIds.clear();
tsPayloadReaders.clear();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
id3Reader = null;
}
/** /**
* Parses TS packet payload data. * Parses TS packet payload data.
*/ */
...@@ -333,7 +351,7 @@ public final class TsExtractor implements Extractor { ...@@ -333,7 +351,7 @@ public final class TsExtractor implements Extractor {
patScratch.skipBits(13); // network_PID (13) patScratch.skipBits(13); // network_PID (13)
} else { } else {
int pid = patScratch.readBits(13); int pid = patScratch.readBits(13);
tsPayloadReaders.put(pid, new PmtReader()); tsPayloadReaders.put(pid, new PmtReader(pid));
} }
} }
} }
...@@ -353,14 +371,16 @@ public final class TsExtractor implements Extractor { ...@@ -353,14 +371,16 @@ public final class TsExtractor implements Extractor {
private final ParsableBitArray pmtScratch; private final ParsableBitArray pmtScratch;
private final ParsableByteArray sectionData; private final ParsableByteArray sectionData;
private final int pid;
private int sectionLength; private int sectionLength;
private int sectionBytesRead; private int sectionBytesRead;
private int crc; private int crc;
public PmtReader() { public PmtReader(int pid) {
pmtScratch = new ParsableBitArray(new byte[5]); pmtScratch = new ParsableBitArray(new byte[5]);
sectionData = new ParsableByteArray(); sectionData = new ParsableByteArray();
this.pid = pid;
} }
@Override @Override
...@@ -413,6 +433,14 @@ public final class TsExtractor implements Extractor { ...@@ -413,6 +433,14 @@ public final class TsExtractor implements Extractor {
// Skip the descriptors. // Skip the descriptors.
sectionData.skipBytes(programInfoLength); sectionData.skipBytes(programInfoLength);
if (mapByType && id3Reader == null) {
// Setup an ID3 track regardless of whether there's a corresponding entry, in case one
// appears intermittently during playback. See [Internal: b/20261500].
EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]);
id3Reader = streamReaderFactory.createStreamReader(TS_STREAM_TYPE_ID3, dummyEsInfo);
id3Reader.init(output, new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
}
int remainingEntriesLength = sectionLength - 9 /* Length of fields before descriptors */ int remainingEntriesLength = sectionLength - 9 /* Length of fields before descriptors */
- programInfoLength - 4 /* CRC length */; - programInfoLength - 4 /* CRC length */;
while (remainingEntriesLength > 0) { while (remainingEntriesLength > 0) {
...@@ -422,21 +450,40 @@ public final class TsExtractor implements Extractor { ...@@ -422,21 +450,40 @@ public final class TsExtractor implements Extractor {
int elementaryPid = pmtScratch.readBits(13); int elementaryPid = pmtScratch.readBits(13);
pmtScratch.skipBits(4); // reserved pmtScratch.skipBits(4); // reserved
int esInfoLength = pmtScratch.readBits(12); // ES_info_length. int esInfoLength = pmtScratch.readBits(12); // ES_info_length.
ElementaryStreamReader.EsInfo esInfo = readEsInfo(sectionData, esInfoLength); EsInfo esInfo = readEsInfo(sectionData, esInfoLength);
if (streamType == 0x06) { if (streamType == 0x06) {
streamType = esInfo.streamType; streamType = esInfo.streamType;
} }
remainingEntriesLength -= esInfoLength + 5; remainingEntriesLength -= esInfoLength + 5;
ElementaryStreamReader pesPayloadReader = streamReaderFactory.onPmtEntry(elementaryPid,
streamType, esInfo, output); int trackId = mapByType ? streamType : elementaryPid;
if (trackIds.get(trackId)) {
continue;
}
trackIds.put(trackId, true);
ElementaryStreamReader pesPayloadReader;
if (mapByType && streamType == TS_STREAM_TYPE_ID3) {
pesPayloadReader = id3Reader;
} else {
pesPayloadReader = streamReaderFactory.createStreamReader(streamType, esInfo);
pesPayloadReader.init(output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE));
}
if (pesPayloadReader != null) { if (pesPayloadReader != null) {
tsPayloadReaders.put(elementaryPid, tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader, timestampAdjuster));
new PesReader(pesPayloadReader, timestampAdjuster));
} }
} }
if (mapByType) {
output.endTracks(); if (!tracksEnded) {
output.endTracks();
}
} else {
tsPayloadReaders.remove(TS_PAT_PID);
tsPayloadReaders.remove(pid);
output.endTracks();
}
tracksEnded = true;
} }
/** /**
...@@ -447,7 +494,7 @@ public final class TsExtractor implements Extractor { ...@@ -447,7 +494,7 @@ public final class TsExtractor implements Extractor {
* @param length The length of descriptors to read from the current position in {@code data}. * @param length The length of descriptors to read from the current position in {@code data}.
* @return The stream info read from the available descriptors. * @return The stream info read from the available descriptors.
*/ */
private ElementaryStreamReader.EsInfo readEsInfo(ParsableByteArray data, int length) { private EsInfo readEsInfo(ParsableByteArray data, int length) {
int descriptorsStartPosition = data.getPosition(); int descriptorsStartPosition = data.getPosition();
int descriptorsEndPosition = descriptorsStartPosition + length; int descriptorsEndPosition = descriptorsStartPosition + length;
int streamType = -1; int streamType = -1;
...@@ -479,7 +526,7 @@ public final class TsExtractor implements Extractor { ...@@ -479,7 +526,7 @@ public final class TsExtractor implements Extractor {
data.skipBytes(positionOfNextDescriptor - data.getPosition()); data.skipBytes(positionOfNextDescriptor - data.getPosition());
} }
data.setPosition(descriptorsEndPosition); data.setPosition(descriptorsEndPosition);
return new ElementaryStreamReader.EsInfo(streamType, language, return new EsInfo(streamType, language,
Arrays.copyOfRange(sectionData.data, descriptorsStartPosition, descriptorsEndPosition)); Arrays.copyOfRange(sectionData.data, descriptorsStartPosition, descriptorsEndPosition));
} }
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
...@@ -41,7 +42,6 @@ import com.google.android.exoplayer2.util.ConditionVariable; ...@@ -41,7 +42,6 @@ import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
/** /**
* A {@link MediaPeriod} that extracts data using an {@link Extractor}. * A {@link MediaPeriod} that extracts data using an {@link Extractor}.
...@@ -68,6 +68,7 @@ import java.util.Arrays; ...@@ -68,6 +68,7 @@ import java.util.Arrays;
private final Runnable maybeFinishPrepareRunnable; private final Runnable maybeFinishPrepareRunnable;
private final Runnable onContinueLoadingRequestedRunnable; private final Runnable onContinueLoadingRequestedRunnable;
private final Handler handler; private final Handler handler;
private final SparseArray<DefaultTrackOutput> sampleQueues;
private Callback callback; private Callback callback;
private SeekMap seekMap; private SeekMap seekMap;
...@@ -77,7 +78,6 @@ import java.util.Arrays; ...@@ -77,7 +78,6 @@ import java.util.Arrays;
private boolean seenFirstTrackSelection; private boolean seenFirstTrackSelection;
private boolean notifyReset; private boolean notifyReset;
private int enabledTrackCount; private int enabledTrackCount;
private DefaultTrackOutput[] sampleQueues;
private TrackGroupArray tracks; private TrackGroupArray tracks;
private long durationUs; private long durationUs;
private boolean[] trackEnabledStates; private boolean[] trackEnabledStates;
...@@ -131,7 +131,7 @@ import java.util.Arrays; ...@@ -131,7 +131,7 @@ import java.util.Arrays;
handler = new Handler(); handler = new Handler();
pendingResetPositionUs = C.TIME_UNSET; pendingResetPositionUs = C.TIME_UNSET;
sampleQueues = new DefaultTrackOutput[0]; sampleQueues = new SparseArray<>();
length = C.LENGTH_UNSET; length = C.LENGTH_UNSET;
} }
...@@ -141,8 +141,9 @@ import java.util.Arrays; ...@@ -141,8 +141,9 @@ import java.util.Arrays;
@Override @Override
public void run() { public void run() {
extractorHolder.release(); extractorHolder.release();
for (DefaultTrackOutput sampleQueue : sampleQueues) { int trackCount = sampleQueues.size();
sampleQueue.disable(); for (int i = 0; i < trackCount; i++) {
sampleQueues.valueAt(i).disable();
} }
} }
}); });
...@@ -178,7 +179,7 @@ import java.util.Arrays; ...@@ -178,7 +179,7 @@ import java.util.Arrays;
Assertions.checkState(trackEnabledStates[track]); Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--; enabledTrackCount--;
trackEnabledStates[track] = false; trackEnabledStates[track] = false;
sampleQueues[track].disable(); sampleQueues.valueAt(track).disable();
streams[i] = null; streams[i] = null;
} }
} }
...@@ -201,9 +202,10 @@ import java.util.Arrays; ...@@ -201,9 +202,10 @@ import java.util.Arrays;
if (!seenFirstTrackSelection) { if (!seenFirstTrackSelection) {
// At the time of the first track selection all queues will be enabled, so we need to disable // At the time of the first track selection all queues will be enabled, so we need to disable
// any that are no longer required. // any that are no longer required.
for (int i = 0; i < sampleQueues.length; i++) { int trackCount = sampleQueues.size();
for (int i = 0; i < trackCount; i++) {
if (!trackEnabledStates[i]) { if (!trackEnabledStates[i]) {
sampleQueues[i].disable(); sampleQueues.valueAt(i).disable();
} }
} }
} }
...@@ -270,11 +272,12 @@ import java.util.Arrays; ...@@ -270,11 +272,12 @@ import java.util.Arrays;
// Treat all seeks into non-seekable media as being to t=0. // Treat all seeks into non-seekable media as being to t=0.
positionUs = seekMap.isSeekable() ? positionUs : 0; positionUs = seekMap.isSeekable() ? positionUs : 0;
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
int trackCount = sampleQueues.size();
// If we're not pending a reset, see if we can seek within the sample queues. // If we're not pending a reset, see if we can seek within the sample queues.
boolean seekInsideBuffer = !isPendingReset(); boolean seekInsideBuffer = !isPendingReset();
for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) { for (int i = 0; seekInsideBuffer && i < trackCount; i++) {
if (trackEnabledStates[i]) { if (trackEnabledStates[i]) {
seekInsideBuffer = sampleQueues[i].skipToKeyframeBefore(positionUs); seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs);
} }
} }
// If we failed to seek within the sample queues, we need to restart. // If we failed to seek within the sample queues, we need to restart.
...@@ -284,8 +287,8 @@ import java.util.Arrays; ...@@ -284,8 +287,8 @@ import java.util.Arrays;
if (loader.isLoading()) { if (loader.isLoading()) {
loader.cancelLoading(); loader.cancelLoading();
} else { } else {
for (int i = 0; i < sampleQueues.length; i++) { for (int i = 0; i < trackCount; i++) {
sampleQueues[i].reset(trackEnabledStates[i]); sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
} }
} }
} }
...@@ -296,7 +299,7 @@ import java.util.Arrays; ...@@ -296,7 +299,7 @@ import java.util.Arrays;
// SampleStream methods. // SampleStream methods.
/* package */ boolean isReady(int track) { /* package */ boolean isReady(int track) {
return loadingFinished || (!isPendingReset() && !sampleQueues[track].isEmpty()); return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty());
} }
/* package */ void maybeThrowError() throws IOException { /* package */ void maybeThrowError() throws IOException {
...@@ -308,7 +311,8 @@ import java.util.Arrays; ...@@ -308,7 +311,8 @@ import java.util.Arrays;
return C.RESULT_NOTHING_READ; return C.RESULT_NOTHING_READ;
} }
return sampleQueues[track].readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); return sampleQueues.valueAt(track).readData(formatHolder, buffer, loadingFinished,
lastSeekPositionUs);
} }
// Loader.Callback implementation. // Loader.Callback implementation.
...@@ -332,8 +336,9 @@ import java.util.Arrays; ...@@ -332,8 +336,9 @@ import java.util.Arrays;
long loadDurationMs, boolean released) { long loadDurationMs, boolean released) {
copyLengthFromLoader(loadable); copyLengthFromLoader(loadable);
if (!released && enabledTrackCount > 0) { if (!released && enabledTrackCount > 0) {
for (int i = 0; i < sampleQueues.length; i++) { int trackCount = sampleQueues.size();
sampleQueues[i].reset(trackEnabledStates[i]); for (int i = 0; i < trackCount; i++) {
sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
} }
callback.onContinueLoadingRequested(this); callback.onContinueLoadingRequested(this);
} }
...@@ -358,11 +363,13 @@ import java.util.Arrays; ...@@ -358,11 +363,13 @@ import java.util.Arrays;
@Override @Override
public TrackOutput track(int id) { public TrackOutput track(int id) {
sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1); DefaultTrackOutput trackOutput = sampleQueues.get(id);
DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); if (trackOutput == null) {
sampleQueue.setUpstreamFormatChangeListener(this); trackOutput = new DefaultTrackOutput(allocator);
sampleQueues[sampleQueues.length - 1] = sampleQueue; trackOutput.setUpstreamFormatChangeListener(this);
return sampleQueue; sampleQueues.put(id, trackOutput);
}
return trackOutput;
} }
@Override @Override
...@@ -390,18 +397,18 @@ import java.util.Arrays; ...@@ -390,18 +397,18 @@ import java.util.Arrays;
if (released || prepared || seekMap == null || !tracksBuilt) { if (released || prepared || seekMap == null || !tracksBuilt) {
return; return;
} }
for (DefaultTrackOutput sampleQueue : sampleQueues) { int trackCount = sampleQueues.size();
if (sampleQueue.getUpstreamFormat() == null) { for (int i = 0; i < trackCount; i++) {
if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
return; return;
} }
} }
loadCondition.close(); loadCondition.close();
int trackCount = sampleQueues.length;
TrackGroup[] trackArray = new TrackGroup[trackCount]; TrackGroup[] trackArray = new TrackGroup[trackCount];
trackEnabledStates = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount];
durationUs = seekMap.getDurationUs(); durationUs = seekMap.getDurationUs();
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat()); trackArray[i] = new TrackGroup(sampleQueues.valueAt(i).getUpstreamFormat());
} }
tracks = new TrackGroupArray(trackArray); tracks = new TrackGroupArray(trackArray);
prepared = true; prepared = true;
...@@ -455,8 +462,9 @@ import java.util.Arrays; ...@@ -455,8 +462,9 @@ import java.util.Arrays;
// a new load. // a new load.
lastSeekPositionUs = 0; lastSeekPositionUs = 0;
notifyReset = prepared; notifyReset = prepared;
for (int i = 0; i < sampleQueues.length; i++) { int trackCount = sampleQueues.size();
sampleQueues[i].reset(!prepared || trackEnabledStates[i]); for (int i = 0; i < trackCount; i++) {
sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]);
} }
loadable.setLoadPosition(0); loadable.setLoadPosition(0);
} }
...@@ -464,17 +472,19 @@ import java.util.Arrays; ...@@ -464,17 +472,19 @@ import java.util.Arrays;
private int getExtractedSamplesCount() { private int getExtractedSamplesCount() {
int extractedSamplesCount = 0; int extractedSamplesCount = 0;
for (DefaultTrackOutput sampleQueue : sampleQueues) { int trackCount = sampleQueues.size();
extractedSamplesCount += sampleQueue.getWriteIndex(); for (int i = 0; i < trackCount; i++) {
extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex();
} }
return extractedSamplesCount; return extractedSamplesCount;
} }
private long getLargestQueuedTimestampUs() { private long getLargestQueuedTimestampUs() {
long largestQueuedTimestampUs = Long.MIN_VALUE; long largestQueuedTimestampUs = Long.MIN_VALUE;
for (DefaultTrackOutput sampleQueue : sampleQueues) { int trackCount = sampleQueues.size();
for (int i = 0; i < trackCount; i++) {
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
sampleQueue.getLargestQueuedTimestampUs()); sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
} }
return largestQueuedTimestampUs; return largestQueuedTimestampUs;
} }
...@@ -523,7 +533,7 @@ import java.util.Arrays; ...@@ -523,7 +533,7 @@ import java.util.Arrays;
@Override @Override
public void skipToKeyframeBefore(long timeUs) { public void skipToKeyframeBefore(long timeUs) {
sampleQueues[track].skipToKeyframeBefore(timeUs); sampleQueues.valueAt(track).skipToKeyframeBefore(timeUs);
} }
} }
......
...@@ -59,6 +59,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput ...@@ -59,6 +59,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
// Accessed only on the loader thread. // Accessed only on the loader thread.
private boolean seenTrack; private boolean seenTrack;
private int seenTrackId;
/** /**
* @param extractor The extractor to wrap. * @param extractor The extractor to wrap.
...@@ -116,8 +117,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput ...@@ -116,8 +117,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
@Override @Override
public TrackOutput track(int id) { public TrackOutput track(int id) {
Assertions.checkState(!seenTrack); Assertions.checkState(!seenTrack || seenTrackId == id);
seenTrack = true; seenTrack = true;
seenTrackId = id;
return this; return this;
} }
......
...@@ -655,7 +655,9 @@ public class DashManifestParser extends DefaultHandler ...@@ -655,7 +655,9 @@ public class DashManifestParser extends DefaultHandler
return MimeTypes.getVideoMediaMimeType(codecs); return MimeTypes.getVideoMediaMimeType(codecs);
} else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
if (codecs != null) { if (codecs != null) {
if (codecs.contains("eia608") || codecs.contains("cea608")) { if (codecs.contains("cea708")) {
return MimeTypes.APPLICATION_CEA708;
} else if (codecs.contains("eia608") || codecs.contains("cea608")) {
return MimeTypes.APPLICATION_CEA608; return MimeTypes.APPLICATION_CEA608;
} }
} }
......
...@@ -257,8 +257,16 @@ import java.util.Locale; ...@@ -257,8 +257,16 @@ import java.util.Locale;
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex,
newVariantIndex); newVariantIndex);
if (chunkMediaSequence < mediaPlaylist.mediaSequence) { if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException(); // We try getting the next chunk without adapting in case that's the reason for falling
return; // behind the live window.
newVariantIndex = oldVariantIndex;
mediaPlaylist = variantPlaylists[newVariantIndex];
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex,
newVariantIndex);
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
}
} }
} }
} else { } else {
...@@ -369,29 +377,29 @@ import java.util.Locale; ...@@ -369,29 +377,29 @@ import java.util.Locale;
} }
} else if (needNewExtractor) { } else if (needNewExtractor) {
// MPEG-2 TS segments, but we need a new extractor. // MPEG-2 TS segments, but we need a new extractor.
// This flag ensures the change of pid between streams does not affect the sample queues.
@DefaultStreamReaderFactory.WorkaroundFlags
int workaroundFlags = DefaultStreamReaderFactory.WORKAROUND_MAP_BY_TYPE;
String codecs = variants[newVariantIndex].format.codecs;
if (!TextUtils.isEmpty(codecs)) {
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
// exist. If we know from the codec attribute that they don't exist, then we can explicitly
// ignore them even if they're declared.
if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_AAC_STREAM;
}
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_H264_STREAM;
}
}
isTimestampMaster = true; isTimestampMaster = true;
if (useInitializedExtractor) { if (useInitializedExtractor) {
extractor = lastLoadedInitializationChunk.extractor; extractor = lastLoadedInitializationChunk.extractor;
} else { } else {
timestampAdjuster = timestampAdjusterProvider.getAdjuster( timestampAdjuster = timestampAdjusterProvider.getAdjuster(
segment.discontinuitySequenceNumber, startTimeUs); segment.discontinuitySequenceNumber, startTimeUs);
// This flag ensures the change of pid between streams does not affect the sample queues.
@DefaultStreamReaderFactory.Flags
int esReaderFactoryFlags = 0;
String codecs = variants[newVariantIndex].format.codecs;
if (!TextUtils.isEmpty(codecs)) {
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
// exist. If we know from the codec attribute that they don't exist, then we can
// explicitly ignore them even if they're declared.
if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
esReaderFactoryFlags |= DefaultStreamReaderFactory.FLAG_IGNORE_AAC_STREAM;
}
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
esReaderFactoryFlags |= DefaultStreamReaderFactory.FLAG_IGNORE_H264_STREAM;
}
}
extractor = new TsExtractor(timestampAdjuster, extractor = new TsExtractor(timestampAdjuster,
new DefaultStreamReaderFactory(workaroundFlags)); new DefaultStreamReaderFactory(esReaderFactoryFlags), true);
} }
} else { } else {
// MPEG-2 TS segments, and we need to continue using the same extractor. // MPEG-2 TS segments, and we need to continue using the same extractor.
......
...@@ -103,8 +103,10 @@ import java.util.List; ...@@ -103,8 +103,10 @@ import java.util.List;
public void release() { public void release() {
continueLoadingHandler.removeCallbacksAndMessages(null); continueLoadingHandler.removeCallbacksAndMessages(null);
manifestFetcher.release(); manifestFetcher.release();
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { if (sampleStreamWrappers != null) {
sampleStreamWrapper.release(); for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.release();
}
} }
} }
...@@ -326,7 +328,7 @@ import java.util.List; ...@@ -326,7 +328,7 @@ import java.util.List;
sampleStreamWrappers = new HlsSampleStreamWrapper[] { sampleStreamWrappers = new HlsSampleStreamWrapper[] {
buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)}; buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)};
pendingPrepareCount = 1; pendingPrepareCount = 1;
sampleStreamWrappers[0].prepare(); sampleStreamWrappers[0].continuePreparing();
return; return;
} }
...@@ -367,16 +369,16 @@ import java.util.List; ...@@ -367,16 +369,16 @@ import java.util.List;
selectedVariants.toArray(variants); selectedVariants.toArray(variants);
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat);
sampleStreamWrapper.prepare();
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
sampleStreamWrapper.continuePreparing();
} }
// Build audio stream wrappers. // Build audio stream wrappers.
for (int i = 0; i < audioVariants.size(); i++) { for (int i = 0; i < audioVariants.size(); i++) {
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO,
baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null); baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null);
sampleStreamWrapper.prepare();
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
sampleStreamWrapper.continuePreparing();
} }
// Build subtitle stream wrappers. // Build subtitle stream wrappers.
......
...@@ -144,8 +144,10 @@ import java.util.LinkedList; ...@@ -144,8 +144,10 @@ import java.util.LinkedList;
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
} }
public void prepare() { public void continuePreparing() {
continueLoading(lastSeekPositionUs); if (!prepared) {
continueLoading(lastSeekPositionUs);
}
} }
/** /**
...@@ -154,7 +156,8 @@ import java.util.LinkedList; ...@@ -154,7 +156,8 @@ import java.util.LinkedList;
*/ */
public void prepareSingleTrack(Format format) { public void prepareSingleTrack(Format format) {
track(0).format(format); track(0).format(format);
endTracks(); sampleQueuesBuilt = true;
maybeFinishPrepare();
} }
public void maybeThrowPrepareError() throws IOException { public void maybeThrowPrepareError() throws IOException {
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
...@@ -52,7 +54,7 @@ public class PlaybackControlView extends FrameLayout { ...@@ -52,7 +54,7 @@ public class PlaybackControlView extends FrameLayout {
public static final int DEFAULT_FAST_FORWARD_MS = 15000; public static final int DEFAULT_FAST_FORWARD_MS = 15000;
public static final int DEFAULT_REWIND_MS = 5000; public static final int DEFAULT_REWIND_MS = 5000;
public static final int DEFAULT_SHOW_DURATION_MS = 5000; public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;
private static final int PROGRESS_BAR_MAX = 1000; private static final int PROGRESS_BAR_MAX = 1000;
private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;
...@@ -74,9 +76,10 @@ public class PlaybackControlView extends FrameLayout { ...@@ -74,9 +76,10 @@ public class PlaybackControlView extends FrameLayout {
private VisibilityListener visibilityListener; private VisibilityListener visibilityListener;
private boolean dragging; private boolean dragging;
private int rewindMs = DEFAULT_REWIND_MS; private int rewindMs;
private int fastForwardMs = DEFAULT_FAST_FORWARD_MS; private int fastForwardMs;
private int showDurationMs = DEFAULT_SHOW_DURATION_MS; private int showTimeoutMs;
private long hideAtMs;
private final Runnable updateProgressAction = new Runnable() { private final Runnable updateProgressAction = new Runnable() {
@Override @Override
...@@ -103,6 +106,22 @@ public class PlaybackControlView extends FrameLayout { ...@@ -103,6 +106,22 @@ public class PlaybackControlView extends FrameLayout {
public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
rewindMs = DEFAULT_REWIND_MS;
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.PlaybackControlView, 0, 0);
try {
rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs);
fastForwardMs = a.getInt(R.styleable.PlaybackControlView_fastforward_increment,
fastForwardMs);
showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs);
} finally {
a.recycle();
}
}
currentWindow = new Timeline.Window(); currentWindow = new Timeline.Window();
formatBuilder = new StringBuilder(); formatBuilder = new StringBuilder();
formatter = new Formatter(formatBuilder, Locale.getDefault()); formatter = new Formatter(formatBuilder, Locale.getDefault());
...@@ -124,7 +143,6 @@ public class PlaybackControlView extends FrameLayout { ...@@ -124,7 +143,6 @@ public class PlaybackControlView extends FrameLayout {
rewindButton.setOnClickListener(componentListener); rewindButton.setOnClickListener(componentListener);
fastForwardButton = findViewById(R.id.ffwd); fastForwardButton = findViewById(R.id.ffwd);
fastForwardButton.setOnClickListener(componentListener); fastForwardButton.setOnClickListener(componentListener);
updateAll();
} }
/** /**
...@@ -169,6 +187,7 @@ public class PlaybackControlView extends FrameLayout { ...@@ -169,6 +187,7 @@ public class PlaybackControlView extends FrameLayout {
*/ */
public void setRewindIncrementMs(int rewindMs) { public void setRewindIncrementMs(int rewindMs) {
this.rewindMs = rewindMs; this.rewindMs = rewindMs;
updateNavigation();
} }
/** /**
...@@ -178,51 +197,60 @@ public class PlaybackControlView extends FrameLayout { ...@@ -178,51 +197,60 @@ public class PlaybackControlView extends FrameLayout {
*/ */
public void setFastForwardIncrementMs(int fastForwardMs) { public void setFastForwardIncrementMs(int fastForwardMs) {
this.fastForwardMs = fastForwardMs; this.fastForwardMs = fastForwardMs;
updateNavigation();
} }
/** /**
* Sets the duration to show the playback control in milliseconds. * Returns the playback controls timeout. The playback controls are automatically hidden after
* this duration of time has elapsed without user input.
* *
* @param showDurationMs The duration in milliseconds. * @return The duration in milliseconds. A non-positive value indicates that the controls will
* remain visible indefinitely.
*/ */
public void setShowDurationMs(int showDurationMs) { public int getShowTimeoutMs() {
this.showDurationMs = showDurationMs; return showTimeoutMs;
} }
/** /**
* Shows the controller for the duration last passed to {@link #setShowDurationMs(int)}, or for * Sets the playback controls timeout. The playback controls are automatically hidden after this
* {@link #DEFAULT_SHOW_DURATION_MS} if {@link #setShowDurationMs(int)} has not been called. * duration of time has elapsed without user input.
*
* @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls
* to remain visible indefinitely.
*/ */
public void show() { public void setShowTimeoutMs(int showTimeoutMs) {
show(showDurationMs); this.showTimeoutMs = showTimeoutMs;
} }
/** /**
* Shows the controller for the {@code durationMs}. If {@code durationMs} is 0 the controller is * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
* shown until {@link #hide()} is called. * be automatically hidden after this duration of time has elapsed without user input.
*
* @param durationMs The duration in milliseconds.
*/ */
public void show(int durationMs) { public void show() {
setVisibility(VISIBLE); if (!isVisible()) {
if (visibilityListener != null) { setVisibility(VISIBLE);
visibilityListener.onVisibilityChange(getVisibility()); if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
}
updateAll();
} }
updateAll(); // Call hideAfterTimeout even if already visible to reset the timeout.
showDurationMs = durationMs; hideAfterTimeout();
hideDeferred();
} }
/** /**
* Hides the controller. * Hides the controller.
*/ */
public void hide() { public void hide() {
setVisibility(GONE); if (isVisible()) {
if (visibilityListener != null) { setVisibility(GONE);
visibilityListener.onVisibilityChange(getVisibility()); if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
}
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
hideAtMs = C.TIME_UNSET;
} }
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
} }
/** /**
...@@ -232,10 +260,15 @@ public class PlaybackControlView extends FrameLayout { ...@@ -232,10 +260,15 @@ public class PlaybackControlView extends FrameLayout {
return getVisibility() == VISIBLE; return getVisibility() == VISIBLE;
} }
private void hideDeferred() { private void hideAfterTimeout() {
removeCallbacks(hideAction); removeCallbacks(hideAction);
if (showDurationMs > 0) { if (showTimeoutMs > 0) {
postDelayed(hideAction, showDurationMs); hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs;
if (isAttachedToWindow()) {
postDelayed(hideAction, showTimeoutMs);
}
} else {
hideAtMs = C.TIME_UNSET;
} }
} }
...@@ -246,7 +279,7 @@ public class PlaybackControlView extends FrameLayout { ...@@ -246,7 +279,7 @@ public class PlaybackControlView extends FrameLayout {
} }
private void updatePlayPauseButton() { private void updatePlayPauseButton() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
boolean playing = player != null && player.getPlayWhenReady(); boolean playing = player != null && player.getPlayWhenReady();
...@@ -258,7 +291,7 @@ public class PlaybackControlView extends FrameLayout { ...@@ -258,7 +291,7 @@ public class PlaybackControlView extends FrameLayout {
} }
private void updateNavigation() { private void updateNavigation() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null;
...@@ -276,13 +309,13 @@ public class PlaybackControlView extends FrameLayout { ...@@ -276,13 +309,13 @@ public class PlaybackControlView extends FrameLayout {
} }
setButtonEnabled(enablePrevious , previousButton); setButtonEnabled(enablePrevious , previousButton);
setButtonEnabled(enableNext, nextButton); setButtonEnabled(enableNext, nextButton);
setButtonEnabled(isSeekable, fastForwardButton); setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton);
setButtonEnabled(isSeekable, rewindButton); setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton);
progressBar.setEnabled(isSeekable); progressBar.setEnabled(isSeekable);
} }
private void updateProgress() { private void updateProgress() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
long duration = player == null ? 0 : player.getDuration(); long duration = player == null ? 0 : player.getDuration();
...@@ -377,14 +410,41 @@ public class PlaybackControlView extends FrameLayout { ...@@ -377,14 +410,41 @@ public class PlaybackControlView extends FrameLayout {
} }
private void rewind() { private void rewind() {
if (rewindMs <= 0) {
return;
}
player.seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); player.seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0));
} }
private void fastForward() { private void fastForward() {
if (fastForwardMs <= 0) {
return;
}
player.seekTo(Math.min(player.getCurrentPosition() + fastForwardMs, player.getDuration())); player.seekTo(Math.min(player.getCurrentPosition() + fastForwardMs, player.getDuration()));
} }
@Override @Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (hideAtMs != C.TIME_UNSET) {
long delayMs = hideAtMs - SystemClock.uptimeMillis();
if (delayMs <= 0) {
hide();
} else {
postDelayed(hideAction, delayMs);
}
}
updateAll();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
if (player == null || event.getAction() != KeyEvent.ACTION_DOWN) { if (player == null || event.getAction() != KeyEvent.ACTION_DOWN) {
return super.dispatchKeyEvent(event); return super.dispatchKeyEvent(event);
...@@ -440,7 +500,7 @@ public class PlaybackControlView extends FrameLayout { ...@@ -440,7 +500,7 @@ public class PlaybackControlView extends FrameLayout {
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
dragging = false; dragging = false;
player.seekTo(positionValue(seekBar.getProgress())); player.seekTo(positionValue(seekBar.getProgress()));
hideDeferred(); hideAfterTimeout();
} }
@Override @Override
...@@ -485,7 +545,7 @@ public class PlaybackControlView extends FrameLayout { ...@@ -485,7 +545,7 @@ public class PlaybackControlView extends FrameLayout {
} else if (playButton == view) { } else if (playButton == view) {
player.setPlayWhenReady(!player.getPlayWhenReady()); player.setPlayWhenReady(!player.getPlayWhenReady());
} }
hideDeferred(); hideAfterTimeout();
} }
} }
......
...@@ -48,8 +48,10 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -48,8 +48,10 @@ public final class SimpleExoPlayerView extends FrameLayout {
private final AspectRatioFrameLayout layout; private final AspectRatioFrameLayout layout;
private final PlaybackControlView controller; private final PlaybackControlView controller;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private boolean useController = true; private boolean useController = true;
private int controllerShowTimeoutMs;
public SimpleExoPlayerView(Context context) { public SimpleExoPlayerView(Context context) {
this(context, null); this(context, null);
...@@ -64,6 +66,9 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -64,6 +66,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
boolean useTextureView = false; boolean useTextureView = false;
int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
int rewindMs = PlaybackControlView.DEFAULT_REWIND_MS;
int fastForwardMs = PlaybackControlView.DEFAULT_FAST_FORWARD_MS;
int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS;
if (attrs != null) { if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.SimpleExoPlayerView, 0, 0); R.styleable.SimpleExoPlayerView, 0, 0);
...@@ -73,6 +78,11 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -73,6 +78,11 @@ public final class SimpleExoPlayerView extends FrameLayout {
useTextureView); useTextureView);
resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode,
AspectRatioFrameLayout.RESIZE_MODE_FIT); AspectRatioFrameLayout.RESIZE_MODE_FIT);
rewindMs = a.getInt(R.styleable.SimpleExoPlayerView_rewind_increment, rewindMs);
fastForwardMs = a.getInt(R.styleable.SimpleExoPlayerView_fastforward_increment,
fastForwardMs);
controllerShowTimeoutMs = a.getInt(R.styleable.SimpleExoPlayerView_show_timeout,
controllerShowTimeoutMs);
} finally { } finally {
a.recycle(); a.recycle();
} }
...@@ -82,12 +92,17 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -82,12 +92,17 @@ public final class SimpleExoPlayerView extends FrameLayout {
componentListener = new ComponentListener(); componentListener = new ComponentListener();
layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame); layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame);
layout.setResizeMode(resizeMode); layout.setResizeMode(resizeMode);
controller = (PlaybackControlView) findViewById(R.id.control);
shutterView = findViewById(R.id.shutter); shutterView = findViewById(R.id.shutter);
subtitleLayout = (SubtitleView) findViewById(R.id.subtitles); subtitleLayout = (SubtitleView) findViewById(R.id.subtitles);
subtitleLayout.setUserDefaultStyle(); subtitleLayout.setUserDefaultStyle();
subtitleLayout.setUserDefaultTextSize(); subtitleLayout.setUserDefaultTextSize();
controller = (PlaybackControlView) findViewById(R.id.control);
controller.hide();
controller.setRewindIncrementMs(rewindMs);
controller.setFastForwardIncrementMs(fastForwardMs);
this.controllerShowTimeoutMs = controllerShowTimeoutMs;
View view = useTextureView ? new TextureView(context) : new SurfaceView(context); View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
...@@ -122,6 +137,9 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -122,6 +137,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
this.player.setVideoSurface(null); this.player.setVideoSurface(null);
} }
this.player = player; this.player = player;
if (useController) {
controller.setPlayer(player);
}
if (player != null) { if (player != null) {
if (surfaceView instanceof TextureView) { if (surfaceView instanceof TextureView) {
player.setVideoTextureView((TextureView) surfaceView); player.setVideoTextureView((TextureView) surfaceView);
...@@ -131,20 +149,36 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -131,20 +149,36 @@ public final class SimpleExoPlayerView extends FrameLayout {
player.setVideoListener(componentListener); player.setVideoListener(componentListener);
player.addListener(componentListener); player.addListener(componentListener);
player.setTextOutput(componentListener); player.setTextOutput(componentListener);
maybeShowController(false);
} else { } else {
shutterView.setVisibility(VISIBLE); shutterView.setVisibility(VISIBLE);
controller.hide();
} }
if (useController) {
controller.setPlayer(player);
}
} }
/** /**
* Set the {@code useController} flag which indicates whether the playback control view should * Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT},
* be used or not. If set to {@code false} the controller is never visible and is disconnected * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or
* from the player. * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}.
*
* @param resizeMode The resize mode.
*/
public void setResizeMode(int resizeMode) {
layout.setResizeMode(resizeMode);
}
/**
* Returns whether the playback controls are enabled.
*/
public boolean getUseController() {
return useController;
}
/**
* Sets whether playback controls are enabled. If set to {@code false} the playback controls are
* never visible and are disconnected from the player.
* *
* @param useController If {@code false} the playback control is never used. * @param useController Whether playback controls should be enabled.
*/ */
public void setUseController(boolean useController) { public void setUseController(boolean useController) {
if (this.useController == useController) { if (this.useController == useController) {
...@@ -160,14 +194,26 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -160,14 +194,26 @@ public final class SimpleExoPlayerView extends FrameLayout {
} }
/** /**
* Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT}, * Returns the playback controls timeout. The playback controls are automatically hidden after
* {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or * this duration of time has elapsed without user input and with playback or buffering in
* {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}. * progress.
* *
* @param resizeMode The resize mode. * @return The timeout in milliseconds. A non-positive value will cause the controller to remain
* visible indefinitely.
*/ */
public void setResizeMode(int resizeMode) { public int getControllerShowTimeoutMs() {
layout.setResizeMode(resizeMode); return controllerShowTimeoutMs;
}
/**
* Sets the playback controls timeout. The playback controls are automatically hidden after this
* duration of time has elapsed without user input and with playback or buffering in progress.
*
* @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause
* the controller to remain visible indefinitely.
*/
public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {
this.controllerShowTimeoutMs = controllerShowTimeoutMs;
} }
/** /**
...@@ -198,15 +244,6 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -198,15 +244,6 @@ public final class SimpleExoPlayerView extends FrameLayout {
} }
/** /**
* Sets the duration to show the playback control in milliseconds.
*
* @param showDurationMs The duration in milliseconds.
*/
public void setControlShowDurationMs(int showDurationMs) {
controller.setShowDurationMs(showDurationMs);
}
/**
* Get the view onto which video is rendered. This is either a {@link SurfaceView} (default) * Get the view onto which video is rendered. This is either a {@link SurfaceView} (default)
* or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true. * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true.
* *
...@@ -218,21 +255,23 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -218,21 +255,23 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override @Override
public boolean onTouchEvent(MotionEvent ev) { public boolean onTouchEvent(MotionEvent ev) {
if (useController && ev.getActionMasked() == MotionEvent.ACTION_DOWN) { if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
if (controller.isVisible()) { return false;
controller.hide(); }
} else { if (controller.isVisible()) {
controller.show(); controller.hide();
} } else {
maybeShowController(true);
} }
return true; return true;
} }
@Override @Override
public boolean onTrackballEvent(MotionEvent ev) { public boolean onTrackballEvent(MotionEvent ev) {
if (!useController) { if (!useController || player == null) {
return false; return false;
} }
controller.show(); maybeShowController(true);
return true; return true;
} }
...@@ -241,6 +280,20 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -241,6 +280,20 @@ public final class SimpleExoPlayerView extends FrameLayout {
return useController ? controller.dispatchKeyEvent(event) : super.dispatchKeyEvent(event); return useController ? controller.dispatchKeyEvent(event) : super.dispatchKeyEvent(event);
} }
private void maybeShowController(boolean isForced) {
if (!useController || player == null) {
return;
}
int playbackState = player.getPlaybackState();
boolean showIndefinitely = playbackState == ExoPlayer.STATE_IDLE
|| playbackState == ExoPlayer.STATE_ENDED || !player.getPlayWhenReady();
boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0;
controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs);
if (isForced || showIndefinitely || wasShowingIndefinitely) {
controller.show();
}
}
private final class ComponentListener implements SimpleExoPlayer.VideoListener, private final class ComponentListener implements SimpleExoPlayer.VideoListener,
TextRenderer.Output, ExoPlayer.EventListener { TextRenderer.Output, ExoPlayer.EventListener {
...@@ -278,9 +331,7 @@ public final class SimpleExoPlayerView extends FrameLayout { ...@@ -278,9 +331,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (useController && playbackState == ExoPlayer.STATE_ENDED) { maybeShowController(false);
controller.show(0);
}
} }
@Override @Override
......
...@@ -231,10 +231,13 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -231,10 +231,13 @@ public class DefaultHttpDataSource implements HttpDataSource {
// Determine the length of the data to be read, after skipping. // Determine the length of the data to be read, after skipping.
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) { if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
long contentLength = getContentLength(connection); if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length bytesToRead = dataSpec.length;
: contentLength != C.LENGTH_UNSET ? contentLength - bytesToSkip } else {
: C.LENGTH_UNSET; long contentLength = getContentLength(connection);
bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip)
: C.LENGTH_UNSET;
}
} else { } else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response // Gzip is enabled. If the server opts to use gzip then the content length in the response
// will be that of the compressed data, which isn't what we want. Furthermore, there isn't a // will be that of the compressed data, which isn't what we want. Furthermore, there isn't a
...@@ -410,11 +413,16 @@ public class DefaultHttpDataSource implements HttpDataSource { ...@@ -410,11 +413,16 @@ public class DefaultHttpDataSource implements HttpDataSource {
connection.setInstanceFollowRedirects(followRedirects); connection.setInstanceFollowRedirects(followRedirects);
connection.setDoOutput(postBody != null); connection.setDoOutput(postBody != null);
if (postBody != null) { if (postBody != null) {
connection.setFixedLengthStreamingMode(postBody.length); connection.setRequestMethod("POST");
connection.connect(); if (postBody.length == 0) {
OutputStream os = connection.getOutputStream(); connection.connect();
os.write(postBody); } else {
os.close(); connection.setFixedLengthStreamingMode(postBody.length);
connection.connect();
OutputStream os = connection.getOutputStream();
os.write(postBody);
os.close();
}
} else { } else {
connection.connect(); connection.connect();
} }
......
...@@ -65,6 +65,7 @@ public final class MimeTypes { ...@@ -65,6 +65,7 @@ public final class MimeTypes {
public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608";
public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708";
public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip"; public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip";
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
......
...@@ -20,10 +20,16 @@ ...@@ -20,10 +20,16 @@
<enum name="fixed_width" value="1"/> <enum name="fixed_width" value="1"/>
<enum name="fixed_height" value="2"/> <enum name="fixed_height" value="2"/>
</attr> </attr>
<attr name="show_timeout" format="integer"/>
<attr name="rewind_increment" format="integer"/>
<attr name="fastforward_increment" format="integer"/>
<declare-styleable name="SimpleExoPlayerView"> <declare-styleable name="SimpleExoPlayerView">
<attr name="use_controller" format="boolean"/> <attr name="use_controller" format="boolean"/>
<attr name="use_texture_view" format="boolean"/> <attr name="use_texture_view" format="boolean"/>
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
<attr name="resize_mode"/> <attr name="resize_mode"/>
</declare-styleable> </declare-styleable>
...@@ -31,4 +37,10 @@ ...@@ -31,4 +37,10 @@
<attr name="resize_mode"/> <attr name="resize_mode"/>
</declare-styleable> </declare-styleable>
<declare-styleable name="PlaybackControlView">
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
</declare-styleable>
</resources> </resources>
...@@ -17,8 +17,8 @@ ...@@ -17,8 +17,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.playbacktests" package="com.google.android.exoplayer2.playbacktests"
android:versionCode="2002" android:versionCode="2003"
android:versionName="2.0.2"> android:versionName="2.0.3">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
......
...@@ -23,7 +23,6 @@ import java.io.File; ...@@ -23,7 +23,6 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import junit.framework.Assert; import junit.framework.Assert;
import junit.framework.TestCase;
/** /**
* A fake {@link ExtractorOutput}. * A fake {@link ExtractorOutput}.
...@@ -37,8 +36,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab ...@@ -37,8 +36,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
*/ */
private static final boolean WRITE_DUMP = false; private static final boolean WRITE_DUMP = false;
private final boolean allowDuplicateTrackIds;
public final SparseArray<FakeTrackOutput> trackOutputs; public final SparseArray<FakeTrackOutput> trackOutputs;
public int numberOfTracks; public int numberOfTracks;
...@@ -46,11 +43,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab ...@@ -46,11 +43,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
public SeekMap seekMap; public SeekMap seekMap;
public FakeExtractorOutput() { public FakeExtractorOutput() {
this(false);
}
public FakeExtractorOutput(boolean allowDuplicateTrackIds) {
this.allowDuplicateTrackIds = allowDuplicateTrackIds;
trackOutputs = new SparseArray<>(); trackOutputs = new SparseArray<>();
} }
...@@ -58,11 +50,10 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab ...@@ -58,11 +50,10 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
public FakeTrackOutput track(int trackId) { public FakeTrackOutput track(int trackId) {
FakeTrackOutput output = trackOutputs.get(trackId); FakeTrackOutput output = trackOutputs.get(trackId);
if (output == null) { if (output == null) {
Assert.assertFalse(tracksEnded);
numberOfTracks++; numberOfTracks++;
output = new FakeTrackOutput(); output = new FakeTrackOutput();
trackOutputs.put(trackId, output); trackOutputs.put(trackId, output);
} else {
TestCase.assertTrue("Duplicate track id: " + trackId, allowDuplicateTrackIds);
} }
return output; return output;
} }
......
...@@ -267,8 +267,8 @@ public class TestUtil { ...@@ -267,8 +267,8 @@ public class TestUtil {
*/ */
public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile,
byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors, byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors,
boolean simulateUnknownLength, boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException,
boolean simulatePartialReads) throws IOException, InterruptedException { InterruptedException {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
.setSimulateIOErrors(simulateIOErrors) .setSimulateIOErrors(simulateIOErrors)
.setSimulateUnknownLength(simulateUnknownLength) .setSimulateUnknownLength(simulateUnknownLength)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment