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

r2.6.1
This commit is contained in:
ojw28 2018-01-03 14:01:53 +00:00 committed by GitHub
commit 2b20780482
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
155 changed files with 5953 additions and 2554 deletions

View File

@ -1,5 +1,3 @@
*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION ***
Before filing an issue:
-----------------------
- Search existing issues, including issues that are closed.

View File

@ -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

View File

@ -1,5 +1,47 @@
# 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

View File

@ -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:

View File

@ -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

View File

@ -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')
}

View File

@ -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">

View File

@ -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};
}
}

View File

@ -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"

View File

@ -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.

View File

@ -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
// MediaSourceEventListener
@Override
public void onLoadError(IOException error) {
printInternalError("loadError", error);
}
// AdaptiveMediaSourceEventListener
@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

View File

@ -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() {

View File

@ -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);

View File

@ -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

View File

@ -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"

View File

@ -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);
}

View File

@ -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,

View File

@ -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;
}
}
}

View File

@ -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);
}
@ -124,6 +132,13 @@ import java.util.List;
return sampleRate;
}
/**
* 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);

View File

@ -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) {

View File

@ -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"

View File

@ -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

View File

@ -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();
}
}
}
}

View File

@ -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 {

View File

@ -19,6 +19,7 @@ import android.content.Context;
import android.net.Uri;
import android.os.SystemClock;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.ViewGroup;
import android.webkit.WebView;
@ -49,10 +50,14 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -66,6 +71,75 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
}
/** Builder for {@link ImaAdsLoader}. */
public static final class Builder {
private final Context context;
private @Nullable ImaSdkSettings imaSdkSettings;
private long vastLoadTimeoutMs;
/**
* Creates a new builder for {@link ImaAdsLoader}.
*
* @param context The context;
*/
public Builder(Context context) {
this.context = Assertions.checkNotNull(context);
vastLoadTimeoutMs = C.TIME_UNSET;
}
/**
* Sets the IMA SDK settings. The provided settings instance's player type and version fields
* may be overwritten.
*
* <p>If this method is not called the default settings will be used.
*
* @param imaSdkSettings The {@link ImaSdkSettings}.
* @return This builder, for convenience.
*/
public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {
this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings);
return this;
}
/**
* Sets the VAST load timeout, in milliseconds.
*
* @param vastLoadTimeoutMs The VAST load timeout, in milliseconds.
* @return This builder, for convenience.
* @see AdsRequest#setVastLoadTimeout(float)
*/
public Builder setVastLoadTimeoutMs(long vastLoadTimeoutMs) {
Assertions.checkArgument(vastLoadTimeoutMs >= 0);
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
return this;
}
/**
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
*
* @param adTagUri The URI of a compatible ad tag to load. See
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* information on compatible ad tags.
* @return The new {@link ImaAdsLoader}.
*/
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs);
}
/**
* Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response.
*
* @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of
* making a request via an ad tag URL.
* @return The new {@link ImaAdsLoader}.
*/
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs);
}
}
private static final boolean DEBUG = false;
private static final String TAG = "ImaAdsLoader";
@ -77,6 +151,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
/** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */
private static final long IMA_DURATION_UNSET = -1L;
/**
* Threshold before the end of content at which IMA is notified that content is complete if the
* player buffers, in milliseconds.
@ -91,9 +168,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
/**
* The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}.
*/
/** The state of ad playback. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
private @interface ImaAdState {}
@ -110,13 +185,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/
private static final int IMA_AD_STATE_PAUSED = 2;
private final Uri adTagUri;
private final @Nullable Uri adTagUri;
private final @Nullable String adsResponse;
private final long vastLoadTimeoutMs;
private final Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks;
private final ImaSdkFactory imaSdkFactory;
private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
private Object pendingAdRequestContext;
private List<String> supportedMimeTypes;
private EventListener eventListener;
private Player player;
private ViewGroup adUiViewGroup;
@ -124,8 +203,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private VideoProgressUpdate lastAdProgress;
private AdsManager adsManager;
private AdErrorEvent pendingAdErrorEvent;
private Timeline timeline;
private long contentDurationMs;
private int podIndexOffset;
private AdPlaybackState adPlaybackState;
// Fields tracking IMA's state.
@ -138,9 +219,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* Whether IMA has sent an ad event to pause content since the last resume content event.
*/
private boolean imaPausedContent;
/**
* The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}.
*/
/** The current ad playback state. */
private @ImaAdState int imaAdState;
/**
* Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been
@ -179,21 +258,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
*/
private boolean sentPendingContentPositionMs;
/**
* Whether {@link #release()} has been called.
*/
private boolean released;
/**
* Creates a new IMA ads loader.
*
* <p>If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead.
*
* @param context The context.
* @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* more information.
*/
public ImaAdsLoader(Context context, Uri adTagUri) {
this(context, adTagUri, null);
this(context, adTagUri, null, null, C.TIME_UNSET);
}
/**
@ -205,9 +282,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* more information.
* @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to
* use the default settings. If set, the player type and version fields may be overwritten.
* @deprecated Use {@link ImaAdsLoader.Builder}.
*/
@Deprecated
public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET);
}
private ImaAdsLoader(
Context context,
@Nullable Uri adTagUri,
@Nullable ImaSdkSettings imaSdkSettings,
@Nullable String adsResponse,
long vastLoadTimeoutMs) {
Assertions.checkArgument(adTagUri != null || adsResponse != null);
this.adTagUri = adTagUri;
this.adsResponse = adsResponse;
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
period = new Timeline.Period();
adCallbacks = new ArrayList<>(1);
imaSdkFactory = ImaSdkFactory.getInstance();
@ -236,8 +327,58 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return adsLoader;
}
/**
* Requests ads, if they have not already been requested. Must be called on the main thread.
*
* <p>Ads will be requested automatically when the player is prepared if this method has not been
* called, so it is only necessary to call this method if you want to request ads before preparing
* the player
*
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
*/
public void requestAds(ViewGroup adUiViewGroup) {
if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) {
// Ads have already been requested.
return;
}
adDisplayContainer.setAdContainer(adUiViewGroup);
pendingAdRequestContext = new Object();
AdsRequest request = imaSdkFactory.createAdsRequest();
if (adTagUri != null) {
request.setAdTagUrl(adTagUri.toString());
} else /* adsResponse != null */ {
request.setAdsResponse(adsResponse);
}
if (vastLoadTimeoutMs != C.TIME_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs);
}
request.setAdDisplayContainer(adDisplayContainer);
request.setContentProgressProvider(this);
request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request);
}
// AdsLoader implementation.
@Override
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
List<String> supportedMimeTypes = new ArrayList<>();
for (@C.ContentType int contentType : contentTypes) {
if (contentType == C.TYPE_DASH) {
supportedMimeTypes.add(MimeTypes.APPLICATION_MPD);
} else if (contentType == C.TYPE_HLS) {
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
} else if (contentType == C.TYPE_OTHER) {
supportedMimeTypes.addAll(Arrays.asList(
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
} else if (contentType == C.TYPE_SS) {
// IMA does not support SmoothStreaming ad media.
}
}
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
}
@Override
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
this.player = player;
@ -247,13 +388,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
lastContentProgress = null;
adDisplayContainer.setAdContainer(adUiViewGroup);
player.addListener(this);
maybeNotifyAdError();
if (adPlaybackState != null) {
// Pass the ad playback state to the player, and resume ads if necessary.
eventListener.onAdPlaybackState(adPlaybackState.copy());
if (imaPausedContent && player.getPlayWhenReady()) {
adsManager.resume();
}
} else if (adsManager != null) {
// Ads have loaded but the ads manager is not initialized.
startAdPlayback();
} else {
requestAds();
// Ads haven't loaded yet, so request them.
requestAds(adUiViewGroup);
}
}
@ -273,7 +420,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void release() {
released = true;
pendingAdRequestContext = null;
if (adsManager != null) {
adsManager.destroy();
adsManager = null;
@ -285,30 +432,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
if (released) {
if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {
adsManager.destroy();
return;
}
pendingAdRequestContext = null;
this.adsManager = adsManager;
adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this);
if (ENABLE_PRELOADING) {
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(true);
adsManager.init(adsRenderingSettings);
if (DEBUG) {
Log.d(TAG, "Initialized with preloading");
}
} else {
adsManager.init();
if (DEBUG) {
Log.d(TAG, "Initialized without preloading");
}
if (player != null) {
// If a player is attached already, start playback immediately.
startAdPlayback();
}
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
adPlaybackState = new AdPlaybackState(adGroupTimesUs);
updateAdPlaybackState();
}
// AdEvent.AdEventListener implementation.
@ -335,15 +470,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// The ad position is not always accurate when using preloading. See [Internal: b/62613240].
AdPodInfo adPodInfo = ad.getAdPodInfo();
int podIndex = adPodInfo.getPodIndex();
adGroupIndex = podIndex == -1 ? adPlaybackState.adGroupCount - 1 : podIndex;
adGroupIndex =
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
int adPosition = adPodInfo.getAdPosition();
int adCountInAdGroup = adPodInfo.getTotalAds();
int adCount = adPodInfo.getTotalAds();
adsManager.start();
if (DEBUG) {
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group "
+ adGroupIndex);
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
}
adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup);
adPlaybackState.setAdCount(adGroupIndex, adCount);
updateAdPlaybackState();
break;
case CONTENT_PAUSE_REQUESTED:
@ -386,19 +521,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "onAdError " + adErrorEvent);
}
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
adPlaybackState = new AdPlaybackState(new long[0]);
updateAdPlaybackState();
}
if (eventListener != null) {
IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError());
eventListener.onLoadError(exception);
if (pendingAdErrorEvent == null) {
pendingAdErrorEvent = adErrorEvent;
}
maybeNotifyAdError();
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
if (player == null) {
return lastContentProgress;
} else if (pendingContentPositionMs != C.TIME_UNSET) {
@ -408,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
return new VideoProgressUpdate(fakePositionMs, contentDurationMs);
} else if (playingAd || contentDurationMs == C.TIME_UNSET) {
} else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} else {
return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
@ -421,7 +560,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
public VideoProgressUpdate getAdProgress() {
if (player == null) {
return lastAdProgress;
} else if (!playingAd) {
} else if (imaAdState == IMA_AD_STATE_NONE) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} else {
long adDuration = player.getDuration();
@ -563,6 +702,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded();
}
if (DEBUG) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged");
}
}
}
@ -604,26 +746,74 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Internal methods.
private void requestAds() {
AdsRequest request = imaSdkFactory.createAdsRequest();
request.setAdTagUrl(adTagUri.toString());
request.setAdDisplayContainer(adDisplayContainer);
request.setContentProgressProvider(this);
adsLoader.requestAds(request);
private void startAdPlayback() {
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
// Set up the ad playback state, skipping ads based on the start position as required.
pendingContentPositionMs = player.getCurrentPosition();
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
adPlaybackState = new AdPlaybackState(adGroupTimesUs);
int adGroupIndexForPosition =
getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs));
if (adGroupIndexForPosition == 0) {
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
pendingContentPositionMs = C.TIME_UNSET;
// There is no preroll and midroll pod indices start at 1.
podIndexOffset = -1;
} else /* adGroupIndexForPosition > 0 */ {
// Skip ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
adPlaybackState.playedAdGroup(i);
}
// Play ads after the midpoint between the ad to play and the one before it, to avoid issues
// with rounding one of the two ad times.
long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition];
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
// We're removing one or more ads, which means that the earliest ad (if any) will be a
// midroll/postroll. Midroll pod indices start at 1.
podIndexOffset = adGroupIndexForPosition - 1;
}
// Start ad playback.
adsManager.init(adsRenderingSettings);
updateAdPlaybackState();
if (DEBUG) {
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
}
}
private void maybeNotifyAdError() {
if (eventListener != null && pendingAdErrorEvent != null) {
IOException exception =
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError());
eventListener.onLoadError(exception);
pendingAdErrorEvent = null;
}
}
private void updateImaStateForPlayerState() {
boolean wasPlayingAd = playingAd;
int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup;
playingAd = player.isPlayingAd();
playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
if (!sentContentComplete) {
boolean adFinished = (wasPlayingAd && !playingAd)
|| playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup();
boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup;
if (adFinished) {
// IMA is waiting for the ad playback to finish so invoke the callback now.
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded();
}
if (DEBUG) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
}
if (!wasPlayingAd && playingAd) {
int adGroupIndex = player.getCurrentAdGroupIndex();
@ -635,7 +825,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
}
playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
}
private void resumeContentInternal() {
@ -717,4 +906,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return adGroupTimesUs;
}
/**
* Returns the index of the ad group that should be played before playing the content at {@code
* playbackPositionUs} when starting playback for the first time. This is the latest ad group at
* or before the specified playback position. If the first ad is after the playback position,
* returns {@link C#INDEX_UNSET}.
*/
private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) {
for (int i = 0; i < adGroupTimesUs.length; i++) {
long adGroupTimeUs = adGroupTimesUs[i];
// A postroll ad is after any position in the content.
if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) {
return i == 0 ? C.INDEX_UNSET : (i - 1);
}
}
return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1);
}
}

