Commit f8a8302f by ojw28 Committed by GitHub

Merge pull request #1879 from google/dev-v2

r2.0.1
parents 7d991cef c381093a
Showing with 1516 additions and 549 deletions
......@@ -22,8 +22,17 @@ and extend, and can be updated through Play Store application updates.
#### Via jCenter ####
The easiest way to get started using ExoPlayer is by including the following in
your project's `build.gradle` file:
The easiest way to get started using ExoPlayer is to add it as a gradle
dependency. You need to make sure you have the jcenter repository included in
the `build.gradle` file in the root of your project:
```gradle
repositories {
jcenter()
}
```
Next, include the following in your module's `build.gradle` file:
```gradle
compile 'com.google.android.exoplayer:exoplayer:rX.X.X'
......
# Release notes #
### r2.0.1 ###
* Fix playback of short duration content
([#1837](https://github.com/google/ExoPlayer/issues/1837)).
* Fix MergingMediaSource preparation issue
([#1853](https://github.com/google/ExoPlayer/issues/1853)).
* Fix live stream buffering (out of memory) issue
([#1825](https://github.com/google/ExoPlayer/issues/1825)).
### r2.0.0 ###
ExoPlayer 2.x is a major iteration of the library. It includes significant API
......
......@@ -39,14 +39,14 @@ android {
productFlavors {
demo
demo_ext
demoExt
}
}
dependencies {
compile project(':library')
demo_extCompile project(path: ':extension-ffmpeg')
demo_extCompile project(path: ':extension-flac')
demo_extCompile project(path: ':extension-opus')
demo_extCompile project(path: ':extension-vp9')
demoExtCompile project(path: ':extension-ffmpeg')
demoExtCompile project(path: ':extension-flac')
demoExtCompile project(path: ':extension-opus')
demoExtCompile project(path: ':extension-vp9')
}
......@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2000"
android:versionName="2.0.0">
android:versionCode="2001"
android:versionName="2.0.1">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
......
......@@ -16,9 +16,33 @@
package com.google.android.exoplayer2.demo;
import android.app.Application;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;
/**
* Placeholder application to facilitate overriding Application methods for debugging and testing.
*/
public class DemoApplication extends Application {
protected String userAgent;
@Override
public void onCreate() {
super.onCreate();
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
}
DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultDataSourceFactory(this, bandwidthMeter,
buildHttpDataSourceFactory(bandwidthMeter));
}
HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
}
}
......@@ -38,9 +38,10 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.TrackInfo;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.io.IOException;
......@@ -54,7 +55,7 @@ import java.util.Locale;
/* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
MappingTrackSelector.EventListener, MetadataRenderer.Output<List<Id3Frame>> {
TrackSelector.EventListener<MappedTrackInfo>, MetadataRenderer.Output<List<Id3Frame>> {
private static final String TAG = "EventLogger";
private static final int MAX_TIMELINE_ITEM_LINES = 3;
......@@ -125,23 +126,24 @@ import java.util.Locale;
// MappingTrackSelector.EventListener
@Override
public void onTracksChanged(TrackInfo trackInfo) {
public void onTrackSelectionsChanged(TrackSelections<? extends MappedTrackInfo> trackSelections) {
Log.d(TAG, "Tracks [");
// Log tracks associated to renderers.
for (int rendererIndex = 0; rendererIndex < trackInfo.rendererCount; rendererIndex++) {
TrackGroupArray trackGroups = trackInfo.getTrackGroups(rendererIndex);
TrackSelection trackSelection = trackInfo.getTrackSelection(rendererIndex);
MappedTrackInfo info = trackSelections.info;
for (int rendererIndex = 0; rendererIndex < trackSelections.length; rendererIndex++) {
TrackGroupArray trackGroups = info.getTrackGroups(rendererIndex);
TrackSelection trackSelection = trackSelections.get(rendererIndex);
if (trackGroups.length > 0) {
Log.d(TAG, " Renderer:" + rendererIndex + " [");
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
TrackGroup trackGroup = trackGroups.get(groupIndex);
String adaptiveSupport = getAdaptiveSupportString(
trackGroup.length, trackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false));
trackGroup.length, info.getAdaptiveSupport(rendererIndex, groupIndex, false));
Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " [");
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);
String formatSupport = getFormatSupportString(
trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
info.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ getFormatString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport);
......@@ -152,7 +154,7 @@ import java.util.Locale;
}
}
// Log tracks not associated with a renderer.
TrackGroupArray trackGroups = trackInfo.getUnassociatedTrackGroups();
TrackGroupArray trackGroups = info.getUnassociatedTrackGroups();
if (trackGroups.length > 0) {
Log.d(TAG, " Renderer:None [");
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
......
......@@ -36,6 +36,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
......@@ -55,15 +56,15 @@ import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.TrackInfo;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlaybackControlView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;
import java.net.CookieHandler;
......@@ -77,7 +78,7 @@ import java.util.UUID;
* An activity that plays media using {@link SimpleExoPlayer}.
*/
public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener,
MappingTrackSelector.EventListener, PlaybackControlView.VisibilityListener {
TrackSelector.EventListener<MappedTrackInfo>, PlaybackControlView.VisibilityListener {
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
public static final String DRM_LICENSE_URL = "drm_license_url";
......@@ -106,7 +107,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private TextView debugTextView;
private Button retryButton;
private String userAgent;
private DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player;
private MappingTrackSelector trackSelector;
......@@ -125,7 +125,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
shouldAutoPlay = true;
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
......@@ -203,7 +202,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
initializePlayer();
} else if (view.getParent() == debugRootView) {
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(),
trackSelector.getTrackInfo(), (int) view.getTag());
trackSelector.getCurrentSelections().info, (int) view.getTag());
}
}
......@@ -222,7 +221,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
DrmSessionManager drmSessionManager = null;
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
if (drmSchemeUuid != null) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
......@@ -316,15 +315,15 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
: uri.getLastPathSegment());
switch (type) {
case Util.TYPE_SS:
case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false),
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
case Util.TYPE_DASH:
case C.TYPE_DASH:
return new DashMediaSource(uri, buildDataSourceFactory(false),
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
case Util.TYPE_HLS:
case C.TYPE_HLS:
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger);
case Util.TYPE_OTHER:
case C.TYPE_OTHER:
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
mainHandler, eventLogger);
default: {
......@@ -333,9 +332,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
}
private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl,
Map<String, String> keyRequestProperties)
throws UnsupportedDrmException {
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(UUID uuid,
String licenseUrl, Map<String, String> keyRequestProperties) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) {
return null;
}
......@@ -376,8 +374,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
* @return A new DataSource factory.
*/
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
return new DefaultDataSourceFactory(this, useBandwidthMeter ? BANDWIDTH_METER : null,
buildHttpDataSourceFactory(useBandwidthMeter));
return ((DemoApplication) getApplication())
.buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
/**
......@@ -388,7 +386,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
* @return A new HttpDataSource factory.
*/
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
return new DefaultHttpDataSourceFactory(userAgent, useBandwidthMeter ? BANDWIDTH_METER : null);
return ((DemoApplication) getApplication())
.buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
// ExoPlayer.EventListener implementation
......@@ -452,8 +451,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
// MappingTrackSelector.EventListener implementation
@Override
public void onTracksChanged(TrackInfo trackInfo) {
public void onTrackSelectionsChanged(TrackSelections<? extends MappedTrackInfo> trackSelections) {
updateButtonVisibilities();
MappedTrackInfo trackInfo = trackSelections.info;
if (trackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_VIDEO)) {
showToast(R.string.error_unsupported_video);
}
......@@ -474,14 +474,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
return;
}
TrackInfo trackInfo = trackSelector.getTrackInfo();
if (trackInfo == null) {
TrackSelections<MappedTrackInfo> trackSelections = trackSelector.getCurrentSelections();
if (trackSelections == null) {
return;
}
int rendererCount = trackInfo.rendererCount;
int rendererCount = trackSelections.length;
for (int i = 0; i < rendererCount; i++) {
TrackGroupArray trackGroups = trackInfo.getTrackGroups(i);
TrackGroupArray trackGroups = trackSelections.info.getTrackGroups(i);
if (trackGroups.length != 0) {
Button button = new Button(this);
int label;
......
......@@ -31,8 +31,8 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.SelectionOverride;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.TrackInfo;
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.MimeTypes;
......@@ -51,7 +51,7 @@ import java.util.Locale;
private final MappingTrackSelector selector;
private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory;
private TrackInfo trackInfo;
private MappedTrackInfo trackInfo;
private int rendererIndex;
private TrackGroupArray trackGroups;
private boolean[] trackGroupsAdaptive;
......@@ -82,7 +82,7 @@ import java.util.Locale;
* @param trackInfo The current track information.
* @param rendererIndex The index of the renderer.
*/
public void showSelectionDialog(Activity activity, CharSequence title, TrackInfo trackInfo,
public void showSelectionDialog(Activity activity, CharSequence title, MappedTrackInfo trackInfo,
int rendererIndex) {
this.trackInfo = trackInfo;
this.rendererIndex = rendererIndex;
......@@ -203,11 +203,7 @@ import java.util.Locale;
@Override
public void onClick(DialogInterface dialog, int which) {
if (isDisabled) {
selector.setRendererDisabled(rendererIndex, true);
return;
}
selector.setRendererDisabled(rendererIndex, false);
selector.setRendererDisabled(rendererIndex, isDisabled);
if (override != null) {
selector.setSelectionOverride(rendererIndex, trackGroups, override);
} else {
......
......@@ -100,7 +100,8 @@ public final class CronetDataSourceTest {
Executor executor, int priority,
Collection<Object> connectionAnnotations,
boolean disableCache,
boolean disableConnectionMigration);
boolean disableConnectionMigration,
boolean allowDirectExecutor);
}
@Mock
......@@ -108,7 +109,7 @@ public final class CronetDataSourceTest {
@Mock
private Predicate<String> mockContentTypePredicate;
@Mock
private TransferListener mockTransferListener;
private TransferListener<CronetDataSource> mockTransferListener;
@Mock
private Clock mockClock;
@Mock
......@@ -143,6 +144,7 @@ public final class CronetDataSourceTest {
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class))).thenReturn(mockUrlRequest);
mockStatusResponse();
......@@ -170,8 +172,8 @@ public final class CronetDataSourceTest {
}
@Test(expected = IllegalStateException.class)
public void testOpeningTwiceThrows() throws HttpDataSourceException, IllegalStateException {
mockResponesStartSuccess();
public void testOpeningTwiceThrows() throws HttpDataSourceException {
mockResponseStartSuccess();
assertConnectionState(CronetDataSource.IDLE_CONNECTION);
dataSourceUnderTest.open(testDataSpec);
......@@ -181,7 +183,7 @@ public final class CronetDataSourceTest {
@Test
public void testCallbackFromPreviousRequest() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec);
dataSourceUnderTest.close();
......@@ -194,6 +196,7 @@ public final class CronetDataSourceTest {
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class))).thenReturn(mockUrlRequest2);
doAnswer(new Answer<Object>() {
@Override
......@@ -214,7 +217,7 @@ public final class CronetDataSourceTest {
@Test
public void testRequestStartCalled() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec);
verify(mockCronetEngine).createRequest(
......@@ -224,13 +227,14 @@ public final class CronetDataSourceTest {
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class));
verify(mockUrlRequest).start();
}
@Test
public void testRequestHeadersSet() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
testResponseHeader.put("Content-Length", Long.toString(5000L));
......@@ -248,13 +252,29 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpen() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testDataSpec));
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@Test
public void testRequestOpenGzippedCompressedReturnsDataSpecLength()
throws HttpDataSourceException {
testResponseHeader.put("Content-Encoding", "gzip");
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
mockResponseStartSuccess();
// Data spec's requested length, 5000. Test response's length, 16,000.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
assertEquals(5000 /* contentLength */, dataSourceUnderTest.open(testDataSpec));
assertConnectionState(CronetDataSource.OPEN_CONNECTION);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@Test
public void testRequestOpenFail() {
mockResponseStartFailure();
......@@ -291,7 +311,7 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpenValidatesStatusCode() {
mockResponesStartSuccess();
mockResponseStartSuccess();
testUrlResponseInfo = createUrlResponseInfo(500); // statusCode
try {
......@@ -308,7 +328,7 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpenValidatesContentTypePredicate() {
mockResponesStartSuccess();
mockResponseStartSuccess();
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false);
try {
......@@ -325,7 +345,7 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpenValidatesContentLength() {
mockResponesStartSuccess();
mockResponseStartSuccess();
// Data spec's requested length, 5000. Test response's length, 16,000.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
......@@ -344,7 +364,7 @@ public final class CronetDataSourceTest {
@Test
public void testPostRequestOpen() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testPostDataSpec));
......@@ -354,7 +374,7 @@ public final class CronetDataSourceTest {
@Test
public void testPostRequestOpenValidatesContentType() {
mockResponesStartSuccess();
mockResponseStartSuccess();
try {
dataSourceUnderTest.open(testPostDataSpec);
......@@ -366,7 +386,7 @@ public final class CronetDataSourceTest {
@Test
public void testPostRequestOpenRejects307Redirects() {
mockResponesStartSuccess();
mockResponseStartSuccess();
mockResponseStartRedirect();
try {
......@@ -380,7 +400,7 @@ public final class CronetDataSourceTest {
@Test
public void testRequestReadTwice() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
mockReadSuccess();
dataSourceUnderTest.open(testDataSpec);
......@@ -402,7 +422,7 @@ public final class CronetDataSourceTest {
@Test
public void testSecondRequestNoContentLength() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
mockReadSuccess();
byte[] returnedBuffer = new byte[8];
......@@ -433,7 +453,23 @@ public final class CronetDataSourceTest {
@Test
public void testReadWithOffset() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
mockReadSuccess();
dataSourceUnderTest.open(testDataSpec);
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
assertEquals(8, bytesRead);
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
}
@Test
public void testReadWithUnsetLength() throws HttpDataSourceException {
testResponseHeader.remove("Content-Length");
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
mockResponseStartSuccess();
mockReadSuccess();
dataSourceUnderTest.open(testDataSpec);
......@@ -447,7 +483,7 @@ public final class CronetDataSourceTest {
@Test
public void testReadReturnsWhatItCan() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
mockReadSuccess();
dataSourceUnderTest.open(testDataSpec);
......@@ -461,7 +497,7 @@ public final class CronetDataSourceTest {
@Test
public void testClosedMeansClosed() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
mockReadSuccess();
int bytesRead = 0;
......@@ -489,7 +525,7 @@ public final class CronetDataSourceTest {
@Test
public void testOverread() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
mockReadSuccess();
// Ask for 16 bytes
......@@ -676,7 +712,7 @@ public final class CronetDataSourceTest {
@Test
public void testExceptionFromTransferListener() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
// Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
// the subsequent open() call succeeds.
......@@ -695,7 +731,7 @@ public final class CronetDataSourceTest {
@Test
public void testReadFailure() throws HttpDataSourceException {
mockResponesStartSuccess();
mockResponseStartSuccess();
mockReadFailure();
dataSourceUnderTest.open(testDataSpec);
......@@ -722,7 +758,7 @@ public final class CronetDataSourceTest {
}).when(mockUrlRequest).getStatus(any(UrlRequest.StatusListener.class));
}
private void mockResponesStartSuccess() {
private void mockResponseStartSuccess() {
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
......
......@@ -300,15 +300,20 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
try {
validateResponse(info);
responseInfo = info;
// Check content length.
contentLength = getContentLength(info.getAllHeaders());
// If a specific length is requested and a specific length is returned but the 2 don't match
// it's an error.
if (currentDataSpec.length != C.LENGTH_UNSET
&& contentLength != C.LENGTH_UNSET
&& currentDataSpec.length != contentLength) {
throw new OpenException("Content length did not match requested length", currentDataSpec,
getCurrentRequestStatus());
if (isCompressed(info)) {
contentLength = currentDataSpec.length;
} else {
// Check content length.
contentLength = getContentLength(info.getAllHeaders());
// If a specific length is requested and a specific length is returned but the 2 don't match
// it's an error.
if (currentDataSpec.length != C.LENGTH_UNSET
&& contentLength != C.LENGTH_UNSET
&& currentDataSpec.length != contentLength) {
throw new OpenException("Content length did not match requested length", currentDataSpec,
getCurrentRequestStatus());
}
}
if (contentLength > 0) {
......@@ -326,6 +331,23 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
}
}
/**
* Returns {@code true} iff the content is compressed.
*
* <p>If {@code true}, clients cannot use the value of content length from the request headers to
* read the data, since Cronet returns the uncompressed data and this content length reflects the
* compressed content length.
*/
private boolean isCompressed(UrlResponseInfo info) {
for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
return !entry.getValue().equalsIgnoreCase("identity");
}
}
return false;
}
private void validateResponse(UrlResponseInfo info) throws HttpDataSourceException {
// Check for a valid response code.
int responseCode = info.getHttpStatusCode();
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException;
......@@ -71,7 +72,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
public void run() {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(null);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler());
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.opus;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException;
......@@ -71,7 +72,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
public void run() {
Looper.prepare();
LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(null);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler());
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.vp9;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException;
......@@ -87,7 +88,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
public void run() {
Looper.prepare();
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(null);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler());
player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
......
......@@ -55,6 +55,7 @@ dependencies {
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'org.mockito:mockito-core:1.9.5'
compile 'com.android.support:support-annotations:24.2.0'
}
android.libraryVariants.all { variant ->
......@@ -95,7 +96,7 @@ publish {
userOrg = 'google'
groupId = 'com.google.android.exoplayer'
artifactId = 'exoplayer'
version = 'r2.0.0'
version = 'r2.0.1'
description = 'The ExoPlayer library.'
website = 'https://github.com/google/ExoPlayer'
}
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 0
tracksEnded = true
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 0
tracksEnded = true
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 0
tracksEnded = true
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true
......@@ -33,4 +33,13 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase {
}, "mp3/bear.mp3", getInstrumentation());
}
public void testTrimmedMp3Sample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new Mp3Extractor();
}
}, "mp3/play-trimmed.mp3", getInstrumentation());
}
}
......@@ -16,8 +16,16 @@
package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
......@@ -61,6 +69,31 @@ public final class TsExtractorTest extends InstrumentationTestCase {
}, "ts/sample.ts", fileData, getInstrumentation());
}
public void testCustomPesReader() throws Exception {
CustomEsReaderFactory factory = new CustomEsReaderFactory();
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
.setSimulateIOErrors(false)
.setSimulateUnknownLength(false)
.setSimulatePartialReads(false).build();
FakeExtractorOutput output = new FakeExtractorOutput();
tsExtractor.init(output);
tsExtractor.seek(input.getPosition());
PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = tsExtractor.read(input, seekPositionHolder);
}
CustomEsReader reader = factory.reader;
assertEquals(2, reader.packetsRead);
TrackOutput trackOutput = reader.getTrackOutput();
assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */));
assertEquals(
Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, "und", null, 0),
((FakeTrackOutput) trackOutput).format);
}
private static void writeJunkData(ByteArrayOutputStream out, int length) throws IOException {
for (int i = 0; i < length; i++) {
if (((byte) i) == TS_SYNC_BYTE) {
......@@ -71,4 +104,62 @@ public final class TsExtractorTest extends InstrumentationTestCase {
}
}
private static final class CustomEsReader extends ElementaryStreamReader {
public int packetsRead = 0;
public CustomEsReader(TrackOutput output, String language) {
super(output);
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
language, null, 0));
}
@Override
public void seek() {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
}
@Override
public void consume(ParsableByteArray data) {
}
@Override
public void packetFinished() {
packetsRead++;
}
public TrackOutput getTrackOutput() {
return output;
}
}
private static final class CustomEsReaderFactory implements ElementaryStreamReader.Factory {
private final ElementaryStreamReader.Factory defaultFactory;
private CustomEsReader reader;
public CustomEsReaderFactory() {
defaultFactory = new DefaultStreamReaderFactory();
}
@Override
public ElementaryStreamReader onPmtEntry(int pid, int streamType,
ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) {
if (streamType == 3) {
// We need to manually avoid a duplicate custom reader creation.
if (reader == null) {
reader = new CustomEsReader(output.track(pid), esInfo.language);
}
return reader;
} else {
return defaultFactory.onPmtEntry(pid, streamType, esInfo, output);
}
}
}
}
......@@ -17,8 +17,11 @@ package com.google.android.exoplayer2;
import android.media.AudioFormat;
import android.media.MediaCodec;
import android.support.annotation.IntDef;
import android.view.Surface;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.UUID;
/**
......@@ -71,54 +74,78 @@ public final class C {
public static final String UTF8_NAME = "UTF-8";
/**
* Crypto modes for a codec.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})
public @interface CryptoMode {}
/**
* @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
*/
@SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
/**
* @see MediaCodec#CRYPTO_MODE_AES_CTR
*/
@SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
/**
* @see MediaCodec#CRYPTO_MODE_AES_CBC
*/
@SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;
/**
* Represents an audio encoding, or an invalid or unset value.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS,
ENCODING_DTS_HD})
public @interface Encoding {}
/**
* Represents a PCM audio encoding, or an invalid or unset value.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT})
public @interface PcmEncoding {}
/**
* @see AudioFormat#ENCODING_INVALID
*/
public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
/**
* @see AudioFormat#ENCODING_PCM_8BIT
*/
public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
/**
* @see AudioFormat#ENCODING_PCM_16BIT
*/
public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
/**
* PCM encoding with 24 bits per sample.
*/
public static final int ENCODING_PCM_24BIT = 0x80000000;
/**
* PCM encoding with 32 bits per sample.
*/
public static final int ENCODING_PCM_32BIT = 0x40000000;
/**
* @see AudioFormat#ENCODING_AC3
*/
@SuppressWarnings("InlinedApi")
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/**
* @see AudioFormat#ENCODING_E_AC3
*/
@SuppressWarnings("InlinedApi")
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
/**
* @see AudioFormat#ENCODING_DTS
*/
@SuppressWarnings("InlinedApi")
public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
/**
* @see AudioFormat#ENCODING_DTS_HD
*/
......@@ -133,47 +160,92 @@ public final class C {
? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
/**
* Flags which can apply to a buffer containing a media sample.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM,
BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY})
public @interface BufferFlags {}
/**
* Indicates that a buffer holds a synchronization sample.
*/
@SuppressWarnings("InlinedApi")
public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;
/**
* Flag for empty buffers that signal that the end of the stream was reached.
*/
@SuppressWarnings("InlinedApi")
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
/**
* Indicates that a buffer is (at least partially) encrypted.
*/
public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000;
/**
* Indicates that a buffer should be decoded but not rendered.
*/
public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
/**
* Track selection flags.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED,
SELECTION_FLAG_AUTOSELECT})
public @interface SelectionFlags {}
/**
* Indicates that the track should be selected if user preferences do not state otherwise.
*/
public static final int SELECTION_FLAG_DEFAULT = 1;
/**
* Indicates that the track must be displayed. Only applies to text tracks.
*/
public static final int SELECTION_FLAG_FORCED = 2;
/**
* Indicates that the player may choose to play the track in absence of an explicit user
* preference.
*/
public static final int SELECTION_FLAG_AUTOSELECT = 4;
/**
* Represents a streaming or other media type.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER})
public @interface ContentType {}
/**
* Value returned by {@link Util#inferContentType(String)} for DASH manifests.
*/
public static final int TYPE_DASH = 0;
/**
* Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests.
*/
public static final int TYPE_SS = 1;
/**
* Value returned by {@link Util#inferContentType(String)} for HLS manifests.
*/
public static final int TYPE_HLS = 2;
/**
* Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or
* Smooth Streaming manifests.
*/
public static final int TYPE_OTHER = 3;
/**
* A return value for methods where the end of an input was encountered.
*/
public static final int RESULT_END_OF_INPUT = -1;
/**
* A return value for methods where the length of parsed data exceeds the maximum length allowed.
*/
public static final int RESULT_MAX_LENGTH_EXCEEDED = -2;
/**
* A return value for methods where nothing was read.
*/
public static final int RESULT_NOTHING_READ = -3;
/**
* A return value for methods where a buffer was read.
*/
public static final int RESULT_BUFFER_READ = -4;
/**
* A return value for methods where a format was read.
*/
......@@ -183,32 +255,26 @@ public final class C {
* A data type constant for data of unknown or unspecified type.
*/
public static final int DATA_TYPE_UNKNOWN = 0;
/**
* A data type constant for media, typically containing media samples.
*/
public static final int DATA_TYPE_MEDIA = 1;
/**
* A data type constant for media, typically containing only initialization data.
*/
public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2;
/**
* A data type constant for drm or encryption data.
*/
public static final int DATA_TYPE_DRM = 3;
/**
* A data type constant for a manifest file.
*/
public static final int DATA_TYPE_MANIFEST = 4;
/**
* A data type constant for time synchronization data.
*/
public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5;
/**
* Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or
* equal to this value.
......@@ -219,32 +285,26 @@ public final class C {
* A type constant for tracks of unknown type.
*/
public static final int TRACK_TYPE_UNKNOWN = -1;
/**
* A type constant for tracks of some default type, where the type itself is unknown.
*/
public static final int TRACK_TYPE_DEFAULT = 0;
/**
* A type constant for audio tracks.
*/
public static final int TRACK_TYPE_AUDIO = 1;
/**
* A type constant for video tracks.
*/
public static final int TRACK_TYPE_VIDEO = 2;
/**
* A type constant for text tracks.
*/
public static final int TRACK_TYPE_TEXT = 3;
/**
* A type constant for metadata tracks.
*/
public static final int TRACK_TYPE_METADATA = 4;
/**
* Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or
* equal to this value.
......@@ -255,27 +315,22 @@ public final class C {
* A selection reason constant for selections whose reasons are unknown or unspecified.
*/
public static final int SELECTION_REASON_UNKNOWN = 0;
/**
* A selection reason constant for an initial track selection.
*/
public static final int SELECTION_REASON_INITIAL = 1;
/**
* A selection reason constant for an manual (i.e. user initiated) track selection.
*/
public static final int SELECTION_REASON_MANUAL = 2;
/**
* A selection reason constant for an adaptive track selection.
*/
public static final int SELECTION_REASON_ADAPTIVE = 3;
/**
* A selection reason constant for a trick play track selection.
*/
public static final int SELECTION_REASON_TRICK_PLAY = 4;
/**
* Applications or extensions may define custom {@code SELECTION_REASON_*} constants greater than
* or equal to this value.
......@@ -364,15 +419,19 @@ public final class C {
public static final int MSG_CUSTOM_BASE = 10000;
/**
* The stereo mode for 360/3D/VR videos.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT})
public @interface StereoMode {}
/**
* Indicates Monoscopic stereo layout, used with 360/3D/VR videos.
*/
public static final int STEREO_MODE_MONO = 0;
/**
* Indicates Top-Bottom stereo layout, used with 360/3D/VR videos.
*/
public static final int STEREO_MODE_TOP_BOTTOM = 1;
/**
* Indicates Left-Right stereo layout, used with 360/3D/VR videos.
*/
......
......@@ -16,7 +16,7 @@
package com.google.android.exoplayer2;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.Util;
......@@ -106,7 +106,7 @@ public final class DefaultLoadControl implements LoadControl {
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
TrackSelections<?> trackSelections) {
targetBufferSize = 0;
for (int i = 0; i < renderers.length; i++) {
if (trackSelections.get(i) != null) {
......
......@@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer2;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Thrown when a non-recoverable playback failure occurs.
......@@ -25,6 +28,12 @@ import java.io.IOException;
public final class ExoPlaybackException extends Exception {
/**
* The type of source that produced the error.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED})
public @interface Type {}
/**
* The error occurred loading data from a {@link MediaSource}.
* <p>
* Call {@link #getSourceException()} to retrieve the underlying cause.
......@@ -47,6 +56,7 @@ public final class ExoPlaybackException extends Exception {
* The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
* {@link #TYPE_UNEXPECTED}.
*/
@Type
public final int type;
/**
......@@ -85,7 +95,8 @@ public final class ExoPlaybackException extends Exception {
return new ExoPlaybackException(TYPE_UNEXPECTED, null, cause, C.INDEX_UNSET);
}
private ExoPlaybackException(int type, String message, Throwable cause, int rendererIndex) {
private ExoPlaybackException(@Type int type, String message, Throwable cause,
int rendererIndex) {
super(message, cause);
this.type = type;
this.rendererIndex = rendererIndex;
......
......@@ -243,16 +243,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline == null || pendingSeekAcks > 0) {
return maskingWindowPositionMs;
} else {
int periodIndex = playbackInfo.periodIndex;
timeline.getPeriod(periodIndex, period);
int windowIndex = period.windowIndex;
timeline.getWindow(windowIndex, window);
if (window.firstPeriodIndex == periodIndex && window.lastPeriodIndex == periodIndex
&& window.getPositionInFirstPeriodUs() == 0
&& window.getDurationUs() == period.getDurationUs()) {
return C.usToMs(playbackInfo.bufferedPositionUs);
}
return getCurrentPosition();
timeline.getPeriod(playbackInfo.periodIndex, period);
return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs);
}
}
......
......@@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
String VERSION = "2.0.0";
String VERSION = "2.0.1";
/**
* The version of the library, expressed as an integer.
......@@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006).
*/
int VERSION_INT = 2000000;
int VERSION_INT = 2000001;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
......@@ -40,20 +40,6 @@ public final class Format implements Parcelable {
public static final int NO_VALUE = -1;
/**
* Indicates that the track should be selected if user preferences do not state otherwise.
*/
public static final int SELECTION_FLAG_DEFAULT = 1;
/**
* Indicates that the track must be displayed. Only applies to text tracks.
*/
public static final int SELECTION_FLAG_FORCED = 2;
/**
* Indicates that the player may choose to play the track in absence of an explicit user
* preference.
*/
public static final int SELECTION_FLAG_AUTOSELECT = 4;
/**
* A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to
* the timestamps of their parent samples.
*/
......@@ -131,6 +117,7 @@ public final class Format implements Parcelable {
* modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link
* C#STEREO_MODE_LEFT_RIGHT}.
*/
@C.StereoMode
public final int stereoMode;
/**
* The projection data for 360/VR video, or null if not applicable.
......@@ -153,6 +140,7 @@ public final class Format implements Parcelable {
* {@link C#ENCODING_PCM_24BIT} and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for
* other media types.
*/
@C.PcmEncoding
public final int pcmEncoding;
/**
* The number of samples to trim from the start of the decoded audio stream.
......@@ -177,6 +165,7 @@ public final class Format implements Parcelable {
/**
* Track selection flags.
*/
@C.SelectionFlags
public final int selectionFlags;
/**
......@@ -218,7 +207,7 @@ public final class Format implements Parcelable {
public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, int maxInputSize, int width, int height, float frameRate,
List<byte[]> initializationData, int rotationDegrees, float pixelWidthHeightRatio,
byte[] projectionData, int stereoMode, DrmInitData drmInitData) {
byte[] projectionData, @C.StereoMode int stereoMode, DrmInitData drmInitData) {
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData,
......@@ -229,7 +218,7 @@ public final class Format implements Parcelable {
public static Format createAudioContainerFormat(String id, String containerMimeType,
String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate,
List<byte[]> initializationData, int selectionFlags, String language) {
List<byte[]> initializationData, @C.SelectionFlags int selectionFlags, String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE,
NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, initializationData,
......@@ -238,25 +227,26 @@ public final class Format implements Parcelable {
public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, int maxInputSize, int channelCount, int sampleRate,
List<byte[]> initializationData, DrmInitData drmInitData, int selectionFlags,
String language) {
List<byte[]> initializationData, DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags, String language) {
return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language);
}
public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, int maxInputSize, int channelCount, int sampleRate, int pcmEncoding,
List<byte[]> initializationData, DrmInitData drmInitData, int selectionFlags,
String language) {
int bitrate, int maxInputSize, int channelCount, int sampleRate,
@C.PcmEncoding int pcmEncoding, List<byte[]> initializationData, DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags, String language) {
return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData,
selectionFlags, language);
}
public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, int maxInputSize, int channelCount, int sampleRate, int pcmEncoding,
int encoderDelay, int encoderPadding, List<byte[]> initializationData,
DrmInitData drmInitData, int selectionFlags, String language) {
int bitrate, int maxInputSize, int channelCount, int sampleRate,
@C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding,
List<byte[]> initializationData, DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags, String language) {
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding,
encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE,
......@@ -266,20 +256,21 @@ public final class Format implements Parcelable {
// Text.
public static Format createTextContainerFormat(String id, String containerMimeType,
String sampleMimeType, String codecs, int bitrate, int selectionFlags, String language) {
String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null);
}
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, int selectionFlags, String language, DrmInitData drmInitData) {
int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
drmInitData, OFFSET_SAMPLE_RELATIVE);
}
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, int selectionFlags, String language, DrmInitData drmInitData,
int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData,
long subsampleOffsetUs) {
return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
......@@ -313,10 +304,10 @@ public final class Format implements Parcelable {
/* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs,
int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees,
float pixelWidthHeightRatio, byte[] projectionData, int stereoMode, int channelCount,
int sampleRate, int pcmEncoding, int encoderDelay, int encoderPadding, int selectionFlags,
String language, long subsampleOffsetUs, List<byte[]> initializationData,
DrmInitData drmInitData) {
float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode,
int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay,
int encoderPadding, @C.SelectionFlags int selectionFlags, String language,
long subsampleOffsetUs, List<byte[]> initializationData, DrmInitData drmInitData) {
this.id = id;
this.containerMimeType = containerMimeType;
this.sampleMimeType = sampleMimeType;
......@@ -343,6 +334,7 @@ public final class Format implements Parcelable {
this.drmInitData = drmInitData;
}
@SuppressWarnings("ResourceType")
/* package */ Format(Parcel in) {
id = in.readString();
containerMimeType = in.readString();
......@@ -388,8 +380,8 @@ public final class Format implements Parcelable {
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
}
public Format copyWithContainerInfo(String id, int bitrate, int width, int height,
int selectionFlags, String language) {
public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height,
@C.SelectionFlags int selectionFlags, String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
......@@ -402,7 +394,7 @@ public final class Format implements Parcelable {
String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
@C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
String language = this.language == null ? manifestFormat.language : this.language;
DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null)
|| this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData;
......
......@@ -17,8 +17,7 @@ package com.google.android.exoplayer2;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.upstream.Allocator;
/**
......@@ -31,10 +30,10 @@ public interface LoadControl {
*
* @param renderers The renderers.
* @param trackGroups The {@link TrackGroup}s from which the selection was made.
* @param trackSelections The {@link TrackSelection}s that were made.
* @param trackSelections The track selections that were made.
*/
void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections);
TrackSelections<?> trackSelections);
/**
* Called by the player when all tracks are disabled.
......
......@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
......@@ -40,6 +41,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
......@@ -80,18 +82,14 @@ public final class SimpleExoPlayer implements ExoPlayer {
/**
* Called when a frame is rendered for the first time since setting the surface, and when a
* frame is rendered for the first time since the renderer was reset.
*
* @param surface The {@link Surface} to which a first frame has been rendered.
* frame is rendered for the first time since a video track was selected.
*/
void onRenderedFirstFrame(Surface surface);
void onRenderedFirstFrame();
/**
* Called when the renderer is disabled.
*
* @param counters {@link DecoderCounters} that were updated by the renderer.
* Called when a video track is no longer selected.
*/
void onVideoDisabled(DecoderCounters counters);
void onVideoTracksDisabled();
}
......@@ -105,9 +103,11 @@ public final class SimpleExoPlayer implements ExoPlayer {
private final int videoRendererCount;
private final int audioRendererCount;
private boolean videoTracksEnabled;
private Format videoFormat;
private Format audioFormat;
private Surface surface;
private SurfaceHolder surfaceHolder;
private TextureView textureView;
private TextRenderer.Output textOutput;
......@@ -121,11 +121,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
private float volume;
private PlaybackParamsHolder playbackParamsHolder;
/* package */ SimpleExoPlayer(Context context, TrackSelector trackSelector,
LoadControl loadControl, DrmSessionManager drmSessionManager,
/* package */ SimpleExoPlayer(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) {
mainHandler = new Handler();
componentListener = new ComponentListener();
trackSelector.addListener(componentListener);
// Build the renderers.
ArrayList<Renderer> renderersList = new ArrayList<>();
......@@ -509,8 +510,9 @@ public final class SimpleExoPlayer implements ExoPlayer {
// Internal methods.
private void buildRenderers(Context context, DrmSessionManager drmSessionManager,
ArrayList<Renderer> renderersList, long allowedVideoJoiningTimeMs) {
private void buildRenderers(Context context,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, ArrayList<Renderer> renderersList,
long allowedVideoJoiningTimeMs) {
MediaCodecVideoRenderer videoRenderer = new MediaCodecVideoRenderer(context,
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, componentListener,
......@@ -601,6 +603,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
private void setVideoSurfaceInternal(Surface surface) {
this.surface = surface;
ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
int count = 0;
for (Renderer renderer : renderers) {
......@@ -618,7 +621,8 @@ public final class SimpleExoPlayer implements ExoPlayer {
private final class ComponentListener implements VideoRendererEventListener,
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<List<Id3Frame>>,
SurfaceHolder.Callback, TextureView.SurfaceTextureListener {
SurfaceHolder.Callback, TextureView.SurfaceTextureListener,
TrackSelector.EventListener<Object> {
// VideoRendererEventListener implementation
......@@ -669,8 +673,8 @@ public final class SimpleExoPlayer implements ExoPlayer {
@Override
public void onRenderedFirstFrame(Surface surface) {
if (videoListener != null) {
videoListener.onRenderedFirstFrame(surface);
if (videoListener != null && SimpleExoPlayer.this.surface == surface) {
videoListener.onRenderedFirstFrame();
}
if (videoDebugListener != null) {
videoDebugListener.onRenderedFirstFrame(surface);
......@@ -679,9 +683,6 @@ public final class SimpleExoPlayer implements ExoPlayer {
@Override
public void onVideoDisabled(DecoderCounters counters) {
if (videoListener != null) {
videoListener.onVideoDisabled(counters);
}
if (videoDebugListener != null) {
videoDebugListener.onVideoDisabled(counters);
}
......@@ -800,6 +801,23 @@ public final class SimpleExoPlayer implements ExoPlayer {
// Do nothing.
}
// TrackSelector.EventListener implementation
@Override
public void onTrackSelectionsChanged(TrackSelections<?> trackSelections) {
boolean videoTracksEnabled = false;
for (int i = 0; i < renderers.length; i++) {
if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelections.get(i) != null) {
videoTracksEnabled = true;
break;
}
}
if (videoListener != null && SimpleExoPlayer.this.videoTracksEnabled && !videoTracksEnabled) {
videoListener.onVideoTracksDisabled();
}
SimpleExoPlayer.this.videoTracksEnabled = videoTracksEnabled;
}
}
@TargetApi(23)
......
......@@ -208,7 +208,9 @@ public final class AudioTrack {
private android.media.AudioTrack audioTrack;
private int sampleRate;
private int channelConfig;
@C.Encoding
private int sourceEncoding;
@C.Encoding
private int targetEncoding;
private boolean passthrough;
private int pcmFrameSize;
......@@ -348,8 +350,8 @@ public final class AudioTrack {
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
* suitable buffer size automatically.
*/
public void configure(String mimeType, int channelCount, int sampleRate, int pcmEncoding,
int specifiedBufferSize) {
public void configure(String mimeType, int channelCount, int sampleRate,
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize) {
int channelConfig;
switch (channelCount) {
case 1:
......@@ -381,7 +383,7 @@ public final class AudioTrack {
}
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
int sourceEncoding;
@C.Encoding int sourceEncoding;
if (passthrough) {
sourceEncoding = getEncodingForMimeType(mimeType);
} else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT
......@@ -470,7 +472,7 @@ public final class AudioTrack {
if (keepSessionIdAudioTrack == null) {
int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
int encoding = C.ENCODING_PCM_16BIT;
@C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate,
channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STATIC, sessionId);
......@@ -962,7 +964,7 @@ public final class AudioTrack {
* @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the
* capacity was insufficient for the output.
*/
private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, int sourceEncoding,
private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding,
ByteBuffer out) {
int offset = buffer.position();
int limit = buffer.limit();
......@@ -1023,6 +1025,7 @@ public final class AudioTrack {
return resampledBuffer;
}
@C.Encoding
private static int getEncodingForMimeType(String mimeType) {
switch (mimeType) {
case MimeTypes.AUDIO_AC3:
......@@ -1038,7 +1041,7 @@ public final class AudioTrack {
}
}
private static int getFramesPerEncodedSample(int encoding, ByteBuffer buffer) {
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) {
return DtsUtil.parseDtsAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_AC3) {
......
......@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
*/
public abstract class Buffer {
@C.BufferFlags
private int flags;
/**
......@@ -58,7 +59,7 @@ public abstract class Buffer {
* @param flags The flags to set, which should be a combination of the {@code C.BUFFER_FLAG_*}
* constants.
*/
public final void setFlags(int flags) {
public final void setFlags(@C.BufferFlags int flags) {
this.flags = flags;
}
......@@ -68,7 +69,7 @@ public abstract class Buffer {
* @param flag The flag to add to this buffer's flags, which should be one of the
* {@code C.BUFFER_FLAG_*} constants.
*/
public final void addFlag(int flag) {
public final void addFlag(@C.BufferFlags int flag) {
flags |= flag;
}
......@@ -77,7 +78,7 @@ public abstract class Buffer {
*
* @param flag The flag to remove.
*/
public final void clearFlag(int flag) {
public final void clearFlag(@C.BufferFlags int flag) {
flags &= ~flag;
}
......@@ -87,7 +88,7 @@ public abstract class Buffer {
* @param flag The flag to check.
* @return Whether the flag is set.
*/
protected final boolean getFlag(int flag) {
protected final boolean getFlag(@C.BufferFlags int flag) {
return (flags & flag) == flag;
}
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.decoder;
import android.annotation.TargetApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
/**
......@@ -34,6 +35,7 @@ public final class CryptoInfo {
/**
* @see android.media.MediaCodec.CryptoInfo#mode
*/
@C.CryptoMode
public int mode;
/**
* @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
......@@ -58,7 +60,7 @@ public final class CryptoInfo {
* @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)
*/
public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
byte[] key, byte[] iv, int mode) {
byte[] key, byte[] iv, @C.CryptoMode int mode) {
this.numSubSamples = numSubSamples;
this.numBytesOfClearData = numBytesOfClearData;
this.numBytesOfEncryptedData = numBytesOfEncryptedData;
......
......@@ -15,7 +15,10 @@
*/
package com.google.android.exoplayer2.decoder;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
/**
......@@ -24,15 +27,20 @@ import java.nio.ByteBuffer;
public class DecoderInputBuffer extends Buffer {
/**
* The buffer replacement mode, which may disable replacement.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({BUFFER_REPLACEMENT_MODE_DISABLED, BUFFER_REPLACEMENT_MODE_NORMAL,
BUFFER_REPLACEMENT_MODE_DIRECT})
public @interface BufferReplacementMode {}
/**
* Disallows buffer replacement.
*/
public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0;
/**
* Allows buffer replacement using {@link ByteBuffer#allocate(int)}.
*/
public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1;
/**
* Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}.
*/
......@@ -53,6 +61,7 @@ public class DecoderInputBuffer extends Buffer {
*/
public long timeUs;
@BufferReplacementMode
private final int bufferReplacementMode;
/**
......@@ -60,7 +69,7 @@ public class DecoderInputBuffer extends Buffer {
* of {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and
* {@link #BUFFER_REPLACEMENT_MODE_DIRECT}.
*/
public DecoderInputBuffer(int bufferReplacementMode) {
public DecoderInputBuffer(@BufferReplacementMode int bufferReplacementMode) {
this.cryptoInfo = new CryptoInfo();
this.bufferReplacementMode = bufferReplacementMode;
}
......
......@@ -16,6 +16,9 @@
package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi;
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A DRM session.
......@@ -24,6 +27,12 @@ import android.annotation.TargetApi;
public interface DrmSession<T extends ExoMediaCrypto> {
/**
* The state of the DRM session.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
@interface State {}
/**
* The session has encountered an error. {@link #getError()} can be used to retrieve the cause.
*/
int STATE_ERROR = 0;
......@@ -50,6 +59,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
* @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
* {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
*/
@State
int getState();
/**
......
......@@ -37,6 +37,6 @@ public interface DrmSessionManager<T extends ExoMediaCrypto> {
/**
* Releases a {@link DrmSession}.
*/
void releaseSession(DrmSession drmSession);
void releaseSession(DrmSession<T> drmSession);
}
......@@ -40,7 +40,7 @@ import java.util.UUID;
* A {@link DrmSessionManager} that supports streaming playbacks using {@link MediaDrm}.
*/
@TargetApi(18)
public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager,
public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
DrmSession<T> {
/**
......@@ -87,6 +87,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
private int openCount;
private boolean provisioningInProgress;
@DrmSession.State
private int state;
private T mediaCrypto;
private Exception lastException;
......@@ -267,7 +268,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
}
@Override
public void releaseSession(DrmSession session) {
public void releaseSession(DrmSession<T> session) {
if (--openCount != 0) {
return;
}
......@@ -291,6 +292,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
// DrmSession implementation.
@Override
@DrmSession.State
public final int getState() {
return state;
}
......
......@@ -15,12 +15,22 @@
*/
package com.google.android.exoplayer2.drm;
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Thrown when the requested DRM scheme is not supported.
*/
public final class UnsupportedDrmException extends Exception {
/**
* The reason for the exception.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({REASON_UNSUPPORTED_SCHEME, REASON_INSTANTIATION_ERROR})
public @interface Reason {}
/**
* The requested DRM scheme is unsupported by the device.
*/
public static final int REASON_UNSUPPORTED_SCHEME = 1;
......@@ -33,12 +43,13 @@ public final class UnsupportedDrmException extends Exception {
/**
* Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
*/
@Reason
public final int reason;
/**
* @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
*/
public UnsupportedDrmException(int reason) {
public UnsupportedDrmException(@Reason int reason) {
this.reason = reason;
}
......@@ -46,7 +57,7 @@ public final class UnsupportedDrmException extends Exception {
* @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
* @param cause The cause of this exception.
*/
public UnsupportedDrmException(int reason, Exception cause) {
public UnsupportedDrmException(@Reason int reason, Exception cause) {
super(cause);
this.reason = reason;
}
......
......@@ -125,7 +125,6 @@ public final class DefaultExtractorInput implements ExtractorInput {
throws IOException, InterruptedException {
ensureSpaceForPeek(length);
int bytesPeeked = Math.min(peekBufferLength - peekBufferPosition, length);
peekBufferLength += length - bytesPeeked;
while (bytesPeeked < length) {
bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked,
allowEndOfInput);
......@@ -134,6 +133,7 @@ public final class DefaultExtractorInput implements ExtractorInput {
}
}
peekBufferPosition += length;
peekBufferLength = Math.max(peekBufferLength, peekBufferPosition);
return true;
}
......
......@@ -298,6 +298,7 @@ public final class DefaultTrackOutput implements TrackOutput {
long offset = extrasHolder.offset;
// Read the signal byte.
scratch.reset(1);
readData(offset, scratch.data, 1);
offset++;
byte signalByte = scratch.data[0];
......@@ -314,9 +315,9 @@ public final class DefaultTrackOutput implements TrackOutput {
// Read the subsample count, if present.
int subsampleCount;
if (subsampleEncryption) {
scratch.reset(2);
readData(offset, scratch.data, 2);
offset += 2;
scratch.setPosition(0);
subsampleCount = scratch.readUnsignedShort();
} else {
subsampleCount = 1;
......@@ -333,7 +334,7 @@ public final class DefaultTrackOutput implements TrackOutput {
}
if (subsampleEncryption) {
int subsampleDataLength = 6 * subsampleCount;
ensureCapacity(scratch, subsampleDataLength);
scratch.reset(subsampleDataLength);
readData(offset, scratch.data, subsampleDataLength);
offset += subsampleDataLength;
scratch.setPosition(0);
......@@ -412,15 +413,6 @@ public final class DefaultTrackOutput implements TrackOutput {
}
}
/**
* Ensure that the passed {@link ParsableByteArray} is of at least the specified limit.
*/
private static void ensureCapacity(ParsableByteArray byteArray, int limit) {
if (byteArray.limit() < limit) {
byteArray.reset(new byte[limit], limit);
}
}
// Called by the loading thread.
/**
......@@ -504,7 +496,8 @@ public final class DefaultTrackOutput implements TrackOutput {
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
byte[] encryptionKey) {
if (!startWriteOperation()) {
infoQueue.commitSampleTimestamp(timeUs);
return;
......@@ -844,8 +837,8 @@ public final class DefaultTrackOutput implements TrackOutput {
}
}
public synchronized void commitSample(long timeUs, int sampleFlags, long offset, int size,
byte[] encryptionKey) {
public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset,
int size, byte[] encryptionKey) {
Assertions.checkState(!upstreamFormatRequired);
commitSampleTimestamp(timeUs);
timesUs[relativeWriteIndex] = timeUs;
......
......@@ -50,7 +50,8 @@ public final class DummyTrackOutput implements TrackOutput {
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
byte[] encryptionKey) {
// Do nothing.
}
......
......@@ -72,6 +72,7 @@ public interface TrackOutput {
* whose metadata is being passed.
* @param encryptionKey The encryption key associated with the sample. May be null.
*/
void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey);
void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
byte[] encryptionKey);
}
......@@ -163,6 +163,9 @@ public final class MatroskaExtractor implements Extractor {
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
private static final int ID_LANGUAGE = 0x22B59C;
private static final int ID_PROJECTION = 0x7670;
private static final int ID_PROJECTION_PRIVATE = 0x7672;
private static final int ID_STEREO_MODE = 0x53B8;
private static final int LACING_NONE = 0;
private static final int LACING_XIPH = 1;
......@@ -264,6 +267,7 @@ public final class MatroskaExtractor implements Extractor {
private int[] blockLacingSampleSizes;
private int blockTrackNumber;
private int blockTrackNumberLength;
@C.BufferFlags
private int blockFlags;
// Sample reading state.
......@@ -361,6 +365,7 @@ public final class MatroskaExtractor implements Extractor {
case ID_CUE_POINT:
case ID_CUE_TRACK_POSITIONS:
case ID_BLOCK_GROUP:
case ID_PROJECTION:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
......@@ -390,6 +395,7 @@ public final class MatroskaExtractor implements Extractor {
case ID_CUE_TIME:
case ID_CUE_CLUSTER_POSITION:
case ID_REFERENCE_BLOCK:
case ID_STEREO_MODE:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_CODEC_ID:
......@@ -401,6 +407,7 @@ public final class MatroskaExtractor implements Extractor {
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
case ID_CODEC_PRIVATE:
case ID_PROJECTION_PRIVATE:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
case ID_SAMPLING_FREQUENCY:
......@@ -655,6 +662,22 @@ public final class MatroskaExtractor implements Extractor {
case ID_BLOCK_DURATION:
blockDurationUs = scaleTimecodeToUs(value);
return;
case ID_STEREO_MODE:
int layout = (int) value;
switch (layout) {
case 0:
currentTrack.stereoMode = C.STEREO_MODE_MONO;
break;
case 1:
currentTrack.stereoMode = C.STEREO_MODE_LEFT_RIGHT;
break;
case 3:
currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;
break;
default:
break;
}
return;
default:
return;
}
......@@ -705,6 +728,10 @@ public final class MatroskaExtractor implements Extractor {
currentTrack.codecPrivate = new byte[contentSize];
input.readFully(currentTrack.codecPrivate, 0, contentSize);
return;
case ID_PROJECTION_PRIVATE:
currentTrack.projectionData = new byte[contentSize];
input.readFully(currentTrack.projectionData, 0, contentSize);
return;
case ID_CONTENT_COMPRESSION_SETTINGS:
// This extractor only supports header stripping, so the payload is the stripped bytes.
currentTrack.sampleStrippedBytes = new byte[contentSize];
......@@ -950,13 +977,9 @@ public final class MatroskaExtractor implements Extractor {
samplePartitionCountRead = true;
}
int samplePartitionDataSize = samplePartitionCount * 4;
if (scratch.limit() < samplePartitionDataSize) {
scratch.reset(new byte[samplePartitionDataSize], samplePartitionDataSize);
}
scratch.reset(samplePartitionDataSize);
input.readFully(scratch.data, 0, samplePartitionDataSize);
sampleBytesRead += samplePartitionDataSize;
scratch.setPosition(0);
scratch.setLimit(samplePartitionDataSize);
short subsampleCount = (short) (1 + (samplePartitionCount / 2));
int subsampleDataSize = 2 + 6 * subsampleCount;
if (encryptionSubsampleDataBuffer == null
......@@ -1295,6 +1318,9 @@ public final class MatroskaExtractor implements Extractor {
public int displayWidth = Format.NO_VALUE;
public int displayHeight = Format.NO_VALUE;
public int displayUnit = DISPLAY_UNIT_PIXELS;
public byte[] projectionData = null;
@C.StereoMode
public int stereoMode = Format.NO_VALUE;
// Audio elements. Initially set to their default values.
public int channelCount = 1;
......@@ -1318,7 +1344,7 @@ public final class MatroskaExtractor implements Extractor {
public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException {
String mimeType;
int maxInputSize = Format.NO_VALUE;
int pcmEncoding = Format.NO_VALUE;
@C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
List<byte[]> initializationData = null;
switch (codecId) {
case CODEC_ID_VP8:
......@@ -1433,9 +1459,9 @@ public final class MatroskaExtractor implements Extractor {
}
Format format;
int selectionFlags = 0;
selectionFlags |= flagDefault ? Format.SELECTION_FLAG_DEFAULT : 0;
selectionFlags |= flagForced ? Format.SELECTION_FLAG_FORCED : 0;
@C.SelectionFlags int selectionFlags = 0;
selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
selectionFlags |= flagForced ? C.SELECTION_FLAG_FORCED : 0;
// TODO: Consider reading the name elements of the tracks and, if present, incorporating them
// into the trackId passed when creating the formats.
if (MimeTypes.isAudio(mimeType)) {
......@@ -1453,7 +1479,7 @@ public final class MatroskaExtractor implements Extractor {
}
format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData,
Format.NO_VALUE, pixelWidthHeightRatio, drmInitData);
Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData);
} else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, selectionFlags, language, drmInitData);
......
......@@ -131,8 +131,12 @@ public final class Mp3Extractor implements Extractor {
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (synchronizedHeaderData == 0 && !synchronizeCatchingEndOfInput(input)) {
return RESULT_END_OF_INPUT;
if (synchronizedHeaderData == 0) {
try {
synchronize(input, false);
} catch (EOFException e) {
return RESULT_END_OF_INPUT;
}
}
if (seeker == null) {
seeker = setupSeeker(input);
......@@ -147,9 +151,20 @@ public final class Mp3Extractor implements Extractor {
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
if (sampleBytesRemaining == 0) {
if (!maybeResynchronize(extractorInput)) {
extractorInput.resetPeekPosition();
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
return RESULT_END_OF_INPUT;
}
scratch.setPosition(0);
int sampleHeaderData = scratch.readInt();
if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK)
|| MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) {
// We have lost synchronization, so attempt to resynchronize starting at the next byte.
extractorInput.skipFully(1);
synchronizedHeaderData = 0;
return RESULT_CONTINUE;
}
MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
if (basisTimeUs == C.TIME_UNSET) {
basisTimeUs = seeker.getTimeUs(extractorInput.getPosition());
if (forcedFirstSampleTimestampUs != C.TIME_UNSET) {
......@@ -175,49 +190,13 @@ public final class Mp3Extractor implements Extractor {
return RESULT_CONTINUE;
}
/**
* Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary.
*/
private boolean maybeResynchronize(ExtractorInput extractorInput)
throws IOException, InterruptedException {
extractorInput.resetPeekPosition();
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
return false;
}
scratch.setPosition(0);
int sampleHeaderData = scratch.readInt();
if ((sampleHeaderData & HEADER_MASK) == (synchronizedHeaderData & HEADER_MASK)) {
int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData);
if (frameSize != C.LENGTH_UNSET) {
MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
return true;
}
}
synchronizedHeaderData = 0;
extractorInput.skipFully(1);
return synchronizeCatchingEndOfInput(extractorInput);
}
private boolean synchronizeCatchingEndOfInput(ExtractorInput input)
throws IOException, InterruptedException {
// An EOFException will be raised if any peek operation was partially satisfied. If a seek
// operation resulted in reading from within the last frame, we may try to peek past the end of
// the file in a partially-satisfied read operation, so we need to catch the exception.
try {
return synchronize(input, false);
} catch (EOFException e) {
return false;
}
}
private boolean synchronize(ExtractorInput input, boolean sniffing)
throws IOException, InterruptedException {
int searched = 0;
int validFrameCount = 0;
int candidateSynchronizedHeaderData = 0;
int peekedId3Bytes = 0;
int searchedBytes = 0;
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
input.resetPeekPosition();
if (input.getPosition() == 0) {
Id3Util.parseId3(input, gaplessInfoHolder);
......@@ -227,14 +206,9 @@ public final class Mp3Extractor implements Extractor {
}
}
while (true) {
if (sniffing && searched == MAX_SNIFF_BYTES) {
return false;
}
if (!sniffing && searched == MAX_SYNC_BYTES) {
throw new ParserException("Searched too many bytes.");
}
if (!input.peekFully(scratch.data, 0, 4, true)) {
return false;
if (!input.peekFully(scratch.data, 0, 4, validFrameCount > 0)) {
// We reached the end of the stream but found at least one valid frame.
break;
}
scratch.setPosition(0);
int headerData = scratch.readInt();
......@@ -242,18 +216,23 @@ public final class Mp3Extractor implements Extractor {
if ((candidateSynchronizedHeaderData != 0
&& (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
|| (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) {
// The header is invalid or doesn't match the candidate header. Try the next byte offset.
// The header doesn't match the candidate header or is invalid. Try the next byte offset.
if (searchedBytes++ == searchLimitBytes) {
if (!sniffing) {
throw new ParserException("Searched too many bytes.");
}
return false;
}
validFrameCount = 0;
candidateSynchronizedHeaderData = 0;
searched++;
if (sniffing) {
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes + searched);
input.advancePeekPosition(peekedId3Bytes + searchedBytes);
} else {
input.skipFully(1);
}
} else {
// The header is valid and matches the candidate header.
// The header matches the candidate header and/or is valid.
validFrameCount++;
if (validFrameCount == 1) {
MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
......@@ -266,7 +245,7 @@ public final class Mp3Extractor implements Extractor {
}
// Prepare to read the synchronized frame.
if (sniffing) {
input.skipFully(peekedId3Bytes + searched);
input.skipFully(peekedId3Bytes + searchedBytes);
} else {
input.resetPeekPosition();
}
......@@ -293,14 +272,17 @@ public final class Mp3Extractor implements Extractor {
long position = input.getPosition();
long length = input.getLength();
int headerData = 0;
Seeker seeker = null;
// Check if there is a Xing header.
int xingBase = (synchronizedHeader.version & 1) != 0
? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1
: (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5
frame.setPosition(xingBase);
int headerData = frame.readInt();
Seeker seeker = null;
if (frame.limit() >= xingBase + 4) {
frame.setPosition(xingBase);
headerData = frame.readInt();
}
if (headerData == XING_HEADER || headerData == INFO_HEADER) {
seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
......@@ -312,7 +294,7 @@ public final class Mp3Extractor implements Extractor {
gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24());
}
input.skipFully(synchronizedHeader.frameSize);
} else {
} else if (frame.limit() >= 40) {
// Check if there is a VBRI header.
frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
headerData = frame.readInt();
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
......@@ -38,6 +39,8 @@ import java.util.List;
*/
/* package */ final class AtomParsers {
private static final String TAG = "AtomParsers";
private static final int TYPE_vide = Util.getIntegerCodeForString("vide");
private static final int TYPE_soun = Util.getIntegerCodeForString("soun");
private static final int TYPE_text = Util.getIntegerCodeForString("text");
......@@ -248,11 +251,16 @@ import java.util.List;
remainingTimestampOffsetChanges--;
}
// Check all the expected samples have been seen.
Assertions.checkArgument(remainingSynchronizationSamples == 0);
Assertions.checkArgument(remainingSamplesAtTimestampDelta == 0);
Assertions.checkArgument(remainingSamplesInChunk == 0);
Assertions.checkArgument(remainingTimestampDeltaChanges == 0);
// If the stbl's child boxes are not consistent the container is malformed, but the stream may
// still be playable.
if (remainingSynchronizationSamples != 0 || remainingSamplesAtTimestampDelta != 0
|| remainingSamplesInChunk != 0 || remainingTimestampDeltaChanges != 0) {
Log.w(TAG, "Inconsistent stbl box for track " + track.id
+ ": remainingSynchronizationSamples " + remainingSynchronizationSamples
+ ", remainingSamplesAtTimestampDelta " + remainingSamplesAtTimestampDelta
+ ", remainingSamplesInChunk " + remainingSamplesInChunk
+ ", remainingTimestampDeltaChanges " + remainingTimestampDeltaChanges);
}
} else {
long[] chunkOffsetsBytes = new long[chunkIterator.length];
int[] chunkSampleCounts = new int[chunkIterator.length];
......@@ -636,7 +644,7 @@ import java.util.List;
0 /* subsample timing is absolute */);
} else if (childAtomType == Atom.TYPE_c608) {
out.format = Format.createTextSampleFormat(Integer.toString(trackId),
MimeTypes.APPLICATION_EIA608, null, Format.NO_VALUE, 0, language, drmInitData);
MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, language, drmInitData);
out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;
}
stsd.setPosition(childStartPosition + childAtomSize);
......@@ -665,6 +673,7 @@ import java.util.List;
List<byte[]> initializationData = null;
String mimeType = null;
byte[] projectionData = null;
@C.StereoMode
int stereoMode = Format.NO_VALUE;
while (childPosition - position < size) {
parent.setPosition(childPosition);
......@@ -889,7 +898,7 @@ import java.util.List;
if (out.format == null && mimeType != null) {
// TODO: Determine the correct PCM encoding.
int pcmEncoding =
@C.PcmEncoding int pcmEncoding =
MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE;
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
......@@ -1169,6 +1178,7 @@ import java.util.List;
public Format format;
public int nalUnitLengthFieldLength;
@Track.Transformation
public int requiredSampleTransformation;
public StsdData(int numberOfEntries) {
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
import android.support.annotation.IntDef;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
......@@ -38,6 +39,8 @@ import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
......@@ -65,6 +68,13 @@ public final class FragmentedMp4Extractor implements Extractor {
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
/**
* Flags controlling the behavior of the extractor.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_SIDELOADED})
public @interface Flags {}
/**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
* The workaround overrides the sync frame flags in the stream, forcing them to false except for
* the first sample in each segment.
......@@ -72,12 +82,10 @@ public final class FragmentedMp4Extractor implements Extractor {
* This flag does nothing if the stream is not a video stream.
*/
public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
/**
* Flag to ignore any tfdt boxes in the stream.
*/
public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2;
/**
* Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
* container.
......@@ -95,6 +103,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static final int STATE_READING_SAMPLE_CONTINUE = 4;
// Workarounds.
@Flags
private final int flags;
private final Track sideloadedTrack;
......@@ -135,18 +144,18 @@ public final class FragmentedMp4Extractor implements Extractor {
}
/**
* @param flags Flags to allow parsing of faulty streams.
* @param flags Flags that control the extractor's behavior.
*/
public FragmentedMp4Extractor(int flags) {
public FragmentedMp4Extractor(@Flags int flags) {
this(flags, null);
}
/**
* @param flags Flags to allow parsing of faulty streams.
* @param flags Flags that control the extractor's behavior.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor
* will not receive a moov box in the input data.
*/
public FragmentedMp4Extractor(int flags, Track sideloadedTrack) {
public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack) {
this.sideloadedTrack = sideloadedTrack;
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
......@@ -422,7 +431,7 @@ public final class FragmentedMp4Extractor implements Extractor {
}
private static void parseMoof(ContainerAtom moof, SparseArray<TrackBundle> trackBundleArray,
int flags, byte[] extendedTypeScratch) throws ParserException {
@Flags int flags, byte[] extendedTypeScratch) throws ParserException {
int moofContainerChildrenSize = moof.containerChildren.size();
for (int i = 0; i < moofContainerChildrenSize; i++) {
Atom.ContainerAtom child = moof.containerChildren.get(i);
......@@ -437,7 +446,7 @@ public final class FragmentedMp4Extractor implements Extractor {
* Parses a traf atom (defined in 14496-12).
*/
private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
int flags, byte[] extendedTypeScratch) throws ParserException {
@Flags int flags, byte[] extendedTypeScratch) throws ParserException {
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags);
if (trackBundle == null) {
......@@ -488,7 +497,7 @@ public final class FragmentedMp4Extractor implements Extractor {
}
private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime,
int flags) {
@Flags int flags) {
int trunCount = 0;
int totalSampleCount = 0;
List<LeafAtom> leafChildren = traf.leafChildren;
......@@ -643,8 +652,8 @@ public final class FragmentedMp4Extractor implements Extractor {
* @param trun The trun atom to decode.
* @return The starting position of samples for the next run.
*/
private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime, int flags,
ParsableByteArray trun, int trackRunStart) {
private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime,
@Flags int flags, ParsableByteArray trun, int trackRunStart) {
trun.setPosition(Atom.HEADER_SIZE);
int fullAtom = trun.readInt();
int atomFlags = Atom.parseFullAtomFlags(fullAtom);
......@@ -994,7 +1003,7 @@ public final class FragmentedMp4Extractor implements Extractor {
}
long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;
int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0)
@C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0)
| (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0);
int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
byte[] encryptionKey = null;
......
......@@ -100,13 +100,14 @@ import java.io.IOException;
while (bytesSearched < bytesToSearch) {
// Read an atom header.
int headerSize = Atom.HEADER_SIZE;
buffer.reset(headerSize);
input.peekFully(buffer.data, 0, headerSize);
buffer.setPosition(0);
long atomSize = buffer.readUnsignedInt();
int atomType = buffer.readInt();
if (atomSize == Atom.LONG_SIZE_PREFIX) {
headerSize = Atom.LONG_HEADER_SIZE;
input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
buffer.setLimit(Atom.LONG_HEADER_SIZE);
atomSize = buffer.readUnsignedLongToLong();
}
......@@ -139,9 +140,7 @@ import java.io.IOException;
if (atomDataSize < 8) {
return false;
}
if (buffer.capacity() < atomDataSize) {
buffer.reset(new byte[atomDataSize], atomDataSize);
}
buffer.reset(atomDataSize);
input.peekFully(buffer.data, 0, atomDataSize);
int brandsCount = atomDataSize / 4;
for (int i = 0; i < brandsCount; i++) {
......
......@@ -15,8 +15,11 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Encapsulates information describing an MP4 track.
......@@ -24,6 +27,12 @@ import com.google.android.exoplayer2.Format;
public final class Track {
/**
* The transformation to apply to samples in the track, if any.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({TRANSFORMATION_NONE, TRANSFORMATION_CEA608_CDAT})
public @interface Transformation {}
/**
* A no-op sample transformation.
*/
public static final int TRANSFORMATION_NONE = 0;
......@@ -66,6 +75,7 @@ public final class Track {
* One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each
* sample.
*/
@Transformation
public final int sampleTransformation;
/**
......@@ -90,7 +100,7 @@ public final class Track {
public final int nalUnitLengthFieldLength;
public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
Format format, int sampleTransformation,
Format format, @Transformation int sampleTransformation,
TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
long[] editListDurations, long[] editListMediaTimes) {
this.id = id;
......
......@@ -49,7 +49,8 @@ import com.google.android.exoplayer2.util.Util;
*/
public final int[] flags;
TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs, int[] flags) {
public TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs,
int[] flags) {
Assertions.checkArgument(sizes.length == timestampsUs.length);
Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length);
......
......@@ -30,7 +30,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
* Extracts EIA-608 data from a RawCC file
* Extracts CEA data from a RawCC file.
*/
public final class RawCcExtractor implements Extractor {
......@@ -68,7 +68,7 @@ public final class RawCcExtractor implements Extractor {
trackOutput = extractorOutput.track(0);
extractorOutput.endTracks();
trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_EIA608,
trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
null, Format.NO_VALUE, 0, null, null));
}
......@@ -154,13 +154,8 @@ public final class RawCcExtractor implements Extractor {
dataScratch.reset();
input.readFully(dataScratch.data, 0, 3);
// only accept EIA-608 packets which have validity (6th bit) == 1 and
// type (7-8th bits) == 0; i.e. ccDataPkt[0] == 0bXXXXX100
int ccValidityAndType = dataScratch.readUnsignedByte() & 0x07;
if (ccValidityAndType == 0x04) {
trackOutput.sampleData(dataScratch, 2);
sampleBytesWritten += 2;
}
trackOutput.sampleData(dataScratch, 3);
sampleBytesWritten += 3;
}
if (sampleBytesWritten > 0) {
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.ts;
import android.support.annotation.IntDef;
import android.util.SparseBooleanArray;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Default implementation for {@link ElementaryStreamReader.Factory}.
*/
public final class DefaultStreamReaderFactory implements ElementaryStreamReader.Factory {
/**
* Flags controlling what workarounds are enabled for elementary stream readers.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {WORKAROUND_ALLOW_NON_IDR_KEYFRAMES, WORKAROUND_IGNORE_AAC_STREAM,
WORKAROUND_IGNORE_H264_STREAM, WORKAROUND_DETECT_ACCESS_UNITS, WORKAROUND_MAP_BY_TYPE})
public @interface WorkaroundFlags {
}
public static final int WORKAROUND_ALLOW_NON_IDR_KEYFRAMES = 1;
public static final int WORKAROUND_IGNORE_AAC_STREAM = 2;
public static final int WORKAROUND_IGNORE_H264_STREAM = 4;
public static final int WORKAROUND_DETECT_ACCESS_UNITS = 8;
public static final int WORKAROUND_MAP_BY_TYPE = 16;
private static final int BASE_EMBEDDED_TRACK_ID = 0x2000; // 0xFF + 1.
private final SparseBooleanArray trackIds;
@WorkaroundFlags
private final int workaroundFlags;
private Id3Reader id3Reader;
private int nextEmbeddedTrackId = BASE_EMBEDDED_TRACK_ID;
public DefaultStreamReaderFactory() {
this(0);
}
public DefaultStreamReaderFactory(int workaroundFlags) {
trackIds = new SparseBooleanArray();
this.workaroundFlags = workaroundFlags;
}
@Override
public ElementaryStreamReader onPmtEntry(int pid, int streamType,
ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) {
if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0 && id3Reader == null) {
// Setup an ID3 track regardless of whether there's a corresponding entry, in case one
// appears intermittently during playback. See b/20261500.
id3Reader = new Id3Reader(output.track(TsExtractor.TS_STREAM_TYPE_ID3));
}
int trackId = (workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0 ? streamType : pid;
if (trackIds.get(trackId)) {
return null;
}
trackIds.put(trackId, true);
switch (streamType) {
case TsExtractor.TS_STREAM_TYPE_MPA:
case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
return new MpegAudioReader(output.track(trackId), esInfo.language);
case TsExtractor.TS_STREAM_TYPE_AAC:
return (workaroundFlags & WORKAROUND_IGNORE_AAC_STREAM) != 0 ? null
: new AdtsReader(output.track(trackId), new DummyTrackOutput(), esInfo.language);
case TsExtractor.TS_STREAM_TYPE_AC3:
case TsExtractor.TS_STREAM_TYPE_E_AC3:
return new Ac3Reader(output.track(trackId), esInfo.language);
case TsExtractor.TS_STREAM_TYPE_DTS:
case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
return new DtsReader(output.track(trackId), esInfo.language);
case TsExtractor.TS_STREAM_TYPE_H262:
return new H262Reader(output.track(trackId));
case TsExtractor.TS_STREAM_TYPE_H264:
return (workaroundFlags & WORKAROUND_IGNORE_H264_STREAM) != 0
? null : new H264Reader(output.track(trackId),
new SeiReader(output.track(nextEmbeddedTrackId++)),
(workaroundFlags & WORKAROUND_ALLOW_NON_IDR_KEYFRAMES) != 0,
(workaroundFlags & WORKAROUND_DETECT_ACCESS_UNITS) != 0);
case TsExtractor.TS_STREAM_TYPE_H265:
return new H265Reader(output.track(trackId),
new SeiReader(output.track(nextEmbeddedTrackId++)));
case TsExtractor.TS_STREAM_TYPE_ID3:
if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0) {
return id3Reader;
} else {
return new Id3Reader(output.track(nextEmbeddedTrackId++));
}
default:
return null;
}
}
}
......@@ -15,13 +15,60 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
/**
* Extracts individual samples from an elementary media stream, preserving original order.
*/
/* package */ abstract class ElementaryStreamReader {
public abstract class ElementaryStreamReader {
/**
* Factory of {@link ElementaryStreamReader} instances.
*/
public interface Factory {
/**
* Returns an {@link ElementaryStreamReader} for a given PMT entry. May return null if the
* stream type is not supported or if the stream already has a reader assigned to it.
*
* @param pid The pid for the PMT entry.
* @param streamType One of the {@link TsExtractor}{@code .TS_STREAM_TYPE_*} constants defining
* the type of the stream.
* @param esInfo The descriptor information linked to the elementary stream.
* @param output The {@link ExtractorOutput} that provides the {@link TrackOutput}s for the
* created readers.
* @return An {@link ElementaryStreamReader} for the elementary streams carried by the provided
* pid. {@code null} if the stream is not supported or if it should be ignored.
*/
ElementaryStreamReader onPmtEntry(int pid, int streamType, EsInfo esInfo,
ExtractorOutput output);
}
/**
* Holds descriptor information associated with an elementary stream.
*/
public static final class EsInfo {
public final int streamType;
public String language;
public byte[] descriptorBytes;
/**
* @param streamType The type of the stream as defined by the
* {@link TsExtractor}{@code .TS_STREAM_TYPE_*}.
* @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
* @param descriptorBytes The descriptor bytes associated to the stream.
*/
public EsInfo(int streamType, String language, byte[] descriptorBytes) {
this.streamType = streamType;
this.language = language;
this.descriptorBytes = descriptorBytes;
}
}
protected final TrackOutput output;
......
......@@ -128,7 +128,7 @@ import java.util.Collections;
if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) {
int bytesWrittenPastStartCode = limit - startCodeOffset;
if (foundFirstFrameInGroup) {
int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
@C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode;
output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null);
isKeyframe = false;
......@@ -136,7 +136,7 @@ import java.util.Collections;
if (startCodeValue == START_GROUP) {
foundFirstFrameInGroup = false;
isKeyframe = true;
} else /* startCode == START_PICTURE */ {
} else /* startCodeValue == START_PICTURE */ {
frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs);
framePosition = totalBytesWritten - bytesWrittenPastStartCode;
pesPtsUsAvailable = false;
......
......@@ -57,7 +57,7 @@ import java.util.List;
/**
* @param output A {@link TrackOutput} to which H.264 samples should be written.
* @param seiReader A reader for EIA-608 samples in SEI NAL units.
* @param seiReader A reader for CEA-608 samples in SEI NAL units.
* @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as
* synchronization samples (key-frames).
* @param detectAccessUnits Whether to split the input stream into access units (samples) based on
......@@ -420,7 +420,7 @@ import java.util.List;
}
private void outputSample(int offset) {
int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
@C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
int size = (int) (nalUnitStartPosition - samplePosition);
output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
}
......
......@@ -64,7 +64,7 @@ import java.util.Collections;
/**
* @param output A {@link TrackOutput} to which H.265 samples should be written.
* @param seiReader A reader for EIA-608 samples in SEI NAL units.
* @param seiReader A reader for CEA-608 samples in SEI NAL units.
*/
public H265Reader(TrackOutput output, SeiReader seiReader) {
super(output);
......@@ -471,7 +471,7 @@ import java.util.Collections;
}
private void outputSample(int offset) {
int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
@C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
int size = (int) (nalUnitStartPosition - samplePosition);
output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
}
......
......@@ -153,8 +153,7 @@ public final class PsExtractor implements Extractor {
input.peekFully(psPacketBuffer.data, 0, 10);
// We only care about the pack_stuffing_length in here, skip the first 77 bits.
psPacketBuffer.setPosition(0);
psPacketBuffer.skipBytes(9);
psPacketBuffer.setPosition(9);
// Last 3 bits is the length.
int packStuffingLength = psPacketBuffer.readUnsignedByte() & 0x07;
......@@ -209,7 +208,7 @@ public final class PsExtractor implements Extractor {
}
}
// The next 2 bytes are the length, once we have that we can consume the complete packet.
// The next 2 bytes are the length. Once we have that we can consume the complete packet.
input.peekFully(psPacketBuffer.data, 0, 2);
psPacketBuffer.setPosition(0);
int payloadLength = psPacketBuffer.readUnsignedShort();
......@@ -219,14 +218,10 @@ public final class PsExtractor implements Extractor {
// Just skip this data.
input.skipFully(pesLength);
} else {
if (psPacketBuffer.capacity() < pesLength) {
// Reallocate for this and future packets.
psPacketBuffer.reset(new byte[pesLength], pesLength);
}
psPacketBuffer.reset(pesLength);
// Read the whole packet and the header for consumption.
input.readFully(psPacketBuffer.data, 0, pesLength);
psPacketBuffer.setPosition(6);
psPacketBuffer.setLimit(pesLength);
payloadReader.consume(psPacketBuffer);
psPacketBuffer.setLimit(psPacketBuffer.capacity());
}
......
......@@ -18,12 +18,12 @@ package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.text.eia608.Eia608Decoder;
import com.google.android.exoplayer2.text.cea.Cea608Decoder;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
/**
* Consumes SEI buffers, outputting contained EIA608 messages to a {@link TrackOutput}.
* Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}.
*/
/* package */ final class SeiReader {
......@@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
public SeiReader(TrackOutput output) {
this.output = output;
output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_EIA608, null,
output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null,
Format.NO_VALUE, 0, null, null));
}
......@@ -51,7 +51,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
payloadSize += b;
} while (b == 0xFF);
// Process the payload.
if (Eia608Decoder.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) {
if (Cea608Decoder.isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) {
// Ignore country_code (1) + provider_code (2) + user_identifier (4)
// + user_data_type_code (1).
seiBuffer.skipBytes(8);
......@@ -60,13 +60,13 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
seiBuffer.skipBytes(1);
int sampleBytes = 0;
for (int i = 0; i < ccCount; i++) {
int ccValidityAndType = seiBuffer.readUnsignedByte() & 0x07;
// Check that validity == 1 and type == 0.
int ccValidityAndType = seiBuffer.peekUnsignedByte() & 0x07;
// Check that validity == 1 and type == 0 (i.e. NTSC_CC_FIELD_1).
if (ccValidityAndType != 0x04) {
seiBuffer.skipBytes(2);
seiBuffer.skipBytes(3);
} else {
sampleBytes += 2;
output.sampleData(seiBuffer, 2);
sampleBytes += 3;
output.sampleData(seiBuffer, 3);
}
}
output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleBytes, 0, null);
......
......@@ -31,6 +31,7 @@ import com.google.android.exoplayer2.C;
/** Bits per sample for the audio data. */
private final int bitsPerSample;
/** The PCM encoding */
@C.PcmEncoding
private final int encoding;
/** Offset to the start of sample data. */
......@@ -39,7 +40,7 @@ import com.google.android.exoplayer2.C;
private long dataSize;
public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment,
int bitsPerSample, int encoding) {
int bitsPerSample, @C.PcmEncoding int encoding) {
this.numChannels = numChannels;
this.sampleRateHz = sampleRateHz;
this.averageBytesPerSecond = averageBytesPerSecond;
......@@ -99,6 +100,7 @@ import com.google.android.exoplayer2.C;
}
/** Returns the PCM encoding. **/
@C.PcmEncoding
public int getEncoding() {
return encoding;
}
......
......@@ -87,7 +87,7 @@ import java.io.IOException;
+ blockAlignment);
}
int encoding = Util.getPcmEncoding(bitsPerSample);
@C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample);
if (encoding == C.ENCODING_INVALID) {
Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample);
return null;
......
......@@ -293,7 +293,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
MediaCrypto mediaCrypto = null;
boolean drmSessionRequiresSecureDecoder = false;
if (drmSession != null) {
int drmSessionState = drmSession.getState();
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState == DrmSession.STATE_OPENED
......@@ -682,7 +682,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
if (drmSession == null) {
return false;
}
int drmSessionState = drmSession.getState();
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
......
......@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
......@@ -79,12 +78,11 @@ public final class ConcatenatingMediaSource implements MediaSource {
}
@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
long positionUs) {
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
int sourceIndex = timeline.getSourceIndexForPeriod(index);
int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex);
MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, callback,
allocator, positionUs);
MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator,
positionUs);
sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex);
return mediaPeriod;
}
......
......@@ -61,12 +61,15 @@ import java.util.Arrays;
private final Handler eventHandler;
private final ExtractorMediaSource.EventListener eventListener;
private final MediaSource.Listener sourceListener;
private final Callback callback;
private final Allocator allocator;
private final Loader loader;
private final ExtractorHolder extractorHolder;
private final ConditionVariable loadCondition;
private final Runnable maybeFinishPrepareRunnable;
private final Runnable onContinueLoadingRequestedRunnable;
private final Handler handler;
private Callback callback;
private SeekMap seekMap;
private boolean tracksBuilt;
private boolean prepared;
......@@ -85,6 +88,7 @@ import java.util.Arrays;
private int extractedSamplesCountAtStartOfLoad;
private boolean loadingFinished;
private boolean released;
/**
* @param uri The {@link Uri} of the media stream.
......@@ -94,30 +98,41 @@ import java.util.Arrays;
* @param eventHandler A handler for events. May be null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param sourceListener A listener to notify when the timeline has been loaded.
* @param callback A callback to receive updates from the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
*/
public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors,
int minLoadableRetryCount, Handler eventHandler,
ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener,
Callback callback, Allocator allocator) {
Allocator allocator) {
this.uri = uri;
this.dataSource = dataSource;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.sourceListener = sourceListener;
this.callback = callback;
this.allocator = allocator;
loader = new Loader("Loader:ExtractorMediaPeriod");
extractorHolder = new ExtractorHolder(extractors, this);
loadCondition = new ConditionVariable();
maybeFinishPrepareRunnable = new Runnable() {
@Override
public void run() {
maybeFinishPrepare();
}
};
onContinueLoadingRequestedRunnable = new Runnable() {
@Override
public void run() {
if (!released) {
callback.onContinueLoadingRequested(ExtractorMediaPeriod.this);
}
}
};
handler = new Handler();
pendingResetPositionUs = C.TIME_UNSET;
sampleQueues = new DefaultTrackOutput[0];
length = C.LENGTH_UNSET;
loadCondition.open();
startLoading();
}
public void release() {
......@@ -126,11 +141,20 @@ import java.util.Arrays;
@Override
public void run() {
extractorHolder.release();
for (DefaultTrackOutput sampleQueue : sampleQueues) {
sampleQueue.disable();
}
}
});
for (DefaultTrackOutput sampleQueue : sampleQueues) {
sampleQueue.disable();
}
handler.removeCallbacksAndMessages(null);
released = true;
}
@Override
public void prepare(Callback callback) {
this.callback = callback;
loadCondition.open();
startLoading();
}
@Override
......@@ -330,7 +354,7 @@ import java.util.Arrays;
return madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY;
}
// ExtractorOutput implementation.
// ExtractorOutput implementation. Called by the loading thread.
@Override
public TrackOutput track(int id) {
......@@ -344,26 +368,26 @@ import java.util.Arrays;
@Override
public void endTracks() {
tracksBuilt = true;
maybeFinishPrepare();
handler.post(maybeFinishPrepareRunnable);
}
@Override
public void seekMap(SeekMap seekMap) {
this.seekMap = seekMap;
maybeFinishPrepare();
handler.post(maybeFinishPrepareRunnable);
}
// UpstreamFormatChangedListener implementation
// UpstreamFormatChangedListener implementation. Called by the loading thread.
@Override
public void onUpstreamFormatChanged(Format format) {
maybeFinishPrepare();
handler.post(maybeFinishPrepareRunnable);
}
// Internal methods.
private void maybeFinishPrepare() {
if (prepared || seekMap == null || !tracksBuilt) {
if (released || prepared || seekMap == null || !tracksBuilt) {
return;
}
for (DefaultTrackOutput sampleQueue : sampleQueues) {
......@@ -576,7 +600,7 @@ import java.util.Arrays;
if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) {
position = input.getPosition();
loadCondition.close();
callback.onContinueLoadingRequested(ExtractorMediaPeriod.this);
handler.post(onContinueLoadingRequestedRunnable);
}
}
} finally {
......
......@@ -23,7 +23,6 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions;
......@@ -148,12 +147,11 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
}
@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
long positionUs) {
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
Assertions.checkArgument(index == 0);
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
this, callback, allocator);
this, allocator);
}
@Override
......
......@@ -19,7 +19,6 @@ import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
......@@ -76,9 +75,8 @@ public final class LoopingMediaSource implements MediaSource {
}
@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
long positionUs) {
return childSource.createPeriod(index % childPeriodCount, callback, allocator, positionUs);
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
return childSource.createPeriod(index % childPeriodCount, allocator, positionUs);
}
@Override
......
......@@ -32,7 +32,7 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Called when preparation completes.
* <p>
* May be called from any thread. After invoking this method, the {@link MediaPeriod} can expect
* Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect
* for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be
* called with the initial track selection.
*
......@@ -43,6 +43,17 @@ public interface MediaPeriod extends SequenceableLoader {
}
/**
* Prepares this media period asynchronously.
* <p>
* {@code callback.onPrepared} is called when preparation completes. If preparation fails,
* {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
*
* @param callback Callback to receive updates from this period, including being notified when
* preparation completes.
*/
void prepare(Callback callback);
/**
* Throws an error that's preventing the period from becoming prepared. Does nothing if no such
* error exists.
* <p>
......
......@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
......@@ -54,18 +53,13 @@ public interface MediaSource {
/**
* Returns a {@link MediaPeriod} corresponding to the period at the specified index.
* <p>
* {@link Callback#onPrepared(MediaPeriod)} is called when the new period is prepared. If
* preparation fails, {@link MediaPeriod#maybeThrowPrepareError()} will throw an
* {@link IOException} if called on the returned instance.
*
* @param index The index of the period.
* @param callback A callback to receive updates from the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param positionUs The player's current playback position.
* @return A new {@link MediaPeriod}.
*/
MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, long positionUs);
MediaPeriod createPeriod(int index, Allocator allocator, long positionUs);
/**
* Releases the period.
......
......@@ -28,20 +28,27 @@ import java.util.IdentityHashMap;
public final MediaPeriod[] periods;
private final Callback callback;
private final IdentityHashMap<SampleStream, Integer> streamPeriodIndices;
private Callback callback;
private int pendingChildPrepareCount;
private TrackGroupArray trackGroups;
private MediaPeriod[] enabledPeriods;
private SequenceableLoader sequenceableLoader;
public MergingMediaPeriod(Callback callback, MediaPeriod... periods) {
public MergingMediaPeriod(MediaPeriod... periods) {
this.periods = periods;
this.callback = callback;
streamPeriodIndices = new IdentityHashMap<>();
}
@Override
public void prepare(Callback callback) {
this.callback = callback;
pendingChildPrepareCount = periods.length;
for (MediaPeriod period : periods) {
period.prepare(this);
}
}
@Override
......
......@@ -15,11 +15,12 @@
*/
package com.google.android.exoplayer2.source;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -37,10 +38,15 @@ public final class MergingMediaSource implements MediaSource {
public static final class IllegalMergeException extends IOException {
/**
* The reason the merge failed.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH})
public @interface Reason {}
/**
* The merge failed because one of the sources being merged has a dynamic window.
*/
public static final int REASON_WINDOWS_ARE_DYNAMIC = 0;
/**
* The merge failed because the sources have different period counts.
*/
......@@ -50,13 +56,14 @@ public final class MergingMediaSource implements MediaSource {
* The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and
* {@link #REASON_PERIOD_COUNT_MISMATCH}.
*/
@Reason
public final int reason;
/**
* @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and
* {@link #REASON_PERIOD_COUNT_MISMATCH}.
*/
public IllegalMergeException(int reason) {
public IllegalMergeException(@Reason int reason) {
this.reason = reason;
}
......@@ -109,16 +116,12 @@ public final class MergingMediaSource implements MediaSource {
}
@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
long positionUs) {
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
// The periods are only referenced after they have all been prepared.
MergingMediaPeriod mergingPeriod = new MergingMediaPeriod(callback, periods);
for (int i = 0; i < periods.length; i++) {
periods[i] = mediaSources[i].createPeriod(index, mergingPeriod, allocator, positionUs);
Assertions.checkState(periods[i] != null, "Child source must not return null period");
periods[i] = mediaSources[i].createPeriod(index, allocator, positionUs);
}
return mergingPeriod;
return new MergingMediaPeriod(periods);
}
@Override
......
......@@ -29,7 +29,7 @@ public interface SequenceableLoader {
/**
* Called by the loader to indicate that it wishes for its {@link #continueLoading(long)} method
* to be called when it can continue to load data.
* to be called when it can continue to load data. Called on the playback thread.
*/
void onContinueLoadingRequested(T source);
......
......@@ -79,6 +79,11 @@ import java.util.Arrays;
}
@Override
public void prepare(Callback callback) {
callback.onPrepared(this);
}
@Override
public void maybeThrowPrepareError() throws IOException {
loader.maybeThrowError();
}
......
......@@ -19,7 +19,6 @@ import android.net.Uri;
import android.os.Handler;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions;
......@@ -95,13 +94,10 @@ public final class SingleSampleMediaSource implements MediaSource {
}
@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
long positionUs) {
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
Assertions.checkArgument(index == 0);
MediaPeriod mediaPeriod = new SingleSampleMediaPeriod(uri, dataSourceFactory, format,
minLoadableRetryCount, eventHandler, eventListener, eventSourceId);
callback.onPrepared(mediaPeriod);
return mediaPeriod;
return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount,
eventHandler, eventListener, eventSourceId);
}
@Override
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.chunk;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.extractor.Extractor;
......@@ -150,7 +151,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
byte[] encryptionKey) {
trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
}
......
......@@ -111,7 +111,8 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
byte[] encryptionKey) {
throw new IllegalStateException("Unexpected sample data in initialization chunk");
}
......
......@@ -48,10 +48,10 @@ import java.util.List;
private final EventDispatcher eventDispatcher;
private final long elapsedRealtimeOffset;
private final LoaderErrorThrower manifestLoaderErrorThrower;
private final Callback callback;
private final Allocator allocator;
private final TrackGroupArray trackGroups;
private Callback callback;
private ChunkSampleStream<DashChunkSource>[] sampleStreams;
private CompositeSequenceableLoader sequenceableLoader;
private DashManifest manifest;
......@@ -61,7 +61,7 @@ import java.util.List;
public DashMediaPeriod(int id, DashManifest manifest, int index,
DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
EventDispatcher eventDispatcher, long elapsedRealtimeOffset,
LoaderErrorThrower manifestLoaderErrorThrower, Callback callback, Allocator allocator) {
LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) {
this.id = id;
this.manifest = manifest;
this.index = index;
......@@ -70,13 +70,11 @@ import java.util.List;
this.eventDispatcher = eventDispatcher;
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.callback = callback;
this.allocator = allocator;
sampleStreams = newSampleStreamArray(0);
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
period = manifest.getPeriod(index);
trackGroups = buildTrackGroups(period);
callback.onPrepared(this);
}
public void updateManifest(DashManifest manifest, int index) {
......@@ -98,6 +96,12 @@ import java.util.List;
}
@Override
public void prepare(Callback callback) {
this.callback = callback;
callback.onPrepared(this);
}
@Override
public void maybeThrowPrepareError() throws IOException {
manifestLoaderErrorThrower.maybeThrowError();
}
......
......@@ -26,7 +26,6 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
......@@ -171,11 +170,10 @@ public final class DashMediaSource implements MediaSource {
}
@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
long positionUs) {
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + index, manifest, index,
chunkSourceFactory, minLoadableRetryCount, eventDispatcher, elapsedRealtimeOffsetMs, loader,
callback, allocator);
allocator);
periodsById.put(mediaPeriod.id, mediaPeriod);
return mediaPeriod;
}
......
......@@ -654,9 +654,10 @@ public class DashManifestParser extends DefaultHandler
} else if (MimeTypes.isVideo(containerMimeType)) {
return MimeTypes.getVideoMediaMimeType(codecs);
} else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
// We currently only support EIA-608 through RawCC
if (codecs != null && codecs.contains("eia608")) {
return MimeTypes.APPLICATION_EIA608;
// We currently only support CEA-608 through RawCC
if (codecs != null
&& (codecs.contains("eia608") || codecs.contains("cea608"))) {
return MimeTypes.APPLICATION_CEA608;
}
return null;
} else if (mimeTypeIsRawText(containerMimeType)) {
......
......@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.DefaultStreamReaderFactory;
import com.google.android.exoplayer2.extractor.ts.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
......@@ -355,20 +356,22 @@ import java.util.Locale;
timestampAdjuster = timestampAdjusterProvider.getAdjuster(segment.discontinuitySequenceNumber,
startTimeUs);
// This flag ensures the change of pid between streams does not affect the sample queues.
int workaroundFlags = TsExtractor.WORKAROUND_MAP_BY_TYPE;
@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 |= TsExtractor.WORKAROUND_IGNORE_AAC_STREAM;
workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_AAC_STREAM;
}
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM;
workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_H264_STREAM;
}
}
extractor = new TsExtractor(timestampAdjuster, workaroundFlags);
extractor = new TsExtractor(timestampAdjuster,
new DefaultStreamReaderFactory(workaroundFlags));
} else {
// MPEG-2 TS segments, and we need to continue using the same extractor.
extractor = previous.extractor;
......
......@@ -50,11 +50,11 @@ import java.util.List;
/* package */ final class HlsMediaPeriod implements MediaPeriod,
Loader.Callback<ParsingLoadable<HlsPlaylist>>, HlsSampleStreamWrapper.Callback {
private final Uri manifestUri;
private final DataSource.Factory dataSourceFactory;
private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher;
private final MediaSource.Listener sourceListener;
private final Callback callback;
private final Allocator allocator;
private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
private final TimestampAdjusterProvider timestampAdjusterProvider;
......@@ -62,7 +62,9 @@ import java.util.List;
private final Handler continueLoadingHandler;
private final Loader manifestFetcher;
private final long preparePositionUs;
private final Runnable continueLoadingRunnable;
private Callback callback;
private int pendingPrepareCount;
private HlsPlaylist playlist;
private boolean seenFirstTrackSelection;
......@@ -72,17 +74,16 @@ import java.util.List;
private HlsSampleStreamWrapper[] sampleStreamWrappers;
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
private CompositeSequenceableLoader sequenceableLoader;
private Runnable continueLoadingRunnable;
public HlsMediaPeriod(Uri manifestUri, DataSource.Factory dataSourceFactory,
int minLoadableRetryCount, EventDispatcher eventDispatcher,
MediaSource.Listener sourceListener, final Callback callback, Allocator allocator,
MediaSource.Listener sourceListener, Allocator allocator,
long positionUs) {
this.manifestUri = manifestUri;
this.dataSourceFactory = dataSourceFactory;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventDispatcher = eventDispatcher;
this.sourceListener = sourceListener;
this.callback = callback;
this.allocator = allocator;
streamWrapperIndices = new IdentityHashMap<>();
timestampAdjusterProvider = new TimestampAdjusterProvider();
......@@ -96,11 +97,6 @@ import java.util.List;
callback.onContinueLoadingRequested(HlsMediaPeriod.this);
}
};
ParsingLoadable<HlsPlaylist> loadable = new ParsingLoadable<>(
dataSourceFactory.createDataSource(), manifestUri, C.DATA_TYPE_MANIFEST, manifestParser);
long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, minLoadableRetryCount);
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
}
public void release() {
......@@ -112,6 +108,15 @@ import java.util.List;
}
@Override
public void prepare(Callback callback) {
this.callback = callback;
ParsingLoadable<HlsPlaylist> loadable = new ParsingLoadable<>(
dataSourceFactory.createDataSource(), manifestUri, C.DATA_TYPE_MANIFEST, manifestParser);
long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, minLoadableRetryCount);
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (sampleStreamWrappers == null) {
manifestFetcher.maybeThrowError();
......@@ -239,13 +244,7 @@ import java.util.List;
eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
playlist = loadable.getResult();
List<HlsSampleStreamWrapper> sampleStreamWrapperList = buildSampleStreamWrappers();
sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()];
sampleStreamWrapperList.toArray(sampleStreamWrappers);
pendingPrepareCount = sampleStreamWrappers.length;
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.prepare();
}
buildAndPrepareSampleStreamWrappers();
}
@Override
......@@ -313,16 +312,16 @@ import java.util.List;
// Internal methods.
private List<HlsSampleStreamWrapper> buildSampleStreamWrappers() {
ArrayList<HlsSampleStreamWrapper> sampleStreamWrappers = new ArrayList<>();
private void buildAndPrepareSampleStreamWrappers() {
String baseUri = playlist.baseUri;
if (playlist instanceof HlsMediaPlaylist) {
HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[] {
HlsMasterPlaylist.HlsUrl.createMediaPlaylistHlsUrl(playlist.baseUri)};
sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants,
null, null));
return sampleStreamWrappers;
sampleStreamWrappers = new HlsSampleStreamWrapper[] {
buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)};
pendingPrepareCount = 1;
sampleStreamWrappers[0].prepare();
return;
}
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
......@@ -351,32 +350,37 @@ import java.util.List;
} else {
// Leave the enabled variants unchanged. They're likely either all video or all audio.
}
List<HlsMasterPlaylist.HlsUrl> audioVariants = masterPlaylist.audios;
List<HlsMasterPlaylist.HlsUrl> subtitleVariants = masterPlaylist.subtitles;
sampleStreamWrappers = new HlsSampleStreamWrapper[(selectedVariants.isEmpty() ? 0 : 1)
+ audioVariants.size() + subtitleVariants.size()];
int currentWrapperIndex = 0;
pendingPrepareCount = sampleStreamWrappers.length;
if (!selectedVariants.isEmpty()) {
HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()];
selectedVariants.toArray(variants);
sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants,
masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat));
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat);
sampleStreamWrapper.prepare();
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
}
// Build the audio stream wrapper if applicable.
List<HlsMasterPlaylist.HlsUrl> audioVariants = masterPlaylist.audios;
if (!audioVariants.isEmpty()) {
HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[audioVariants.size()];
audioVariants.toArray(variants);
sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null,
null));
// Build audio stream wrappers.
for (int i = 0; i < audioVariants.size(); i++) {
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO,
baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null);
sampleStreamWrapper.prepare();
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
}
// Build the text stream wrapper if applicable.
List<HlsMasterPlaylist.HlsUrl> subtitleVariants = masterPlaylist.subtitles;
if (!subtitleVariants.isEmpty()) {
HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[subtitleVariants.size()];
subtitleVariants.toArray(variants);
sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null,
null));
// Build subtitle stream wrappers.
for (int i = 0; i < subtitleVariants.size(); i++) {
HlsMasterPlaylist.HlsUrl url = subtitleVariants.get(i);
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT,
baseUri, new HlsMasterPlaylist.HlsUrl[] {url}, null, null);
sampleStreamWrapper.prepareSingleTrack(url.format);
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
}
return sampleStreamWrappers;
}
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, String baseUri,
......
......@@ -21,7 +21,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.upstream.Allocator;
......@@ -73,11 +72,10 @@ public final class HlsMediaSource implements MediaSource {
}
@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
long positionUs) {
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
Assertions.checkArgument(index == 0);
return new HlsMediaPeriod(manifestUri, dataSourceFactory, minLoadableRetryCount,
eventDispatcher, sourceListener, callback, allocator, positionUs);
eventDispatcher, sourceListener, allocator, positionUs);
}
@Override
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source.hls;
import android.os.Handler;
import android.text.TextUtils;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
......@@ -80,13 +82,15 @@ import java.util.LinkedList;
private final HlsChunkSource.HlsChunkHolder nextChunkHolder;
private final SparseArray<DefaultTrackOutput> sampleQueues;
private final LinkedList<HlsMediaChunk> mediaChunks;
private final Runnable maybeFinishPrepareRunnable;
private final Handler handler;
private volatile boolean sampleQueuesBuilt;
private boolean sampleQueuesBuilt;
private boolean prepared;
private int enabledTrackCount;
private Format downstreamTrackFormat;
private int upstreamChunkUid;
private boolean released;
// Tracks are complicated in HLS. See documentation of buildTracks for details.
// Indexed by track (as exposed by this source).
......@@ -129,6 +133,13 @@ import java.util.LinkedList;
nextChunkHolder = new HlsChunkSource.HlsChunkHolder();
sampleQueues = new SparseArray<>();
mediaChunks = new LinkedList<>();
maybeFinishPrepareRunnable = new Runnable() {
@Override
public void run() {
maybeFinishPrepare();
}
};
handler = new Handler();
lastSeekPositionUs = positionUs;
pendingResetPositionUs = positionUs;
}
......@@ -137,6 +148,15 @@ import java.util.LinkedList;
continueLoading(lastSeekPositionUs);
}
/**
* Prepares a sample stream wrapper for which the master playlist provides enough information to
* prepare.
*/
public void prepareSingleTrack(Format format) {
track(0).format(format);
endTracks();
}
public void maybeThrowPrepareError() throws IOException {
maybeThrowError();
}
......@@ -245,6 +265,8 @@ import java.util.LinkedList;
sampleQueues.valueAt(i).disable();
}
loader.release();
handler.removeCallbacksAndMessages(null);
released = true;
}
public long getLargestQueuedTimestampUs() {
......@@ -454,7 +476,7 @@ import java.util.LinkedList;
@Override
public void endTracks() {
sampleQueuesBuilt = true;
maybeFinishPrepare();
handler.post(maybeFinishPrepareRunnable);
}
@Override
......@@ -462,17 +484,17 @@ import java.util.LinkedList;
// Do nothing.
}
// UpstreamFormatChangedListener implementation.
// UpstreamFormatChangedListener implementation. Called by the loading thread.
@Override
public void onUpstreamFormatChanged(Format format) {
maybeFinishPrepare();
handler.post(maybeFinishPrepareRunnable);
}
// Internal methods.
private void maybeFinishPrepare() {
if (prepared || !sampleQueuesBuilt) {
if (released || prepared || !sampleQueuesBuilt) {
return;
}
int sampleQueueCount = sampleQueues.size();
......@@ -558,7 +580,7 @@ import java.util.LinkedList;
if (i == primaryExtractorTrackIndex) {
Format[] formats = new Format[chunkSourceTrackCount];
for (int j = 0; j < chunkSourceTrackCount; j++) {
formats[j] = getSampleFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat);
formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat);
}
trackGroups[i] = new TrackGroup(formats);
primaryTrackGroupIndex = i;
......@@ -567,11 +589,11 @@ import java.util.LinkedList;
if (primaryExtractorTrackType == PRIMARY_TYPE_VIDEO) {
if (MimeTypes.isAudio(sampleFormat.sampleMimeType)) {
trackFormat = muxedAudioFormat;
} else if (MimeTypes.APPLICATION_EIA608.equals(sampleFormat.sampleMimeType)) {
} else if (MimeTypes.APPLICATION_CEA608.equals(sampleFormat.sampleMimeType)) {
trackFormat = muxedCaptionFormat;
}
}
trackGroups[i] = new TrackGroup(getSampleFormat(trackFormat, sampleFormat));
trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat));
}
}
this.trackGroups = new TrackGroupArray(trackGroups);
......@@ -590,18 +612,25 @@ import java.util.LinkedList;
}
/**
* Derives a sample format corresponding to a given container format, by combining it with sample
* level information obtained from a second sample format.
* Derives a track format corresponding to a given container format, by combining it with sample
* level information obtained from the samples.
*
* @param containerFormat The container format for which the sample format should be derived.
* @param containerFormat The container format for which the track format should be derived.
* @param sampleFormat A sample format from which to obtain sample level information.
* @return The derived sample format.
* @return The derived track format.
*/
private static Format getSampleFormat(Format containerFormat, Format sampleFormat) {
private static Format deriveFormat(Format containerFormat, Format sampleFormat) {
if (containerFormat == null) {
return sampleFormat;
}
return sampleFormat.copyWithContainerInfo(containerFormat.id, containerFormat.bitrate,
String codecs = null;
int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
if (sampleTrackType == C.TRACK_TYPE_AUDIO) {
codecs = getAudioCodecs(containerFormat.codecs);
} else if (sampleTrackType == C.TRACK_TYPE_VIDEO) {
codecs = getVideoCodecs(containerFormat.codecs);
}
return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate,
containerFormat.width, containerFormat.height, containerFormat.selectionFlags,
containerFormat.language);
}
......@@ -614,4 +643,29 @@ import java.util.LinkedList;
return pendingResetPositionUs != C.TIME_UNSET;
}
private static String getAudioCodecs(String codecs) {
return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO);
}
private static String getVideoCodecs(String codecs) {
return getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO);
}
private static String getCodecsOfType(String codecs, int trackType) {
if (TextUtils.isEmpty(codecs)) {
return null;
}
String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)");
StringBuilder builder = new StringBuilder();
for (String codec : codecArray) {
if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
if (builder.length() > 0) {
builder.append(",");
}
builder.append(codec);
}
}
return builder.length() > 0 ? builder.toString() : null;
}
}
......@@ -15,18 +15,29 @@
*/
package com.google.android.exoplayer2.source.hls.playlist;
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Represents an HLS playlist.
*/
public abstract class HlsPlaylist {
/**
* The type of playlist.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_MASTER, TYPE_MEDIA})
public @interface Type {}
public static final int TYPE_MASTER = 0;
public static final int TYPE_MEDIA = 1;
public final String baseUri;
@Type
public final int type;
protected HlsPlaylist(String baseUri, int type) {
protected HlsPlaylist(String baseUri, @Type int type) {
this.baseUri = baseUri;
this.type = type;
}
......
......@@ -62,7 +62,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final String BOOLEAN_TRUE = "YES";
private static final String BOOLEAN_FALSE = "NO";
private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\"");
private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\"");
private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
......@@ -138,7 +137,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
while (iterator.hasNext()) {
line = iterator.next();
if (line.startsWith(TAG_MEDIA)) {
int selectionFlags = parseSelectionFlags(line);
@C.SelectionFlags int selectionFlags = parseSelectionFlags(line);
String uri = parseOptionalStringAttr(line, REGEX_URI);
String name = parseStringAttr(line, REGEX_NAME);
String language = parseOptionalStringAttr(line, REGEX_LANGUAGE);
......@@ -162,7 +161,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
case TYPE_CLOSED_CAPTIONS:
if ("CC1".equals(parseOptionalStringAttr(line, REGEX_INSTREAM_ID))) {
muxedCaptionFormat = Format.createTextContainerFormat(name,
MimeTypes.APPLICATION_M3U8, MimeTypes.APPLICATION_EIA608, null, Format.NO_VALUE,
MimeTypes.APPLICATION_M3U8, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE,
selectionFlags, language);
}
break;
......@@ -200,11 +199,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
muxedCaptionFormat);
}
@C.SelectionFlags
private static int parseSelectionFlags(String line) {
return (parseBooleanAttribute(line, REGEX_DEFAULT, false) ? Format.SELECTION_FLAG_DEFAULT : 0)
| (parseBooleanAttribute(line, REGEX_FORCED, false) ? Format.SELECTION_FLAG_FORCED : 0)
| (parseBooleanAttribute(line, REGEX_AUTOSELECT, false) ? Format.SELECTION_FLAG_AUTOSELECT
: 0);
return (parseBooleanAttribute(line, REGEX_DEFAULT, false) ? C.SELECTION_FLAG_DEFAULT : 0)
| (parseBooleanAttribute(line, REGEX_FORCED, false) ? C.SELECTION_FLAG_FORCED : 0)
| (parseBooleanAttribute(line, REGEX_AUTOSELECT, false) ? C.SELECTION_FLAG_AUTOSELECT : 0);
}
private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
......
......@@ -46,23 +46,22 @@ import java.util.ArrayList;
private final LoaderErrorThrower manifestLoaderErrorThrower;
private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher;
private final Callback callback;
private final Allocator allocator;
private final TrackGroupArray trackGroups;
private final TrackEncryptionBox[] trackEncryptionBoxes;
private Callback callback;
private SsManifest manifest;
private ChunkSampleStream<SsChunkSource>[] sampleStreams;
private CompositeSequenceableLoader sequenceableLoader;
public SsMediaPeriod(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory,
int minLoadableRetryCount, EventDispatcher eventDispatcher,
LoaderErrorThrower manifestLoaderErrorThrower, Callback callback, Allocator allocator) {
LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) {
this.chunkSourceFactory = chunkSourceFactory;
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventDispatcher = eventDispatcher;
this.callback = callback;
this.allocator = allocator;
trackGroups = buildTrackGroups(manifest);
......@@ -77,7 +76,6 @@ import java.util.ArrayList;
this.manifest = manifest;
sampleStreams = newSampleStreamArray(0);
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
callback.onPrepared(this);
}
public void updateManifest(SsManifest manifest) {
......@@ -95,6 +93,12 @@ import java.util.ArrayList;
}
@Override
public void prepare(Callback callback) {
this.callback = callback;
callback.onPrepared(this);
}
@Override
public void maybeThrowPrepareError() throws IOException {
manifestLoaderErrorThrower.maybeThrowError();
}
......
......@@ -24,7 +24,6 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaPeriod.Callback;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
......@@ -122,11 +121,10 @@ public final class SsMediaSource implements MediaSource,
}
@Override
public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator,
long positionUs) {
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
Assertions.checkArgument(index == 0);
SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount,
eventDispatcher, manifestLoader, callback, allocator);
eventDispatcher, manifestLoader, allocator);
mediaPeriods.add(period);
return period;
}
......
......@@ -18,9 +18,12 @@ package com.google.android.exoplayer2.text;
import android.annotation.TargetApi;
import android.graphics.Color;
import android.graphics.Typeface;
import android.support.annotation.IntDef;
import android.view.accessibility.CaptioningManager;
import android.view.accessibility.CaptioningManager.CaptionStyle;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A compatibility wrapper for {@link CaptionStyle}.
......@@ -28,25 +31,28 @@ import com.google.android.exoplayer2.util.Util;
public final class CaptionStyleCompat {
/**
* The type of edge, which may be none.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({EDGE_TYPE_NONE, EDGE_TYPE_OUTLINE, EDGE_TYPE_DROP_SHADOW, EDGE_TYPE_RAISED,
EDGE_TYPE_DEPRESSED})
public @interface EdgeType {}
/**
* Edge type value specifying no character edges.
*/
public static final int EDGE_TYPE_NONE = 0;
/**
* Edge type value specifying uniformly outlined character edges.
*/
public static final int EDGE_TYPE_OUTLINE = 1;
/**
* Edge type value specifying drop-shadowed character edges.
*/
public static final int EDGE_TYPE_DROP_SHADOW = 2;
/**
* Edge type value specifying raised bevel character edges.
*/
public static final int EDGE_TYPE_RAISED = 3;
/**
* Edge type value specifying depressed bevel character edges.
*/
......@@ -88,6 +94,7 @@ public final class CaptionStyleCompat {
* <li>{@link #EDGE_TYPE_DEPRESSED}
* </ul>
*/
@EdgeType
public final int edgeType;
/**
......@@ -126,8 +133,8 @@ public final class CaptionStyleCompat {
* @param edgeColor See {@link #edgeColor}.
* @param typeface See {@link #typeface}.
*/
public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, int edgeType,
int edgeColor, Typeface typeface) {
public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor,
@EdgeType int edgeType, int edgeColor, Typeface typeface) {
this.foregroundColor = foregroundColor;
this.backgroundColor = backgroundColor;
this.windowColor = windowColor;
......@@ -137,6 +144,7 @@ public final class CaptionStyleCompat {
}
@TargetApi(19)
@SuppressWarnings("ResourceType")
private static CaptionStyleCompat createFromCaptionStyleV19(
CaptioningManager.CaptionStyle captionStyle) {
return new CaptionStyleCompat(
......@@ -145,6 +153,7 @@ public final class CaptionStyleCompat {
}
@TargetApi(21)
@SuppressWarnings("ResourceType")
private static CaptionStyleCompat createFromCaptionStyleV21(
CaptioningManager.CaptionStyle captionStyle) {
return new CaptionStyleCompat(
......
......@@ -15,7 +15,10 @@
*/
package com.google.android.exoplayer2.text;
import android.support.annotation.IntDef;
import android.text.Layout.Alignment;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Contains information about a specific cue, including textual content and formatting data.
......@@ -26,6 +29,13 @@ public class Cue {
* An unset position or width.
*/
public static final float DIMEN_UNSET = Float.MIN_VALUE;
/**
* The type of anchor, which may be unset.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END})
public @interface AnchorType {}
/**
* An unset anchor or line type value.
*/
......@@ -44,6 +54,13 @@ public class Cue {
* box.
*/
public static final int ANCHOR_TYPE_END = 2;
/**
* The type of line, which may be unset.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_UNSET, LINE_TYPE_FRACTION, LINE_TYPE_NUMBER})
public @interface LineType {}
/**
* Value for {@link #lineType} when {@link #line} is a fractional position.
*/
......@@ -83,6 +100,7 @@ public class Cue {
* -1). For horizontal text the size of the first line of the cue is its height, and the start
* and end of the viewport are the top and bottom respectively.
*/
@LineType
public final int lineType;
/**
* The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START},
......@@ -92,6 +110,7 @@ public class Cue {
* and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box
* respectively.
*/
@AnchorType
public final int lineAnchor;
/**
* The fractional position of the {@link #positionAnchor} of the cue box within the viewport in
......@@ -110,6 +129,7 @@ public class Cue {
* and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box
* respectively.
*/
@AnchorType
public final int positionAnchor;
/**
* The size of the cue box in the writing direction specified as a fraction of the viewport size
......@@ -137,8 +157,8 @@ public class Cue {
* @param positionAnchor See {@link #positionAnchor}.
* @param size See {@link #size}.
*/
public Cue(CharSequence text, Alignment textAlignment, float line, int lineType, int lineAnchor,
float position, int positionAnchor, float size) {
public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType,
@AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) {
this.text = text;
this.textAlignment = textAlignment;
this.line = line;
......
......@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.text;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.eia608.Eia608Decoder;
import com.google.android.exoplayer2.text.cea.Cea608Decoder;
import com.google.android.exoplayer2.text.subrip.SubripDecoder;
import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;
......@@ -57,7 +57,7 @@ public interface SubtitleDecoderFactory {
* <li>TTML ({@link TtmlDecoder})</li>
* <li>SubRip ({@link SubripDecoder})</li>
* <li>TX3G ({@link Tx3gDecoder})</li>
* <li>Eia608 ({@link Eia608Decoder})</li>
* <li>Cea608 ({@link Cea608Decoder})</li>
* </ul>
*/
SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() {
......@@ -93,8 +93,8 @@ public interface SubtitleDecoderFactory {
return Class.forName("com.google.android.exoplayer2.text.subrip.SubripDecoder");
case MimeTypes.APPLICATION_TX3G:
return Class.forName("com.google.android.exoplayer2.text.tx3g.Tx3gDecoder");
case MimeTypes.APPLICATION_EIA608:
return Class.forName("com.google.android.exoplayer2.text.eia608.Eia608Decoder");
case MimeTypes.APPLICATION_CEA608:
return Class.forName("com.google.android.exoplayer2.text.cea.Cea608Decoder");
default:
return null;
}
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.text.cea;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoder;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import java.util.LinkedList;
import java.util.TreeSet;
/**
* Base class for subtitle parsers for CEA captions.
*/
/* package */ abstract class CeaDecoder implements SubtitleDecoder {
private static final int NUM_INPUT_BUFFERS = 10;
private static final int NUM_OUTPUT_BUFFERS = 2;
private final LinkedList<SubtitleInputBuffer> availableInputBuffers;
private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
private final TreeSet<SubtitleInputBuffer> queuedInputBuffers;
private SubtitleInputBuffer dequeuedInputBuffer;
private long playbackPositionUs;
public CeaDecoder() {
availableInputBuffers = new LinkedList<>();
for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
availableInputBuffers.add(new SubtitleInputBuffer());
}
availableOutputBuffers = new LinkedList<>();
for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
availableOutputBuffers.add(new CeaOutputBuffer(this));
}
queuedInputBuffers = new TreeSet<>();
}
@Override
public abstract String getName();
@Override
public void setPositionUs(long positionUs) {
playbackPositionUs = positionUs;
}
@Override
public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException {
Assertions.checkState(dequeuedInputBuffer == null);
if (availableInputBuffers.isEmpty()) {
return null;
}
dequeuedInputBuffer = availableInputBuffers.pollFirst();
return dequeuedInputBuffer;
}
@Override
public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException {
Assertions.checkArgument(inputBuffer != null);
Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);
queuedInputBuffers.add(inputBuffer);
dequeuedInputBuffer = null;
}
@Override
public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {
if (availableOutputBuffers.isEmpty()) {
return null;
}
// iterate through all available input buffers whose timestamps are less than or equal
// to the current playback position; processing input buffers for future content should
// be deferred until they would be applicable
while (!queuedInputBuffers.isEmpty()
&& queuedInputBuffers.first().timeUs <= playbackPositionUs) {
SubtitleInputBuffer inputBuffer = queuedInputBuffers.pollFirst();
// If the input buffer indicates we've reached the end of the stream, we can
// return immediately with an output buffer propagating that
if (inputBuffer.isEndOfStream()) {
SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
releaseInputBuffer(inputBuffer);
return outputBuffer;
}
decode(inputBuffer);
// check if we have any caption updates to report
if (isNewSubtitleDataAvailable()) {
// Even if the subtitle is decode-only; we need to generate it to consume the data so it
// isn't accidentally prepended to the next subtitle
Subtitle subtitle = createSubtitle();
if (!inputBuffer.isDecodeOnly()) {
SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
outputBuffer.setContent(inputBuffer.timeUs, subtitle, 0);
releaseInputBuffer(inputBuffer);
return outputBuffer;
}
}
releaseInputBuffer(inputBuffer);
}
return null;
}
private void releaseInputBuffer(SubtitleInputBuffer inputBuffer) {
inputBuffer.clear();
availableInputBuffers.add(inputBuffer);
}
protected void releaseOutputBuffer(SubtitleOutputBuffer outputBuffer) {
outputBuffer.clear();
availableOutputBuffers.add(outputBuffer);
}
@Override
public void flush() {
playbackPositionUs = 0;
while (!queuedInputBuffers.isEmpty()) {
releaseInputBuffer(queuedInputBuffers.pollFirst());
}
if (dequeuedInputBuffer != null) {
releaseInputBuffer(dequeuedInputBuffer);
dequeuedInputBuffer = null;
}
}
@Override
public void release() {
// Do nothing
}
/**
* Returns whether there is data available to create a new {@link Subtitle}.
*/
protected abstract boolean isNewSubtitleDataAvailable();
/**
* Creates a {@link Subtitle} from the available data.
*/
protected abstract Subtitle createSubtitle();
/**
* Filters and processes the raw data, providing {@link Subtitle}s via {@link #createSubtitle()}
* when sufficient data has been processed.
*/
protected abstract void decode(SubtitleInputBuffer inputBuffer);
}
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