Commit 2b207804 by ojw28 Committed by GitHub

Merge pull request #3659 from google/dev-v2-r2.6.1

r2.6.1
parents 1b66908f b704a1d6
Showing with 1683 additions and 812 deletions
*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION ***
Before filing an issue:
-----------------------
- Search existing issues, including issues that are closed.
......
......@@ -68,7 +68,7 @@ individually.
In addition to library modules, ExoPlayer has multiple extension modules that
depend on external libraries to provide additional functionality. Some
extensions are available from JCenter, whereas others must be built manaully.
extensions are available from JCenter, whereas others must be built manually.
Browse the [extensions directory][] and their individual READMEs for details.
More information on the library and extension modules that are available from
......
# Release notes #
### 2.6.1 ###
* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`,
`DashMediaSource` and `SingleSampleMediaSource`.
* Use the same listener `MediaSourceEventListener` for all MediaSource
implementations.
* IMA extension:
* Support non-ExtractorMediaSource ads
([#3302](https://github.com/google/ExoPlayer/issues/3302)).
* Skip ads before the ad preceding the player's initial seek position
([#3527](https://github.com/google/ExoPlayer/issues/3527)).
* Fix ad loading when there is no preroll.
* Add an option to turn off hiding controls during ad playback
([#3532](https://github.com/google/ExoPlayer/issues/3532)).
* Support specifying an ads response instead of an ad tag
([#3548](https://github.com/google/ExoPlayer/issues/3548)).
* Support overriding the ad load timeout
([#3556](https://github.com/google/ExoPlayer/issues/3556)).
* DASH: Support time zone designators in ISO8601 UTCTiming elements
([#3524](https://github.com/google/ExoPlayer/issues/3524)).
* Audio:
* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option
to use this with `FfmpegAudioRenderer`.
* Add support for extracting 32-bit WAVE files
([#3379](https://github.com/google/ExoPlayer/issues/3379)).
* Support extraction and decoding of Dolby Atmos
([#2465](https://github.com/google/ExoPlayer/issues/2465)).
* Fix handling of playback parameter changes while paused when followed by a
seek.
* SimpleExoPlayer: Allow multiple audio and video debug listeners.
* DefaultTrackSelector: Support undefined language text track selection when the
preferred language is not available
([#2980](https://github.com/google/ExoPlayer/issues/2980)).
* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and
to choose whether size or time constraints are prioritized.
* Use surfaceless context for secure `DummySurface`, if available
([#3558](https://github.com/google/ExoPlayer/issues/3558)).
* FLV: Fix playback of live streams that do not contain an audio track
([#3188](https://github.com/google/ExoPlayer/issues/3188)).
* CEA-608: Fix handling of row count changes in roll-up mode
([#3513](https://github.com/google/ExoPlayer/issues/3513)).
### 2.6.0 ###
* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0".
......@@ -142,7 +184,7 @@
easy and seamless way of incorporating display ads into ExoPlayer playbacks.
You can read more about the IMA extension
[here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea).
* MediaSession extension: Provides an easy to to connect ExoPlayer with
* MediaSession extension: Provides an easy way to connect ExoPlayer with
MediaSessionCompat in the Android Support Library.
* RTMP extension: An extension for playing streams over RTMP.
* Build: Made it easier for application developers to depend on a local checkout
......
......@@ -17,7 +17,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.novoda:bintray-release:0.5.0'
}
// Workaround for the following test coverage issue. Remove when fixed:
......
......@@ -17,8 +17,8 @@ project.ext {
// However, please note that the core media playback functionality provided
// by the library requires API level 16 or greater.
minSdkVersion = 14
compileSdkVersion = 26
targetSdkVersion = 26
compileSdkVersion = 27
targetSdkVersion = 27
buildToolsVersion = '26.0.2'
testSupportLibraryVersion = '0.5'
supportLibraryVersion = '27.0.0'
......@@ -28,7 +28,7 @@ project.ext {
junitVersion = '4.12'
truthVersion = '0.35'
robolectricVersion = '3.4.2'
releaseVersion = '2.6.0'
releaseVersion = '2.6.1'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
......
......@@ -43,5 +43,7 @@ android {
dependencies {
compile project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-ui')
compile project(modulePrefix + 'library-dash')
compile project(modulePrefix + 'library-hls')
compile project(modulePrefix + 'extension-ima')
}
......@@ -15,11 +15,11 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.imademo"
android:versionCode="2600"
android:versionName="2.6.0">
android:versionCode="2601"
android:versionName="2.6.1">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false">
......
......@@ -17,15 +17,21 @@ package com.google.android.exoplayer2.imademo;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
......@@ -37,12 +43,12 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
/**
* Manages the {@link ExoPlayer}, the IMA plugin and all video playback.
*/
/* package */ final class PlayerManager {
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
private final ImaAdsLoader adsLoader;
private final DataSource.Factory manifestDataSourceFactory;
private final DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player;
private long contentPosition;
......@@ -50,6 +56,14 @@ import com.google.android.exoplayer2.util.Util;
public PlayerManager(Context context) {
String adTag = context.getString(R.string.ad_tag_url);
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
manifestDataSourceFactory =
new DefaultDataSourceFactory(
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
mediaDataSourceFactory =
new DefaultDataSourceFactory(
context,
Util.getUserAgent(context, context.getString(R.string.application_name)),
new DefaultBandwidthMeter());
}
public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) {
......@@ -69,17 +83,21 @@ import com.google.android.exoplayer2.util.Util;
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
Util.getUserAgent(context, context.getString(R.string.application_name)));
// Produces Extractor instances for parsing the content media (i.e. not the ad).
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
// This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url);
MediaSource contentMediaSource = new ExtractorMediaSource(
Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null);
MediaSource contentMediaSource =
new ExtractorMediaSource.Factory(dataSourceFactory)
.createMediaSource(Uri.parse(contentUrl));
// Compose the content media source into a new AdsMediaSource with both ads and content.
MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory,
adsLoader, simpleExoPlayerView.getOverlayFrameLayout());
MediaSource mediaSourceWithAds =
new AdsMediaSource(
contentMediaSource,
/* adMediaSourceFactory= */ this,
adsLoader,
simpleExoPlayerView.getOverlayFrameLayout(),
/* eventHandler= */ null,
/* eventListener= */ null);
// Prepare the player with the source.
player.seekTo(contentPosition);
......@@ -103,4 +121,32 @@ import com.google.android.exoplayer2.util.Util;
adsLoader.release();
}
// AdsMediaSource.MediaSourceFactory implementation.
@Override
public MediaSource createMediaSource(
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
@ContentType int type = Util.inferContentType(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
manifestDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_SS:
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
}
}
......@@ -16,14 +16,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2600"
android:versionName="2.6.0">
android:versionCode="2601"
android:versionName="2.6.1">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
<application
android:label="@string/application_name"
......
......@@ -16,14 +16,43 @@
package com.google.android.exoplayer2.demo;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.Locale;
import java.util.UUID;
/**
* Utility methods for demo application.
*/
/*package*/ final class DemoUtil {
/* package */ final class DemoUtil {
/**
* Derives a DRM {@link UUID} from {@code drmScheme}.
*
* @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or
* {@code "clearkey"}.
* @return The derived {@link UUID}.
* @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}.
*/
public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException {
switch (Util.toLowerInvariant(drmScheme)) {
case "widevine":
return C.WIDEVINE_UUID;
case "playready":
return C.PLAYREADY_UUID;
case "clearkey":
return C.CLEARKEY_UUID;
default:
try {
return UUID.fromString(drmScheme);
} catch (RuntimeException e) {
throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME);
}
}
}
/**
* Builds a track name for display.
......
......@@ -38,10 +38,10 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
......@@ -52,12 +52,15 @@ import java.io.IOException;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Logs player events using {@link Log}.
*/
/* package */ final class EventLogger implements Player.EventListener, MetadataOutput,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener {
/** Logs player events using {@link Log}. */
/* package */ final class EventLogger
implements Player.EventListener,
MetadataOutput,
AudioRendererEventListener,
VideoRendererEventListener,
MediaSourceEventListener,
AdsMediaSource.EventListener,
DefaultDrmSessionManager.EventListener {
private static final String TAG = "EventLogger";
private static final int MAX_TIMELINE_ITEM_LINES = 3;
......@@ -320,19 +323,19 @@ import java.util.Locale;
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
}
// ExtractorMediaSource.EventListener
@Override
public void onLoadError(IOException error) {
printInternalError("loadError", error);
}
// AdaptiveMediaSourceEventListener
// MediaSourceEventListener
@Override
public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs) {
public void onLoadStarted(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs) {
// Do nothing.
}
......@@ -369,6 +372,23 @@ import java.util.Locale;
// Do nothing.
}
// AdsMediaSource.EventListener
@Override
public void onAdLoadError(IOException error) {
printInternalError("adLoadError", error);
}
@Override
public void onAdClicked() {
// Do nothing.
}
@Override
public void onAdTapped() {
// Do nothing.
}
// Internal methods
private void printInternalError(String type, Exception e) {
......@@ -467,6 +487,9 @@ import java.util.Locale;
}
}
// Suppressing reference equality warning because the track group stored in the track selection
// must point to the exact track group object to be considered part of it.
@SuppressWarnings("ReferenceEquality")
private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
int trackIndex) {
return getTrackStatusString(selection != null && selection.getTrackGroup() == group
......
......@@ -23,6 +23,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
......@@ -46,13 +47,13 @@ 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.UnsupportedDrmException;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
......@@ -84,7 +85,7 @@ import java.util.UUID;
public class PlayerActivity extends Activity implements OnClickListener,
PlaybackControlView.VisibilityListener {
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
public static final String DRM_LICENSE_URL = "drm_license_url";
public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
public static final String DRM_MULTI_SESSION = "drm_multi_session";
......@@ -99,6 +100,9 @@ public class PlayerActivity extends Activity implements OnClickListener,
public static final String EXTENSION_LIST_EXTRA = "extension_list";
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
// For backwards compatibility.
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER;
static {
......@@ -231,8 +235,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
} else if (view.getParent() == debugRootView) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(),
trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag());
trackSelectionHelper.showSelectionDialog(
this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag());
}
}
}
......@@ -257,10 +261,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
lastSeenTrackGroupArray = null;
eventLogger = new EventLogger(trackSelector);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
if (drmSchemeUuid != null) {
if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false);
......@@ -269,6 +271,9 @@ public class PlayerActivity extends Activity implements OnClickListener,
errorStringId = R.string.error_drm_not_supported;
} else {
try {
String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA
: DRM_SCHEME_UUID_EXTRA;
UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl,
keyRequestPropertiesArray, multiSession);
} catch (UnsupportedDrmException e) {
......@@ -295,8 +300,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
player.addListener(new PlayerEventListener());
player.addListener(eventLogger);
player.addMetadataOutput(eventLogger);
player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger);
player.addAudioDebugListener(eventLogger);
player.addVideoDebugListener(eventLogger);
simpleExoPlayerView.setPlayer(player);
player.setPlayWhenReady(shouldAutoPlay);
......@@ -329,7 +334,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
}
MediaSource[] mediaSources = new MediaSource[uris.length];
for (int i = 0; i < uris.length; i++) {
mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger);
}
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources);
......@@ -357,21 +362,30 @@ public class PlayerActivity extends Activity implements OnClickListener,
updateButtonVisibilities();
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
private MediaSource buildMediaSource(
Uri uri,
String overrideExtension,
@Nullable Handler handler,
@Nullable MediaSourceEventListener listener) {
@ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
: Util.inferContentType("." + overrideExtension);
switch (type) {
case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false),
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
case C.TYPE_DASH:
return new DashMediaSource(uri, buildDataSourceFactory(false),
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false))
.createMediaSource(uri, handler, listener);
case C.TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false))
.createMediaSource(uri, handler, listener);
case C.TYPE_HLS:
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger);
return new HlsMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_OTHER:
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
mainHandler, eventLogger);
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener);
default: {
throw new IllegalStateException("Unsupported type: " + type);
}
......@@ -458,7 +472,22 @@ public class PlayerActivity extends Activity implements OnClickListener,
// The demo app has a non-null overlay frame layout.
simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup);
}
return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup);
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
new AdsMediaSource.MediaSourceFactory() {
@Override
public MediaSource createMediaSource(
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
return PlayerActivity.this.buildMediaSource(
uri, /* overrideExtension= */ null, handler, listener);
}
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
}
};
return new AdsMediaSource(
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
}
private void releaseAdsLoader() {
......
......@@ -32,8 +32,8 @@ import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
......@@ -202,7 +202,11 @@ public class SampleChooserActivity extends Activity {
break;
case "drm_scheme":
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
drmUuid = getDrmUuid(reader.nextString());
try {
drmUuid = DemoUtil.getDrmUuid(reader.nextString());
} catch (UnsupportedDrmException e) {
throw new ParserException(e);
}
break;
case "drm_license_url":
Assertions.checkState(!insidePlaylist,
......@@ -270,23 +274,6 @@ public class SampleChooserActivity extends Activity {
return group;
}
private UUID getDrmUuid(String typeString) throws ParserException {
switch (Util.toLowerInvariant(typeString)) {
case "widevine":
return C.WIDEVINE_UUID;
case "playready":
return C.PLAYREADY_UUID;
case "clearkey":
return C.CLEARKEY_UUID;
default:
try {
return UUID.fromString(typeString);
} catch (RuntimeException e) {
throw new ParserException("Unsupported drm type: " + typeString);
}
}
}
}
private static final class SampleAdapter extends BaseExpandableListAdapter {
......@@ -393,7 +380,7 @@ public class SampleChooserActivity extends Activity {
public void updateIntent(Intent intent) {
Assertions.checkNotNull(intent);
intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString());
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString());
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession);
......
......@@ -40,6 +40,7 @@ dependencies {
compile files('libs/cronet_impl_common_java.jar')
compile files('libs/cronet_impl_native_java.jar')
androidTestCompile project(modulePrefix + 'library')
androidTestCompile project(modulePrefix + 'testutils')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
......
......@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.ext.cronet">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
<application android:debuggable="true"
android:allowBackup="false"
......
......@@ -19,10 +19,10 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
......@@ -46,9 +46,7 @@ public final class ByteArrayUploadDataProviderTest {
@Before
public void setUp() {
System.setProperty("dexmaker.dexcache",
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
initMocks(this);
MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this);
byteBuffer = ByteBuffer.allocate(TEST_DATA.length);
byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA);
}
......
......@@ -31,13 +31,13 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import android.net.Uri;
import android.os.ConditionVariable;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
......@@ -107,9 +107,7 @@ public final class CronetDataSourceTest {
@Before
public void setUp() throws Exception {
System.setProperty("dexmaker.dexcache",
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
initMocks(this);
MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this);
dataSourceUnderTest = spy(
new CronetDataSource(
mockCronetEngine,
......
......@@ -21,6 +21,8 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
......@@ -41,6 +43,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
*/
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
private final boolean enableFloatOutput;
private FfmpegDecoder decoder;
public FfmpegAudioRenderer() {
......@@ -55,7 +59,23 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false);
}
/**
* @param eventHandler A handler to use when delivering events to {@code eventListener}. 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 audioSink The sink to which audio will be output.
* @param enableFloatOutput Whether to enable 32-bit float audio format, if supported on the
* device/build and if the input format may have bit depth higher than 16-bit. When using
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch
* adjustment.
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioSink audioSink, boolean enableFloatOutput) {
super(eventHandler, eventListener, null, false, audioSink);
this.enableFloatOutput = enableFloatOutput;
}
@Override
......@@ -64,7 +84,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
String sampleMimeType = format.sampleMimeType;
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) {
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
......@@ -82,7 +102,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.sampleMimeType, format.initializationData);
format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format));
return decoder;
}
......@@ -90,8 +110,32 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
public Format getOutputFormat() {
int channelCount = decoder.getChannelCount();
int sampleRate = decoder.getSampleRate();
@C.PcmEncoding int encoding = decoder.getEncoding();
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
Format.NO_VALUE, channelCount, sampleRate, C.ENCODING_PCM_16BIT, null, null, 0, null);
Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null);
}
private boolean isOutputSupported(Format inputFormat) {
return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT);
}
private boolean shouldUseFloatOutput(Format inputFormat) {
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
return false;
}
switch (inputFormat.sampleMimeType) {
case MimeTypes.AUDIO_RAW:
// For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit.
return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT
|| inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT
|| inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT;
case MimeTypes.AUDIO_AC3:
// AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding.
return false;
default:
// For all other formats, assume that it's worth using 32-bit float encoding.
return true;
}
}
}
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
......@@ -29,11 +30,15 @@ import java.util.List;
/* package */ final class FfmpegDecoder extends
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
// Space for 64 ms of 6 channel 48 kHz 16-bit PCM audio.
private static final int OUTPUT_BUFFER_SIZE = 1536 * 6 * 2 * 2;
// Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio.
private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2;
// Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio.
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
private final String codecName;
private final byte[] extraData;
private final @C.Encoding int encoding;
private final int outputBufferSize;
private long nativeContext; // May be reassigned on resetting the codec.
private boolean hasOutputFormat;
......@@ -41,14 +46,17 @@ import java.util.List;
private volatile int sampleRate;
public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
String mimeType, List<byte[]> initializationData) throws FfmpegDecoderException {
String mimeType, List<byte[]> initializationData, boolean outputFloat)
throws FfmpegDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!FfmpegLibrary.isAvailable()) {
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
}
codecName = FfmpegLibrary.getCodecName(mimeType);
extraData = getExtraData(mimeType, initializationData);
nativeContext = ffmpegInitialize(codecName, extraData);
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
nativeContext = ffmpegInitialize(codecName, extraData, outputFloat);
if (nativeContext == 0) {
throw new FfmpegDecoderException("Initialization failed.");
}
......@@ -81,8 +89,8 @@ import java.util.List;
}
ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, OUTPUT_BUFFER_SIZE);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, OUTPUT_BUFFER_SIZE);
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
if (result < 0) {
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
}
......@@ -125,6 +133,13 @@ import java.util.List;
}
/**
* Returns the encoding of output audio.
*/
public @C.Encoding int getEncoding() {
return encoding;
}
/**
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
* not required.
*/
......@@ -153,7 +168,7 @@ import java.util.List;
}
}
private native long ffmpegInitialize(String codecName, byte[] extraData);
private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat);
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
ByteBuffer outputData, int outputSize);
private native int ffmpegGetChannelCount(long context);
......
......@@ -57,8 +57,10 @@ extern "C" {
#define ERROR_STRING_BUFFER_LENGTH 256
// Request a format corresponding to AudioFormat.ENCODING_PCM_16BIT.
static const AVSampleFormat OUTPUT_FORMAT = AV_SAMPLE_FMT_S16;
// Output format corresponding to AudioFormat.ENCODING_PCM_16BIT.
static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
/**
* Returns the AVCodec with the specified name, or NULL if it is not available.
......@@ -71,7 +73,7 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName);
* Returns the created context.
*/
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
jbyteArray extraData);
jbyteArray extraData, jboolean outputFloat);
/**
* Decodes the packet into the output buffer, returning the number of bytes
......@@ -107,13 +109,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
return getCodecByName(env, codecName) != NULL;
}
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData) {
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
jboolean outputFloat) {
AVCodec *codec = getCodecByName(env, codecName);
if (!codec) {
LOGE("Codec not found.");
return 0L;
}
return (jlong) createContext(env, codec, extraData);
return (jlong) createContext(env, codec, extraData, outputFloat);
}
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
......@@ -177,7 +180,8 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
LOGE("Unexpected error finding codec %d.", codecId);
return 0L;
}
return (jlong) createContext(env, codec, extraData);
return (jlong) createContext(env, codec, extraData,
context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
}
avcodec_flush_buffers(context);
......@@ -201,13 +205,14 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
}
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
jbyteArray extraData) {
jbyteArray extraData, jboolean outputFloat) {
AVCodecContext *context = avcodec_alloc_context3(codec);
if (!context) {
LOGE("Failed to allocate context.");
return NULL;
}
context->request_sample_fmt = OUTPUT_FORMAT;
context->request_sample_fmt =
outputFloat ? OUTPUT_FORMAT_PCM_FLOAT : OUTPUT_FORMAT_PCM_16BIT;
if (extraData) {
jsize size = env->GetArrayLength(extraData);
context->extradata_size = size;
......@@ -275,7 +280,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0);
av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0);
av_opt_set_int(resampleContext, "out_sample_fmt", OUTPUT_FORMAT, 0);
// The output format is always the requested format.
av_opt_set_int(resampleContext, "out_sample_fmt",
context->request_sample_fmt, 0);
result = avresample_open(resampleContext);
if (result < 0) {
logError("avresample_open", result);
......@@ -285,7 +292,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
context->opaque = resampleContext;
}
int inSampleSize = av_get_bytes_per_sample(sampleFormat);
int outSampleSize = av_get_bytes_per_sample(OUTPUT_FORMAT);
int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
int outSamples = avresample_get_out_samples(resampleContext, sampleCount);
int bufferOutSize = outSampleSize * channelCount * outSamples;
if (outSize + bufferOutSize > outputSize) {
......
......@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.flac.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
<application android:debuggable="true"
android:allowBackup="false"
......
......@@ -25,6 +25,14 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
*/
public class FlacExtractorTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
}
public void testSample() throws Exception {
ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
......
......@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
......@@ -36,6 +37,14 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka";
@Override
protected void setUp() throws Exception {
super.setUp();
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
}
public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_FLAC_URI);
}
......@@ -76,12 +85,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
uri,
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
MatroskaExtractor.FACTORY,
null,
null);
MediaSource mediaSource =
new ExtractorMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
.setExtractorsFactory(MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
Looper.loop();
......@@ -100,7 +108,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
Looper.myLooper().quit();
}
}
}
}
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.flac;
import android.os.Handler;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
......@@ -52,6 +53,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
} else {
......
......@@ -52,8 +52,8 @@ public final class ImaAdsMediaSource implements MediaSource {
}
/**
* Constructs a new source that inserts ads linearly with the content specified by
* {@code contentMediaSource}.
* Constructs a new source that inserts ads linearly with the content specified by {@code
* contentMediaSource}.
*
* @param contentMediaSource The {@link MediaSource} providing the content to play.
* @param dataSourceFactory Factory for data sources used to load ad media.
......@@ -62,9 +62,13 @@ public final class ImaAdsMediaSource implements MediaSource {
* @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.
*/
public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler,
@Nullable AdsMediaSource.AdsListener eventListener) {
public ImaAdsMediaSource(
MediaSource contentMediaSource,
DataSource.Factory dataSourceFactory,
ImaAdsLoader imaAdsLoader,
ViewGroup adUiViewGroup,
@Nullable Handler eventHandler,
@Nullable AdsMediaSource.EventListener eventListener) {
adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader,
adUiViewGroup, eventHandler, eventListener);
}
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.mediasession;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
......@@ -330,6 +331,7 @@ public final class MediaSessionConnector {
private final ExoPlayerEventListener exoPlayerEventListener;
private final MediaSessionCallback mediaSessionCallback;
private final PlaybackController playbackController;
private final String metadataExtrasPrefix;
private final Map<String, CommandReceiver> commandMap;
private Player player;
......@@ -356,15 +358,15 @@ public final class MediaSessionConnector {
/**
* Creates an instance. Must be called on the same thread that is used to construct the player
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
* <p>
* Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}.
*
* <p>Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
* @param playbackController A {@link PlaybackController} for handling playback actions.
*/
public MediaSessionConnector(MediaSessionCompat mediaSession,
PlaybackController playbackController) {
this(mediaSession, playbackController, true);
public MediaSessionConnector(
MediaSessionCompat mediaSession, PlaybackController playbackController) {
this(mediaSession, playbackController, true, null);
}
/**
......@@ -372,17 +374,23 @@ public final class MediaSessionConnector {
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
*
* @param mediaSession The {@link MediaSessionCompat} to connect to.
* @param playbackController A {@link PlaybackController} for handling playback actions, or
* {@code null} if the connector should handle playback actions directly.
* @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
* null} if the connector should handle playback actions directly.
* @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If
* {@code false}, you need to maintain the metadata of the media session yourself (provide at
* least the duration to allow clients to show a progress bar).
* @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
* queue item to the session metadata.
*/
public MediaSessionConnector(MediaSessionCompat mediaSession,
PlaybackController playbackController, boolean doMaintainMetadata) {
public MediaSessionConnector(
MediaSessionCompat mediaSession,
PlaybackController playbackController,
boolean doMaintainMetadata,
@Nullable String metadataExtrasPrefix) {
this.mediaSession = mediaSession;
this.playbackController = playbackController != null ? playbackController
: new DefaultPlaybackController();
this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
: Looper.getMainLooper());
this.doMaintainMetadata = doMaintainMetadata;
......@@ -553,6 +561,25 @@ public final class MediaSessionConnector {
MediaSessionCompat.QueueItem queueItem = queue.get(i);
if (queueItem.getQueueId() == activeQueueItemId) {
MediaDescriptionCompat description = queueItem.getDescription();
Bundle extras = description.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
Object value = extras.get(key);
if (value instanceof String) {
builder.putString(metadataExtrasPrefix + key, (String) value);
} else if (value instanceof CharSequence) {
builder.putText(metadataExtrasPrefix + key, (CharSequence) value);
} else if (value instanceof Long) {
builder.putLong(metadataExtrasPrefix + key, (Long) value);
} else if (value instanceof Integer) {
builder.putLong(metadataExtrasPrefix + key, (Integer) value);
} else if (value instanceof Bitmap) {
builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value);
} else if (value instanceof RatingCompat) {
builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value);
}
}
}
if (description.getTitle() != null) {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
String.valueOf(description.getTitle()));
......
......@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.opus.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
<application android:debuggable="true"
android:allowBackup="false"
......
......@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
......@@ -36,6 +37,14 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm";
@Override
protected void setUp() throws Exception {
super.setUp();
if (!OpusLibrary.isAvailable()) {
fail("Opus library not available.");
}
}
public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_OPUS_URI);
}
......@@ -76,12 +85,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
uri,
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"),
MatroskaExtractor.FACTORY,
null,
null);
MediaSource mediaSource =
new ExtractorMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
.setExtractorsFactory(MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
Looper.loop();
......
......@@ -76,6 +76,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
if (!OpusLibrary.isAvailable()
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
} else {
......
......@@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)"
VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable:
* Download the [Android NDK][] and set its location in an environment variable.
Only versions up to NDK 15c are supported currently (see [#3520][]).
```
NDK_PATH="<path to Android NDK>"
......@@ -70,6 +71,7 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[#3520]: https://github.com/google/ExoPlayer/issues/3520
## Notes ##
......
......@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.vp9.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
<application android:debuggable="true"
android:allowBackup="false"
......
......@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
......@@ -42,6 +43,14 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
private static final String TAG = "VpxPlaybackTest";
@Override
protected void setUp() throws Exception {
super.setUp();
if (!VpxLibrary.isAvailable()) {
fail("Vpx library not available.");
}
}
public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_URI);
}
......@@ -105,12 +114,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
uri,
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"),
MatroskaExtractor.FACTORY,
null,
null);
MediaSource mediaSource =
new ExtractorMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
.setExtractorsFactory(MatroskaExtractor.FACTORY)
.createMediaSource(uri);
player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer,
LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER,
new VpxVideoSurfaceView(context)));
......@@ -132,7 +140,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
Looper.myLooper().quit();
}
}
}
}
......@@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.core.test">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
<application android:debuggable="true"
android:allowBackup="false"
......
......@@ -27,155 +27,155 @@ track 0:
initializationData:
sample count = 38
sample 0:
time = 1858196
time = 1871586
flags = 1
data = length 384, hash E801184A
sample 1:
time = 1882196
time = 1895586
flags = 1
data = length 384, hash 53C6CF9C
sample 2:
time = 1906196
time = 1919586
flags = 1
data = length 384, hash 19A8D99F
sample 3:
time = 1930196
time = 1943586
flags = 1
data = length 384, hash E47EB43F
sample 4:
time = 1954196
time = 1967586
flags = 1
data = length 384, hash 4EA329E7
sample 5:
time = 1978196
time = 1991586
flags = 1
data = length 384, hash 1CCAAE62
sample 6:
time = 2002196
time = 2015586
flags = 1
data = length 384, hash ED3F8C66
sample 7:
time = 2026196
time = 2039586
flags = 1
data = length 384, hash D3D646B6
sample 8:
time = 2050196
time = 2063586
flags = 1
data = length 384, hash 68CD1574
sample 9:
time = 2074196
time = 2087586
flags = 1
data = length 384, hash 8CEAB382
sample 10:
time = 2098196
time = 2111586
flags = 1
data = length 384, hash D54B1C48
sample 11:
time = 2122196
time = 2135586
flags = 1
data = length 384, hash FFE2EE90
sample 12:
time = 2146196
time = 2159586
flags = 1
data = length 384, hash BFE8A673
sample 13:
time = 2170196
time = 2183586
flags = 1
data = length 384, hash 978B1C92
sample 14:
time = 2194196
time = 2207586
flags = 1
data = length 384, hash 810CC71E
sample 15:
time = 2218196
time = 2231586
flags = 1
data = length 384, hash 44FE42D9
sample 16:
time = 2242196
time = 2255586
flags = 1
data = length 384, hash 2F5BB02C
sample 17:
time = 2266196
time = 2279586
flags = 1
data = length 384, hash 77DDB90
sample 18:
time = 2290196
time = 2303586
flags = 1
data = length 384, hash 24FB5EDA
sample 19:
time = 2314196
time = 2327586
flags = 1
data = length 384, hash E73203C6
sample 20:
time = 2338196
time = 2351586
flags = 1
data = length 384, hash 14B525F1
sample 21:
time = 2362196
time = 2375586
flags = 1
data = length 384, hash 5E0F4E2E
sample 22:
time = 2386196
time = 2399586
flags = 1
data = length 384, hash 67EE4E31
sample 23:
time = 2410196
time = 2423586
flags = 1
data = length 384, hash 2E04EC4C
sample 24:
time = 2434196
time = 2447586
flags = 1
data = length 384, hash 852CABA7
sample 25:
time = 2458196
time = 2471586
flags = 1
data = length 384, hash 19928903
sample 26:
time = 2482196
time = 2495586
flags = 1
data = length 384, hash 5DA42021
sample 27:
time = 2506196
time = 2519586
flags = 1
data = length 384, hash 45B20B7C
sample 28:
time = 2530196
time = 2543586
flags = 1
data = length 384, hash D108A215
sample 29:
time = 2554196
time = 2567586
flags = 1
data = length 384, hash BD25DB7C
sample 30:
time = 2578196
time = 2591586
flags = 1
data = length 384, hash DA7F9861
sample 31:
time = 2602196
time = 2615586
flags = 1
data = length 384, hash CCD576F
sample 32:
time = 2626196
time = 2639586
flags = 1
data = length 384, hash 405C1EB5
sample 33:
time = 2650196
time = 2663586
flags = 1
data = length 384, hash 6640B74E
sample 34:
time = 2674196
time = 2687586
flags = 1
data = length 384, hash B4E5937A
sample 35:
time = 2698196
time = 2711586
flags = 1
data = length 384, hash CEE17733
sample 36:
time = 2722196
time = 2735586
flags = 1
data = length 384, hash 2A0DA733
sample 37:
time = 2746196
time = 2759586
flags = 1
data = length 384, hash 97F4129B
tracksEnded = true
......@@ -25,5 +25,9 @@ track 0:
language = null
drmInitData = -
initializationData:
sample count = 0
sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true
......@@ -25,5 +25,9 @@ track 0:
language = null
drmInitData = -
initializationData:
sample count = 0
sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true
......@@ -25,5 +25,9 @@ track 0:
language = null
drmInitData = -
initializationData:
sample count = 0
sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = 1828
numberOfTracks = 2
track 0:
format:
......
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = 1828
numberOfTracks = 3
track 0:
format:
......
......@@ -23,9 +23,9 @@ import android.test.MoreAsserts;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import java.util.HashMap;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Tests {@link OfflineLicenseHelper}.
......@@ -38,7 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
setUpMockito(this);
MockitoUtil.setUpMockito(this);
when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3});
offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback,
null);
......@@ -156,14 +156,4 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
new byte[] {1, 4, 7, 0, 3, 6}));
}
/**
* Sets up Mockito for an instrumentation test.
*/
private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
System.setProperty("dexmaker.dexcache",
instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
MockitoAnnotations.initMocks(instrumentationTestCase);
}
}
......@@ -16,9 +16,13 @@
package com.google.android.exoplayer2.extractor.mp4;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Collections;
import java.util.List;
/**
* Unit test for {@link FragmentedMp4Extractor}.
......@@ -26,26 +30,23 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception {
ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4",
getInstrumentation());
ExtractorAsserts.assertBehavior(getExtractorFactory(Collections.<Format>emptyList()),
"mp4/sample_fragmented.mp4", getInstrumentation());
}
public void testSampleWithSeiPayloadParsing() throws Exception {
// Enabling the CEA-608 track enables SEI payload parsing.
ExtractorAsserts.assertBehavior(
getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
}
private static ExtractorFactory getExtractorFactory() {
return getExtractorFactory(0);
ExtractorFactory extractorFactory = getExtractorFactory(Collections.singletonList(
Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)));
ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4",
getInstrumentation());
}
private static ExtractorFactory getExtractorFactory(final int flags) {
private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
return new ExtractorFactory() {
@Override
public Extractor create() {
return new FragmentedMp4Extractor(flags, null);
return new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats);
}
};
}
......
......@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
/**
......@@ -123,9 +123,14 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/
private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
MediaSource mediaSource = new FakeMediaSource(timeline, null);
return TestUtil.extractTimelineFromMediaSource(
new ClippingMediaSource(mediaSource, startMs, endMs));
FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
try {
return testRunner.prepareSource();
} finally {
testRunner.release();
}
}
}
......@@ -23,7 +23,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import junit.framework.TestCase;
......@@ -208,18 +208,22 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly,
mediaSourceWithAds);
// Prepare and assert timeline contains ad groups.
Timeline timeline = TestUtil.extractTimelineFromMediaSource(mediaSource);
TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1);
// Create all periods and assert period creation of child media sources has been called.
TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000);
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0));
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0));
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
try {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1);
// Create all periods and assert period creation of child media sources has been called.
testRunner.assertPrepareAndReleaseAllPeriods();
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0));
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0));
} finally {
testRunner.release();
}
}
/**
......@@ -234,7 +238,12 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
}
ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic,
new FakeShuffleOrder(mediaSources.length), mediaSources);
return TestUtil.extractTimelineFromMediaSource(mediaSource);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
try {
return testRunner.prepareSource();
} finally {
testRunner.release();
}
}
private static FakeTimeline createFakeTimeline(int periodCount, int windowId) {
......
......@@ -21,7 +21,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import junit.framework.TestCase;
......@@ -30,12 +30,13 @@ import junit.framework.TestCase;
*/
public class LoopingMediaSourceTest extends TestCase {
private final Timeline multiWindowTimeline;
private FakeTimeline multiWindowTimeline;
public LoopingMediaSourceTest() {
multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource(
new FakeTimeline(new TimelineWindowDefinition(1, 111),
new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null));
@Override
public void setUp() throws Exception {
super.setUp();
multiWindowTimeline = new FakeTimeline(new TimelineWindowDefinition(1, 111),
new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333));
}
public void testSingleLoop() {
......@@ -109,10 +110,14 @@ public class LoopingMediaSourceTest extends TestCase {
* the looping timeline.
*/
private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) {
MediaSource mediaSource = new FakeMediaSource(timeline, null);
return TestUtil.extractTimelineFromMediaSource(
new LoopingMediaSource(mediaSource, loopCount));
FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
try {
return testRunner.prepareSource();
} finally {
testRunner.release();
}
}
}
......@@ -17,11 +17,11 @@ package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Tests for {@link CachedRegionTracker}.
......@@ -46,7 +46,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
setUpMockito(this);
MockitoUtil.setUpMockito(this);
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
......@@ -123,14 +123,4 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0);
}
/**
* Sets up Mockito for an instrumentation test.
*/
private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
System.setProperty("dexmaker.dexcache",
instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
MockitoAnnotations.initMocks(instrumentationTestCase);
}
}
......@@ -127,8 +127,8 @@ public final class C {
*/
@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})
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3,
ENCODING_DTS, ENCODING_DTS_HD})
public @interface Encoding {}
/**
......@@ -136,7 +136,7 @@ public final class C {
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT})
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT})
public @interface PcmEncoding {}
/**
* @see AudioFormat#ENCODING_INVALID
......@@ -159,6 +159,10 @@ public final class C {
*/
public static final int ENCODING_PCM_32BIT = 0x40000000;
/**
* @see AudioFormat#ENCODING_PCM_FLOAT
*/
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
/**
* @see AudioFormat#ENCODING_AC3
*/
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
......@@ -421,6 +425,11 @@ public final class C {
public static final int SELECTION_FLAG_AUTOSELECT = 4;
/**
* Represents an undetermined language as an ISO 639 alpha-3 language code.
*/
public static final String LANGUAGE_UNDETERMINED = "und";
/**
* Represents a streaming or other media type.
*/
@Retention(RetentionPolicy.SOURCE)
......
......@@ -51,9 +51,14 @@ public final class DefaultLoadControl implements LoadControl {
*/
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
private static final int ABOVE_HIGH_WATERMARK = 0;
private static final int BETWEEN_WATERMARKS = 1;
private static final int BELOW_LOW_WATERMARK = 2;
/**
* The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control
* automatically determines its target buffer size.
*/
public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET;
/** The default prioritization of buffer time constraints over size constraints. */
public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true;
private final DefaultAllocator allocator;
......@@ -61,6 +66,8 @@ public final class DefaultLoadControl implements LoadControl {
private final long maxBufferUs;
private final long bufferForPlaybackUs;
private final long bufferForPlaybackAfterRebufferUs;
private final int targetBufferBytesOverwrite;
private final boolean prioritizeTimeOverSizeThresholds;
private final PriorityTaskManager priorityTaskManager;
private int targetBufferSize;
......@@ -79,8 +86,14 @@ public final class DefaultLoadControl implements LoadControl {
* @param allocator The {@link DefaultAllocator} used by the loader.
*/
public DefaultLoadControl(DefaultAllocator allocator) {
this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
this(
allocator,
DEFAULT_MIN_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DEFAULT_TARGET_BUFFER_BYTES,
DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
}
/**
......@@ -96,10 +109,27 @@ public final class DefaultLoadControl implements LoadControl {
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
* buffer depletion rather than a user action.
* @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the
* target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[],
* TrackSelectionArray)}.
* @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
*/
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
public DefaultLoadControl(
DefaultAllocator allocator,
int minBufferMs,
int maxBufferMs,
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs,
int targetBufferBytes,
boolean prioritizeTimeOverSizeThresholds) {
this(
allocator,
minBufferMs,
maxBufferMs,
bufferForPlaybackMs,
bufferForPlaybackAfterRebufferMs,
targetBufferBytes,
prioritizeTimeOverSizeThresholds,
null);
}
......@@ -116,18 +146,30 @@ public final class DefaultLoadControl implements LoadControl {
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
* buffer depletion rather than a user action.
* @param priorityTaskManager If not null, registers itself as a task with priority
* {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
* periods.
* @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the
* target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[],
* TrackSelectionArray)}.
* @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
* constraints over buffer size constraints.
* @param priorityTaskManager If not null, registers itself as a task with priority {@link
* C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
*/
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
public DefaultLoadControl(
DefaultAllocator allocator,
int minBufferMs,
int maxBufferMs,
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs,
int targetBufferBytes,
boolean prioritizeTimeOverSizeThresholds,
PriorityTaskManager priorityTaskManager) {
this.allocator = allocator;
minBufferUs = minBufferMs * 1000L;
maxBufferUs = maxBufferMs * 1000L;
targetBufferBytesOverwrite = targetBufferBytes;
bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
this.priorityTaskManager = priorityTaskManager;
}
......@@ -139,12 +181,10 @@ public final class DefaultLoadControl implements LoadControl {
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
targetBufferSize = 0;
for (int i = 0; i < renderers.length; i++) {
if (trackSelections.get(i) != null) {
targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType());
}
}
targetBufferSize =
targetBufferBytesOverwrite == C.LENGTH_UNSET
? calculateTargetBufferSize(renderers, trackSelections)
: targetBufferBytesOverwrite;
allocator.setTargetBufferSize(targetBufferSize);
}
......@@ -166,16 +206,28 @@ public final class DefaultLoadControl implements LoadControl {
@Override
public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) {
long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs;
return minBufferDurationUs <= 0
|| bufferedDurationUs >= minBufferDurationUs
|| (!prioritizeTimeOverSizeThresholds
&& allocator.getTotalBytesAllocated() >= targetBufferSize);
}
@Override
public boolean shouldContinueLoading(long bufferedDurationUs) {
int bufferTimeState = getBufferTimeState(bufferedDurationUs);
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
boolean wasBuffering = isBuffering;
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
if (prioritizeTimeOverSizeThresholds) {
isBuffering =
bufferedDurationUs < minBufferUs // below low watermark
|| (bufferedDurationUs <= maxBufferUs // between watermarks
&& isBuffering
&& !targetBufferSizeReached);
} else {
isBuffering =
!targetBufferSizeReached
&& (bufferedDurationUs < minBufferUs // below low watermark
|| (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks
}
if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) {
priorityTaskManager.add(C.PRIORITY_PLAYBACK);
......@@ -186,9 +238,23 @@ public final class DefaultLoadControl implements LoadControl {
return isBuffering;
}
private int getBufferTimeState(long bufferedDurationUs) {
return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK
: (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS);
/**
* Calculate target buffer size in bytes based on the selected tracks. The player will try not to
* exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}.
*
* @param renderers The renderers for which the track were selected.
* @param trackSelectionArray The selected tracks.
* @return The target buffer size in bytes.
*/
protected int calculateTargetBufferSize(
Renderer[] renderers, TrackSelectionArray trackSelectionArray) {
int targetBufferSize = 0;
for (int i = 0; i < renderers.length; i++) {
if (trackSelectionArray.get(i) != null) {
targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType());
}
}
return targetBufferSize;
}
private void reset(boolean resetAllocator) {
......
......@@ -1666,11 +1666,11 @@ import java.io.IOException;
// Undo the effect of previous call to associate no-sample renderers with empty tracks
// so the mediaPeriod receives back whatever it sent us before.
disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams);
updatePeriodTrackSelectorResult(trackSelectorResult);
// Disable streams on the period and get new streams for updated/newly-enabled tracks.
positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
sampleStreams, streamResetFlags, positionUs);
associateNoSampleRenderersWithEmptySampleStream(sampleStreams);
periodTrackSelectorResult = trackSelectorResult;
// Update whether we have enabled tracks and sanity check the expected streams are non-null.
hasEnabledTracks = false;
......@@ -1692,6 +1692,7 @@ import java.io.IOException;
}
public void release() {
updatePeriodTrackSelectorResult(null);
try {
if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
......@@ -1704,6 +1705,36 @@ import java.io.IOException;
}
}
private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) {
if (periodTrackSelectorResult != null) {
disableTrackSelectionsInResult(periodTrackSelectorResult);
}
periodTrackSelectorResult = trackSelectorResult;
if (periodTrackSelectorResult != null) {
enableTrackSelectionsInResult(periodTrackSelectorResult);
}
}
private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
TrackSelection trackSelection = trackSelectorResult.selections.get(i);
if (rendererEnabled && trackSelection != null) {
trackSelection.enable();
}
}
}
private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
TrackSelection trackSelection = trackSelectorResult.selections.get(i);
if (rendererEnabled && trackSelection != null) {
trackSelection.disable();
}
}
}
/**
* For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy
* {@link EmptySampleStream} that was associated with it.
......
......@@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo {
* The version of the library expressed as a string, for example "1.2.3".
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.6.0";
public static final String VERSION = "2.6.1";
/**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.0";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.1";
/**
* The version of the library expressed as an integer, for example 1002003.
......@@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2006000;
public static final int VERSION_INT = 2006001;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
......@@ -368,6 +368,8 @@ public interface Player {
* @param windowIndex The index of the window.
* @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
* the window's default position.
* @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided
* {@code windowIndex} is not within the bounds of the current timeline.
*/
void seekTo(int windowIndex, long positionMs);
......
......@@ -91,6 +91,8 @@ public class SimpleExoPlayer implements ExoPlayer {
private final CopyOnWriteArraySet<VideoListener> videoListeners;
private final CopyOnWriteArraySet<TextOutput> textOutputs;
private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs;
private final CopyOnWriteArraySet<VideoRendererEventListener> videoDebugListeners;
private final CopyOnWriteArraySet<AudioRendererEventListener> audioDebugListeners;
private final int videoRendererCount;
private final int audioRendererCount;
......@@ -103,8 +105,6 @@ public class SimpleExoPlayer implements ExoPlayer {
private int videoScalingMode;
private SurfaceHolder surfaceHolder;
private TextureView textureView;
private AudioRendererEventListener audioDebugListener;
private VideoRendererEventListener videoDebugListener;
private DecoderCounters videoDecoderCounters;
private DecoderCounters audioDecoderCounters;
private int audioSessionId;
......@@ -117,6 +117,8 @@ public class SimpleExoPlayer implements ExoPlayer {
videoListeners = new CopyOnWriteArraySet<>();
textOutputs = new CopyOnWriteArraySet<>();
metadataOutputs = new CopyOnWriteArraySet<>();
videoDebugListeners = new CopyOnWriteArraySet<>();
audioDebugListeners = new CopyOnWriteArraySet<>();
Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
Handler eventHandler = new Handler(eventLooper);
renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener,
......@@ -576,18 +578,64 @@ public class SimpleExoPlayer implements ExoPlayer {
* Sets a listener to receive debug events from the video renderer.
*
* @param listener The listener.
* @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}.
*/
@Deprecated
public void setVideoDebugListener(VideoRendererEventListener listener) {
videoDebugListener = listener;
videoDebugListeners.clear();
if (listener != null) {
addVideoDebugListener(listener);
}
}
/**
* Adds a listener to receive debug events from the video renderer.
*
* @param listener The listener.
*/
public void addVideoDebugListener(VideoRendererEventListener listener) {
videoDebugListeners.add(listener);
}
/**
* Removes a listener to receive debug events from the video renderer.
*
* @param listener The listener.
*/
public void removeVideoDebugListener(VideoRendererEventListener listener) {
videoDebugListeners.remove(listener);
}
/**
* Sets a listener to receive debug events from the audio renderer.
*
* @param listener The listener.
* @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}.
*/
@Deprecated
public void setAudioDebugListener(AudioRendererEventListener listener) {
audioDebugListener = listener;
audioDebugListeners.clear();
if (listener != null) {
addAudioDebugListener(listener);
}
}
/**
* Adds a listener to receive debug events from the audio renderer.
*
* @param listener The listener.
*/
public void addAudioDebugListener(AudioRendererEventListener listener) {
audioDebugListeners.add(listener);
}
/**
* Removes a listener to receive debug events from the audio renderer.
*
* @param listener The listener.
*/
public void removeAudioDebugListener(AudioRendererEventListener listener) {
audioDebugListeners.remove(listener);
}
// ExoPlayer implementation
......@@ -678,7 +726,7 @@ public class SimpleExoPlayer implements ExoPlayer {
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
player.setPlaybackParameters(playbackParameters);
}
......@@ -817,15 +865,15 @@ public class SimpleExoPlayer implements ExoPlayer {
// Internal methods.
/**
* Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}.
* Creates the {@link ExoPlayer} implementation used by this instance.
*
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @return A new {@link ExoPlayer} instance.
*/
protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) {
protected ExoPlayer createExoPlayerImpl(
Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
return new ExoPlayerImpl(renderers, trackSelector, loadControl);
}
......@@ -877,7 +925,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onVideoEnabled(DecoderCounters counters) {
videoDecoderCounters = counters;
if (videoDebugListener != null) {
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoEnabled(counters);
}
}
......@@ -885,7 +933,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
long initializationDurationMs) {
if (videoDebugListener != null) {
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
initializationDurationMs);
}
......@@ -894,14 +942,14 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onVideoInputFormatChanged(Format format) {
videoFormat = format;
if (videoDebugListener != null) {
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoInputFormatChanged(format);
}
}
@Override
public void onDroppedFrames(int count, long elapsed) {
if (videoDebugListener != null) {
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onDroppedFrames(count, elapsed);
}
}
......@@ -913,7 +961,7 @@ public class SimpleExoPlayer implements ExoPlayer {
videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
pixelWidthHeightRatio);
}
if (videoDebugListener != null) {
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
pixelWidthHeightRatio);
}
......@@ -926,14 +974,14 @@ public class SimpleExoPlayer implements ExoPlayer {
videoListener.onRenderedFirstFrame();
}
}
if (videoDebugListener != null) {
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onRenderedFirstFrame(surface);
}
}
@Override
public void onVideoDisabled(DecoderCounters counters) {
if (videoDebugListener != null) {
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoDisabled(counters);
}
videoFormat = null;
......@@ -945,7 +993,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onAudioEnabled(DecoderCounters counters) {
audioDecoderCounters = counters;
if (audioDebugListener != null) {
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioEnabled(counters);
}
}
......@@ -953,7 +1001,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onAudioSessionId(int sessionId) {
audioSessionId = sessionId;
if (audioDebugListener != null) {
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioSessionId(sessionId);
}
}
......@@ -961,7 +1009,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
long initializationDurationMs) {
if (audioDebugListener != null) {
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,
initializationDurationMs);
}
......@@ -970,7 +1018,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onAudioInputFormatChanged(Format format) {
audioFormat = format;
if (audioDebugListener != null) {
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioInputFormatChanged(format);
}
}
......@@ -978,14 +1026,14 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs,
long elapsedSinceLastFeedMs) {
if (audioDebugListener != null) {
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}
@Override
public void onAudioDisabled(DecoderCounters counters) {
if (audioDebugListener != null) {
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioDisabled(counters);
}
audioFormat = null;
......
......@@ -15,6 +15,10 @@
*/
package com.google.android.exoplayer2.audio;
import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE0;
import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE1;
import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData;
......@@ -181,7 +185,14 @@ public final class Ac3Util {
channelCount += 2;
}
}
return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE,
String mimeType = MimeTypes.AUDIO_E_AC3;
if (data.bytesLeft() > 0) {
nextByte = data.readUnsignedByte();
if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a
mimeType = MimeTypes.AUDIO_ATMOS;
}
}
return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE,
Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
}
......@@ -198,29 +209,176 @@ public final class Ac3Util {
boolean isEac3 = data.readBits(5) == 16;
data.setPosition(initialPosition);
String mimeType;
int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED;
int streamType = STREAM_TYPE_UNDEFINED;
int sampleRate;
int acmod;
int frameSize;
int sampleCount;
boolean lfeon;
int channelCount;
if (isEac3) {
mimeType = MimeTypes.AUDIO_E_AC3;
// Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2.
data.skipBits(16); // syncword
streamType = data.readBits(2);
data.skipBits(3); // substreamid
frameSize = (data.readBits(11) + 1) * 2;
int fscod = data.readBits(2);
int audioBlocks;
int numblkscod;
if (fscod == 3) {
numblkscod = 3;
sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)];
audioBlocks = 6;
} else {
int numblkscod = data.readBits(2);
numblkscod = data.readBits(2);
audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod];
sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
}
sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks;
acmod = data.readBits(3);
lfeon = data.readBit();
channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
data.skipBits(5 + 5); // bsid, dialnorm
if (data.readBit()) { // compre
data.skipBits(8); // compr
}
if (acmod == 0) {
data.skipBits(5); // dialnorm2
if (data.readBit()) { // compr2e
data.skipBits(8); // compr2
}
}
if (streamType == STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape
data.skipBits(16); // chanmap
}
if (data.readBit()) { // mixmdate
if (acmod > 2) {
data.skipBits(2); // dmixmod
}
if ((acmod & 0x01) != 0 && acmod > 2) {
data.skipBits(3 + 3); // ltrtcmixlev, lorocmixlev
}
if ((acmod & 0x04) != 0) {
data.skipBits(6); // ltrtsurmixlev, lorosurmixlev
}
if (lfeon && data.readBit()) { // lfemixlevcode
data.skipBits(5); // lfemixlevcod
}
if (streamType == STREAM_TYPE_TYPE0) {
if (data.readBit()) { // pgmscle
data.skipBits(6); //pgmscl
}
if (acmod == 0 && data.readBit()) { // pgmscl2e
data.skipBits(6); // pgmscl2
}
if (data.readBit()) { // extpgmscle
data.skipBits(6); // extpgmscl
}
int mixdef = data.readBits(2);
if (mixdef == 1) {
data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl
} else if (mixdef == 2) {
data.skipBits(12); // mixdata
} else if (mixdef == 3) {
int mixdeflen = data.readBits(5);
if (data.readBit()) { // mixdata2e
data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl
if (data.readBit()) { // extpgmlscle
data.skipBits(4); // extpgmlscl
}
if (data.readBit()) { // extpgmcscle
data.skipBits(4); // extpgmcscl
}
if (data.readBit()) { // extpgmrscle
data.skipBits(4); // extpgmrscl
}
if (data.readBit()) { // extpgmlsscle
data.skipBits(4); // extpgmlsscl
}
if (data.readBit()) { // extpgmrsscle
data.skipBits(4); // extpgmrsscl
}
if (data.readBit()) { // extpgmlfescle
data.skipBits(4); // extpgmlfescl
}
if (data.readBit()) { // dmixscle
data.skipBits(4); // dmixscl
}
if (data.readBit()) { // addche
if (data.readBit()) { // extpgmaux1scle
data.skipBits(4); // extpgmaux1scl
}
if (data.readBit()) { // extpgmaux2scle
data.skipBits(4); // extpgmaux2scl
}
}
}
if (data.readBit()) { // mixdata3e
data.skipBits(5); // spchdat
if (data.readBit()) { // addspchdate
data.skipBits(5 + 2); // spchdat1, spchan1att
if (data.readBit()) { // addspdat1e
data.skipBits(5 + 3); // spchdat2, spchan2att
}
}
}
data.skipBits(8 * (mixdeflen + 2)); // mixdata
data.byteAlign(); // mixdatafill
}
if (acmod < 2) {
if (data.readBit()) { // paninfoe
data.skipBits(8 + 6); // panmean, paninfo
}
if (acmod == 0) {
if (data.readBit()) { // paninfo2e
data.skipBits(8 + 6); // panmean2, paninfo2
}
}
}
if (data.readBit()) { // frmmixcfginfoe
if (numblkscod == 0) {
data.skipBits(5); // blkmixcfginfo[0]
} else {
for (int blk = 0; blk < audioBlocks; blk++) {
if (data.readBit()) { // blkmixcfginfoe
data.skipBits(5); // blkmixcfginfo[blk]
}
}
}
}
}
}
if (data.readBit()) { // infomdate
data.skipBits(3 + 1 + 1); // bsmod, copyrightb, origbs
if (acmod == 2) {
data.skipBits(2 + 2); // dsurmod, dheadphonmod
}
if (acmod >= 6) {
data.skipBits(2); // dsurexmod
}
if (data.readBit()) { // audioprodie
data.skipBits(5 + 2 + 1); // mixlevel, roomtyp, adconvtyp
}
if (acmod == 0 && data.readBit()) { // audioprodi2e
data.skipBits(5 + 2 + 1); // mixlevel2, roomtyp2, adconvtyp2
}
if (fscod < 3) {
data.skipBit(); // sourcefscod
}
}
if (streamType == 0 && numblkscod != 3) {
data.skipBit(); // convsync
}
if (streamType == 2 && (numblkscod == 3 || data.readBit())) { // blkid
data.skipBits(6); // frmsizecod
}
mimeType = MimeTypes.AUDIO_E_AC3;
if (data.readBit()) { // addbsie
int addbsil = data.readBits(6);
if (addbsil == 1 && data.readBits(8) == 1) { // addbsi
mimeType = MimeTypes.AUDIO_ATMOS;
}
}
} else /* is AC-3 */ {
mimeType = MimeTypes.AUDIO_AC3;
data.skipBits(16 + 16); // syncword, crc1
......@@ -240,9 +398,9 @@ public final class Ac3Util {
}
sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
lfeon = data.readBit();
channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
}
boolean lfeon = data.readBit();
int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize,
sampleCount);
}
......
......@@ -25,14 +25,13 @@ import java.nio.ByteBuffer;
* A sink that consumes audio data.
* <p>
* Before starting playback, specify the input audio format by calling
* {@link #configure(String, int, int, int, int, int[], int, int)}.
* {@link #configure(int, int, int, int, int[], int, int)}.
* <p>
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
* when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
* <p>
* Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format
* changes. The sink will be reinitialized on the next call to
* {@link #handleBuffer(ByteBuffer, long)}.
* Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format changes.
* The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
* <p>
* Call {@link #reset()} to prepare the sink to receive audio data from a new playback position.
* <p>
......@@ -76,7 +75,7 @@ public interface AudioSink {
*
* @param bufferSize The size of the sink's buffer, in bytes.
* @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for
* PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the
* PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the
* buffered media can have a variable bitrate so the duration may be unknown.
* @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds.
*/
......@@ -166,13 +165,12 @@ public interface AudioSink {
void setListener(Listener listener);
/**
* Returns whether it's possible to play audio in the specified format using encoded audio
* passthrough.
* Returns whether it's possible to play audio in the specified encoding.
*
* @param mimeType The format mime type.
* @return Whether it's possible to play audio in the format using encoded audio passthrough.
* @param encoding The audio encoding.
* @return Whether it's possible to play audio in the specified encoding.
*/
boolean isPassthroughSupported(String mimeType);
boolean isEncodingSupported(@C.Encoding int encoding);
/**
* Returns the playback position in the stream starting at zero, in microseconds, or
......@@ -186,12 +184,9 @@ public interface AudioSink {
/**
* Configures (or reconfigures) the sink.
*
* @param inputMimeType The MIME type of audio data provided in the input buffers.
* @param inputEncoding The encoding of audio data provided in the input buffers.
* @param inputChannelCount The number of channels.
* @param inputSampleRate The sample rate in Hz.
* @param inputPcmEncoding For PCM formats, the encoding used. One of
* {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT}
* and {@link C#ENCODING_PCM_32BIT}.
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
* suitable buffer size.
* @param outputChannels A mapping from input to output channels that is applied to this sink's
......@@ -205,9 +200,9 @@ public interface AudioSink {
* immediately preceding the next call to {@link #reset()} or this method.
* @throws ConfigurationException If an error occurs configuring the sink.
*/
void configure(String inputMimeType, int inputChannelCount, int inputSampleRate,
@C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels,
int trimStartSamples, int trimEndSamples) throws ConfigurationException;
void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate,
int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples,
int trimEndSamples) throws ConfigurationException;
/**
* Starts or resumes consuming audio if initialized.
......@@ -228,8 +223,7 @@ public interface AudioSink {
* Returns whether the data was handled in full. If the data was not handled in full then the same
* {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
* except in the case of an intervening call to {@link #reset()} (or to
* {@link #configure(String, int, int, int, int, int[], int, int)} that causes the sink to be
* reset).
* {@link #configure(int, int, int, int, int[], int, int)} that causes the sink to be reset).
*
* @param buffer The buffer containing audio data.
* @param presentationTimeUs The presentation timestamp of the buffer in microseconds.
......
......@@ -51,8 +51,6 @@ import java.util.Arrays;
/**
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
* to start using the new channel map.
*
* @see AudioSink#configure(String, int, int, int, int, int[], int, int)
*/
public void setChannelMap(int[] outputChannels) {
pendingOutputChannels = outputChannels;
......
......@@ -51,6 +51,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private boolean passthroughEnabled;
private boolean codecNeedsDiscardChannelsWorkaround;
private android.media.MediaFormat passthroughMediaFormat;
@C.Encoding
private int pcmEncoding;
private int channelCount;
private int encoderDelay;
......@@ -177,6 +178,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
&& mediaCodecSelector.getPassthroughDecoderInfo() != null) {
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
}
if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.isEncodingSupported(format.pcmEncoding))
|| !audioSink.isEncodingSupported(C.ENCODING_PCM_16BIT)) {
// Assume the decoder outputs 16-bit PCM, unless the input is raw.
return FORMAT_UNSUPPORTED_SUBTYPE;
}
boolean requiresSecureDecryption = false;
DrmInitData drmInitData = format.drmInitData;
if (drmInitData != null) {
......@@ -219,14 +225,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
/**
* Returns whether encoded audio passthrough should be used for playing back the input format.
* This implementation returns true if the {@link AudioSink} indicates that passthrough is
* supported.
* This implementation returns true if the {@link AudioSink} indicates that encoded audio output
* is supported.
*
* @param mimeType The type of input media.
* @return Whether passthrough playback is supported.
*/
protected boolean allowPassthrough(String mimeType) {
return audioSink.isPassthroughSupported(mimeType);
@C.Encoding int encoding = MimeTypes.getEncoding(mimeType);
return encoding != C.ENCODING_INVALID && audioSink.isEncodingSupported(encoding);
}
@Override
......@@ -272,10 +279,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
throws ExoPlaybackException {
boolean passthrough = passthroughMediaFormat != null;
String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME)
: MimeTypes.AUDIO_RAW;
MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
@C.Encoding int encoding;
MediaFormat format;
if (passthroughMediaFormat != null) {
encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME));
format = passthroughMediaFormat;
} else {
encoding = pcmEncoding;
format = outputFormat;
}
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int[] channelMap;
......@@ -289,8 +301,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
try {
audioSink.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap,
encoderDelay, encoderPadding);
audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay,
encoderPadding);
} catch (AudioSink.ConfigurationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
......
......@@ -200,6 +200,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
protected abstract int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
Format format);
/**
* Returns whether the audio sink can accept audio in the specified encoding.
*
* @param encoding The audio encoding.
* @return Whether the audio sink can accept audio in the specified encoding.
*/
protected final boolean supportsOutputEncoding(@C.Encoding int encoding) {
return audioSink.isEncodingSupported(encoding);
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) {
......@@ -329,8 +339,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
if (audioTrackNeedsConfigure) {
Format outputFormat = getOutputFormat();
audioSink.configure(outputFormat.sampleMimeType, outputFormat.channelCount,
outputFormat.sampleRate, outputFormat.pcmEncoding, 0, null, encoderDelay, encoderPadding);
audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount,
outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding);
audioTrackNeedsConfigure = false;
}
......
......@@ -28,13 +28,24 @@ public interface SeekMap {
final class Unseekable implements SeekMap {
private final long durationUs;
private final long startPosition;
/**
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
* the duration is unknown.
*/
public Unseekable(long durationUs) {
this(durationUs, 0);
}
/**
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
* the duration is unknown.
* @param startPosition The position (byte offset) of the start of the media.
*/
public Unseekable(long durationUs, long startPosition) {
this.durationUs = durationUs;
this.startPosition = startPosition;
}
@Override
......@@ -49,7 +60,7 @@ public interface SeekMap {
@Override
public long getPosition(long timeUs) {
return 0;
return startPosition;
}
}
......@@ -78,7 +89,8 @@ public interface SeekMap {
*
* @param timeUs A seek position in microseconds.
* @return The corresponding position (byte offset) in the stream from which data can be provided
* to the extractor, or 0 if {@code #isSeekable()} returns false.
* to the extractor. If {@link #isSeekable()} returns false then the returned value will be
* independent of {@code timeUs}, and will indicate the start of the media in the stream.
*/
long getPosition(long timeUs);
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.flv;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
......@@ -25,11 +26,13 @@ import com.google.android.exoplayer2.extractor.SeekMap;
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;
/**
* Facilitates the extraction of data from the FLV container format.
* Extracts data from the FLV container format.
*/
public final class FlvExtractor implements Extractor, SeekMap {
public final class FlvExtractor implements Extractor {
/**
* Factory for {@link FlvExtractor} instances.
......@@ -43,16 +46,22 @@ public final class FlvExtractor implements Extractor, SeekMap {
};
// Header sizes.
private static final int FLV_HEADER_SIZE = 9;
private static final int FLV_TAG_HEADER_SIZE = 11;
// Parser states.
/**
* Extractor states.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_READING_FLV_HEADER, STATE_SKIPPING_TO_TAG_HEADER, STATE_READING_TAG_HEADER,
STATE_READING_TAG_DATA})
private @interface States {}
private static final int STATE_READING_FLV_HEADER = 1;
private static final int STATE_SKIPPING_TO_TAG_HEADER = 2;
private static final int STATE_READING_TAG_HEADER = 3;
private static final int STATE_READING_TAG_DATA = 4;
// Header sizes.
private static final int FLV_HEADER_SIZE = 9;
private static final int FLV_TAG_HEADER_SIZE = 11;
// Tag types.
private static final int TAG_TYPE_AUDIO = 8;
private static final int TAG_TYPE_VIDEO = 9;
......@@ -61,33 +70,31 @@ public final class FlvExtractor implements Extractor, SeekMap {
// FLV container identifier.
private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
// Temporary buffers.
private final ParsableByteArray scratch;
private final ParsableByteArray headerBuffer;
private final ParsableByteArray tagHeaderBuffer;
private final ParsableByteArray tagData;
private final ScriptTagPayloadReader metadataReader;
// Extractor outputs.
private ExtractorOutput extractorOutput;
// State variables.
private int parserState;
private @States int state;
private long mediaTagTimestampOffsetUs;
private int bytesToNextTagHeader;
public int tagType;
public int tagDataSize;
public long tagTimestampUs;
// Tags readers.
private int tagType;
private int tagDataSize;
private long tagTimestampUs;
private boolean outputSeekMap;
private AudioTagPayloadReader audioReader;
private VideoTagPayloadReader videoReader;
private ScriptTagPayloadReader metadataReader;
public FlvExtractor() {
scratch = new ParsableByteArray(4);
headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
tagData = new ParsableByteArray();
parserState = STATE_READING_FLV_HEADER;
metadataReader = new ScriptTagPayloadReader();
state = STATE_READING_FLV_HEADER;
mediaTagTimestampOffsetUs = C.TIME_UNSET;
}
@Override
......@@ -128,7 +135,8 @@ public final class FlvExtractor implements Extractor, SeekMap {
@Override
public void seek(long position, long timeUs) {
parserState = STATE_READING_FLV_HEADER;
state = STATE_READING_FLV_HEADER;
mediaTagTimestampOffsetUs = C.TIME_UNSET;
bytesToNextTagHeader = 0;
}
......@@ -141,7 +149,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
InterruptedException {
while (true) {
switch (parserState) {
switch (state) {
case STATE_READING_FLV_HEADER:
if (!readFlvHeader(input)) {
return RESULT_END_OF_INPUT;
......@@ -160,6 +168,9 @@ public final class FlvExtractor implements Extractor, SeekMap {
return RESULT_CONTINUE;
}
break;
default:
// Never happens.
throw new IllegalStateException();
}
}
}
......@@ -191,15 +202,11 @@ public final class FlvExtractor implements Extractor, SeekMap {
videoReader = new VideoTagPayloadReader(
extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
}
if (metadataReader == null) {
metadataReader = new ScriptTagPayloadReader(null);
}
extractorOutput.endTracks();
extractorOutput.seekMap(this);
// We need to skip any additional content in the FLV header, plus the 4 byte previous tag size.
bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4;
parserState = STATE_SKIPPING_TO_TAG_HEADER;
state = STATE_SKIPPING_TO_TAG_HEADER;
return true;
}
......@@ -213,7 +220,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException {
input.skipFully(bytesToNextTagHeader);
bytesToNextTagHeader = 0;
parserState = STATE_READING_TAG_HEADER;
state = STATE_READING_TAG_HEADER;
}
/**
......@@ -236,7 +243,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
tagTimestampUs = tagHeaderBuffer.readUnsignedInt24();
tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L;
tagHeaderBuffer.skipBytes(3); // streamId
parserState = STATE_READING_TAG_DATA;
state = STATE_READING_TAG_DATA;
return true;
}
......@@ -251,17 +258,24 @@ public final class FlvExtractor implements Extractor, SeekMap {
private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
boolean wasConsumed = true;
if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
audioReader.consume(prepareTagData(input), tagTimestampUs);
ensureReadyForMediaOutput();
audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs);
} else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
videoReader.consume(prepareTagData(input), tagTimestampUs);
} else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
ensureReadyForMediaOutput();
videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs);
} else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) {
metadataReader.consume(prepareTagData(input), tagTimestampUs);
long durationUs = metadataReader.getDurationUs();
if (durationUs != C.TIME_UNSET) {
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
outputSeekMap = true;
}
} else {
input.skipFully(tagDataSize);
wasConsumed = false;
}
bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
parserState = STATE_SKIPPING_TO_TAG_HEADER;
state = STATE_SKIPPING_TO_TAG_HEADER;
return wasConsumed;
}
......@@ -277,21 +291,15 @@ public final class FlvExtractor implements Extractor, SeekMap {
return tagData;
}
// SeekMap implementation.
@Override
public boolean isSeekable() {
return false;
}
@Override
public long getDurationUs() {
return metadataReader.getDurationUs();
}
@Override
public long getPosition(long timeUs) {
return 0;
private void ensureReadyForMediaOutput() {
if (!outputSeekMap) {
extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
outputSeekMap = true;
}
if (mediaTagTimestampOffsetUs == C.TIME_UNSET) {
mediaTagTimestampOffsetUs =
metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
}
}
}
......@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.flv;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Date;
......@@ -44,11 +43,8 @@ import java.util.Map;
private long durationUs;
/**
* @param output A {@link TrackOutput} to which samples should be written.
*/
public ScriptTagPayloadReader(TrackOutput output) {
super(output);
public ScriptTagPayloadReader() {
super(null);
durationUs = C.TIME_UNSET;
}
......
......@@ -53,7 +53,7 @@ import java.util.Locale;
import java.util.UUID;
/**
* Extracts data from a Matroska or WebM file.
* Extracts data from the Matroska and WebM container formats.
*/
public final class MatroskaExtractor implements Extractor {
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.Util;
/**
......@@ -26,27 +27,46 @@ import com.google.android.exoplayer2.util.Util;
private static final int BITS_PER_BYTE = 8;
private final long firstFramePosition;
private final int frameSize;
private final long dataSize;
private final int bitrate;
private final long durationUs;
public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) {
/**
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param firstFramePosition The position of the first frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the first frame.
*/
public ConstantBitrateSeeker(long inputLength, long firstFramePosition,
MpegAudioHeader mpegAudioHeader) {
this.firstFramePosition = firstFramePosition;
this.bitrate = bitrate;
durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength);
this.frameSize = mpegAudioHeader.frameSize;
this.bitrate = mpegAudioHeader.bitrate;
if (inputLength == C.LENGTH_UNSET) {
dataSize = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET;
} else {
dataSize = inputLength - firstFramePosition;
durationUs = getTimeUs(inputLength);
}
}
@Override
public boolean isSeekable() {
return durationUs != C.TIME_UNSET;
return dataSize != C.LENGTH_UNSET;
}
@Override
public long getPosition(long timeUs) {
if (durationUs == C.TIME_UNSET) {
return 0;
if (dataSize == C.LENGTH_UNSET) {
return firstFramePosition;
}
timeUs = Util.constrainValue(timeUs, 0, durationUs);
return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
// Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / frameSize) * frameSize;
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize);
// Add data start position.
return firstFramePosition + positionOffset;
}
@Override
......
......@@ -38,7 +38,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Extracts data from an MP3 file.
* Extracts data from the MP3 container format.
*/
public final class Mp3Extractor implements Extractor {
......@@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor {
int seekHeader = getSeekFrameHeader(frame, xingBase);
Seeker seeker;
if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) {
seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
// If there is a Xing header, read gapless playback metadata at a fixed offset.
input.resetPeekPosition();
......@@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor {
return getConstantBitrateSeeker(input);
}
} else if (seekHeader == SEEK_HEADER_VBRI) {
seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
input.skipFully(synchronizedHeader.frameSize);
} else { // seekerHeader == SEEK_HEADER_UNSET
// This frame doesn't contain seeking information, so reset the peek position.
......@@ -393,8 +393,7 @@ public final class Mp3Extractor implements Extractor {
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate,
input.getLength());
return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader);
}
/**
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.ParsableByteArray;
......@@ -25,21 +26,23 @@ import com.google.android.exoplayer2.util.Util;
*/
/* package */ final class VbriSeeker implements Mp3Extractor.Seeker {
private static final String TAG = "VbriSeeker";
/**
* Returns a {@link VbriSeeker} for seeking in the stream, if required information is present.
* Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it.
*
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param position The position of the start of this frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the frame.
* @param frame The data in this audio frame, with its position set to immediately after the
* 'VBRI' tag.
* @param position The position (byte offset) of the start of this frame in the stream.
* @param inputLength The length of the stream in bytes.
* @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
* information is not present.
*/
public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
long position, long inputLength) {
public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
ParsableByteArray frame) {
frame.skipBytes(10);
int numFrames = frame.readInt();
if (numFrames <= 0) {
......@@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util;
int entrySize = frame.readUnsignedShort();
frame.skipBytes(2);
// Skip the frame containing the VBRI header.
position += mpegAudioHeader.frameSize;
long minPosition = position + mpegAudioHeader.frameSize;
// Read table of contents entries.
long[] timesUs = new long[entryCount + 1];
long[] positions = new long[entryCount + 1];
timesUs[0] = 0L;
positions[0] = position;
for (int index = 1; index < timesUs.length; index++) {
long[] timesUs = new long[entryCount];
long[] positions = new long[entryCount];
for (int index = 0; index < entryCount; index++) {
timesUs[index] = (index * durationUs) / entryCount;
// Ensure positions do not fall within the frame containing the VBRI header. This constraint
// will normally only apply to the first entry in the table.
positions[index] = Math.max(position, minPosition);
int segmentSize;
switch (entrySize) {
case 1:
......@@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
position += segmentSize * scale;
timesUs[index] = index * durationUs / entryCount;
positions[index] =
inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position);
}
if (inputLength != C.LENGTH_UNSET && inputLength != position) {
Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position);
}
return new VbriSeeker(timesUs, positions, durationUs);
}
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.ParsableByteArray;
......@@ -25,24 +26,25 @@ import com.google.android.exoplayer2.util.Util;
*/
/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
private static final String TAG = "XingSeeker";
/**
* Returns a {@link XingSeeker} for seeking in the stream, if required information is present.
* Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it.
*
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param position The position of the start of this frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the frame.
* @param frame The data in this audio frame, with its position set to immediately after the
* 'Xing' or 'Info' tag.
* @param position The position (byte offset) of the start of this frame in the stream.
* @param inputLength The length of the stream in bytes.
* @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
* information is not present.
*/
public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
long position, long inputLength) {
public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
ParsableByteArray frame) {
int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
int sampleRate = mpegAudioHeader.sampleRate;
long firstFramePosition = position + mpegAudioHeader.frameSize;
int flags = frame.readInt();
int frameCount;
......@@ -54,45 +56,49 @@ import com.google.android.exoplayer2.util.Util;
sampleRate);
if ((flags & 0x06) != 0x06) {
// If the size in bytes or table of contents is missing, the stream is not seekable.
return new XingSeeker(firstFramePosition, durationUs, inputLength);
return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs);
}
long sizeBytes = frame.readUnsignedIntToInt();
frame.skipBytes(1);
long[] tableOfContents = new long[99];
for (int i = 0; i < 99; i++) {
long dataSize = frame.readUnsignedIntToInt();
long[] tableOfContents = new long[100];
for (int i = 0; i < 100; i++) {
tableOfContents[i] = frame.readUnsignedByte();
}
// TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
// delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
// padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte();
return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents,
sizeBytes, mpegAudioHeader.frameSize);
if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) {
Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize));
}
return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs, dataSize,
tableOfContents);
}
private final long firstFramePosition;
private final long dataStartPosition;
private final int xingFrameSize;
private final long durationUs;
private final long inputLength;
/**
* Data size, including the XING frame.
*/
private final long dataSize;
/**
* Entries are in the range [0, 255], but are stored as long integers for convenience.
*/
private final long[] tableOfContents;
private final long sizeBytes;
private final int headerSize;
private XingSeeker(long firstFramePosition, long durationUs, long inputLength) {
this(firstFramePosition, durationUs, inputLength, null, 0, 0);
private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) {
this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null);
}
private XingSeeker(long firstFramePosition, long durationUs, long inputLength,
long[] tableOfContents, long sizeBytes, int headerSize) {
this.firstFramePosition = firstFramePosition;
private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize,
long[] tableOfContents) {
this.dataStartPosition = dataStartPosition;
this.xingFrameSize = xingFrameSize;
this.durationUs = durationUs;
this.inputLength = inputLength;
this.dataSize = dataSize;
this.tableOfContents = tableOfContents;
this.sizeBytes = sizeBytes;
this.headerSize = headerSize;
}
@Override
......@@ -103,53 +109,45 @@ import com.google.android.exoplayer2.util.Util;
@Override
public long getPosition(long timeUs) {
if (!isSeekable()) {
return firstFramePosition;
return dataStartPosition + xingFrameSize;
}
float percent = timeUs * 100f / durationUs;
float fx;
if (percent <= 0f) {
fx = 0f;
} else if (percent >= 100f) {
fx = 256f;
double percent = (timeUs * 100d) / durationUs;
double scaledPosition;
if (percent <= 0) {
scaledPosition = 0;
} else if (percent >= 100) {
scaledPosition = 256;
} else {
int a = (int) percent;
float fa, fb;
if (a == 0) {
fa = 0f;
} else {
fa = tableOfContents[a - 1];
}
if (a < 99) {
fb = tableOfContents[a];
} else {
fb = 256f;
}
fx = fa + (fb - fa) * (percent - a);
int prevTableIndex = (int) percent;
double prevScaledPosition = tableOfContents[prevTableIndex];
double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
// Linearly interpolate between the two scaled positions.
double interpolateFraction = percent - prevTableIndex;
scaledPosition = prevScaledPosition
+ (interpolateFraction * (nextScaledPosition - prevScaledPosition));
}
long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition;
long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1
: firstFramePosition - headerSize + sizeBytes - 1;
return Math.min(position, maximumPosition);
long positionOffset = Math.round((scaledPosition / 256) * dataSize);
// Ensure returned positions skip the frame containing the XING header.
positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
return dataStartPosition + positionOffset;
}
@Override
public long getTimeUs(long position) {
if (!isSeekable() || position < firstFramePosition) {
long positionOffset = position - dataStartPosition;
if (!isSeekable() || positionOffset <= xingFrameSize) {
return 0L;
}
double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes;
int previousTocPosition =
Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1;
long previousTime = getTimeUsForTocPosition(previousTocPosition);
// Linearly interpolate the time taking into account the next entry.
long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1];
long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition];
long nextTime = getTimeUsForTocPosition(previousTocPosition + 1);
long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime)
* (offsetByte - previousByte) / (nextByte - previousByte));
return previousTime + timeOffset;
double scaledPosition = (positionOffset * 256d) / dataSize;
int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true);
long prevTimeUs = getTimeUsForTableIndex(prevTableIndex);
long prevScaledPosition = tableOfContents[prevTableIndex];
long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1);
long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
// Linearly interpolate between the two table entries.
double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0
: ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition));
return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs));
}
@Override
......@@ -158,11 +156,13 @@ import com.google.android.exoplayer2.util.Util;
}
/**
* Returns the time in microseconds corresponding to a table of contents position, which is
* interpreted as a percentage of the stream's duration between 0 and 100.
* Returns the time in microseconds for a given table index.
*
* @param tableIndex A table index in the range [0, 100].
* @return The corresponding time in microseconds.
*/
private long getTimeUsForTocPosition(int tocPosition) {
return durationUs * tocPosition / 100;
private long getTimeUsForTableIndex(int tableIndex) {
return (durationUs * tableIndex) / 100;
}
}
......@@ -46,13 +46,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.UUID;
/**
* Facilitates the extraction of data from the fragmented mp4 container format.
* Extracts data from the FMP4 container format.
*/
public final class FragmentedMp4Extractor implements Extractor {
......@@ -73,8 +74,8 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK,
FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS})
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED,
FLAG_WORKAROUND_IGNORE_EDIT_LISTS})
public @interface Flags {}
/**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
......@@ -94,19 +95,14 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
public static final int FLAG_ENABLE_EMSG_TRACK = 4;
/**
* Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages
* contained within SEI NAL units in the stream will be delivered as samples to this track.
*/
public static final int FLAG_ENABLE_CEA608_TRACK = 8;
/**
* Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
* container.
*/
private static final int FLAG_SIDELOADED = 16;
private static final int FLAG_SIDELOADED = 8;
/**
* Flag to ignore any edit lists in the stream.
*/
public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 32;
public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 16;
private static final String TAG = "FragmentedMp4Extractor";
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
......@@ -124,7 +120,8 @@ public final class FragmentedMp4Extractor implements Extractor {
@Flags private final int flags;
private final Track sideloadedTrack;
// Manifest DRM data.
// Sideloaded data.
private final List<Format> closedCaptionFormats;
private final DrmInitData sideloadedDrmInitData;
// Track-linked data bundle, accessible as a whole through trackID.
......@@ -193,15 +190,33 @@ public final class FragmentedMp4Extractor implements Extractor {
* @param flags Flags that control the extractor's behavior.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor
* will not receive a moov box in the input data.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks.
* will not receive a moov box in the input data. Null if a moov box is expected.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
* pssh boxes (if present) will be used.
*/
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
Track sideloadedTrack, DrmInitData sideloadedDrmInitData) {
this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData,
Collections.<Format>emptyList());
}
/**
* @param flags Flags that control the extractor's behavior.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor
* will not receive a moov box in the input data. Null if a moov box is expected.
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
* pssh boxes (if present) will be used.
* @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
* caption channels to expose.
*/
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List<Format> closedCaptionFormats) {
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
this.timestampAdjuster = timestampAdjuster;
this.sideloadedTrack = sideloadedTrack;
this.sideloadedDrmInitData = sideloadedDrmInitData;
this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats);
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalPrefix = new ParsableByteArray(5);
......@@ -330,7 +345,8 @@ public final class FragmentedMp4Extractor implements Extractor {
currentTrackBundle = null;
endOfMdatPosition = atomPosition + atomSize;
if (!haveOutputSeekMap) {
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
// This must be the first mdat in the stream.
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition));
haveOutputSeekMap = true;
}
parserState = STATE_READING_ENCRYPTION_DATA;
......@@ -483,12 +499,13 @@ public final class FragmentedMp4Extractor implements Extractor {
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
Format.OFFSET_SAMPLE_RELATIVE));
}
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) {
TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1,
C.TRACK_TYPE_TEXT);
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0,
null));
cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput};
if (cea608TrackOutputs == null) {
cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()];
for (int i = 0; i < cea608TrackOutputs.length; i++) {
TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT);
output.format(closedCaptionFormats.get(i));
cea608TrackOutputs[i] = output;
}
}
}
......@@ -1123,7 +1140,7 @@ public final class FragmentedMp4Extractor implements Extractor {
output.sampleData(nalStartCode, 4);
// Write the NAL unit type byte.
output.sampleData(nalPrefix, 1);
processSeiNalUnitPayload = cea608TrackOutputs != null
processSeiNalUnitPayload = cea608TrackOutputs.length > 0
&& NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
sampleBytesWritten += 5;
sampleSize += nalUnitLengthFieldLengthDiff;
......
......@@ -41,7 +41,7 @@ import java.util.List;
import java.util.Stack;
/**
* Extracts data from an unfragmented MP4 file.
* Extracts data from the MP4 container format.
*/
public final class Mp4Extractor implements Extractor, SeekMap {
......
......@@ -186,7 +186,7 @@ import java.io.IOException;
return start;
}
long offset = pageSize * (granuleDistance <= 0 ? 2 : 1);
long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
long nextPosition = input.getPosition() - offset
+ (granuleDistance * (end - start) / (endGranule - startGranule));
......
......@@ -118,8 +118,9 @@ import java.util.List;
case 14:
case 15:
return 256 << (blockSizeCode - 8);
default:
return -1;
}
return -1;
}
private class FlacOggSeeker implements OggSeeker, SeekMap {
......
......@@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
/**
* Ogg {@link Extractor}.
* Extracts data from the Ogg container format.
*/
public class OggExtractor implements Extractor {
......
......@@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
* Extracts CEA data from a RawCC file.
* Extracts data from the RawCC container format.
*/
public final class RawCcExtractor implements Extractor {
......
......@@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
* Extracts samples from (E-)AC-3 bitstreams.
* Extracts data from (E-)AC-3 bitstreams.
*/
public final class Ac3Extractor implements Extractor {
......
......@@ -39,7 +39,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
private static final int STATE_READING_HEADER = 1;
private static final int STATE_READING_SAMPLE = 2;
private static final int HEADER_SIZE = 8;
private static final int HEADER_SIZE = 128;
private final ParsableBitArray headerScratchBits;
private final ParsableByteArray headerScratchBytes;
......
......@@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
* Extracts samples from AAC bit streams with ADTS framing.
* Extracts data from AAC bit streams with ADTS framing.
*/
public final class AdtsExtractor implements Extractor {
......
......@@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.IOException;
/**
* Facilitates the extraction of data from the MPEG-2 PS container format.
* Extracts data from the MPEG-2 PS container format.
*/
public final class PsExtractor implements Extractor {
......
......@@ -45,7 +45,7 @@ import java.util.Collections;
import java.util.List;
/**
* Facilitates the extraction of data from the MPEG-2 TS container format.
* Extracts data from the MPEG-2 TS container format.
*/
public final class TsExtractor implements Extractor {
......
......@@ -23,13 +23,14 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
/** {@link Extractor} to extract samples from a WAV byte stream. */
public final class WavExtractor implements Extractor, SeekMap {
/**
* Extracts data from WAV byte streams.
*/
public final class WavExtractor implements Extractor {
/**
* Factory for {@link WavExtractor} instances.
......@@ -93,7 +94,7 @@ public final class WavExtractor implements Extractor, SeekMap {
if (!wavHeader.hasDataBounds()) {
WavHeaderReader.skipToData(input, wavHeader);
extractorOutput.seekMap(this);
extractorOutput.seekMap(wavHeader);
}
int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true);
......@@ -113,20 +114,4 @@ public final class WavExtractor implements Extractor, SeekMap {
return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
}
// SeekMap implementation.
@Override
public long getDurationUs() {
return wavHeader.getDurationUs();
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getPosition(long timeUs) {
return wavHeader.getPosition(timeUs);
}
}
......@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.extractor.wav;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Util;
/** Header for a WAV file. */
/*package*/ final class WavHeader {
/* package */ final class WavHeader implements SeekMap {
/** Number of audio chanels. */
private final int numChannels;
......@@ -49,12 +51,58 @@ import com.google.android.exoplayer2.C;
this.encoding = encoding;
}
/** Returns the duration in microseconds of this WAV. */
// Setting bounds.
/**
* Sets the data start position and size in bytes of sample data in this WAV.
*
* @param dataStartPosition The data start position in bytes.
* @param dataSize The data size in bytes.
*/
public void setDataBounds(long dataStartPosition, long dataSize) {
this.dataStartPosition = dataStartPosition;
this.dataSize = dataSize;
}
/** Returns whether the data start position and size have been set. */
public boolean hasDataBounds() {
return dataStartPosition != 0 && dataSize != 0;
}
// SeekMap implementation.
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getDurationUs() {
long numFrames = dataSize / blockAlignment;
return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz;
}
@Override
public long getPosition(long timeUs) {
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
// Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / blockAlignment) * blockAlignment;
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment);
// Add data start position.
return dataStartPosition + positionOffset;
}
// Misc getters.
/**
* Returns the time in microseconds for the given position in bytes.
*
* @param position The position in bytes.
*/
public long getTimeUs(long position) {
return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
}
/** Returns the bytes per frame of this WAV. */
public int getBytesPerFrame() {
return blockAlignment;
......@@ -75,33 +123,8 @@ import com.google.android.exoplayer2.C;
return numChannels;
}
/** Returns the position in bytes in this WAV for the given time in microseconds. */
public long getPosition(long timeUs) {
long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
// Round down to nearest frame.
long position = (unroundedPosition / blockAlignment) * blockAlignment;
return Math.min(position, dataSize - blockAlignment) + dataStartPosition;
}
/** Returns the time in microseconds for the given position in bytes in this WAV. */
public long getTimeUs(long position) {
return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
}
/** Returns true if the data start position and size have been set. */
public boolean hasDataBounds() {
return dataStartPosition != 0 && dataSize != 0;
}
/** Sets the start position and size in bytes of sample data in this WAV. */
public void setDataBounds(long dataStartPosition, long dataSize) {
this.dataStartPosition = dataStartPosition;
this.dataSize = dataSize;
}
/** Returns the PCM encoding. **/
@C.PcmEncoding
public int getEncoding() {
public @C.PcmEncoding int getEncoding() {
return encoding;
}
......
......@@ -31,6 +31,8 @@ import java.io.IOException;
/** Integer PCM audio data. */
private static final int TYPE_PCM = 0x0001;
/** Float PCM audio data. */
private static final int TYPE_FLOAT = 0x0003;
/** Extended WAVE format. */
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
......@@ -87,14 +89,22 @@ import java.io.IOException;
+ blockAlignment);
}
@C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample);
if (encoding == C.ENCODING_INVALID) {
Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample);
return null;
@C.PcmEncoding int encoding;
switch (type) {
case TYPE_PCM:
case TYPE_WAVE_FORMAT_EXTENSIBLE:
encoding = Util.getPcmEncoding(bitsPerSample);
break;
case TYPE_FLOAT:
encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;
break;
default:
Log.e(TAG, "Unsupported WAV format type: " + type);
return null;
}
if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) {
Log.e(TAG, "Unsupported WAV format type: " + type);
if (encoding == C.ENCODING_INVALID) {
Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type);
return null;
}
......
......@@ -20,6 +20,7 @@ import android.annotation.TargetApi;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecList;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
......@@ -120,7 +121,7 @@ public final class MediaCodecUtil {
* exists.
* @throws DecoderQueryException If there was an error querying the available decoders.
*/
public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
throws DecoderQueryException {
List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure);
return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
......@@ -140,27 +141,34 @@ public final class MediaCodecUtil {
public static synchronized List<MediaCodecInfo> getDecoderInfos(String mimeType,
boolean secure) throws DecoderQueryException {
CodecKey key = new CodecKey(mimeType, secure);
List<MediaCodecInfo> decoderInfos = decoderInfosCache.get(key);
if (decoderInfos != null) {
return decoderInfos;
List<MediaCodecInfo> cachedDecoderInfos = decoderInfosCache.get(key);
if (cachedDecoderInfos != null) {
return cachedDecoderInfos;
}
MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21
? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16();
decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
ArrayList<MediaCodecInfo> decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType);
if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) {
// Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the
// legacy path. We also try this path on API levels 22 and 23 as a defensive measure.
mediaCodecList = new MediaCodecListCompatV16();
decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType);
if (!decoderInfos.isEmpty()) {
Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
+ ". Assuming: " + decoderInfos.get(0).name);
}
}
if (MimeTypes.AUDIO_ATMOS.equals(mimeType)) {
// E-AC3 decoders can decode Atmos streams, but in 2-D rather than 3-D.
CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure);
ArrayList<MediaCodecInfo> eac3DecoderInfos =
getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType);
decoderInfos.addAll(eac3DecoderInfos);
}
applyWorkarounds(decoderInfos);
decoderInfos = Collections.unmodifiableList(decoderInfos);
decoderInfosCache.put(key, decoderInfos);
return decoderInfos;
List<MediaCodecInfo> unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos);
decoderInfosCache.put(key, unmodifiableDecoderInfos);
return unmodifiableDecoderInfos;
}
/**
......@@ -212,10 +220,21 @@ public final class MediaCodecUtil {
// Internal methods.
private static List<MediaCodecInfo> getDecoderInfosInternal(
CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
/**
* Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by
* {@code mediaCodecList}.
*
* @param key The codec key.
* @param mediaCodecList The codec list.
* @param requestedMimeType The originally requested MIME type, which may differ from the codec
* key MIME type if the codec key is being considered as a fallback.
* @return The codec information for usable codecs matching the specified key.
* @throws DecoderQueryException If there was an error querying the available decoders.
*/
private static ArrayList<MediaCodecInfo> getDecoderInfosInternal(CodecKey key,
MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException {
try {
List<MediaCodecInfo> decoderInfos = new ArrayList<>();
ArrayList<MediaCodecInfo> decoderInfos = new ArrayList<>();
String mimeType = key.mimeType;
int numberOfCodecs = mediaCodecList.getCodecCount();
boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();
......@@ -223,7 +242,7 @@ public final class MediaCodecUtil {
for (int i = 0; i < numberOfCodecs; i++) {
android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);
String codecName = codecInfo.getName();
if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) {
if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit, requestedMimeType)) {
for (String supportedType : codecInfo.getSupportedTypes()) {
if (supportedType.equalsIgnoreCase(mimeType)) {
try {
......@@ -265,9 +284,16 @@ public final class MediaCodecUtil {
/**
* Returns whether the specified codec is usable for decoding on the current device.
*
* @param info The codec information.
* @param name The name of the codec
* @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.
* @param requestedMimeType The originally requested MIME type, which may differ from the codec
* key MIME type if the codec key is being considered as a fallback.
* @return Whether the specified codec is usable for decoding on the current device.
*/
private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name,
boolean secureDecodersExplicit) {
boolean secureDecodersExplicit, String requestedMimeType) {
if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) {
return false;
}
......@@ -356,6 +382,12 @@ public final class MediaCodecUtil {
return false;
}
// MTK E-AC3 decoder doesn't support decoding Atmos streams in 2-D. See [Internal: b/69400041].
if (MimeTypes.AUDIO_ATMOS.equals(requestedMimeType)
&& "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
return false;
}
return true;
}
......
......@@ -112,7 +112,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
if (internalStreams[i] == null) {
sampleStreams[i] = null;
} else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) {
sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs,
sampleStreams[i] = new ClippingSampleStream(internalStreams[i], startUs, endUs,
pendingInitialDiscontinuity);
}
streams[i] = sampleStreams[i];
......@@ -222,9 +222,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
/**
* Wraps a {@link SampleStream} and clips its samples.
*/
private static final class ClippingSampleStream implements SampleStream {
private final class ClippingSampleStream implements SampleStream {
private final MediaPeriod mediaPeriod;
private final SampleStream stream;
private final long startUs;
private final long endUs;
......@@ -232,9 +231,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
private boolean pendingDiscontinuity;
private boolean sentEos;
public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs,
long endUs, boolean pendingDiscontinuity) {
this.mediaPeriod = mediaPeriod;
public ClippingSampleStream(SampleStream stream, long startUs, long endUs,
boolean pendingDiscontinuity) {
this.stream = stream;
this.startUs = startUs;
this.endUs = endUs;
......@@ -278,9 +276,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding);
return C.RESULT_FORMAT_READ;
}
if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ
&& buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ
&& mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
if (endUs != C.TIME_END_OF_SOURCE
&& ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)
|| (result == C.RESULT_NOTHING_READ
&& getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
buffer.clear();
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
sentEos = true;
......
/*
* Copyright (C) 2017 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.source;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
/**
* Media period that wraps a media source and defers calling its
* {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link #createPeriod()}
* has been called. This is useful if you need to return a media period immediately but the media
* source that should create it is not yet prepared.
*/
public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
public final MediaSource mediaSource;
private final MediaPeriodId id;
private final Allocator allocator;
private MediaPeriod mediaPeriod;
private Callback callback;
private long preparePositionUs;
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
this.id = id;
this.allocator = allocator;
this.mediaSource = mediaSource;
}
/**
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()}
* to release the period.
*/
public void createPeriod() {
mediaPeriod = mediaSource.createPeriod(id, allocator);
if (callback != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
/**
* Releases the period.
*/
public void releasePeriod() {
if (mediaPeriod != null) {
mediaSource.releasePeriod(mediaPeriod);
}
}
@Override
public void prepare(Callback callback, long preparePositionUs) {
this.callback = callback;
this.preparePositionUs = preparePositionUs;
if (mediaPeriod != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
} else {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public TrackGroupArray getTrackGroups() {
return mediaPeriod.getTrackGroups();
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs);
}
@Override
public void discardBuffer(long positionUs) {
mediaPeriod.discardBuffer(positionUs);
}
@Override
public long readDiscontinuity() {
return mediaPeriod.readDiscontinuity();
}
@Override
public long getBufferedPositionUs() {
return mediaPeriod.getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
return mediaPeriod.seekToUs(positionUs);
}
@Override
public long getNextLoadPositionUs() {
return mediaPeriod.getNextLoadPositionUs();
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
callback.onContinueLoadingRequested(this);
}
// MediaPeriod.Callback implementation
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
callback.onPrepared(this);
}
}
......@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
......@@ -758,111 +757,4 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
}
/**
* Media period used for periods created from unprepared media sources exposed through
* {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes
* available.
*/
private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
public final MediaSource mediaSource;
private final MediaPeriodId id;
private final Allocator allocator;
private MediaPeriod mediaPeriod;
private Callback callback;
private long preparePositionUs;
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
this.id = id;
this.allocator = allocator;
this.mediaSource = mediaSource;
}
public void createPeriod() {
mediaPeriod = mediaSource.createPeriod(id, allocator);
if (callback != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
public void releasePeriod() {
if (mediaPeriod != null) {
mediaSource.releasePeriod(mediaPeriod);
}
}
@Override
public void prepare(Callback callback, long preparePositionUs) {
this.callback = callback;
this.preparePositionUs = preparePositionUs;
if (mediaPeriod != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
} else {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public TrackGroupArray getTrackGroups() {
return mediaPeriod.getTrackGroups();
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs);
}
@Override
public void discardBuffer(long positionUs) {
mediaPeriod.discardBuffer(positionUs);
}
@Override
public long readDiscontinuity() {
return mediaPeriod.readDiscontinuity();
}
@Override
public long getBufferedPositionUs() {
return mediaPeriod.getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
return mediaPeriod.seekToUs(positionUs);
}
@Override
public long getNextLoadPositionUs() {
return mediaPeriod.getNextLoadPositionUs();
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
callback.onContinueLoadingRequested(this);
}
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
callback.onPrepared(this);
}
}
}
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import android.net.Uri;
import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
......@@ -28,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
......@@ -74,11 +76,10 @@ import java.util.Arrays;
private final Uri uri;
private final DataSource dataSource;
private final int minLoadableRetryCount;
private final Handler eventHandler;
private final ExtractorMediaSource.EventListener eventListener;
private final EventDispatcher eventDispatcher;
private final Listener listener;
private final Allocator allocator;
private final String customCacheKey;
@Nullable private final String customCacheKey;
private final long continueLoadingCheckIntervalBytes;
private final Loader loader;
private final ExtractorHolder extractorHolder;
......@@ -117,8 +118,7 @@ import java.util.Arrays;
* @param dataSource The data source to read the media.
* @param extractors The extractors to use to read the data source.
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
* @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 eventDispatcher A dispatcher to notify of events.
* @param listener A listener to notify when information about the period changes.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
......@@ -126,15 +126,20 @@ import java.util.Arrays;
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
*/
public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors,
int minLoadableRetryCount, Handler eventHandler,
ExtractorMediaSource.EventListener eventListener, Listener listener,
Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) {
public ExtractorMediaPeriod(
Uri uri,
DataSource dataSource,
Extractor[] extractors,
int minLoadableRetryCount,
EventDispatcher eventDispatcher,
Listener listener,
Allocator allocator,
@Nullable String customCacheKey,
int continueLoadingCheckIntervalBytes) {
this.uri = uri;
this.dataSource = dataSource;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.eventDispatcher = eventDispatcher;
this.listener = listener;
this.allocator = allocator;
this.customCacheKey = customCacheKey;
......@@ -303,7 +308,8 @@ import java.util.Arrays;
@Override
public long readDiscontinuity() {
if (notifyDiscontinuity) {
if (notifyDiscontinuity
&& (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) {
notifyDiscontinuity = false;
return lastSeekPositionUs;
}
......@@ -399,38 +405,75 @@ import java.util.Arrays;
@Override
public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs) {
copyLengthFromLoader(loadable);
loadingFinished = true;
if (durationUs == C.TIME_UNSET) {
long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0
: largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable());
}
eventDispatcher.loadCompleted(
loadable.dataSpec,
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded);
copyLengthFromLoader(loadable);
loadingFinished = true;
callback.onContinueLoadingRequested(this);
}
@Override
public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
if (released) {
return;
}
copyLengthFromLoader(loadable);
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
if (enabledTrackCount > 0) {
callback.onContinueLoadingRequested(this);
eventDispatcher.loadCanceled(
loadable.dataSpec,
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded);
if (!released) {
copyLengthFromLoader(loadable);
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
if (enabledTrackCount > 0) {
callback.onContinueLoadingRequested(this);
}
}
}
@Override
public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs, IOException error) {
boolean isErrorFatal = isLoadableExceptionFatal(error);
eventDispatcher.loadError(
loadable.dataSpec,
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded,
error,
/* wasCanceled= */ isErrorFatal);
copyLengthFromLoader(loadable);
notifyLoadError(error);
if (isLoadableExceptionFatal(error)) {
if (isErrorFatal) {
return Loader.DONT_RETRY_FATAL;
}
int extractedSamplesCount = getExtractedSamplesCount();
......@@ -606,17 +649,6 @@ import java.util.Arrays;
return e instanceof UnrecognizedInputFormatException;
}
private void notifyLoadError(final IOException error) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onLoadError(error);
}
});
}
}
private final class SampleStreamImpl implements SampleStream {
private final int track;
......@@ -663,7 +695,9 @@ import java.util.Arrays;
private boolean pendingExtractorSeek;
private long seekTimeUs;
private DataSpec dataSpec;
private long length;
private long bytesLoaded;
public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder,
ConditionVariable loadCondition) {
......@@ -699,7 +733,8 @@ import java.util.Arrays;
ExtractorInput input = null;
try {
long position = positionHolder.position;
length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey));
dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey);
length = dataSource.open(dataSpec);
if (length != C.LENGTH_UNSET) {
length += position;
}
......@@ -723,6 +758,7 @@ import java.util.Arrays;
result = Extractor.RESULT_CONTINUE;
} else if (input != null) {
positionHolder.position = input.getPosition();
bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition;
}
Util.closeQuietly(dataSource);
}
......
......@@ -35,7 +35,8 @@ import java.io.IOException;
* player to load and read the media.</li>
* </ul>
* All methods are called on the player's internal playback thread, as described in the
* {@link ExoPlayer} Javadoc.
* {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances
* should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once.
*/
public interface MediaSource {
......@@ -150,6 +151,8 @@ public interface MediaSource {
/**
* Starts preparation of the source.
* <p>
* Should not be called directly from application code.
*
* @param player The player for which this source is being prepared.
* @param isTopLevelSource Whether this source has been passed directly to
......@@ -162,6 +165,8 @@ public interface MediaSource {
/**
* Throws any pending error encountered while loading or refreshing source information.
* <p>
* Should not be called directly from application code.
*/
void maybeThrowSourceInfoRefreshError() throws IOException;
......@@ -169,6 +174,8 @@ public interface MediaSource {
* Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called
* multiple times with the same period identifier without an intervening call to
* {@link #releasePeriod(MediaPeriod)}.
* <p>
* Should not be called directly from application code.
*
* @param id The identifier of the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
......@@ -178,6 +185,8 @@ public interface MediaSource {
/**
* Releases the period.
* <p>
* Should not be called directly from application code.
*
* @param mediaPeriod The period to release.
*/
......@@ -186,8 +195,7 @@ public interface MediaSource {
/**
* Releases the source.
* <p>
* This method should be called when the source is no longer required. It may be called in any
* state.
* Should not be called directly from application code.
*/
void releaseSource();
......
......@@ -15,13 +15,11 @@
*/
package com.google.android.exoplayer2.source;
import android.net.Uri;
import android.os.Handler;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
......@@ -43,14 +41,14 @@ import java.util.Arrays;
*/
private static final int INITIAL_SAMPLE_SIZE = 1024;
private final Uri uri;
private final DataSpec dataSpec;
private final DataSource.Factory dataSourceFactory;
private final int minLoadableRetryCount;
private final Handler eventHandler;
private final EventListener eventListener;
private final int eventSourceId;
private final EventDispatcher eventDispatcher;
private final TrackGroupArray tracks;
private final ArrayList<SampleStreamImpl> sampleStreams;
private final long durationUs;
// Package private to avoid thunk methods.
/* package */ final Loader loader;
/* package */ final Format format;
......@@ -62,16 +60,20 @@ import java.util.Arrays;
/* package */ int sampleSize;
private int errorCount;
public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format,
int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
int eventSourceId, boolean treatLoadErrorsAsEndOfStream) {
this.uri = uri;
public SingleSampleMediaPeriod(
DataSpec dataSpec,
DataSource.Factory dataSourceFactory,
Format format,
long durationUs,
int minLoadableRetryCount,
EventDispatcher eventDispatcher,
boolean treatLoadErrorsAsEndOfStream) {
this.dataSpec = dataSpec;
this.dataSourceFactory = dataSourceFactory;
this.format = format;
this.durationUs = durationUs;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.eventSourceId = eventSourceId;
this.eventDispatcher = eventDispatcher;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
tracks = new TrackGroupArray(new TrackGroup(format));
sampleStreams = new ArrayList<>();
......@@ -125,7 +127,9 @@ import java.util.Arrays;
if (loadingFinished || loader.isLoading()) {
return false;
}
loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this,
loader.startLoading(
new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()),
this,
minLoadableRetryCount);
return true;
}
......@@ -158,6 +162,18 @@ import java.util.Arrays;
@Override
public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs) {
eventDispatcher.loadCompleted(
loadable.dataSpec,
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
format,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
loadable.sampleSize);
sampleSize = loadable.sampleSize;
sampleData = loadable.sampleData;
loadingFinished = true;
......@@ -167,34 +183,46 @@ import java.util.Arrays;
@Override
public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
boolean released) {
// Do nothing.
eventDispatcher.loadCanceled(
loadable.dataSpec,
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
loadable.sampleSize);
}
@Override
public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
IOException error) {
notifyLoadError(error);
errorCount++;
if (treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount) {
boolean cancel = treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount;
eventDispatcher.loadError(
loadable.dataSpec,
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
format,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
loadable.sampleSize,
error,
/* wasCanceled= */ cancel);
if (cancel) {
loadingFinished = true;
return Loader.DONT_RETRY;
}
return Loader.RETRY;
}
// Internal methods.
private void notifyLoadError(final IOException e) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onLoadError(eventSourceId, e);
}
});
}
}
private final class SampleStreamImpl implements SampleStream {
private static final int STREAM_STATE_SEND_FORMAT = 0;
......@@ -259,14 +287,15 @@ import java.util.Arrays;
/* package */ static final class SourceLoadable implements Loadable {
private final Uri uri;
public final DataSpec dataSpec;
private final DataSource dataSource;
private int sampleSize;
private byte[] sampleData;
public SourceLoadable(Uri uri, DataSource dataSource) {
this.uri = uri;
public SourceLoadable(DataSpec dataSpec, DataSource dataSource) {
this.dataSpec = dataSpec;
this.dataSource = dataSource;
}
......@@ -286,7 +315,7 @@ import java.util.Arrays;
sampleSize = 0;
try {
// Create and open the input.
dataSource.open(new DataSpec(uri));
dataSource.open(dataSpec);
// Load the sample data.
int result = 0;
while (result != C.RESULT_END_OF_INPUT) {
......
......@@ -62,8 +62,11 @@ public final class TrackGroupArray {
* @param group The group.
* @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists.
*/
@SuppressWarnings("ReferenceEquality")
public int indexOf(TrackGroup group) {
for (int i = 0; i < length; i++) {
// Suppressed reference equality warning because this is looking for the index of a specific
// TrackGroup object, not the index of a potential equal TrackGroup.
if (trackGroups[i] == group) {
return i;
}
......@@ -71,6 +74,13 @@ public final class TrackGroupArray {
return C.INDEX_UNSET;
}
/**
* Returns whether this track group array is empty.
*/
public boolean isEmpty() {
return length == 0;
}
@Override
public int hashCode() {
if (hashCode == 0) {
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.ads;
import android.view.ViewGroup;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import java.io.IOException;
......@@ -72,6 +73,15 @@ public interface AdsLoader {
}
/**
* Sets the supported content types for ad media. Must be called before the first call to
* {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Subsequent calls may be ignored.
*
* @param contentTypes The supported content types for ad media. Each element must be one of
* {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}.
*/
void setSupportedContentTypes(@C.ContentType int... contentTypes);
/**
* Attaches a player that will play ads loaded using this instance. Called on the main thread by
* {@link AdsMediaSource}.
*
......
......@@ -16,12 +16,11 @@
package com.google.android.exoplayer2.source.chunk;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.SequenceableLoader;
......
......@@ -33,7 +33,6 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
......@@ -185,7 +184,7 @@ public final class Cea608Decoder extends CeaDecoder {
private final ParsableByteArray ccData;
private final int packetLength;
private final int selectedField;
private final LinkedList<CueBuilder> cueBuilders;
private final ArrayList<CueBuilder> cueBuilders;
private CueBuilder currentCueBuilder;
private List<Cue> cues;
......@@ -200,7 +199,7 @@ public final class Cea608Decoder extends CeaDecoder {
public Cea608Decoder(String mimeType, int accessibilityChannel) {
ccData = new ParsableByteArray();
cueBuilders = new LinkedList<>();
cueBuilders = new ArrayList<>();
currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
switch (accessibilityChannel) {
......@@ -230,8 +229,8 @@ public final class Cea608Decoder extends CeaDecoder {
cues = null;
lastCues = null;
setCaptionMode(CC_MODE_UNKNOWN);
setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT);
resetCueBuilders();
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
repeatableControlSet = false;
repeatableControlCc1 = 0;
repeatableControlCc2 = 0;
......@@ -434,16 +433,16 @@ public final class Cea608Decoder extends CeaDecoder {
private void handleMiscCode(byte cc2) {
switch (cc2) {
case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
captionRowCount = 2;
setCaptionMode(CC_MODE_ROLL_UP);
setCaptionRowCount(2);
return;
case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
captionRowCount = 3;
setCaptionMode(CC_MODE_ROLL_UP);
setCaptionRowCount(3);
return;
case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
captionRowCount = 4;
setCaptionMode(CC_MODE_ROLL_UP);
setCaptionRowCount(4);
return;
case CTRL_RESUME_CAPTION_LOADING:
setCaptionMode(CC_MODE_POP_ON);
......@@ -451,6 +450,9 @@ public final class Cea608Decoder extends CeaDecoder {
case CTRL_RESUME_DIRECT_CAPTIONING:
setCaptionMode(CC_MODE_PAINT_ON);
return;
default:
// Fall through.
break;
}
if (captionMode == CC_MODE_UNKNOWN) {
......@@ -484,6 +486,9 @@ public final class Cea608Decoder extends CeaDecoder {
case CTRL_DELETE_TO_END_OF_ROW:
// TODO: implement
break;
default:
// Fall through.
break;
}
}
......@@ -515,8 +520,13 @@ public final class Cea608Decoder extends CeaDecoder {
}
}
private void setCaptionRowCount(int captionRowCount) {
this.captionRowCount = captionRowCount;
currentCueBuilder.setCaptionRowCount(captionRowCount);
}
private void resetCueBuilders() {
currentCueBuilder.reset(captionMode, captionRowCount);
currentCueBuilder.reset(captionMode);
cueBuilders.clear();
cueBuilders.add(currentCueBuilder);
}
......@@ -594,12 +604,14 @@ public final class Cea608Decoder extends CeaDecoder {
public CueBuilder(int captionMode, int captionRowCount) {
preambleStyles = new ArrayList<>();
midrowStyles = new ArrayList<>();
rolledUpCaptions = new LinkedList<>();
rolledUpCaptions = new ArrayList<>();
captionStringBuilder = new SpannableStringBuilder();
reset(captionMode, captionRowCount);
reset(captionMode);
setCaptionRowCount(captionRowCount);
}
public void reset(int captionMode, int captionRowCount) {
public void reset(int captionMode) {
this.captionMode = captionMode;
preambleStyles.clear();
midrowStyles.clear();
rolledUpCaptions.clear();
......@@ -607,11 +619,13 @@ public final class Cea608Decoder extends CeaDecoder {
row = BASE_ROW;
indent = 0;
tabOffset = 0;
this.captionMode = captionMode;
this.captionRowCount = captionRowCount;
underlineStartPosition = POSITION_UNSET;
}
public void setCaptionRowCount(int captionRowCount) {
this.captionRowCount = captionRowCount;
}
public boolean isEmpty() {
return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty()
&& captionStringBuilder.length() == 0;
......@@ -726,8 +740,10 @@ public final class Cea608Decoder extends CeaDecoder {
// The number of empty columns after the end of the text, in the same range.
int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length();
int startEndPaddingDelta = startPadding - endPadding;
if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) {
// Treat approximately centered pop-on captions are middle aligned.
if (captionMode == CC_MODE_POP_ON && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) {
// Treat approximately centered pop-on captions as middle aligned. We also treat captions
// that are wider than they should be in this way. See
// https://github.com/google/ExoPlayer/issues/3534.
position = 0.5f;
positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
} else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) {
......
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