View File

@ -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);
}

View File

@ -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()));

View File

@ -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"

View File

@ -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();

View File

@ -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 {

View File

@ -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 ##

View File

@ -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"

View File

@ -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();
}
}
}
}

View File

@ -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"

View File

@ -25,309 +25,313 @@ track 0:
language = null
drmInitData = -
initializationData:
sample count = 76
sample count = 77
sample 0:
time = 945782
time = 928568
flags = 1
data = length 384, hash F7E344F4
sample 1:
time = 952568
flags = 1
data = length 384, hash 14EF6AFD
sample 1:
time = 969782
sample 2:
time = 976568
flags = 1
data = length 384, hash 61C9B92C
sample 2:
time = 993782
sample 3:
time = 1000568
flags = 1
data = length 384, hash ABE1368
sample 3:
time = 1017782
sample 4:
time = 1024568
flags = 1
data = length 384, hash 6A3B8547
sample 4:
time = 1041782
sample 5:
time = 1048568
flags = 1
data = length 384, hash 30E905FA
sample 5:
time = 1065782
sample 6:
time = 1072568
flags = 1
data = length 384, hash 21A267CD
sample 6:
time = 1089782
sample 7:
time = 1096568
flags = 1
data = length 384, hash D96A2651
sample 7:
time = 1113782
sample 8:
time = 1120568
flags = 1
data = length 384, hash 72340177
sample 8:
time = 1137782
sample 9:
time = 1144568
flags = 1
data = length 384, hash 9345E744
sample 9:
time = 1161782
sample 10:
time = 1168568
flags = 1
data = length 384, hash FDE39E3A
sample 10:
time = 1185782
sample 11:
time = 1192568
flags = 1
data = length 384, hash F0B7465
sample 11:
time = 1209782
sample 12:
time = 1216568
flags = 1
data = length 384, hash 3693AB86
sample 12:
time = 1233782
sample 13:
time = 1240568
flags = 1
data = length 384, hash F39719B1
sample 13:
time = 1257782
sample 14:
time = 1264568
flags = 1
data = length 384, hash DA3958DC
sample 14:
time = 1281782
sample 15:
time = 1288568
flags = 1
data = length 384, hash FDC7599F
sample 15:
time = 1305782
sample 16:
time = 1312568
flags = 1
data = length 384, hash AEFF8471
sample 16:
time = 1329782
sample 17:
time = 1336568
flags = 1
data = length 384, hash 89C92C19
sample 17:
time = 1353782
sample 18:
time = 1360568
flags = 1
data = length 384, hash 5C786A4B
sample 18:
time = 1377782
sample 19:
time = 1384568
flags = 1
data = length 384, hash 5ACA8B
sample 19:
time = 1401782
sample 20:
time = 1408568
flags = 1
data = length 384, hash 7755974C
sample 20:
time = 1425782
sample 21:
time = 1432568
flags = 1
data = length 384, hash 3934B73C
sample 21:
time = 1449782
sample 22:
time = 1456568
flags = 1
data = length 384, hash DDD70A2F
sample 22:
time = 1473782
sample 23:
time = 1480568
flags = 1
data = length 384, hash 8FACE2EF
sample 23:
time = 1497782
sample 24:
time = 1504568
flags = 1
data = length 384, hash 4A602591
sample 24:
time = 1521782
sample 25:
time = 1528568
flags = 1
data = length 384, hash D019AA2D
sample 25:
time = 1545782
sample 26:
time = 1552568
flags = 1
data = length 384, hash 8A680B9D
sample 26:
time = 1569782
sample 27:
time = 1576568
flags = 1
data = length 384, hash B655C959
sample 27:
time = 1593782
sample 28:
time = 1600568
flags = 1
data = length 384, hash 2168336B
sample 28:
time = 1617782
sample 29:
time = 1624568
flags = 1
data = length 384, hash D77F6D31
sample 29:
time = 1641782
sample 30:
time = 1648568
flags = 1
data = length 384, hash 524B4B2F
sample 30:
time = 1665782
sample 31:
time = 1672568
flags = 1
data = length 384, hash 4752DDFC
sample 31:
time = 1689782
sample 32:
time = 1696568
flags = 1
data = length 384, hash E786727F
sample 32:
time = 1713782
sample 33:
time = 1720568
flags = 1
data = length 384, hash 5DA6FB8C
sample 33:
time = 1737782
sample 34:
time = 1744568
flags = 1
data = length 384, hash 92F24269
sample 34:
time = 1761782
sample 35:
time = 1768568
flags = 1
data = length 384, hash CD0A3BA1
sample 35:
time = 1785782
sample 36:
time = 1792568
flags = 1
data = length 384, hash 7D00409F
sample 36:
time = 1809782
sample 37:
time = 1816568
flags = 1
data = length 384, hash D7ADB5FA
sample 37:
time = 1833782
sample 38:
time = 1840568
flags = 1
data = length 384, hash 4A140209
sample 38:
time = 1857782
sample 39:
time = 1864568
flags = 1
data = length 384, hash E801184A
sample 39:
time = 1881782
sample 40:
time = 1888568
flags = 1
data = length 384, hash 53C6CF9C
sample 40:
time = 1905782
sample 41:
time = 1912568
flags = 1
data = length 384, hash 19A8D99F
sample 41:
time = 1929782
sample 42:
time = 1936568
flags = 1
data = length 384, hash E47EB43F
sample 42:
time = 1953782
sample 43:
time = 1960568
flags = 1
data = length 384, hash 4EA329E7
sample 43:
time = 1977782
sample 44:
time = 1984568
flags = 1
data = length 384, hash 1CCAAE62
sample 44:
time = 2001782
sample 45:
time = 2008568
flags = 1
data = length 384, hash ED3F8C66
sample 45:
time = 2025782
sample 46:
time = 2032568
flags = 1
data = length 384, hash D3D646B6
sample 46:
time = 2049782
sample 47:
time = 2056568
flags = 1
data = length 384, hash 68CD1574
sample 47:
time = 2073782
sample 48:
time = 2080568
flags = 1
data = length 384, hash 8CEAB382
sample 48:
time = 2097782
sample 49:
time = 2104568
flags = 1
data = length 384, hash D54B1C48
sample 49:
time = 2121782
sample 50:
time = 2128568
flags = 1
data = length 384, hash FFE2EE90
sample 50:
time = 2145782
sample 51:
time = 2152568
flags = 1
data = length 384, hash BFE8A673
sample 51:
time = 2169782
sample 52:
time = 2176568
flags = 1
data = length 384, hash 978B1C92
sample 52:
time = 2193782
sample 53:
time = 2200568
flags = 1
data = length 384, hash 810CC71E
sample 53:
time = 2217782
sample 54:
time = 2224568
flags = 1
data = length 384, hash 44FE42D9
sample 54:
time = 2241782
sample 55:
time = 2248568
flags = 1
data = length 384, hash 2F5BB02C
sample 55:
time = 2265782
sample 56:
time = 2272568
flags = 1
data = length 384, hash 77DDB90
sample 56:
time = 2289782
sample 57:
time = 2296568
flags = 1
data = length 384, hash 24FB5EDA
sample 57:
time = 2313782
sample 58:
time = 2320568
flags = 1
data = length 384, hash E73203C6
sample 58:
time = 2337782
sample 59:
time = 2344568
flags = 1
data = length 384, hash 14B525F1
sample 59:
time = 2361782
sample 60:
time = 2368568
flags = 1
data = length 384, hash 5E0F4E2E
sample 60:
time = 2385782
sample 61:
time = 2392568
flags = 1
data = length 384, hash 67EE4E31
sample 61:
time = 2409782
sample 62:
time = 2416568
flags = 1
data = length 384, hash 2E04EC4C
sample 62:
time = 2433782
sample 63:
time = 2440568
flags = 1
data = length 384, hash 852CABA7
sample 63:
time = 2457782
sample 64:
time = 2464568
flags = 1
data = length 384, hash 19928903
sample 64:
time = 2481782
sample 65:
time = 2488568
flags = 1
data = length 384, hash 5DA42021
sample 65:
time = 2505782
sample 66:
time = 2512568
flags = 1
data = length 384, hash 45B20B7C
sample 66:
time = 2529782
sample 67:
time = 2536568
flags = 1
data = length 384, hash D108A215
sample 67:
time = 2553782
sample 68:
time = 2560568
flags = 1
data = length 384, hash BD25DB7C
sample 68:
time = 2577782
sample 69:
time = 2584568
flags = 1
data = length 384, hash DA7F9861
sample 69:
time = 2601782
sample 70:
time = 2608568
flags = 1
data = length 384, hash CCD576F
sample 70:
time = 2625782
sample 71:
time = 2632568
flags = 1
data = length 384, hash 405C1EB5
sample 71:
time = 2649782
sample 72:
time = 2656568
flags = 1
data = length 384, hash 6640B74E
sample 72:
time = 2673782
sample 73:
time = 2680568
flags = 1
data = length 384, hash B4E5937A
sample 73:
time = 2697782
sample 74:
time = 2704568
flags = 1
data = length 384, hash CEE17733
sample 74:
time = 2721782
sample 75:
time = 2728568
flags = 1
data = length 384, hash 2A0DA733
sample 75:
time = 2745782
sample 76:
time = 2752568
flags = 1
data = length 384, hash 97F4129B
tracksEnded = true

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = 1828
numberOfTracks = 2
track 0:
format:

View File

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = 1828
numberOfTracks = 3
track 0:
format:

View File

@ -27,7 +27,8 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeRenderer;
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.FakeTrackSelection;
import com.google.android.exoplayer2.testutil.FakeTrackSelector;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@ -66,7 +67,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes a single period.
*/
public void testPlaySinglePeriodTimeline() throws Exception {
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
Object manifest = new Object();
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
@ -85,10 +86,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes three periods.
*/
public void testPlayMultiPeriodTimeline() throws Exception {
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0));
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
.setTimeline(timeline).setRenderers(renderer)
@ -105,10 +103,7 @@ public final class ExoPlayerTest extends TestCase {
* source.
*/
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10));
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
@ -149,7 +144,7 @@ public final class ExoPlayerTest extends TestCase {
}
public void testRepreparationGivesFreshSourceInfo() throws Exception {
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
Object firstSourceManifest = new Object();
MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest,
@ -216,10 +211,7 @@ public final class ExoPlayerTest extends TestCase {
}
public void testRepeatModeChanges() throws Exception {
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000));
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1
@ -239,7 +231,7 @@ public final class ExoPlayerTest extends TestCase {
}
public void testShuffleModeEnabledChanges() throws Exception {
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000));
Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource[] fakeMediaSources = {
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
@ -262,7 +254,6 @@ public final class ExoPlayerTest extends TestCase {
}
public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000));
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased")
.setRepeatMode(Player.REPEAT_MODE_ALL)
@ -272,15 +263,13 @@ public final class ExoPlayerTest extends TestCase {
.setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish.
.build();
new ExoPlayerTestRunner.Builder()
.setTimeline(fakeTimeline).setRenderers(renderer).setActionSchedule(actionSchedule)
.setRenderers(renderer).setActionSchedule(actionSchedule)
.build().start().blockUntilEnded(TIMEOUT_MS);
assertTrue(renderer.isEnded);
}
public void testSeekProcessedCallback() throws Exception {
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000));
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback")
// Initial seek before timeline preparation finished.
.pause().seek(10).waitForPlaybackState(Player.STATE_READY)
@ -311,4 +300,138 @@ public final class ExoPlayerTest extends TestCase {
assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2));
}
public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource mediaSource =
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
FakeTrackSelector trackSelector = new FakeTrackSelector();
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector)
.build().start().blockUntilEnded(TIMEOUT_MS);
List<FakeTrackSelection> createdTrackSelections = trackSelector.getSelectedTrackSelections();
int numSelectionsEnabled = 0;
// Assert that all tracks selection are disabled at the end of the playback.
for (FakeTrackSelection trackSelection : createdTrackSelections) {
assertFalse(trackSelection.isEnabled);
numSelectionsEnabled += trackSelection.enableCount;
}
// There are 2 renderers, and track selections are made once (1 period).
// Track selections are not reused, so there are 2 track selections made.
assertEquals(2, createdTrackSelections.size());
// There should be 2 track selections enabled in total.
assertEquals(2, numSelectionsEnabled);
}
public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
MediaSource mediaSource =
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
FakeTrackSelector trackSelector = new FakeTrackSelector();
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector)
.build().start().blockUntilEnded(TIMEOUT_MS);
List<FakeTrackSelection> createdTrackSelections = trackSelector.getSelectedTrackSelections();
int numSelectionsEnabled = 0;
// Assert that all tracks selection are disabled at the end of the playback.
for (FakeTrackSelection trackSelection : createdTrackSelections) {
assertFalse(trackSelection.isEnabled);
numSelectionsEnabled += trackSelection.enableCount;
}
// There are 2 renderers, and track selections are made twice (2 periods).
// Track selections are not reused, so there are 4 track selections made.
assertEquals(4, createdTrackSelections.size());
// There should be 4 track selections enabled in total.
assertEquals(4, numSelectionsEnabled);
}
public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade()
throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource mediaSource =
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
final FakeTrackSelector trackSelector = new FakeTrackSelector();
ActionSchedule disableTrackAction = new ActionSchedule.Builder("testChangeTrackSelection")
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(new Runnable() {
@Override
public void run() {
trackSelector.setRendererDisabled(0, true);
}
}).build();
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector)
.setActionSchedule(disableTrackAction)
.build().start().blockUntilEnded(TIMEOUT_MS);
List<FakeTrackSelection> createdTrackSelections = trackSelector.getSelectedTrackSelections();
int numSelectionsEnabled = 0;
// Assert that all tracks selection are disabled at the end of the playback.
for (FakeTrackSelection trackSelection : createdTrackSelections) {
assertFalse(trackSelection.isEnabled);
numSelectionsEnabled += trackSelection.enableCount;
}
// There are 2 renderers, and track selections are made twice.
// Track selections are not reused, so there are 4 track selections made.
assertEquals(4, createdTrackSelections.size());
// Initially there are 2 track selections enabled.
// The second time one renderer is disabled, so only 1 track selection should be enabled.
assertEquals(3, numSelectionsEnabled);
}
public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed()
throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource mediaSource =
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true);
ActionSchedule disableTrackAction = new ActionSchedule.Builder("testReuseTrackSelection")
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(new Runnable() {
@Override
public void run() {
trackSelector.setRendererDisabled(0, true);
}
}).build();
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector)
.setActionSchedule(disableTrackAction)
.build().start().blockUntilEnded(TIMEOUT_MS);
List<FakeTrackSelection> createdTrackSelections = trackSelector.getSelectedTrackSelections();
int numSelectionsEnabled = 0;
// Assert that all tracks selection are disabled at the end of the playback.
for (FakeTrackSelection trackSelection : createdTrackSelections) {
assertFalse(trackSelection.isEnabled);
numSelectionsEnabled += trackSelection.enableCount;
}
// There are 2 renderers, and track selections are made twice.
// TrackSelections are reused, so there are only 2 track selections made for 2 renderers.
assertEquals(2, createdTrackSelections.size());
// Initially there are 2 track selections enabled.
// The second time one renderer is disabled, so only 1 track selection should be enabled.
assertEquals(3, numSelectionsEnabled);
}
}

View File

@ -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);
}
}

View File

@ -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());
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() {
return getExtractorFactory(0);
}
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);
}
};
}

View File

@ -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();
}
}
}

View File

@ -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);
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.
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));
// 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) {

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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
@ -158,6 +158,10 @@ public final class C {
* PCM encoding with 32 bits per sample.
*/
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
*/
@ -420,6 +424,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.
*/

View File

@ -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) {

View File

@ -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.

View File

@ -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}

View File

@ -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);

View File

@ -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;

View File

@ -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);
}

View File

@ -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.

View File

@ -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;

View File

@ -29,15 +29,14 @@ import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.LinkedList;
/**
* Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback
@ -174,7 +173,7 @@ public final class DefaultAudioSink implements AudioSink {
private final ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets;
private final AudioTrackUtil audioTrackUtil;
private final LinkedList<PlaybackParametersCheckpoint> playbackParametersCheckpoints;
private final ArrayDeque<PlaybackParametersCheckpoint> playbackParametersCheckpoints;
@Nullable private Listener listener;
/**
@ -182,13 +181,13 @@ public final class DefaultAudioSink implements AudioSink {
*/
private AudioTrack keepSessionIdAudioTrack;
private AudioTrack audioTrack;
private boolean isInputPcm;
private int inputSampleRate;
private int sampleRate;
private int channelConfig;
private @C.Encoding int encoding;
private @C.Encoding int outputEncoding;
private AudioAttributes audioAttributes;
private boolean passthrough;
private boolean processingEnabled;
private int bufferSize;
private long bufferSizeUs;
@ -277,7 +276,7 @@ public final class DefaultAudioSink implements AudioSink {
drainingAudioProcessorIndex = C.INDEX_UNSET;
this.audioProcessors = new AudioProcessor[0];
outputBuffers = new ByteBuffer[0];
playbackParametersCheckpoints = new LinkedList<>();
playbackParametersCheckpoints = new ArrayDeque<>();
}
@Override
@ -286,9 +285,15 @@ public final class DefaultAudioSink implements AudioSink {
}
@Override
public boolean isPassthroughSupported(String mimeType) {
return audioCapabilities != null
&& audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType));
public boolean isEncodingSupported(@C.Encoding int encoding) {
if (isEncodingPcm(encoding)) {
// AudioTrack supports 16-bit integer PCM output in all platform API versions, and float
// output from platform API version 21 only. Other integer PCM encodings are resampled by this
// sink to 16-bit PCM.
return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21;
} else {
return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding);
}
}
@Override
@ -331,18 +336,20 @@ public final class DefaultAudioSink implements AudioSink {
}
@Override
public void configure(String inputMimeType, int inputChannelCount, int inputSampleRate,
@C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels,
int trimStartSamples, int trimEndSamples) throws ConfigurationException {
public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate,
int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples,
int trimEndSamples) throws ConfigurationException {
boolean flush = false;
this.inputSampleRate = inputSampleRate;
int channelCount = inputChannelCount;
int sampleRate = inputSampleRate;
@C.Encoding int encoding;
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(inputMimeType);
boolean flush = false;
if (!passthrough) {
encoding = inputPcmEncoding;
pcmFrameSize = Util.getPcmFrameSize(inputPcmEncoding, channelCount);
isInputPcm = isEncodingPcm(inputEncoding);
if (isInputPcm) {
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
}
@C.Encoding int encoding = inputEncoding;
boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT;
if (processingEnabled) {
trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples);
channelMappingAudioProcessor.setChannelMap(outputChannels);
for (AudioProcessor audioProcessor : availableAudioProcessors) {
@ -357,11 +364,6 @@ public final class DefaultAudioSink implements AudioSink {
encoding = audioProcessor.getOutputEncoding();
}
}
if (flush) {
resetAudioProcessors();
}
} else {
encoding = getEncodingForMimeType(inputMimeType);
}
int channelConfig;
@ -411,11 +413,11 @@ public final class DefaultAudioSink implements AudioSink {
// Workaround for Nexus Player not reporting support for mono passthrough.
// (See [Internal: b/34268671].)
if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) {
if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
}
if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate
if (!flush && isInitialized() && outputEncoding == encoding && this.sampleRate == sampleRate
&& this.channelConfig == channelConfig) {
// We already have an audio track with the correct sample rate, channel config and encoding.
return;
@ -423,16 +425,24 @@ public final class DefaultAudioSink implements AudioSink {
reset();
this.encoding = encoding;
this.passthrough = passthrough;
this.processingEnabled = processingEnabled;
this.sampleRate = sampleRate;
this.channelConfig = channelConfig;
outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT;
outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount);
outputEncoding = encoding;
if (isInputPcm) {
outputPcmFrameSize = Util.getPcmFrameSize(outputEncoding, channelCount);
}
if (specifiedBufferSize != 0) {
bufferSize = specifiedBufferSize;
} else if (passthrough) {
} else if (isInputPcm) {
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding);
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
int maxAppBufferSize = (int) Math.max(minBufferSize,
durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
bufferSize = Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize);
} else {
// TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into
// account. [Internal: b/25181305]
if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) {
@ -442,21 +452,9 @@ public final class DefaultAudioSink implements AudioSink {
// DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.
bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND);
}
} else {
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding);
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
int maxAppBufferSize = (int) Math.max(minBufferSize,
durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize
: multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize
: multipliedBufferSize;
}
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize);
// The old playback parameters may no longer be applicable so try to reset them now.
setPlaybackParameters(playbackParameters);
bufferSizeUs =
isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
}
private void resetAudioProcessors() {
@ -487,6 +485,13 @@ public final class DefaultAudioSink implements AudioSink {
releasingConditionVariable.block();
audioTrack = initializeAudioTrack();
// The old playback parameters may no longer be applicable so try to reset them now.
setPlaybackParameters(playbackParameters);
// Flush and reset active audio processors.
resetAudioProcessors();
int audioSessionId = audioTrack.getAudioSessionId();
if (enablePreV21AudioSessionWorkaround) {
if (Util.SDK_INT < 21) {
@ -574,7 +579,7 @@ public final class DefaultAudioSink implements AudioSink {
return true;
}
if (passthrough && framesPerEncodedSample == 0) {
if (!isInputPcm && framesPerEncodedSample == 0) {
// If this is the first encoded sample, calculate the sample size in frames.
framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer);
}
@ -618,20 +623,19 @@ public final class DefaultAudioSink implements AudioSink {
}
}
if (passthrough) {
submittedEncodedFrames += framesPerEncodedSample;
} else {
if (isInputPcm) {
submittedPcmBytes += buffer.remaining();
} else {
submittedEncodedFrames += framesPerEncodedSample;
}
inputBuffer = buffer;
}
if (passthrough) {
// Passthrough buffers are not processed.
writeBuffer(inputBuffer, presentationTimeUs);
} else {
if (processingEnabled) {
processBuffers(presentationTimeUs);
} else {
writeBuffer(inputBuffer, presentationTimeUs);
}
if (!inputBuffer.hasRemaining()) {
@ -679,10 +683,9 @@ public final class DefaultAudioSink implements AudioSink {
}
@SuppressWarnings("ReferenceEquality")
private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs)
throws WriteException {
private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException {
if (!buffer.hasRemaining()) {
return true;
return;
}
if (outputBuffer != null) {
Assertions.checkArgument(outputBuffer == buffer);
@ -701,7 +704,7 @@ public final class DefaultAudioSink implements AudioSink {
}
int bytesRemaining = buffer.remaining();
int bytesWritten = 0;
if (Util.SDK_INT < 21) { // passthrough == false
if (Util.SDK_INT < 21) { // isInputPcm == true
// Work out how many bytes we can write without the risk of blocking.
int bytesPending =
(int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize));
@ -728,17 +731,15 @@ public final class DefaultAudioSink implements AudioSink {
throw new WriteException(bytesWritten);
}
if (!passthrough) {
if (isInputPcm) {
writtenPcmBytes += bytesWritten;
}
if (bytesWritten == bytesRemaining) {
if (passthrough) {
if (!isInputPcm) {
writtenEncodedFrames += framesPerEncodedSample;
}
outputBuffer = null;
return true;
}
return false;
}
@Override
@ -758,7 +759,7 @@ public final class DefaultAudioSink implements AudioSink {
private boolean drainAudioProcessorsToEndOfStream() throws WriteException {
boolean audioProcessorNeedsEndOfStream = false;
if (drainingAudioProcessorIndex == C.INDEX_UNSET) {
drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0;
drainingAudioProcessorIndex = processingEnabled ? 0 : audioProcessors.length;
audioProcessorNeedsEndOfStream = true;
}
while (drainingAudioProcessorIndex < audioProcessors.length) {
@ -799,8 +800,8 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
if (passthrough) {
// The playback parameters are always the default in passthrough mode.
if (isInitialized() && !processingEnabled) {
// The playback parameters are always the default if processing is disabled.
this.playbackParameters = PlaybackParameters.DEFAULT;
return this.playbackParameters;
}
@ -1076,7 +1077,7 @@ public final class DefaultAudioSink implements AudioSink {
audioTimestampSet = false;
}
}
if (getLatencyMethod != null && !passthrough) {
if (getLatencyMethod != null && isInputPcm) {
try {
// Compute the audio track latency, excluding the latency due to the buffer (leaving
// latency due to the mixer and audio hardware driver).
@ -1115,11 +1116,11 @@ public final class DefaultAudioSink implements AudioSink {
}
private long getSubmittedFrames() {
return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize);
return isInputPcm ? (submittedPcmBytes / pcmFrameSize) : submittedEncodedFrames;
}
private long getWrittenFrames() {
return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize);
return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames;
}
private void resetSyncParams() {
@ -1212,20 +1213,10 @@ public final class DefaultAudioSink implements AudioSink {
MODE_STATIC, audioSessionId);
}
@C.Encoding
private static int getEncodingForMimeType(String mimeType) {
switch (mimeType) {
case MimeTypes.AUDIO_AC3:
return C.ENCODING_AC3;
case MimeTypes.AUDIO_E_AC3:
return C.ENCODING_E_AC3;
case MimeTypes.AUDIO_DTS:
return C.ENCODING_DTS;
case MimeTypes.AUDIO_DTS_HD:
return C.ENCODING_DTS_HD;
default:
return C.ENCODING_INVALID;
}
private static boolean isEncodingPcm(@C.Encoding int encoding) {
return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT
|| encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT
|| encoding == C.ENCODING_PCM_FLOAT;
}
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {

View File

@ -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());
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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

View File

@ -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);
}
/**

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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.
@ -93,20 +94,15 @@ public final class FragmentedMp4Extractor implements Extractor {
* messages in the stream will be delivered as samples to this track.
*/
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;

View File

@ -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 {

View File

@ -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));

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -15,309 +15,10 @@
*/
package com.google.android.exoplayer2.source;
import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
/**
* Interface for callbacks to be notified of adaptive {@link MediaSource} events.
* Interface for callbacks to be notified of {@link MediaSource} events.
*
* @deprecated Use {@link MediaSourceEventListener}.
*/
public interface AdaptiveMediaSourceEventListener {
/**
* Called when a load begins.
*
* @param dataSpec Defines the data being loaded.
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
* being loaded.
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
* to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
* @param trackFormat The format of the track to which the data belongs. Null if the data does
* not belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
* load is not for media data.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began.
*/
void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs);
/**
* Called when a load ends.
*
* @param dataSpec Defines the data being loaded.
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
* being loaded.
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
* to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
* @param trackFormat The format of the track to which the data belongs. Null if the data does
* not belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
* load is not for media data.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended.
* @param loadDurationMs The duration of the load.
* @param bytesLoaded The number of bytes that were loaded.
*/
void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded);
/**
* Called when a load is canceled.
*
* @param dataSpec Defines the data being loaded.
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
* being loaded.
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
* to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
* @param trackFormat The format of the track to which the data belongs. Null if the data does
* not belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
* load is not for media data.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was
* canceled.
* @param loadDurationMs The duration of the load up to the point at which it was canceled.
* @param bytesLoaded The number of bytes that were loaded prior to cancelation.
*/
void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded);
/**
* Called when a load error occurs.
* <p>
* The error may or may not have resulted in the load being canceled, as indicated by the
* {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will
* <em>not</em> be called in addition to this method.
* <p>
* This method being called does not indicate that playback has failed, or that it will fail. The
* player may be able to recover from the error and continue. Hence applications should
* <em>not</em> implement this method to display a user visible error or initiate an application
* level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
* such behavior). This method is called to provide the application with an opportunity to log the
* error if it wishes to do so.
*
* @param dataSpec Defines the data being loaded.
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
* being loaded.
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
* to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
* @param trackFormat The format of the track to which the data belongs. Null if the data does
* not belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
* load is not for media data.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error
* occurred.
* @param loadDurationMs The duration of the load up to the point at which the error occurred.
* @param bytesLoaded The number of bytes that were loaded prior to the error.
* @param error The load error.
* @param wasCanceled Whether the load was canceled as a result of the error.
*/
void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded,
IOException error, boolean wasCanceled);
/**
* Called when data is removed from the back of a media buffer, typically so that it can be
* re-buffered in a different format.
*
* @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
* @param mediaStartTimeMs The start time of the media being discarded.
* @param mediaEndTimeMs The end time of the media being discarded.
*/
void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs);
/**
* Called when a downstream format change occurs (i.e. when the format of the media being read
* from one or more {@link SampleStream}s provided by the source changes).
*
* @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
* @param trackFormat The format of the track to which the data belongs. Null if the data does
* not belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaTimeMs The media time at which the change occurred.
*/
void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason,
Object trackSelectionData, long mediaTimeMs);
/**
* Dispatches events to a {@link AdaptiveMediaSourceEventListener}.
*/
final class EventDispatcher {
private final Handler handler;
private final AdaptiveMediaSourceEventListener listener;
private final long mediaTimeOffsetMs;
public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) {
this(handler, listener, 0);
}
public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener,
long mediaTimeOffsetMs) {
this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
this.listener = listener;
this.mediaTimeOffsetMs = mediaTimeOffsetMs;
}
public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) {
return new EventDispatcher(handler, listener, mediaTimeOffsetMs);
}
public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs);
}
public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType,
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) {
if (listener != null) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
trackSelectionData, adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs);
}
});
}
}
public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
long loadDurationMs, long bytesLoaded) {
loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded);
}
public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType,
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
final long loadDurationMs, final long bytesLoaded) {
if (listener != null) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat,
trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded);
}
});
}
}
public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
long loadDurationMs, long bytesLoaded) {
loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded);
}
public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType,
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
final long loadDurationMs, final long bytesLoaded) {
if (listener != null) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat,
trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded);
}
});
}
}
public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) {
loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded,
error, wasCanceled);
}
public void loadError(final DataSpec dataSpec, final int dataType, final int trackType,
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
final long loadDurationMs, final long bytesLoaded, final IOException error,
final boolean wasCanceled) {
if (listener != null) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
trackSelectionData, adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded,
error, wasCanceled);
}
});
}
}
public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs,
final long mediaEndTimeUs) {
if (listener != null) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs));
}
});
}
}
public void downstreamFormatChanged(final int trackType, final Format trackFormat,
final int trackSelectionReason, final Object trackSelectionData,
final long mediaTimeUs) {
if (listener != null) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason,
trackSelectionData, adjustMediaTime(mediaTimeUs));
}
});
}
}
private long adjustMediaTime(long mediaTimeUs) {
long mediaTimeMs = C.usToMs(mediaTimeUs);
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
}
}
}
@Deprecated
public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {}

View File

@ -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;

View File

@ -0,0 +1,139 @@
/*
* 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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -17,14 +17,19 @@ 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.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
@ -40,10 +45,12 @@ import java.io.IOException;
* Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
*/
public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener {
/**
* Listener of {@link ExtractorMediaSource} events.
*
* @deprecated Use {@link MediaSourceEventListener}.
*/
@Deprecated
public interface EventListener {
/**
@ -89,8 +96,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory;
private final int minLoadableRetryCount;
private final Handler eventHandler;
private final EventListener eventListener;
private final EventDispatcher eventDispatcher;
private final String customCacheKey;
private final int continueLoadingCheckIntervalBytes;
@ -98,6 +104,127 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
private long timelineDurationUs;
private boolean timelineIsSeekable;
/** Factory for {@link ExtractorMediaSource}s. */
public static final class Factory implements AdsMediaSource.MediaSourceFactory {
private final DataSource.Factory dataSourceFactory;
private @Nullable ExtractorsFactory extractorsFactory;
private @Nullable String customCacheKey;
private int minLoadableRetryCount;
private int continueLoadingCheckIntervalBytes;
private boolean isCreateCalled;
/**
* Creates a new factory for {@link ExtractorMediaSource}s.
*
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
*/
public Factory(DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA;
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
}
/**
* Sets the factory for {@link Extractor}s to process the media stream. The default value is an
* instance of {@link DefaultExtractorsFactory}.
*
* @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
* possible formats are known, pass a factory that instantiates extractors for those
* formats.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) {
Assertions.checkState(!isCreateCalled);
this.extractorsFactory = extractorsFactory;
return this;
}
/**
* Sets the custom key that uniquely identifies the original stream. Used for cache indexing.
* The default value is {@code null}.
*
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for
* cache indexing.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setCustomCacheKey(String customCacheKey) {
Assertions.checkState(!isCreateCalled);
this.customCacheKey = customCacheKey;
return this;
}
/**
* Sets the minimum number of times to retry if a loading error occurs. The default value is
* {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}.
*
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {
Assertions.checkState(!isCreateCalled);
this.minLoadableRetryCount = minLoadableRetryCount;
return this;
}
/**
* Sets the number of bytes that should be loaded between each invocation of {@link
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is
* {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}.
*
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between
* each invocation of {@link
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) {
Assertions.checkState(!isCreateCalled);
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
return this;
}
/**
* Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events
* will not be delivered.
*
* @param uri The {@link Uri}.
* @return The new {@link ExtractorMediaSource}.
*/
public ExtractorMediaSource createMediaSource(Uri uri) {
return createMediaSource(uri, null, null);
}
/**
* Returns a new {@link ExtractorMediaSource} using the current parameters.
*
* @param uri The {@link Uri}.
* @param eventHandler A handler for events.
* @param eventListener A listener of events.
* @return The new {@link ExtractorMediaSource}.
*/
@Override
public ExtractorMediaSource createMediaSource(
Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) {
isCreateCalled = true;
if (extractorsFactory == null) {
extractorsFactory = new DefaultExtractorsFactory();
}
return new ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory,
minLoadableRetryCount, eventHandler, eventListener, customCacheKey,
continueLoadingCheckIntervalBytes);
}
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_OTHER};
}
}
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
@ -106,9 +233,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
* Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
* @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.
* @deprecated Use {@link Factory} instead.
*/
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) {
@Deprecated
public ExtractorMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory,
Handler eventHandler,
EventListener eventListener) {
this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null);
}
@ -122,9 +255,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
* @deprecated Use {@link Factory} instead.
*/
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener,
@Deprecated
public ExtractorMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory,
Handler eventHandler,
EventListener eventListener,
String customCacheKey) {
this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES);
@ -143,16 +282,43 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
* indexing. May be null.
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
* invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
* @deprecated Use {@link Factory} instead.
*/
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler,
EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) {
@Deprecated
public ExtractorMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory,
int minLoadableRetryCount,
Handler eventHandler,
EventListener eventListener,
String customCacheKey,
int continueLoadingCheckIntervalBytes) {
this(
uri,
dataSourceFactory,
extractorsFactory,
minLoadableRetryCount,
eventHandler,
eventListener == null ? null : new EventListenerWrapper(eventListener),
customCacheKey,
continueLoadingCheckIntervalBytes);
}
private ExtractorMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory,
int minLoadableRetryCount,
@Nullable Handler eventHandler,
@Nullable MediaSourceEventListener eventListener,
@Nullable String customCacheKey,
int continueLoadingCheckIntervalBytes) {
this.uri = uri;
this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
}
@ -171,9 +337,16 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
Assertions.checkArgument(id.periodIndex == 0);
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
this, allocator, customCacheKey, continueLoadingCheckIntervalBytes);
return new ExtractorMediaPeriod(
uri,
dataSourceFactory.createDataSource(),
extractorsFactory.createExtractors(),
minLoadableRetryCount,
eventDispatcher,
this,
allocator,
customCacheKey,
continueLoadingCheckIntervalBytes);
}
@Override
@ -208,4 +381,94 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null);
}
/**
* Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in
* {@link MediaSourceEventListener}.
*/
private static final class EventListenerWrapper implements MediaSourceEventListener {
private final EventListener eventListener;
public EventListenerWrapper(EventListener eventListener) {
this.eventListener = Assertions.checkNotNull(eventListener);
}
@Override
public void onLoadStarted(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs) {
// Do nothing.
}
@Override
public void onLoadCompleted(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded) {
// Do nothing.
}
@Override
public void onLoadCanceled(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded) {
// Do nothing.
}
@Override
public void onLoadError(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded,
IOException error,
boolean wasCanceled) {
eventListener.onLoadError(error);
}
@Override
public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
// Do nothing.
}
@Override
public void onDownstreamFormatChanged(
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaTimeMs) {
// Do nothing.
}
}
}

View File

@ -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();

View File

@ -0,0 +1,491 @@
/*
* 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 android.os.Handler;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
/** Interface for callbacks to be notified of {@link MediaSource} events. */
public interface MediaSourceEventListener {
/**
* Called when a load begins.
*
* @param dataSpec Defines the data being loaded.
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
* being loaded.
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to
* media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
* belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
* load is not for media data.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began.
*/
void onLoadStarted(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs);
/**
* Called when a load ends.
*
* @param dataSpec Defines the data being loaded.
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
* being loaded.
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to
* media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
* belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
* load is not for media data.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended.
* @param loadDurationMs The duration of the load.
* @param bytesLoaded The number of bytes that were loaded.
*/
void onLoadCompleted(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded);
/**
* Called when a load is canceled.
*
* @param dataSpec Defines the data being loaded.
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
* being loaded.
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to
* media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
* belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
* load is not for media data.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was
* canceled.
* @param loadDurationMs The duration of the load up to the point at which it was canceled.
* @param bytesLoaded The number of bytes that were loaded prior to cancelation.
*/
void onLoadCanceled(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded);
/**
* Called when a load error occurs.
*
* <p>The error may or may not have resulted in the load being canceled, as indicated by the
* {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will
* <em>not</em> be called in addition to this method.
*
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error and continue. Hence applications should
* <em>not</em> implement this method to display a user visible error or initiate an application
* level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
* such behavior). This method is called to provide the application with an opportunity to log the
* error if it wishes to do so.
*
* @param dataSpec Defines the data being loaded.
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
* being loaded.
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to
* media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
* belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
* load is not for media data.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error
* occurred.
* @param loadDurationMs The duration of the load up to the point at which the error occurred.
* @param bytesLoaded The number of bytes that were loaded prior to the error.
* @param error The load error.
* @param wasCanceled Whether the load was canceled as a result of the error.
*/
void onLoadError(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded,
IOException error,
boolean wasCanceled);
/**
* Called when data is removed from the back of a media buffer, typically so that it can be
* re-buffered in a different format.
*
* @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
* @param mediaStartTimeMs The start time of the media being discarded.
* @param mediaEndTimeMs The end time of the media being discarded.
*/
void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs);
/**
* Called when a downstream format change occurs (i.e. when the format of the media being read
* from one or more {@link SampleStream}s provided by the source changes).
*
* @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
* belong to a track.
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
* @param trackSelectionData Optional data associated with the selection of the track to which the
* data belongs. Null if the data does not belong to a track.
* @param mediaTimeMs The media time at which the change occurred.
*/
void onDownstreamFormatChanged(
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaTimeMs);
/** Dispatches events to a {@link MediaSourceEventListener}. */
final class EventDispatcher {
@Nullable private final Handler handler;
@Nullable private final MediaSourceEventListener listener;
private final long mediaTimeOffsetMs;
public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
this(handler, listener, 0);
}
public EventDispatcher(
@Nullable Handler handler,
@Nullable MediaSourceEventListener listener,
long mediaTimeOffsetMs) {
this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
this.listener = listener;
this.mediaTimeOffsetMs = mediaTimeOffsetMs;
}
public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) {
return new EventDispatcher(handler, listener, mediaTimeOffsetMs);
}
public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
loadStarted(
dataSpec,
dataType,
C.TRACK_TYPE_UNKNOWN,
null,
C.SELECTION_REASON_UNKNOWN,
null,
C.TIME_UNSET,
C.TIME_UNSET,
elapsedRealtimeMs);
}
public void loadStarted(
final DataSpec dataSpec,
final int dataType,
final int trackType,
final Format trackFormat,
final int trackSelectionReason,
final Object trackSelectionData,
final long mediaStartTimeUs,
final long mediaEndTimeUs,
final long elapsedRealtimeMs) {
if (listener != null && handler != null) {
handler.post(
new Runnable() {
@Override
public void run() {
listener.onLoadStarted(
dataSpec,
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs),
elapsedRealtimeMs);
}
});
}
}
public void loadCompleted(
DataSpec dataSpec,
int dataType,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded) {
loadCompleted(
dataSpec,
dataType,
C.TRACK_TYPE_UNKNOWN,
null,
C.SELECTION_REASON_UNKNOWN,
null,
C.TIME_UNSET,
C.TIME_UNSET,
elapsedRealtimeMs,
loadDurationMs,
bytesLoaded);
}
public void loadCompleted(
final DataSpec dataSpec,
final int dataType,
final int trackType,
final Format trackFormat,
final int trackSelectionReason,
final Object trackSelectionData,
final long mediaStartTimeUs,
final long mediaEndTimeUs,
final long elapsedRealtimeMs,
final long loadDurationMs,
final long bytesLoaded) {
if (listener != null && handler != null) {
handler.post(
new Runnable() {
@Override
public void run() {
listener.onLoadCompleted(
dataSpec,
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs),
elapsedRealtimeMs,
loadDurationMs,
bytesLoaded);
}
});
}
}
public void loadCanceled(
DataSpec dataSpec,
int dataType,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded) {
loadCanceled(
dataSpec,
dataType,
C.TRACK_TYPE_UNKNOWN,
null,
C.SELECTION_REASON_UNKNOWN,
null,
C.TIME_UNSET,
C.TIME_UNSET,
elapsedRealtimeMs,
loadDurationMs,
bytesLoaded);
}
public void loadCanceled(
final DataSpec dataSpec,
final int dataType,
final int trackType,
final Format trackFormat,
final int trackSelectionReason,
final Object trackSelectionData,
final long mediaStartTimeUs,
final long mediaEndTimeUs,
final long elapsedRealtimeMs,
final long loadDurationMs,
final long bytesLoaded) {
if (listener != null && handler != null) {
handler.post(
new Runnable() {
@Override
public void run() {
listener.onLoadCanceled(
dataSpec,
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs),
elapsedRealtimeMs,
loadDurationMs,
bytesLoaded);
}
});
}
}
public void loadError(
DataSpec dataSpec,
int dataType,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded,
IOException error,
boolean wasCanceled) {
loadError(
dataSpec,
dataType,
C.TRACK_TYPE_UNKNOWN,
null,
C.SELECTION_REASON_UNKNOWN,
null,
C.TIME_UNSET,
C.TIME_UNSET,
elapsedRealtimeMs,
loadDurationMs,
bytesLoaded,
error,
wasCanceled);
}
public void loadError(
final DataSpec dataSpec,
final int dataType,
final int trackType,
final Format trackFormat,
final int trackSelectionReason,
final Object trackSelectionData,
final long mediaStartTimeUs,
final long mediaEndTimeUs,
final long elapsedRealtimeMs,
final long loadDurationMs,
final long bytesLoaded,
final IOException error,
final boolean wasCanceled) {
if (listener != null && handler != null) {
handler.post(
new Runnable() {
@Override
public void run() {
listener.onLoadError(
dataSpec,
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs),
elapsedRealtimeMs,
loadDurationMs,
bytesLoaded,
error,
wasCanceled);
}
});
}
}
public void upstreamDiscarded(
final int trackType, final long mediaStartTimeUs, final long mediaEndTimeUs) {
if (listener != null && handler != null) {
handler.post(
new Runnable() {
@Override
public void run() {
listener.onUpstreamDiscarded(
trackType, adjustMediaTime(mediaStartTimeUs), adjustMediaTime(mediaEndTimeUs));
}
});
}
}
public void downstreamFormatChanged(
final int trackType,
final Format trackFormat,
final int trackSelectionReason,
final Object trackSelectionData,
final long mediaTimeUs) {
if (listener != null && handler != null) {
handler.post(
new Runnable() {
@Override
public void run() {
listener.onDownstreamFormatChanged(
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaTimeUs));
}
});
}
}
private long adjustMediaTime(long mediaTimeUs) {
long mediaTimeMs = C.usToMs(mediaTimeUs);
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
}
}
}

View File

@ -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) {

View File

@ -17,11 +17,14 @@ package com.google.android.exoplayer2.source;
import android.net.Uri;
import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
@ -32,7 +35,10 @@ public final class SingleSampleMediaSource implements MediaSource {
/**
* Listener of {@link SingleSampleMediaSource} events.
*
* @deprecated Use {@link MediaSourceEventListener}.
*/
@Deprecated
public interface EventListener {
/**
@ -45,18 +51,110 @@ public final class SingleSampleMediaSource implements MediaSource {
}
/** Factory for {@link SingleSampleMediaSource}. */
public static final class Factory {
private final DataSource.Factory dataSourceFactory;
private int minLoadableRetryCount;
private boolean treatLoadErrorsAsEndOfStream;
private boolean isCreateCalled;
/**
* Creates a factory for {@link SingleSampleMediaSource}s.
*
* @param dataSourceFactory The factory from which the {@link DataSource} to read the media will
* be obtained.
*/
public Factory(DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory);
this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT;
}
/**
* Sets the minimum number of times to retry if a loading error occurs. The default value is
* {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}.
*
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {
Assertions.checkState(!isCreateCalled);
this.minLoadableRetryCount = minLoadableRetryCount;
return this;
}
/**
* Sets whether load errors will be treated as end-of-stream signal (load errors will not be
* propagated). The default value is false.
*
* @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample
* streams, treating them as ended instead. If false, load errors will be propagated
* normally by {@link SampleStream#maybeThrowError()}.
* @return This factory, for convenience.
* @throws IllegalStateException If one of the {@code create} methods has already been called.
*/
public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) {
Assertions.checkState(!isCreateCalled);
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
return this;
}
/**
* Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events
* will not be delivered.
*
* @param uri The {@link Uri}.
* @param format The {@link Format} of the media stream.
* @param durationUs The duration of the media stream in microseconds.
* @return The new {@link ExtractorMediaSource}.
*/
public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) {
return createMediaSource(uri, format, durationUs, null, null);
}
/**
* Returns a new {@link SingleSampleMediaSource} using the current parameters.
*
* @param uri The {@link Uri}.
* @param format The {@link Format} of the media stream.
* @param durationUs The duration of the media stream in microseconds.
* @param eventHandler A handler for events.
* @param eventListener A listener of events., Format format, long durationUs
* @return The newly built {@link SingleSampleMediaSource}.
*/
public SingleSampleMediaSource createMediaSource(
Uri uri,
Format format,
long durationUs,
@Nullable Handler eventHandler,
@Nullable MediaSourceEventListener eventListener) {
isCreateCalled = true;
return new SingleSampleMediaSource(
uri,
dataSourceFactory,
format,
durationUs,
minLoadableRetryCount,
eventHandler,
eventListener,
treatLoadErrorsAsEndOfStream);
}
}
/**
* The default minimum number of times to retry loading data prior to failing.
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
private final Uri uri;
private final DataSpec dataSpec;
private final DataSource.Factory dataSourceFactory;
private final Format format;
private final long durationUs;
private final MediaSourceEventListener.EventDispatcher eventDispatcher;
private final int minLoadableRetryCount;
private final Handler eventHandler;
private final EventListener eventListener;
private final int eventSourceId;
private final boolean treatLoadErrorsAsEndOfStream;
private final Timeline timeline;
@ -66,9 +164,11 @@ public final class SingleSampleMediaSource implements MediaSource {
* be obtained.
* @param format The {@link Format} associated with the output track.
* @param durationUs The duration of the media stream in microseconds.
* @deprecated Use {@link Factory} instead.
*/
public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
long durationUs) {
@Deprecated
public SingleSampleMediaSource(
Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) {
this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
}
@ -79,10 +179,16 @@ public final class SingleSampleMediaSource implements MediaSource {
* @param format The {@link Format} associated with the output track.
* @param durationUs The duration of the media stream in microseconds.
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
* @deprecated Use {@link Factory} instead.
*/
public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
long durationUs, int minLoadableRetryCount) {
this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false);
@Deprecated
public SingleSampleMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
Format format,
long durationUs,
int minLoadableRetryCount) {
this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, false);
}
/**
@ -98,18 +204,46 @@ public final class SingleSampleMediaSource implements MediaSource {
* @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample
* streams, treating them as ended instead. If false, load errors will be propagated normally
* by {@link SampleStream#maybeThrowError()}.
* @deprecated Use {@link Factory} instead.
*/
public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
int eventSourceId, boolean treatLoadErrorsAsEndOfStream) {
this.uri = uri;
@Deprecated
public SingleSampleMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
Format format,
long durationUs,
int minLoadableRetryCount,
Handler eventHandler,
EventListener eventListener,
int eventSourceId,
boolean treatLoadErrorsAsEndOfStream) {
this(
uri,
dataSourceFactory,
format,
durationUs,
minLoadableRetryCount,
eventHandler,
eventListener == null ? null : new EventListenerWrapper(eventListener, eventSourceId),
treatLoadErrorsAsEndOfStream);
}
private SingleSampleMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
Format format,
long durationUs,
int minLoadableRetryCount,
Handler eventHandler,
MediaSourceEventListener eventListener,
boolean treatLoadErrorsAsEndOfStream) {
this.dataSourceFactory = dataSourceFactory;
this.format = format;
this.durationUs = durationUs;
this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.eventSourceId = eventSourceId;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
dataSpec = new DataSpec(uri);
timeline = new SinglePeriodTimeline(durationUs, true);
}
@ -128,8 +262,14 @@ public final class SingleSampleMediaSource implements MediaSource {
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
Assertions.checkArgument(id.periodIndex == 0);
return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount,
eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream);
return new SingleSampleMediaPeriod(
dataSpec,
dataSourceFactory,
format,
durationUs,
minLoadableRetryCount,
eventDispatcher,
treatLoadErrorsAsEndOfStream);
}
@Override
@ -142,4 +282,97 @@ public final class SingleSampleMediaSource implements MediaSource {
// Do nothing.
}
/**
* Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in
* {@link MediaSourceEventListener}.
*/
private static final class EventListenerWrapper implements MediaSourceEventListener {
private final EventListener eventListener;
private final int eventSourceId;
public EventListenerWrapper(EventListener eventListener, int eventSourceId) {
this.eventListener = Assertions.checkNotNull(eventListener);
this.eventSourceId = eventSourceId;
}
@Override
public void onLoadStarted(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs) {
// Do nothing.
}
@Override
public void onLoadCompleted(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded) {
// Do nothing.
}
@Override
public void onLoadCanceled(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded) {
// Do nothing.
}
@Override
public void onLoadError(
DataSpec dataSpec,
int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs,
long loadDurationMs,
long bytesLoaded,
IOException error,
boolean wasCanceled) {
eventListener.onLoadError(eventSourceId, error);
}
@Override
public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
// Do nothing.
}
@Override
public void onDownstreamFormatChanged(
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaTimeMs) {
// Do nothing.
}
}
}

View File

@ -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) {

View File

@ -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;
@ -71,6 +72,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}.

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.ads;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable;
@ -23,16 +24,19 @@ import android.view.ViewGroup;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.DeferredMediaPeriod;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -40,10 +44,33 @@ import java.util.Map;
*/
public final class AdsMediaSource implements MediaSource {
/**
* Listener for events relating to ad loading.
*/
public interface AdsListener {
/** Factory for creating {@link MediaSource}s to play ad media. */
public interface MediaSourceFactory {
/**
* Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}.
*
* @param uri The URI of the media or manifest to play.
* @param handler A handler for listener events. May be null if delivery of events is not
* required.
* @param listener A listener for events. May be null if delivery of events is not required.
* @return The new media source.
*/
MediaSource createMediaSource(
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener);
/**
* Returns the content types supported by media sources created by this factory. Each element
* should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link
* C#TYPE_OTHER}.
*
* @return The content types supported by media sources created by this factory.
*/
int[] getSupportedTypes();
}
/** Listener for ads media source events. */
public interface EventListener extends MediaSourceEventListener {
/**
* Called if there was an error loading ads. The media source will load the content without ads
@ -69,17 +96,15 @@ public final class AdsMediaSource implements MediaSource {
private static final String TAG = "AdsMediaSource";
private final MediaSource contentMediaSource;
private final DataSource.Factory dataSourceFactory;
private final MediaSourceFactory adMediaSourceFactory;
private final AdsLoader adsLoader;
private final ViewGroup adUiViewGroup;
@Nullable private final Handler eventHandler;
@Nullable private final EventListener eventListener;
private final Handler mainHandler;
private final ComponentListener componentListener;
private final Map<MediaPeriod, MediaSource> adMediaSourceByMediaPeriod;
private final Map<MediaSource, List<DeferredMediaPeriod>> deferredMediaPeriodByAdMediaSource;
private final Timeline.Period period;
@Nullable
private final Handler eventHandler;
@Nullable
private final AdsListener eventListener;
private Handler playerHandler;
private ExoPlayer player;
@ -94,22 +119,31 @@ public final class AdsMediaSource implements MediaSource {
private MediaSource.Listener listener;
/**
* 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}. Ad media is loaded using {@link ExtractorMediaSource}.
*
* @param contentMediaSource The {@link MediaSource} providing the content to play.
* @param dataSourceFactory Factory for data sources used to load ad media.
* @param adsLoader The loader for ads.
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
*/
public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
AdsLoader adsLoader, ViewGroup adUiViewGroup) {
this(contentMediaSource, dataSourceFactory, adsLoader, adUiViewGroup, null, null);
public AdsMediaSource(
MediaSource contentMediaSource,
DataSource.Factory dataSourceFactory,
AdsLoader adsLoader,
ViewGroup adUiViewGroup) {
this(
contentMediaSource,
dataSourceFactory,
adsLoader,
adUiViewGroup,
/* eventHandler= */ null,
/* eventListener= */ null);
}
/**
* 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}. Ad media is loaded using {@link ExtractorMediaSource}.
*
* @param contentMediaSource The {@link MediaSource} providing the content to play.
* @param dataSourceFactory Factory for data sources used to load ad media.
@ -118,21 +152,53 @@ public final class AdsMediaSource 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 AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler,
@Nullable AdsListener eventListener) {
public AdsMediaSource(
MediaSource contentMediaSource,
DataSource.Factory dataSourceFactory,
AdsLoader adsLoader,
ViewGroup adUiViewGroup,
@Nullable Handler eventHandler,
@Nullable EventListener eventListener) {
this(
contentMediaSource,
new ExtractorMediaSource.Factory(dataSourceFactory),
adsLoader,
adUiViewGroup,
eventHandler,
eventListener);
}
/**
* 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 adMediaSourceFactory Factory for media sources used to load ad media.
* @param adsLoader The loader for ads.
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @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 AdsMediaSource(
MediaSource contentMediaSource,
MediaSourceFactory adMediaSourceFactory,
AdsLoader adsLoader,
ViewGroup adUiViewGroup,
@Nullable Handler eventHandler,
@Nullable EventListener eventListener) {
this.contentMediaSource = contentMediaSource;
this.dataSourceFactory = dataSourceFactory;
this.adMediaSourceFactory = adMediaSourceFactory;
this.adsLoader = adsLoader;
this.adUiViewGroup = adUiViewGroup;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
mainHandler = new Handler(Looper.getMainLooper());
componentListener = new ComponentListener();
adMediaSourceByMediaPeriod = new HashMap<>();
deferredMediaPeriodByAdMediaSource = new HashMap<>();
period = new Timeline.Period();
adGroupMediaSources = new MediaSource[0][];
adDurationsUs = new long[0][];
adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes());
}
@Override
@ -173,9 +239,9 @@ public final class AdsMediaSource implements MediaSource {
final int adGroupIndex = id.adGroupIndex;
final int adIndexInAdGroup = id.adIndexInAdGroup;
if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
MediaSource adMediaSource = new ExtractorMediaSource(
adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory,
new DefaultExtractorsFactory(), mainHandler, componentListener);
Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup];
final MediaSource adMediaSource =
adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener);
int oldAdCount = adGroupMediaSources[id.adGroupIndex].length;
if (adIndexInAdGroup >= oldAdCount) {
int adCount = adIndexInAdGroup + 1;
@ -185,30 +251,37 @@ public final class AdsMediaSource implements MediaSource {
Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
}
adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
adMediaSource.prepareSource(player, false, new Listener() {
deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList<DeferredMediaPeriod>());
adMediaSource.prepareSource(player, false, new MediaSource.Listener() {
@Override
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
Object manifest) {
onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline);
@Nullable Object manifest) {
onAdSourceInfoRefreshed(adMediaSource, adGroupIndex, adIndexInAdGroup, timeline);
}
});
}
MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator);
adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource);
return mediaPeriod;
DeferredMediaPeriod deferredMediaPeriod =
new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator);
List<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
if (mediaPeriods == null) {
deferredMediaPeriod.createPeriod();
} else {
// Keep track of the deferred media period so it can be populated with the real media period
// when the source's info becomes available.
mediaPeriods.add(deferredMediaPeriod);
}
return deferredMediaPeriod;
} else {
return contentMediaSource.createPeriod(id, allocator);
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator);
mediaPeriod.createPeriod();
return mediaPeriod;
}
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) {
adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
} else {
contentMediaSource.releasePeriod(mediaPeriod);
}
((DeferredMediaPeriod) mediaPeriod).releasePeriod();
}
@Override
@ -263,9 +336,17 @@ public final class AdsMediaSource implements MediaSource {
maybeUpdateSourceInfo();
}
private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) {
private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex,
int adIndexInAdGroup, Timeline timeline) {
Assertions.checkArgument(timeline.getPeriodCount() == 1);
adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs();
if (deferredMediaPeriodByAdMediaSource.containsKey(mediaSource)) {
List<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
for (int i = 0; i < mediaPeriods.size(); i++) {
mediaPeriods.get(i).createPeriod();
}
deferredMediaPeriodByAdMediaSource.remove(mediaSource);
}
maybeUpdateSourceInfo();
}
@ -280,11 +361,8 @@ public final class AdsMediaSource implements MediaSource {
}
}
/**
* Listener for component events. All methods are called on the main thread.
*/
private final class ComponentListener implements AdsLoader.EventListener,
ExtractorMediaSource.EventListener {
/** Listener for component events. All methods are called on the main thread. */
private final class ComponentListener implements AdsLoader.EventListener {
@Override
public void onAdPlaybackState(final AdPlaybackState adPlaybackState) {

View File

@ -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;

View File

@ -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) {

View File

@ -104,7 +104,7 @@ public final class Cea708Decoder extends CeaDecoder {
private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes)
private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes)
private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes)
private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes)
private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes)
private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes)
private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes)
private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes)
@ -464,7 +464,7 @@ public final class Cea708Decoder extends CeaDecoder {
case COMMAND_DF1:
case COMMAND_DF2:
case COMMAND_DF3:
case COMMAND_DS4:
case COMMAND_DF4:
case COMMAND_DF5:
case COMMAND_DF6:
case COMMAND_DF7:

Some files were not shown because too many files have changed in this diff Show More