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: Before filing an issue:
----------------------- -----------------------
- Search existing issues, including issues that are closed. - 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 In addition to library modules, ExoPlayer has multiple extension modules that
depend on external libraries to provide additional functionality. Some 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. Browse the [extensions directory][] and their individual READMEs for details.
More information on the library and extension modules that are available from More information on the library and extension modules that are available from

View file

@ -1,5 +1,47 @@
# Release notes # # 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 ### ### 2.6.0 ###
* Removed "r" prefix from versions. This release is "2.6.0", not "r2.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. easy and seamless way of incorporating display ads into ExoPlayer playbacks.
You can read more about the IMA extension You can read more about the IMA extension
[here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). [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. MediaSessionCompat in the Android Support Library.
* RTMP extension: An extension for playing streams over RTMP. * RTMP extension: An extension for playing streams over RTMP.
* Build: Made it easier for application developers to depend on a local checkout * Build: Made it easier for application developers to depend on a local checkout

View file

@ -17,7 +17,7 @@ buildscript {
google() google()
} }
dependencies { 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' classpath 'com.novoda:bintray-release:0.5.0'
} }
// Workaround for the following test coverage issue. Remove when fixed: // 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 // However, please note that the core media playback functionality provided
// by the library requires API level 16 or greater. // by the library requires API level 16 or greater.
minSdkVersion = 14 minSdkVersion = 14
compileSdkVersion = 26 compileSdkVersion = 27
targetSdkVersion = 26 targetSdkVersion = 27
buildToolsVersion = '26.0.2' buildToolsVersion = '26.0.2'
testSupportLibraryVersion = '0.5' testSupportLibraryVersion = '0.5'
supportLibraryVersion = '27.0.0' supportLibraryVersion = '27.0.0'
@ -28,7 +28,7 @@ project.ext {
junitVersion = '4.12' junitVersion = '4.12'
truthVersion = '0.35' truthVersion = '0.35'
robolectricVersion = '3.4.2' robolectricVersion = '3.4.2'
releaseVersion = '2.6.0' releaseVersion = '2.6.1'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix

View file

@ -43,5 +43,7 @@ android {
dependencies { dependencies {
compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-ui') compile project(modulePrefix + 'library-ui')
compile project(modulePrefix + 'library-dash')
compile project(modulePrefix + 'library-hls')
compile project(modulePrefix + 'extension-ima') compile project(modulePrefix + 'extension-ima')
} }

View file

@ -15,11 +15,11 @@
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.imademo" package="com.google.android.exoplayer2.imademo"
android:versionCode="2600" android:versionCode="2601"
android:versionName="2.6.0"> android:versionName="2.6.1">
<uses-permission android:name="android.permission.INTERNET"/> <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" <application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">

View file

@ -17,15 +17,21 @@ package com.google.android.exoplayer2.imademo;
import android.content.Context; import android.content.Context;
import android.net.Uri; 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.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; 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.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; 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.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.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection; 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.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
* Manages the {@link ExoPlayer}, the IMA plugin and all video playback. /* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
*/
/* package */ final class PlayerManager {
private final ImaAdsLoader adsLoader; private final ImaAdsLoader adsLoader;
private final DataSource.Factory manifestDataSourceFactory;
private final DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private long contentPosition; private long contentPosition;
@ -50,6 +56,14 @@ import com.google.android.exoplayer2.util.Util;
public PlayerManager(Context context) { public PlayerManager(Context context) {
String adTag = context.getString(R.string.ad_tag_url); String adTag = context.getString(R.string.ad_tag_url);
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); 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) { public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) {
@ -69,17 +83,21 @@ import com.google.android.exoplayer2.util.Util;
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
Util.getUserAgent(context, context.getString(R.string.application_name))); 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). // This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url); String contentUrl = context.getString(R.string.content_url);
MediaSource contentMediaSource = new ExtractorMediaSource( MediaSource contentMediaSource =
Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null); new ExtractorMediaSource.Factory(dataSourceFactory)
.createMediaSource(Uri.parse(contentUrl));
// Compose the content media source into a new AdsMediaSource with both ads and content. // Compose the content media source into a new AdsMediaSource with both ads and content.
MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, MediaSource mediaSourceWithAds =
adsLoader, simpleExoPlayerView.getOverlayFrameLayout()); new AdsMediaSource(
contentMediaSource,
/* adMediaSourceFactory= */ this,
adsLoader,
simpleExoPlayerView.getOverlayFrameLayout(),
/* eventHandler= */ null,
/* eventListener= */ null);
// Prepare the player with the source. // Prepare the player with the source.
player.seekTo(contentPosition); player.seekTo(contentPosition);
@ -103,4 +121,32 @@ import com.google.android.exoplayer2.util.Util;
adsLoader.release(); 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo"
android:versionCode="2600" android:versionCode="2601"
android:versionName="2.6.0"> android:versionName="2.6.1">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/> <uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" 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 <application
android:label="@string/application_name" android:label="@string/application_name"

View file

@ -16,14 +16,43 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.Locale; import java.util.Locale;
import java.util.UUID;
/** /**
* Utility methods for demo application. * 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. * 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.PrivFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; 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;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
@ -52,12 +52,15 @@ import java.io.IOException;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.Locale; import java.util.Locale;
/** /** Logs player events using {@link Log}. */
* Logs player events using {@link Log}. /* package */ final class EventLogger
*/ implements Player.EventListener,
/* package */ final class EventLogger implements Player.EventListener, MetadataOutput, MetadataOutput,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, AudioRendererEventListener,
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener { VideoRendererEventListener,
MediaSourceEventListener,
AdsMediaSource.EventListener,
DefaultDrmSessionManager.EventListener {
private static final String TAG = "EventLogger"; private static final String TAG = "EventLogger";
private static final int MAX_TIMELINE_ITEM_LINES = 3; private static final int MAX_TIMELINE_ITEM_LINES = 3;
@ -320,19 +323,19 @@ import java.util.Locale;
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
} }
// ExtractorMediaSource.EventListener // MediaSourceEventListener
@Override @Override
public void onLoadError(IOException error) { public void onLoadStarted(
printInternalError("loadError", error); DataSpec dataSpec,
} int dataType,
int trackType,
// AdaptiveMediaSourceEventListener Format trackFormat,
int trackSelectionReason,
@Override Object trackSelectionData,
public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, long mediaStartTimeMs,
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs,
long mediaEndTimeMs, long elapsedRealtimeMs) { long elapsedRealtimeMs) {
// Do nothing. // Do nothing.
} }
@ -369,6 +372,23 @@ import java.util.Locale;
// Do nothing. // 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 // Internal methods
private void printInternalError(String type, Exception e) { 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, private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
int trackIndex) { int trackIndex) {
return getTrackStatusString(selection != null && selection.getTrackGroup() == group return getTrackStatusString(selection != null && selection.getTrackGroup() == group

View file

@ -23,6 +23,7 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; 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.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.UnsupportedDrmException; 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.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; 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.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
@ -84,7 +85,7 @@ import java.util.UUID;
public class PlayerActivity extends Activity implements OnClickListener, public class PlayerActivity extends Activity implements OnClickListener,
PlaybackControlView.VisibilityListener { 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_LICENSE_URL = "drm_license_url";
public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
public static final String DRM_MULTI_SESSION = "drm_multi_session"; 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 EXTENSION_LIST_EXTRA = "extension_list";
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; 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 DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER; private static final CookieManager DEFAULT_COOKIE_MANAGER;
static { static {
@ -231,8 +235,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
} else if (view.getParent() == debugRootView) { } else if (view.getParent() == debugRootView) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) { if (mappedTrackInfo != null) {
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), trackSelectionHelper.showSelectionDialog(
trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag()); this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag());
} }
} }
} }
@ -257,10 +261,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
lastSeenTrackGroupArray = null; lastSeenTrackGroupArray = null;
eventLogger = new EventLogger(trackSelector); 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; 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 drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); 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; errorStringId = R.string.error_drm_not_supported;
} else { } else {
try { 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, drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl,
keyRequestPropertiesArray, multiSession); keyRequestPropertiesArray, multiSession);
} catch (UnsupportedDrmException e) { } catch (UnsupportedDrmException e) {
@ -295,8 +300,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
player.addListener(new PlayerEventListener()); player.addListener(new PlayerEventListener());
player.addListener(eventLogger); player.addListener(eventLogger);
player.addMetadataOutput(eventLogger); player.addMetadataOutput(eventLogger);
player.setAudioDebugListener(eventLogger); player.addAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger); player.addVideoDebugListener(eventLogger);
simpleExoPlayerView.setPlayer(player); simpleExoPlayerView.setPlayer(player);
player.setPlayWhenReady(shouldAutoPlay); player.setPlayWhenReady(shouldAutoPlay);
@ -329,7 +334,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
} }
MediaSource[] mediaSources = new MediaSource[uris.length]; MediaSource[] mediaSources = new MediaSource[uris.length];
for (int i = 0; i < uris.length; i++) { 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] MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources); : new ConcatenatingMediaSource(mediaSources);
@ -357,21 +362,30 @@ public class PlayerActivity extends Activity implements OnClickListener,
updateButtonVisibilities(); 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) @ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
: Util.inferContentType("." + overrideExtension); : Util.inferContentType("." + overrideExtension);
switch (type) { switch (type) {
case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false),
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
case C.TYPE_DASH: case C.TYPE_DASH:
return new DashMediaSource(uri, buildDataSourceFactory(false), return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); 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: case C.TYPE_HLS:
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); return new HlsMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
mainHandler, eventLogger); .createMediaSource(uri, handler, listener);
default: { default: {
throw new IllegalStateException("Unsupported type: " + type); 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. // The demo app has a non-null overlay frame layout.
simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); 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() { private void releaseAdsLoader() {

View file

@ -32,8 +32,8 @@ import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; 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.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -202,7 +202,11 @@ public class SampleChooserActivity extends Activity {
break; break;
case "drm_scheme": case "drm_scheme":
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: 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; break;
case "drm_license_url": case "drm_license_url":
Assertions.checkState(!insidePlaylist, Assertions.checkState(!insidePlaylist,
@ -270,23 +274,6 @@ public class SampleChooserActivity extends Activity {
return group; 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 { private static final class SampleAdapter extends BaseExpandableListAdapter {
@ -393,7 +380,7 @@ public class SampleChooserActivity extends Activity {
public void updateIntent(Intent intent) { public void updateIntent(Intent intent) {
Assertions.checkNotNull(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_LICENSE_URL, drmLicenseUrl);
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession); 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_common_java.jar')
compile files('libs/cronet_impl_native_java.jar') compile files('libs/cronet_impl_native_java.jar')
androidTestCompile project(modulePrefix + 'library') androidTestCompile project(modulePrefix + 'library')
androidTestCompile project(modulePrefix + 'testutils')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion

View file

@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.ext.cronet"> 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" <application android:debuggable="true"
android:allowBackup="false" android:allowBackup="false"

View file

@ -19,10 +19,10 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
@ -46,9 +46,7 @@ public final class ByteArrayUploadDataProviderTest {
@Before @Before
public void setUp() { public void setUp() {
System.setProperty("dexmaker.dexcache", MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this);
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
initMocks(this);
byteBuffer = ByteBuffer.allocate(TEST_DATA.length); byteBuffer = ByteBuffer.allocate(TEST_DATA.length);
byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA); 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.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import android.net.Uri; import android.net.Uri;
import android.os.ConditionVariable; import android.os.ConditionVariable;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.C; 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.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
@ -107,9 +107,7 @@ public final class CronetDataSourceTest {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
System.setProperty("dexmaker.dexcache", MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this);
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
initMocks(this);
dataSourceUnderTest = spy( dataSourceUnderTest = spy(
new CronetDataSource( new CronetDataSource(
mockCronetEngine, mockCronetEngine,

View file

@ -21,6 +21,8 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; 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.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; 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 static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
private final boolean enableFloatOutput;
private FfmpegDecoder decoder; private FfmpegDecoder decoder;
public FfmpegAudioRenderer() { public FfmpegAudioRenderer() {
@ -55,7 +59,23 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
*/ */
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) { 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 @Override
@ -64,7 +84,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
String sampleMimeType = format.sampleMimeType; String sampleMimeType = format.sampleMimeType;
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) { if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) { } else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
@ -82,7 +102,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException { throws FfmpegDecoderException {
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.sampleMimeType, format.initializationData); format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format));
return decoder; return decoder;
} }
@ -90,8 +110,32 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
public Format getOutputFormat() { public Format getOutputFormat() {
int channelCount = decoder.getChannelCount(); int channelCount = decoder.getChannelCount();
int sampleRate = decoder.getSampleRate(); int sampleRate = decoder.getSampleRate();
@C.PcmEncoding int encoding = decoder.getEncoding();
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, 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; 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.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
@ -29,11 +30,15 @@ import java.util.List;
/* package */ final class FfmpegDecoder extends /* package */ final class FfmpegDecoder extends
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> { SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
// Space for 64 ms of 6 channel 48 kHz 16-bit PCM audio. // Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio.
private static final int OUTPUT_BUFFER_SIZE = 1536 * 6 * 2 * 2; 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 String codecName;
private final byte[] extraData; 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 long nativeContext; // May be reassigned on resetting the codec.
private boolean hasOutputFormat; private boolean hasOutputFormat;
@ -41,14 +46,17 @@ import java.util.List;
private volatile int sampleRate; private volatile int sampleRate;
public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, 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]); super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!FfmpegLibrary.isAvailable()) { if (!FfmpegLibrary.isAvailable()) {
throw new FfmpegDecoderException("Failed to load decoder native libraries."); throw new FfmpegDecoderException("Failed to load decoder native libraries.");
} }
codecName = FfmpegLibrary.getCodecName(mimeType); codecName = FfmpegLibrary.getCodecName(mimeType);
extraData = getExtraData(mimeType, initializationData); 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) { if (nativeContext == 0) {
throw new FfmpegDecoderException("Initialization failed."); throw new FfmpegDecoderException("Initialization failed.");
} }
@ -81,8 +89,8 @@ import java.util.List;
} }
ByteBuffer inputData = inputBuffer.data; ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit(); int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, OUTPUT_BUFFER_SIZE); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, OUTPUT_BUFFER_SIZE); int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
if (result < 0) { if (result < 0) {
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result); return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
} }
@ -124,6 +132,13 @@ import java.util.List;
return sampleRate; 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 * Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
* not required. * 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, private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
ByteBuffer outputData, int outputSize); ByteBuffer outputData, int outputSize);
private native int ffmpegGetChannelCount(long context); private native int ffmpegGetChannelCount(long context);

View file

@ -57,8 +57,10 @@ extern "C" {
#define ERROR_STRING_BUFFER_LENGTH 256 #define ERROR_STRING_BUFFER_LENGTH 256
// Request a format corresponding to AudioFormat.ENCODING_PCM_16BIT. // Output format corresponding to AudioFormat.ENCODING_PCM_16BIT.
static const AVSampleFormat OUTPUT_FORMAT = AV_SAMPLE_FMT_S16; 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. * 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. * Returns the created context.
*/ */
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
jbyteArray extraData); jbyteArray extraData, jboolean outputFloat);
/** /**
* Decodes the packet into the output buffer, returning the number of bytes * 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; 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); AVCodec *codec = getCodecByName(env, codecName);
if (!codec) { if (!codec) {
LOGE("Codec not found."); LOGE("Codec not found.");
return 0L; return 0L;
} }
return (jlong) createContext(env, codec, extraData); return (jlong) createContext(env, codec, extraData, outputFloat);
} }
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, 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); LOGE("Unexpected error finding codec %d.", codecId);
return 0L; 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); avcodec_flush_buffers(context);
@ -201,13 +205,14 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
} }
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
jbyteArray extraData) { jbyteArray extraData, jboolean outputFloat) {
AVCodecContext *context = avcodec_alloc_context3(codec); AVCodecContext *context = avcodec_alloc_context3(codec);
if (!context) { if (!context) {
LOGE("Failed to allocate context."); LOGE("Failed to allocate context.");
return NULL; return NULL;
} }
context->request_sample_fmt = OUTPUT_FORMAT; context->request_sample_fmt =
outputFloat ? OUTPUT_FORMAT_PCM_FLOAT : OUTPUT_FORMAT_PCM_16BIT;
if (extraData) { if (extraData) {
jsize size = env->GetArrayLength(extraData); jsize size = env->GetArrayLength(extraData);
context->extradata_size = size; 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, "in_sample_rate", sampleRate, 0);
av_opt_set_int(resampleContext, "out_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, "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); result = avresample_open(resampleContext);
if (result < 0) { if (result < 0) {
logError("avresample_open", result); logError("avresample_open", result);
@ -285,7 +292,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
context->opaque = resampleContext; context->opaque = resampleContext;
} }
int inSampleSize = av_get_bytes_per_sample(sampleFormat); 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 outSamples = avresample_get_out_samples(resampleContext, sampleCount);
int bufferOutSize = outSampleSize * channelCount * outSamples; int bufferOutSize = outSampleSize * channelCount * outSamples;
if (outSize + bufferOutSize > outputSize) { if (outSize + bufferOutSize > outputSize) {

View file

@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.flac.test"> 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" <application android:debuggable="true"
android:allowBackup="false" android:allowBackup="false"

View file

@ -25,6 +25,14 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
*/ */
public class FlacExtractorTest extends InstrumentationTestCase { 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 { public void testSample() throws Exception {
ExtractorAsserts.assertBehavior(new ExtractorFactory() { ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override @Override

View file

@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource; 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.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; 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"; 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 { public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_FLAC_URI); playUri(BEAR_FLAC_URI);
} }
@ -76,12 +85,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
DefaultTrackSelector trackSelector = new DefaultTrackSelector(); DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource( MediaSource mediaSource =
uri, new ExtractorMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"), new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
MatroskaExtractor.FACTORY, .setExtractorsFactory(MatroskaExtractor.FACTORY)
null, .createMediaSource(uri);
null);
player.prepare(mediaSource); player.prepare(mediaSource);
player.setPlayWhenReady(true); player.setPlayWhenReady(true);
Looper.loop(); Looper.loop();
@ -100,7 +108,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
Looper.myLooper().quit(); Looper.myLooper().quit();
} }
} }
} }
} }

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import android.os.Handler; import android.os.Handler;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
@ -52,6 +53,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
if (!FlacLibrary.isAvailable() if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
} else { } else {

View file

@ -19,6 +19,7 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.WebView; 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.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.util.Assertions; 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.io.IOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -66,6 +71,75 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); 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 boolean DEBUG = false;
private static final String TAG = "ImaAdsLoader"; 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_TYPE = "google/exo.ext.ima";
private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; 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 * Threshold before the end of content at which IMA is notified that content is complete if the
* player buffers, in milliseconds. * 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:" private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
/** /** The state of ad playback. */
* The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}.
*/
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
private @interface ImaAdState {} 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 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 Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks; private final List<VideoAdPlayerCallback> adCallbacks;
private final ImaSdkFactory imaSdkFactory; private final ImaSdkFactory imaSdkFactory;
private final AdDisplayContainer adDisplayContainer; private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
private Object pendingAdRequestContext;
private List<String> supportedMimeTypes;
private EventListener eventListener; private EventListener eventListener;
private Player player; private Player player;
private ViewGroup adUiViewGroup; private ViewGroup adUiViewGroup;
@ -124,8 +203,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private VideoProgressUpdate lastAdProgress; private VideoProgressUpdate lastAdProgress;
private AdsManager adsManager; private AdsManager adsManager;
private AdErrorEvent pendingAdErrorEvent;
private Timeline timeline; private Timeline timeline;
private long contentDurationMs; private long contentDurationMs;
private int podIndexOffset;
private AdPlaybackState adPlaybackState; private AdPlaybackState adPlaybackState;
// Fields tracking IMA's state. // 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. * Whether IMA has sent an ad event to pause content since the last resume content event.
*/ */
private boolean imaPausedContent; private boolean imaPausedContent;
/** /** The current ad playback state. */
* The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}.
*/
private @ImaAdState int imaAdState; private @ImaAdState int imaAdState;
/** /**
* Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been * 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. * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
*/ */
private boolean sentPendingContentPositionMs; private boolean sentPendingContentPositionMs;
/**
* Whether {@link #release()} has been called.
*/
private boolean released;
/** /**
* Creates a new IMA ads loader. * 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 context The context.
* @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See * @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 * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* more information. * more information.
*/ */
public ImaAdsLoader(Context context, Uri adTagUri) { 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. * more information.
* @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to * @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. * 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) { 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.adTagUri = adTagUri;
this.adsResponse = adsResponse;
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
period = new Timeline.Period(); period = new Timeline.Period();
adCallbacks = new ArrayList<>(1); adCallbacks = new ArrayList<>(1);
imaSdkFactory = ImaSdkFactory.getInstance(); imaSdkFactory = ImaSdkFactory.getInstance();
@ -236,8 +327,58 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return adsLoader; 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. // 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 @Override
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
this.player = player; this.player = player;
@ -247,13 +388,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
lastContentProgress = null; lastContentProgress = null;
adDisplayContainer.setAdContainer(adUiViewGroup); adDisplayContainer.setAdContainer(adUiViewGroup);
player.addListener(this); player.addListener(this);
maybeNotifyAdError();
if (adPlaybackState != null) { if (adPlaybackState != null) {
// Pass the ad playback state to the player, and resume ads if necessary.
eventListener.onAdPlaybackState(adPlaybackState.copy()); eventListener.onAdPlaybackState(adPlaybackState.copy());
if (imaPausedContent && player.getPlayWhenReady()) { if (imaPausedContent && player.getPlayWhenReady()) {
adsManager.resume(); adsManager.resume();
} }
} else if (adsManager != null) {
// Ads have loaded but the ads manager is not initialized.
startAdPlayback();
} else { } 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 @Override
public void release() { public void release() {
released = true; pendingAdRequestContext = null;
if (adsManager != null) { if (adsManager != null) {
adsManager.destroy(); adsManager.destroy();
adsManager = null; adsManager = null;
@ -285,30 +432,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override @Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
if (released) { if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {
adsManager.destroy(); adsManager.destroy();
return; return;
} }
pendingAdRequestContext = null;
this.adsManager = adsManager; this.adsManager = adsManager;
adsManager.addAdErrorListener(this); adsManager.addAdErrorListener(this);
adsManager.addAdEventListener(this); adsManager.addAdEventListener(this);
if (ENABLE_PRELOADING) { if (player != null) {
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); // If a player is attached already, start playback immediately.
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); startAdPlayback();
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");
}
} }
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
adPlaybackState = new AdPlaybackState(adGroupTimesUs);
updateAdPlaybackState();
} }
// AdEvent.AdEventListener implementation. // 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]. // The ad position is not always accurate when using preloading. See [Internal: b/62613240].
AdPodInfo adPodInfo = ad.getAdPodInfo(); AdPodInfo adPodInfo = ad.getAdPodInfo();
int podIndex = adPodInfo.getPodIndex(); int podIndex = adPodInfo.getPodIndex();
adGroupIndex = podIndex == -1 ? adPlaybackState.adGroupCount - 1 : podIndex; adGroupIndex =
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
int adPosition = adPodInfo.getAdPosition(); int adPosition = adPodInfo.getAdPosition();
int adCountInAdGroup = adPodInfo.getTotalAds(); int adCount = adPodInfo.getTotalAds();
adsManager.start(); adsManager.start();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group " Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
+ adGroupIndex);
} }
adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); adPlaybackState.setAdCount(adGroupIndex, adCount);
updateAdPlaybackState(); updateAdPlaybackState();
break; break;
case CONTENT_PAUSE_REQUESTED: case CONTENT_PAUSE_REQUESTED:
@ -386,19 +521,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "onAdError " + adErrorEvent); Log.d(TAG, "onAdError " + adErrorEvent);
} }
if (adsManager == null) { if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
adPlaybackState = new AdPlaybackState(new long[0]); adPlaybackState = new AdPlaybackState(new long[0]);
updateAdPlaybackState(); updateAdPlaybackState();
} }
if (eventListener != null) { if (pendingAdErrorEvent == null) {
IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); pendingAdErrorEvent = adErrorEvent;
eventListener.onLoadError(exception);
} }
maybeNotifyAdError();
} }
// ContentProgressProvider implementation. // ContentProgressProvider implementation.
@Override @Override
public VideoProgressUpdate getContentProgress() { public VideoProgressUpdate getContentProgress() {
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
if (player == null) { if (player == null) {
return lastContentProgress; return lastContentProgress;
} else if (pendingContentPositionMs != C.TIME_UNSET) { } 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 elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
return new VideoProgressUpdate(fakePositionMs, contentDurationMs); 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; return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} else { } else {
return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
@ -421,7 +560,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
public VideoProgressUpdate getAdProgress() { public VideoProgressUpdate getAdProgress() {
if (player == null) { if (player == null) {
return lastAdProgress; return lastAdProgress;
} else if (!playingAd) { } else if (imaAdState == IMA_AD_STATE_NONE) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY; return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} else { } else {
long adDuration = player.getDuration(); 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++) { for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(); 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. // Internal methods.
private void requestAds() { private void startAdPlayback() {
AdsRequest request = imaSdkFactory.createAdsRequest(); ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
request.setAdTagUrl(adTagUri.toString()); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
request.setAdDisplayContainer(adDisplayContainer); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
request.setContentProgressProvider(this); adsRenderingSettings.setMimeTypes(supportedMimeTypes);
adsLoader.requestAds(request);
// 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() { private void updateImaStateForPlayerState() {
boolean wasPlayingAd = playingAd; boolean wasPlayingAd = playingAd;
int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup;
playingAd = player.isPlayingAd(); playingAd = player.isPlayingAd();
playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
if (!sentContentComplete) { if (!sentContentComplete) {
boolean adFinished = (wasPlayingAd && !playingAd) boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup;
|| playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup();
if (adFinished) { if (adFinished) {
// IMA is waiting for the ad playback to finish so invoke the callback now. // 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. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (int i = 0; i < adCallbacks.size(); i++) { for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(); adCallbacks.get(i).onEnded();
} }
if (DEBUG) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
} }
if (!wasPlayingAd && playingAd) { if (!wasPlayingAd && playingAd) {
int adGroupIndex = player.getCurrentAdGroupIndex(); 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() { private void resumeContentInternal() {
@ -717,4 +906,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return adGroupTimesUs; 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 * Constructs a new source that inserts ads linearly with the content specified by {@code
* {@code contentMediaSource}. * contentMediaSource}.
* *
* @param contentMediaSource The {@link MediaSource} providing the content to play. * @param contentMediaSource The {@link MediaSource} providing the content to play.
* @param dataSourceFactory Factory for data sources used to load ad media. * @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 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 eventListener A listener of events. May be null if delivery of events is not required.
*/ */
public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, public ImaAdsMediaSource(
ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, MediaSource contentMediaSource,
@Nullable AdsMediaSource.AdsListener eventListener) { DataSource.Factory dataSourceFactory,
ImaAdsLoader imaAdsLoader,
ViewGroup adUiViewGroup,
@Nullable Handler eventHandler,
@Nullable AdsMediaSource.EventListener eventListener) {
adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader, adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader,
adUiViewGroup, eventHandler, eventListener); adUiViewGroup, eventHandler, eventListener);
} }

View file

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.mediasession; package com.google.android.exoplayer2.ext.mediasession;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@ -330,6 +331,7 @@ public final class MediaSessionConnector {
private final ExoPlayerEventListener exoPlayerEventListener; private final ExoPlayerEventListener exoPlayerEventListener;
private final MediaSessionCallback mediaSessionCallback; private final MediaSessionCallback mediaSessionCallback;
private final PlaybackController playbackController; private final PlaybackController playbackController;
private final String metadataExtrasPrefix;
private final Map<String, CommandReceiver> commandMap; private final Map<String, CommandReceiver> commandMap;
private Player player; 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 * 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...)}. * 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 mediaSession The {@link MediaSessionCompat} to connect to.
* @param playbackController A {@link PlaybackController} for handling playback actions. * @param playbackController A {@link PlaybackController} for handling playback actions.
*/ */
public MediaSessionConnector(MediaSessionCompat mediaSession, public MediaSessionConnector(
PlaybackController playbackController) { MediaSessionCompat mediaSession, PlaybackController playbackController) {
this(mediaSession, playbackController, true); this(mediaSession, playbackController, true, null);
} }
/** /**
@ -372,17 +374,23 @@ public final class MediaSessionConnector {
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
* *
* @param mediaSession The {@link MediaSessionCompat} to connect to. * @param mediaSession The {@link MediaSessionCompat} to connect to.
* @param playbackController A {@link PlaybackController} for handling playback actions, or * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
* {@code null} if the connector should handle playback actions directly. * null} if the connector should handle playback actions directly.
* @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If * @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 * {@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). * 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, public MediaSessionConnector(
PlaybackController playbackController, boolean doMaintainMetadata) { MediaSessionCompat mediaSession,
PlaybackController playbackController,
boolean doMaintainMetadata,
@Nullable String metadataExtrasPrefix) {
this.mediaSession = mediaSession; this.mediaSession = mediaSession;
this.playbackController = playbackController != null ? playbackController this.playbackController = playbackController != null ? playbackController
: new DefaultPlaybackController(); : new DefaultPlaybackController();
this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
: Looper.getMainLooper()); : Looper.getMainLooper());
this.doMaintainMetadata = doMaintainMetadata; this.doMaintainMetadata = doMaintainMetadata;
@ -553,6 +561,25 @@ public final class MediaSessionConnector {
MediaSessionCompat.QueueItem queueItem = queue.get(i); MediaSessionCompat.QueueItem queueItem = queue.get(i);
if (queueItem.getQueueId() == activeQueueItemId) { if (queueItem.getQueueId() == activeQueueItemId) {
MediaDescriptionCompat description = queueItem.getDescription(); 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) { if (description.getTitle() != null) {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
String.valueOf(description.getTitle())); String.valueOf(description.getTitle()));

View file

@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.opus.test"> 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" <application android:debuggable="true"
android:allowBackup="false" 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.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource; 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.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; 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"; 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 { public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_OPUS_URI); playUri(BEAR_OPUS_URI);
} }
@ -76,12 +85,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
DefaultTrackSelector trackSelector = new DefaultTrackSelector(); DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource( MediaSource mediaSource =
uri, new ExtractorMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"), new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
MatroskaExtractor.FACTORY, .setExtractorsFactory(MatroskaExtractor.FACTORY)
null, .createMediaSource(uri);
null);
player.prepare(mediaSource); player.prepare(mediaSource);
player.setPlayWhenReady(true); player.setPlayWhenReady(true);
Looper.loop(); Looper.loop();

View file

@ -76,6 +76,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
if (!OpusLibrary.isAvailable() if (!OpusLibrary.isAvailable()
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
} else { } else {

View file

@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)"
VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" 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>" 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 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[#3520]: https://github.com/google/ExoPlayer/issues/3520
## Notes ## ## Notes ##

View file

@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.vp9.test"> 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" <application android:debuggable="true"
android:allowBackup="false" 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.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource; 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.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
@ -42,6 +43,14 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
private static final String TAG = "VpxPlaybackTest"; 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 { public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_URI); playUri(BEAR_URI);
} }
@ -105,12 +114,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
DefaultTrackSelector trackSelector = new DefaultTrackSelector(); DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource( MediaSource mediaSource =
uri, new ExtractorMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"), new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
MatroskaExtractor.FACTORY, .setExtractorsFactory(MatroskaExtractor.FACTORY)
null, .createMediaSource(uri);
null);
player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer,
LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER,
new VpxVideoSurfaceView(context))); new VpxVideoSurfaceView(context)));
@ -132,7 +140,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
Looper.myLooper().quit(); Looper.myLooper().quit();
} }
} }
} }
} }

View file

@ -18,7 +18,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.core.test"> 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" <application android:debuggable="true"
android:allowBackup="false" android:allowBackup="false"

View file

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

View file

@ -27,155 +27,155 @@ track 0:
initializationData: initializationData:
sample count = 38 sample count = 38
sample 0: sample 0:
time = 1858196 time = 1871586
flags = 1 flags = 1
data = length 384, hash E801184A data = length 384, hash E801184A
sample 1: sample 1:
time = 1882196 time = 1895586
flags = 1 flags = 1
data = length 384, hash 53C6CF9C data = length 384, hash 53C6CF9C
sample 2: sample 2:
time = 1906196 time = 1919586
flags = 1 flags = 1
data = length 384, hash 19A8D99F data = length 384, hash 19A8D99F
sample 3: sample 3:
time = 1930196 time = 1943586
flags = 1 flags = 1
data = length 384, hash E47EB43F data = length 384, hash E47EB43F
sample 4: sample 4:
time = 1954196 time = 1967586
flags = 1 flags = 1
data = length 384, hash 4EA329E7 data = length 384, hash 4EA329E7
sample 5: sample 5:
time = 1978196 time = 1991586
flags = 1 flags = 1
data = length 384, hash 1CCAAE62 data = length 384, hash 1CCAAE62
sample 6: sample 6:
time = 2002196 time = 2015586
flags = 1 flags = 1
data = length 384, hash ED3F8C66 data = length 384, hash ED3F8C66
sample 7: sample 7:
time = 2026196 time = 2039586
flags = 1 flags = 1
data = length 384, hash D3D646B6 data = length 384, hash D3D646B6
sample 8: sample 8:
time = 2050196 time = 2063586
flags = 1 flags = 1
data = length 384, hash 68CD1574 data = length 384, hash 68CD1574
sample 9: sample 9:
time = 2074196 time = 2087586
flags = 1 flags = 1
data = length 384, hash 8CEAB382 data = length 384, hash 8CEAB382
sample 10: sample 10:
time = 2098196 time = 2111586
flags = 1 flags = 1
data = length 384, hash D54B1C48 data = length 384, hash D54B1C48
sample 11: sample 11:
time = 2122196 time = 2135586
flags = 1 flags = 1
data = length 384, hash FFE2EE90 data = length 384, hash FFE2EE90
sample 12: sample 12:
time = 2146196 time = 2159586
flags = 1 flags = 1
data = length 384, hash BFE8A673 data = length 384, hash BFE8A673
sample 13: sample 13:
time = 2170196 time = 2183586
flags = 1 flags = 1
data = length 384, hash 978B1C92 data = length 384, hash 978B1C92
sample 14: sample 14:
time = 2194196 time = 2207586
flags = 1 flags = 1
data = length 384, hash 810CC71E data = length 384, hash 810CC71E
sample 15: sample 15:
time = 2218196 time = 2231586
flags = 1 flags = 1
data = length 384, hash 44FE42D9 data = length 384, hash 44FE42D9
sample 16: sample 16:
time = 2242196 time = 2255586
flags = 1 flags = 1
data = length 384, hash 2F5BB02C data = length 384, hash 2F5BB02C
sample 17: sample 17:
time = 2266196 time = 2279586
flags = 1 flags = 1
data = length 384, hash 77DDB90 data = length 384, hash 77DDB90
sample 18: sample 18:
time = 2290196 time = 2303586
flags = 1 flags = 1
data = length 384, hash 24FB5EDA data = length 384, hash 24FB5EDA
sample 19: sample 19:
time = 2314196 time = 2327586
flags = 1 flags = 1
data = length 384, hash E73203C6 data = length 384, hash E73203C6
sample 20: sample 20:
time = 2338196 time = 2351586
flags = 1 flags = 1
data = length 384, hash 14B525F1 data = length 384, hash 14B525F1
sample 21: sample 21:
time = 2362196 time = 2375586
flags = 1 flags = 1
data = length 384, hash 5E0F4E2E data = length 384, hash 5E0F4E2E
sample 22: sample 22:
time = 2386196 time = 2399586
flags = 1 flags = 1
data = length 384, hash 67EE4E31 data = length 384, hash 67EE4E31
sample 23: sample 23:
time = 2410196 time = 2423586
flags = 1 flags = 1
data = length 384, hash 2E04EC4C data = length 384, hash 2E04EC4C
sample 24: sample 24:
time = 2434196 time = 2447586
flags = 1 flags = 1
data = length 384, hash 852CABA7 data = length 384, hash 852CABA7
sample 25: sample 25:
time = 2458196 time = 2471586
flags = 1 flags = 1
data = length 384, hash 19928903 data = length 384, hash 19928903
sample 26: sample 26:
time = 2482196 time = 2495586
flags = 1 flags = 1
data = length 384, hash 5DA42021 data = length 384, hash 5DA42021
sample 27: sample 27:
time = 2506196 time = 2519586
flags = 1 flags = 1
data = length 384, hash 45B20B7C data = length 384, hash 45B20B7C
sample 28: sample 28:
time = 2530196 time = 2543586
flags = 1 flags = 1
data = length 384, hash D108A215 data = length 384, hash D108A215
sample 29: sample 29:
time = 2554196 time = 2567586
flags = 1 flags = 1
data = length 384, hash BD25DB7C data = length 384, hash BD25DB7C
sample 30: sample 30:
time = 2578196 time = 2591586
flags = 1 flags = 1
data = length 384, hash DA7F9861 data = length 384, hash DA7F9861
sample 31: sample 31:
time = 2602196 time = 2615586
flags = 1 flags = 1
data = length 384, hash CCD576F data = length 384, hash CCD576F
sample 32: sample 32:
time = 2626196 time = 2639586
flags = 1 flags = 1
data = length 384, hash 405C1EB5 data = length 384, hash 405C1EB5
sample 33: sample 33:
time = 2650196 time = 2663586
flags = 1 flags = 1
data = length 384, hash 6640B74E data = length 384, hash 6640B74E
sample 34: sample 34:
time = 2674196 time = 2687586
flags = 1 flags = 1
data = length 384, hash B4E5937A data = length 384, hash B4E5937A
sample 35: sample 35:
time = 2698196 time = 2711586
flags = 1 flags = 1
data = length 384, hash CEE17733 data = length 384, hash CEE17733
sample 36: sample 36:
time = 2722196 time = 2735586
flags = 1 flags = 1
data = length 384, hash 2A0DA733 data = length 384, hash 2A0DA733
sample 37: sample 37:
time = 2746196 time = 2759586
flags = 1 flags = 1
data = length 384, hash 97F4129B data = length 384, hash 97F4129B
tracksEnded = true tracksEnded = true

View file

@ -25,5 +25,9 @@ track 0:
language = null language = null
drmInitData = - drmInitData = -
initializationData: initializationData:
sample count = 0 sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true tracksEnded = true

View file

@ -25,5 +25,9 @@ track 0:
language = null language = null
drmInitData = - drmInitData = -
initializationData: initializationData:
sample count = 0 sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true tracksEnded = true

View file

@ -25,5 +25,9 @@ track 0:
language = null language = null
drmInitData = - drmInitData = -
initializationData: initializationData:
sample count = 0 sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true tracksEnded = true

View file

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

View file

@ -1,7 +1,7 @@
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = 1828
numberOfTracks = 3 numberOfTracks = 3
track 0: track 0:
format: 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.FakeRenderer;
import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; 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. * Tests playback of a source that exposes a single period.
*/ */
public void testPlaySinglePeriodTimeline() throws Exception { public void testPlaySinglePeriodTimeline() throws Exception {
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
Object manifest = new Object(); Object manifest = new Object();
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
@ -85,10 +86,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes three periods. * Tests playback of a source that exposes three periods.
*/ */
public void testPlayMultiPeriodTimeline() throws Exception { public void testPlayMultiPeriodTimeline() throws Exception {
Timeline timeline = new FakeTimeline( Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0));
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
.setTimeline(timeline).setRenderers(renderer) .setTimeline(timeline).setRenderers(renderer)
@ -105,10 +103,7 @@ public final class ExoPlayerTest extends TestCase {
* source. * source.
*/ */
public void testReadAheadToEndDoesNotResetRenderer() throws Exception { public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
Timeline timeline = new FakeTimeline( Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10));
final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
@ -149,7 +144,7 @@ public final class ExoPlayerTest extends TestCase {
} }
public void testRepreparationGivesFreshSourceInfo() throws Exception { 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); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
Object firstSourceManifest = new Object(); Object firstSourceManifest = new Object();
MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest, MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest,
@ -216,10 +211,7 @@ public final class ExoPlayerTest extends TestCase {
} }
public void testRepeatModeChanges() throws Exception { public void testRepeatModeChanges() throws Exception {
Timeline timeline = new FakeTimeline( Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000));
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1 ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1 .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1
@ -239,7 +231,7 @@ public final class ExoPlayerTest extends TestCase {
} }
public void testShuffleModeEnabledChanges() throws Exception { public void testShuffleModeEnabledChanges() throws Exception {
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource[] fakeMediaSources = { MediaSource[] fakeMediaSources = {
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
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 { public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000));
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased") ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased")
.setRepeatMode(Player.REPEAT_MODE_ALL) .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. .setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish.
.build(); .build();
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
.setTimeline(fakeTimeline).setRenderers(renderer).setActionSchedule(actionSchedule) .setRenderers(renderer).setActionSchedule(actionSchedule)
.build().start().blockUntilEnded(TIMEOUT_MS); .build().start().blockUntilEnded(TIMEOUT_MS);
assertTrue(renderer.isEnded); assertTrue(renderer.isEnded);
} }
public void testSeekProcessedCallback() throws Exception { public void testSeekProcessedCallback() throws Exception {
Timeline timeline = new FakeTimeline( Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
new TimelineWindowDefinition(true, false, 100000),
new TimelineWindowDefinition(true, false, 100000));
ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback")
// Initial seek before timeline preparation finished. // Initial seek before timeline preparation finished.
.pause().seek(10).waitForPlaybackState(Player.STATE_READY) .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)); 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 android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import java.util.HashMap; import java.util.HashMap;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** /**
* Tests {@link OfflineLicenseHelper}. * Tests {@link OfflineLicenseHelper}.
@ -38,7 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
setUpMockito(this); MockitoUtil.setUpMockito(this);
when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3});
offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback,
null); null);
@ -156,14 +156,4 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
new byte[] {1, 4, 7, 0, 3, 6})); 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; package com.google.android.exoplayer2.extractor.mp4;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; 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}. * Unit test for {@link FragmentedMp4Extractor}.
@ -26,26 +30,23 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4", ExtractorAsserts.assertBehavior(getExtractorFactory(Collections.<Format>emptyList()),
getInstrumentation()); "mp4/sample_fragmented.mp4", getInstrumentation());
} }
public void testSampleWithSeiPayloadParsing() throws Exception { public void testSampleWithSeiPayloadParsing() throws Exception {
// Enabling the CEA-608 track enables SEI payload parsing. // Enabling the CEA-608 track enables SEI payload parsing.
ExtractorAsserts.assertBehavior( ExtractorFactory extractorFactory = getExtractorFactory(Collections.singletonList(
getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)));
"mp4/sample_fragmented_sei.mp4", getInstrumentation()); ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4",
getInstrumentation());
} }
private static ExtractorFactory getExtractorFactory() { private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
return getExtractorFactory(0);
}
private static ExtractorFactory getExtractorFactory(final int flags) {
return new ExtractorFactory() { return new ExtractorFactory() {
@Override @Override
public Extractor create() { 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.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; 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 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. * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/ */
private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
MediaSource mediaSource = new FakeMediaSource(timeline, null); FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
return TestUtil.extractTimelineFromMediaSource( ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs);
new ClippingMediaSource(mediaSource, 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.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; 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 com.google.android.exoplayer2.testutil.TimelineAsserts;
import junit.framework.TestCase; import junit.framework.TestCase;
@ -208,18 +208,22 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly,
mediaSourceWithAds); mediaSourceWithAds);
// Prepare and assert timeline contains ad groups. MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
Timeline timeline = TestUtil.extractTimelineFromMediaSource(mediaSource); try {
TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); 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. // Create all periods and assert period creation of child media sources has been called.
TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000); testRunner.assertPrepareAndReleaseAllPeriods();
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0));
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 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, ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic,
new FakeShuffleOrder(mediaSources.length), mediaSources); 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) { 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.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; 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 com.google.android.exoplayer2.testutil.TimelineAsserts;
import junit.framework.TestCase; import junit.framework.TestCase;
@ -30,12 +30,13 @@ import junit.framework.TestCase;
*/ */
public class LoopingMediaSourceTest extends TestCase { public class LoopingMediaSourceTest extends TestCase {
private final Timeline multiWindowTimeline; private FakeTimeline multiWindowTimeline;
public LoopingMediaSourceTest() { @Override
multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource( public void setUp() throws Exception {
new FakeTimeline(new TimelineWindowDefinition(1, 111), super.setUp();
new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null)); multiWindowTimeline = new FakeTimeline(new TimelineWindowDefinition(1, 111),
new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333));
} }
public void testSingleLoop() { public void testSingleLoop() {
@ -109,10 +110,14 @@ public class LoopingMediaSourceTest extends TestCase {
* the looping timeline. * the looping timeline.
*/ */
private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) {
MediaSource mediaSource = new FakeMediaSource(timeline, null); FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
return TestUtil.extractTimelineFromMediaSource( LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount);
new LoopingMediaSource(mediaSource, 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 android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.MockitoUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** /**
* Tests for {@link CachedRegionTracker}. * Tests for {@link CachedRegionTracker}.
@ -46,7 +46,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
setUpMockito(this); MockitoUtil.setUpMockito(this);
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); 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); 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) @Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, @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_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3,
ENCODING_DTS_HD}) ENCODING_DTS, ENCODING_DTS_HD})
public @interface Encoding {} public @interface Encoding {}
/** /**
@ -136,7 +136,7 @@ public final class C {
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, @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 {} public @interface PcmEncoding {}
/** /**
* @see AudioFormat#ENCODING_INVALID * @see AudioFormat#ENCODING_INVALID
@ -158,6 +158,10 @@ public final class C {
* PCM encoding with 32 bits per sample. * PCM encoding with 32 bits per sample.
*/ */
public static final int ENCODING_PCM_32BIT = 0x40000000; 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 * @see AudioFormat#ENCODING_AC3
*/ */
@ -420,6 +424,11 @@ public final class C {
*/ */
public static final int SELECTION_FLAG_AUTOSELECT = 4; 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. * 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; 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; * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control
private static final int BELOW_LOW_WATERMARK = 2; * 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; private final DefaultAllocator allocator;
@ -61,6 +66,8 @@ public final class DefaultLoadControl implements LoadControl {
private final long maxBufferUs; private final long maxBufferUs;
private final long bufferForPlaybackUs; private final long bufferForPlaybackUs;
private final long bufferForPlaybackAfterRebufferUs; private final long bufferForPlaybackAfterRebufferUs;
private final int targetBufferBytesOverwrite;
private final boolean prioritizeTimeOverSizeThresholds;
private final PriorityTaskManager priorityTaskManager; private final PriorityTaskManager priorityTaskManager;
private int targetBufferSize; private int targetBufferSize;
@ -79,8 +86,14 @@ public final class DefaultLoadControl implements LoadControl {
* @param allocator The {@link DefaultAllocator} used by the loader. * @param allocator The {@link DefaultAllocator} used by the loader.
*/ */
public DefaultLoadControl(DefaultAllocator allocator) { public DefaultLoadControl(DefaultAllocator allocator) {
this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, this(
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); 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 * @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 * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
* buffer depletion rather than a user action. * 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, public DefaultLoadControl(
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { DefaultAllocator allocator,
this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, int minBufferMs,
int maxBufferMs,
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs,
int targetBufferBytes,
boolean prioritizeTimeOverSizeThresholds) {
this(
allocator,
minBufferMs,
maxBufferMs,
bufferForPlaybackMs,
bufferForPlaybackAfterRebufferMs,
targetBufferBytes,
prioritizeTimeOverSizeThresholds,
null); null);
} }
@ -116,18 +146,30 @@ public final class DefaultLoadControl implements LoadControl {
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * @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 * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
* buffer depletion rather than a user action. * buffer depletion rather than a user action.
* @param priorityTaskManager If not null, registers itself as a task with priority * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the
* {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[],
* periods. * 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, public DefaultLoadControl(
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs, DefaultAllocator allocator,
int minBufferMs,
int maxBufferMs,
int bufferForPlaybackMs,
int bufferForPlaybackAfterRebufferMs,
int targetBufferBytes,
boolean prioritizeTimeOverSizeThresholds,
PriorityTaskManager priorityTaskManager) { PriorityTaskManager priorityTaskManager) {
this.allocator = allocator; this.allocator = allocator;
minBufferUs = minBufferMs * 1000L; minBufferUs = minBufferMs * 1000L;
maxBufferUs = maxBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L;
targetBufferBytesOverwrite = targetBufferBytes;
bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
this.priorityTaskManager = priorityTaskManager; this.priorityTaskManager = priorityTaskManager;
} }
@ -139,12 +181,10 @@ public final class DefaultLoadControl implements LoadControl {
@Override @Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) { TrackSelectionArray trackSelections) {
targetBufferSize = 0; targetBufferSize =
for (int i = 0; i < renderers.length; i++) { targetBufferBytesOverwrite == C.LENGTH_UNSET
if (trackSelections.get(i) != null) { ? calculateTargetBufferSize(renderers, trackSelections)
targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); : targetBufferBytesOverwrite;
}
}
allocator.setTargetBufferSize(targetBufferSize); allocator.setTargetBufferSize(targetBufferSize);
} }
@ -166,16 +206,28 @@ public final class DefaultLoadControl implements LoadControl {
@Override @Override
public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) {
long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; return minBufferDurationUs <= 0
|| bufferedDurationUs >= minBufferDurationUs
|| (!prioritizeTimeOverSizeThresholds
&& allocator.getTotalBytesAllocated() >= targetBufferSize);
} }
@Override @Override
public boolean shouldContinueLoading(long bufferedDurationUs) { public boolean shouldContinueLoading(long bufferedDurationUs) {
int bufferTimeState = getBufferTimeState(bufferedDurationUs);
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
boolean wasBuffering = isBuffering; boolean wasBuffering = isBuffering;
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK if (prioritizeTimeOverSizeThresholds) {
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); 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 (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) { if (isBuffering) {
priorityTaskManager.add(C.PRIORITY_PLAYBACK); priorityTaskManager.add(C.PRIORITY_PLAYBACK);
@ -186,9 +238,23 @@ public final class DefaultLoadControl implements LoadControl {
return isBuffering; return isBuffering;
} }
private int getBufferTimeState(long bufferedDurationUs) { /**
return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK * Calculate target buffer size in bytes based on the selected tracks. The player will try not to
: (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS); * 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) { 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 // Undo the effect of previous call to associate no-sample renderers with empty tracks
// so the mediaPeriod receives back whatever it sent us before. // so the mediaPeriod receives back whatever it sent us before.
disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams);
updatePeriodTrackSelectorResult(trackSelectorResult);
// Disable streams on the period and get new streams for updated/newly-enabled tracks. // Disable streams on the period and get new streams for updated/newly-enabled tracks.
positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
sampleStreams, streamResetFlags, positionUs); sampleStreams, streamResetFlags, positionUs);
associateNoSampleRenderersWithEmptySampleStream(sampleStreams); associateNoSampleRenderersWithEmptySampleStream(sampleStreams);
periodTrackSelectorResult = trackSelectorResult;
// Update whether we have enabled tracks and sanity check the expected streams are non-null. // Update whether we have enabled tracks and sanity check the expected streams are non-null.
hasEnabledTracks = false; hasEnabledTracks = false;
@ -1692,6 +1692,7 @@ import java.io.IOException;
} }
public void release() { public void release() {
updatePeriodTrackSelectorResult(null);
try { try {
if (info.endPositionUs != C.TIME_END_OF_SOURCE) { if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); 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 * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy
* {@link EmptySampleStream} that was associated with it. * {@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". * 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. // 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}. * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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. * 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). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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} * 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 windowIndex The index of the window.
* @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
* the window's default position. * 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); 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<VideoListener> videoListeners;
private final CopyOnWriteArraySet<TextOutput> textOutputs; private final CopyOnWriteArraySet<TextOutput> textOutputs;
private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs; private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs;
private final CopyOnWriteArraySet<VideoRendererEventListener> videoDebugListeners;
private final CopyOnWriteArraySet<AudioRendererEventListener> audioDebugListeners;
private final int videoRendererCount; private final int videoRendererCount;
private final int audioRendererCount; private final int audioRendererCount;
@ -103,8 +105,6 @@ public class SimpleExoPlayer implements ExoPlayer {
private int videoScalingMode; private int videoScalingMode;
private SurfaceHolder surfaceHolder; private SurfaceHolder surfaceHolder;
private TextureView textureView; private TextureView textureView;
private AudioRendererEventListener audioDebugListener;
private VideoRendererEventListener videoDebugListener;
private DecoderCounters videoDecoderCounters; private DecoderCounters videoDecoderCounters;
private DecoderCounters audioDecoderCounters; private DecoderCounters audioDecoderCounters;
private int audioSessionId; private int audioSessionId;
@ -117,6 +117,8 @@ public class SimpleExoPlayer implements ExoPlayer {
videoListeners = new CopyOnWriteArraySet<>(); videoListeners = new CopyOnWriteArraySet<>();
textOutputs = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>();
metadataOutputs = new CopyOnWriteArraySet<>(); metadataOutputs = new CopyOnWriteArraySet<>();
videoDebugListeners = new CopyOnWriteArraySet<>();
audioDebugListeners = new CopyOnWriteArraySet<>();
Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
Handler eventHandler = new Handler(eventLooper); Handler eventHandler = new Handler(eventLooper);
renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, 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. * Sets a listener to receive debug events from the video renderer.
* *
* @param listener The listener. * @param listener The listener.
* @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}.
*/ */
@Deprecated
public void setVideoDebugListener(VideoRendererEventListener listener) { 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. * Sets a listener to receive debug events from the audio renderer.
* *
* @param listener The listener. * @param listener The listener.
* @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}.
*/ */
@Deprecated
public void setAudioDebugListener(AudioRendererEventListener listener) { 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 // ExoPlayer implementation
@ -678,7 +726,7 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
@Override @Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) { public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
player.setPlaybackParameters(playbackParameters); player.setPlaybackParameters(playbackParameters);
} }
@ -817,15 +865,15 @@ public class SimpleExoPlayer implements ExoPlayer {
// Internal methods. // 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 renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} 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. * @return A new {@link ExoPlayer} instance.
*/ */
protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, protected ExoPlayer createExoPlayerImpl(
LoadControl loadControl) { Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
return new ExoPlayerImpl(renderers, trackSelector, loadControl); return new ExoPlayerImpl(renderers, trackSelector, loadControl);
} }
@ -877,7 +925,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void onVideoEnabled(DecoderCounters counters) { public void onVideoEnabled(DecoderCounters counters) {
videoDecoderCounters = counters; videoDecoderCounters = counters;
if (videoDebugListener != null) { for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoEnabled(counters); videoDebugListener.onVideoEnabled(counters);
} }
} }
@ -885,7 +933,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
long initializationDurationMs) { long initializationDurationMs) {
if (videoDebugListener != null) { for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
initializationDurationMs); initializationDurationMs);
} }
@ -894,14 +942,14 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void onVideoInputFormatChanged(Format format) { public void onVideoInputFormatChanged(Format format) {
videoFormat = format; videoFormat = format;
if (videoDebugListener != null) { for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoInputFormatChanged(format); videoDebugListener.onVideoInputFormatChanged(format);
} }
} }
@Override @Override
public void onDroppedFrames(int count, long elapsed) { public void onDroppedFrames(int count, long elapsed) {
if (videoDebugListener != null) { for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onDroppedFrames(count, elapsed); videoDebugListener.onDroppedFrames(count, elapsed);
} }
} }
@ -913,7 +961,7 @@ public class SimpleExoPlayer implements ExoPlayer {
videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
pixelWidthHeightRatio); pixelWidthHeightRatio);
} }
if (videoDebugListener != null) { for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
pixelWidthHeightRatio); pixelWidthHeightRatio);
} }
@ -926,14 +974,14 @@ public class SimpleExoPlayer implements ExoPlayer {
videoListener.onRenderedFirstFrame(); videoListener.onRenderedFirstFrame();
} }
} }
if (videoDebugListener != null) { for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onRenderedFirstFrame(surface); videoDebugListener.onRenderedFirstFrame(surface);
} }
} }
@Override @Override
public void onVideoDisabled(DecoderCounters counters) { public void onVideoDisabled(DecoderCounters counters) {
if (videoDebugListener != null) { for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
videoDebugListener.onVideoDisabled(counters); videoDebugListener.onVideoDisabled(counters);
} }
videoFormat = null; videoFormat = null;
@ -945,7 +993,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void onAudioEnabled(DecoderCounters counters) { public void onAudioEnabled(DecoderCounters counters) {
audioDecoderCounters = counters; audioDecoderCounters = counters;
if (audioDebugListener != null) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioEnabled(counters); audioDebugListener.onAudioEnabled(counters);
} }
} }
@ -953,7 +1001,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void onAudioSessionId(int sessionId) { public void onAudioSessionId(int sessionId) {
audioSessionId = sessionId; audioSessionId = sessionId;
if (audioDebugListener != null) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioSessionId(sessionId); audioDebugListener.onAudioSessionId(sessionId);
} }
} }
@ -961,7 +1009,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
long initializationDurationMs) { long initializationDurationMs) {
if (audioDebugListener != null) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,
initializationDurationMs); initializationDurationMs);
} }
@ -970,7 +1018,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void onAudioInputFormatChanged(Format format) { public void onAudioInputFormatChanged(Format format) {
audioFormat = format; audioFormat = format;
if (audioDebugListener != null) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioInputFormatChanged(format); audioDebugListener.onAudioInputFormatChanged(format);
} }
} }
@ -978,14 +1026,14 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override @Override
public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs,
long elapsedSinceLastFeedMs) { long elapsedSinceLastFeedMs) {
if (audioDebugListener != null) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
} }
} }
@Override @Override
public void onAudioDisabled(DecoderCounters counters) { public void onAudioDisabled(DecoderCounters counters) {
if (audioDebugListener != null) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
audioDebugListener.onAudioDisabled(counters); audioDebugListener.onAudioDisabled(counters);
} }
audioFormat = null; audioFormat = null;

View file

@ -15,6 +15,10 @@
*/ */
package com.google.android.exoplayer2.audio; 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.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
@ -181,7 +185,14 @@ public final class Ac3Util {
channelCount += 2; 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); Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
} }
@ -198,29 +209,176 @@ public final class Ac3Util {
boolean isEac3 = data.readBits(5) == 16; boolean isEac3 = data.readBits(5) == 16;
data.setPosition(initialPosition); data.setPosition(initialPosition);
String mimeType; String mimeType;
int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; int streamType = STREAM_TYPE_UNDEFINED;
int sampleRate; int sampleRate;
int acmod; int acmod;
int frameSize; int frameSize;
int sampleCount; int sampleCount;
boolean lfeon;
int channelCount;
if (isEac3) { 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 data.skipBits(16); // syncword
streamType = data.readBits(2); streamType = data.readBits(2);
data.skipBits(3); // substreamid data.skipBits(3); // substreamid
frameSize = (data.readBits(11) + 1) * 2; frameSize = (data.readBits(11) + 1) * 2;
int fscod = data.readBits(2); int fscod = data.readBits(2);
int audioBlocks; int audioBlocks;
int numblkscod;
if (fscod == 3) { if (fscod == 3) {
numblkscod = 3;
sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)];
audioBlocks = 6; audioBlocks = 6;
} else { } else {
int numblkscod = data.readBits(2); numblkscod = data.readBits(2);
audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod]; audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod];
sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
} }
sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks; sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks;
acmod = data.readBits(3); 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 */ { } else /* is AC-3 */ {
mimeType = MimeTypes.AUDIO_AC3; mimeType = MimeTypes.AUDIO_AC3;
data.skipBits(16 + 16); // syncword, crc1 data.skipBits(16 + 16); // syncword, crc1
@ -240,9 +398,9 @@ public final class Ac3Util {
} }
sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; 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, return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize,
sampleCount); sampleCount);
} }

View file

@ -25,14 +25,13 @@ import java.nio.ByteBuffer;
* A sink that consumes audio data. * A sink that consumes audio data.
* <p> * <p>
* Before starting playback, specify the input audio format by calling * 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> * <p>
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * 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. * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
* <p> * <p>
* Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format * Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format changes.
* changes. The sink will be reinitialized on the next call to * The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
* {@link #handleBuffer(ByteBuffer, long)}.
* <p> * <p>
* Call {@link #reset()} to prepare the sink to receive audio data from a new playback position. * Call {@link #reset()} to prepare the sink to receive audio data from a new playback position.
* <p> * <p>
@ -76,7 +75,7 @@ public interface AudioSink {
* *
* @param bufferSize The size of the sink's buffer, in bytes. * @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 * @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. * 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. * @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds.
*/ */
@ -166,13 +165,12 @@ public interface AudioSink {
void setListener(Listener listener); void setListener(Listener listener);
/** /**
* Returns whether it's possible to play audio in the specified format using encoded audio * Returns whether it's possible to play audio in the specified encoding.
* passthrough.
* *
* @param mimeType The format mime type. * @param encoding The audio encoding.
* @return Whether it's possible to play audio in the format using encoded audio passthrough. * @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 * 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. * 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 inputChannelCount The number of channels.
* @param inputSampleRate The sample rate in Hz. * @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 * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
* suitable buffer size. * suitable buffer size.
* @param outputChannels A mapping from input to output channels that is applied to this sink's * @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. * immediately preceding the next call to {@link #reset()} or this method.
* @throws ConfigurationException If an error occurs configuring the sink. * @throws ConfigurationException If an error occurs configuring the sink.
*/ */
void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate,
@C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples,
int trimStartSamples, int trimEndSamples) throws ConfigurationException; int trimEndSamples) throws ConfigurationException;
/** /**
* Starts or resumes consuming audio if initialized. * 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 * 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, * {@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 * 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 * {@link #configure(int, int, int, int, int[], int, int)} that causes the sink to be reset).
* reset).
* *
* @param buffer The buffer containing audio data. * @param buffer The buffer containing audio data.
* @param presentationTimeUs The presentation timestamp of the buffer in microseconds. * @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)} * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
* to start using the new channel map. * to start using the new channel map.
*
* @see AudioSink#configure(String, int, int, int, int, int[], int, int)
*/ */
public void setChannelMap(int[] outputChannels) { public void setChannelMap(int[] outputChannels) {
pendingOutputChannels = outputChannels; pendingOutputChannels = outputChannels;

View file

@ -29,15 +29,14 @@ import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList;
/** /**
* Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback * 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 ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets; private final long[] playheadOffsets;
private final AudioTrackUtil audioTrackUtil; private final AudioTrackUtil audioTrackUtil;
private final LinkedList<PlaybackParametersCheckpoint> playbackParametersCheckpoints; private final ArrayDeque<PlaybackParametersCheckpoint> playbackParametersCheckpoints;
@Nullable private Listener listener; @Nullable private Listener listener;
/** /**
@ -182,13 +181,13 @@ public final class DefaultAudioSink implements AudioSink {
*/ */
private AudioTrack keepSessionIdAudioTrack; private AudioTrack keepSessionIdAudioTrack;
private AudioTrack audioTrack; private AudioTrack audioTrack;
private boolean isInputPcm;
private int inputSampleRate; private int inputSampleRate;
private int sampleRate; private int sampleRate;
private int channelConfig; private int channelConfig;
private @C.Encoding int encoding;
private @C.Encoding int outputEncoding; private @C.Encoding int outputEncoding;
private AudioAttributes audioAttributes; private AudioAttributes audioAttributes;
private boolean passthrough; private boolean processingEnabled;
private int bufferSize; private int bufferSize;
private long bufferSizeUs; private long bufferSizeUs;
@ -277,7 +276,7 @@ public final class DefaultAudioSink implements AudioSink {
drainingAudioProcessorIndex = C.INDEX_UNSET; drainingAudioProcessorIndex = C.INDEX_UNSET;
this.audioProcessors = new AudioProcessor[0]; this.audioProcessors = new AudioProcessor[0];
outputBuffers = new ByteBuffer[0]; outputBuffers = new ByteBuffer[0];
playbackParametersCheckpoints = new LinkedList<>(); playbackParametersCheckpoints = new ArrayDeque<>();
} }
@Override @Override
@ -286,9 +285,15 @@ public final class DefaultAudioSink implements AudioSink {
} }
@Override @Override
public boolean isPassthroughSupported(String mimeType) { public boolean isEncodingSupported(@C.Encoding int encoding) {
return audioCapabilities != null if (isEncodingPcm(encoding)) {
&& audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); // 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 @Override
@ -331,18 +336,20 @@ public final class DefaultAudioSink implements AudioSink {
} }
@Override @Override
public void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate,
@C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples,
int trimStartSamples, int trimEndSamples) throws ConfigurationException { int trimEndSamples) throws ConfigurationException {
boolean flush = false;
this.inputSampleRate = inputSampleRate; this.inputSampleRate = inputSampleRate;
int channelCount = inputChannelCount; int channelCount = inputChannelCount;
int sampleRate = inputSampleRate; int sampleRate = inputSampleRate;
@C.Encoding int encoding; isInputPcm = isEncodingPcm(inputEncoding);
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(inputMimeType); if (isInputPcm) {
boolean flush = false; pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
if (!passthrough) { }
encoding = inputPcmEncoding; @C.Encoding int encoding = inputEncoding;
pcmFrameSize = Util.getPcmFrameSize(inputPcmEncoding, channelCount); boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT;
if (processingEnabled) {
trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples);
channelMappingAudioProcessor.setChannelMap(outputChannels); channelMappingAudioProcessor.setChannelMap(outputChannels);
for (AudioProcessor audioProcessor : availableAudioProcessors) { for (AudioProcessor audioProcessor : availableAudioProcessors) {
@ -357,11 +364,6 @@ public final class DefaultAudioSink implements AudioSink {
encoding = audioProcessor.getOutputEncoding(); encoding = audioProcessor.getOutputEncoding();
} }
} }
if (flush) {
resetAudioProcessors();
}
} else {
encoding = getEncodingForMimeType(inputMimeType);
} }
int channelConfig; int channelConfig;
@ -411,11 +413,11 @@ public final class DefaultAudioSink implements AudioSink {
// Workaround for Nexus Player not reporting support for mono passthrough. // Workaround for Nexus Player not reporting support for mono passthrough.
// (See [Internal: b/34268671].) // (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; 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) { && this.channelConfig == channelConfig) {
// We already have an audio track with the correct sample rate, channel config and encoding. // We already have an audio track with the correct sample rate, channel config and encoding.
return; return;
@ -423,16 +425,24 @@ public final class DefaultAudioSink implements AudioSink {
reset(); reset();
this.encoding = encoding; this.processingEnabled = processingEnabled;
this.passthrough = passthrough;
this.sampleRate = sampleRate; this.sampleRate = sampleRate;
this.channelConfig = channelConfig; this.channelConfig = channelConfig;
outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; outputEncoding = encoding;
outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount); if (isInputPcm) {
outputPcmFrameSize = Util.getPcmFrameSize(outputEncoding, channelCount);
}
if (specifiedBufferSize != 0) { if (specifiedBufferSize != 0) {
bufferSize = specifiedBufferSize; 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 // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into
// account. [Internal: b/25181305] // account. [Internal: b/25181305]
if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { 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. // 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); 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); bufferSizeUs =
isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
// The old playback parameters may no longer be applicable so try to reset them now.
setPlaybackParameters(playbackParameters);
} }
private void resetAudioProcessors() { private void resetAudioProcessors() {
@ -487,6 +485,13 @@ public final class DefaultAudioSink implements AudioSink {
releasingConditionVariable.block(); releasingConditionVariable.block();
audioTrack = initializeAudioTrack(); 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(); int audioSessionId = audioTrack.getAudioSessionId();
if (enablePreV21AudioSessionWorkaround) { if (enablePreV21AudioSessionWorkaround) {
if (Util.SDK_INT < 21) { if (Util.SDK_INT < 21) {
@ -574,7 +579,7 @@ public final class DefaultAudioSink implements AudioSink {
return true; return true;
} }
if (passthrough && framesPerEncodedSample == 0) { if (!isInputPcm && framesPerEncodedSample == 0) {
// If this is the first encoded sample, calculate the sample size in frames. // If this is the first encoded sample, calculate the sample size in frames.
framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer);
} }
@ -618,20 +623,19 @@ public final class DefaultAudioSink implements AudioSink {
} }
} }
if (passthrough) { if (isInputPcm) {
submittedEncodedFrames += framesPerEncodedSample;
} else {
submittedPcmBytes += buffer.remaining(); submittedPcmBytes += buffer.remaining();
} else {
submittedEncodedFrames += framesPerEncodedSample;
} }
inputBuffer = buffer; inputBuffer = buffer;
} }
if (passthrough) { if (processingEnabled) {
// Passthrough buffers are not processed.
writeBuffer(inputBuffer, presentationTimeUs);
} else {
processBuffers(presentationTimeUs); processBuffers(presentationTimeUs);
} else {
writeBuffer(inputBuffer, presentationTimeUs);
} }
if (!inputBuffer.hasRemaining()) { if (!inputBuffer.hasRemaining()) {
@ -679,10 +683,9 @@ public final class DefaultAudioSink implements AudioSink {
} }
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException {
throws WriteException {
if (!buffer.hasRemaining()) { if (!buffer.hasRemaining()) {
return true; return;
} }
if (outputBuffer != null) { if (outputBuffer != null) {
Assertions.checkArgument(outputBuffer == buffer); Assertions.checkArgument(outputBuffer == buffer);
@ -701,7 +704,7 @@ public final class DefaultAudioSink implements AudioSink {
} }
int bytesRemaining = buffer.remaining(); int bytesRemaining = buffer.remaining();
int bytesWritten = 0; 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. // Work out how many bytes we can write without the risk of blocking.
int bytesPending = int bytesPending =
(int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize));
@ -728,17 +731,15 @@ public final class DefaultAudioSink implements AudioSink {
throw new WriteException(bytesWritten); throw new WriteException(bytesWritten);
} }
if (!passthrough) { if (isInputPcm) {
writtenPcmBytes += bytesWritten; writtenPcmBytes += bytesWritten;
} }
if (bytesWritten == bytesRemaining) { if (bytesWritten == bytesRemaining) {
if (passthrough) { if (!isInputPcm) {
writtenEncodedFrames += framesPerEncodedSample; writtenEncodedFrames += framesPerEncodedSample;
} }
outputBuffer = null; outputBuffer = null;
return true;
} }
return false;
} }
@Override @Override
@ -758,7 +759,7 @@ public final class DefaultAudioSink implements AudioSink {
private boolean drainAudioProcessorsToEndOfStream() throws WriteException { private boolean drainAudioProcessorsToEndOfStream() throws WriteException {
boolean audioProcessorNeedsEndOfStream = false; boolean audioProcessorNeedsEndOfStream = false;
if (drainingAudioProcessorIndex == C.INDEX_UNSET) { if (drainingAudioProcessorIndex == C.INDEX_UNSET) {
drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; drainingAudioProcessorIndex = processingEnabled ? 0 : audioProcessors.length;
audioProcessorNeedsEndOfStream = true; audioProcessorNeedsEndOfStream = true;
} }
while (drainingAudioProcessorIndex < audioProcessors.length) { while (drainingAudioProcessorIndex < audioProcessors.length) {
@ -799,8 +800,8 @@ public final class DefaultAudioSink implements AudioSink {
@Override @Override
public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
if (passthrough) { if (isInitialized() && !processingEnabled) {
// The playback parameters are always the default in passthrough mode. // The playback parameters are always the default if processing is disabled.
this.playbackParameters = PlaybackParameters.DEFAULT; this.playbackParameters = PlaybackParameters.DEFAULT;
return this.playbackParameters; return this.playbackParameters;
} }
@ -1076,7 +1077,7 @@ public final class DefaultAudioSink implements AudioSink {
audioTimestampSet = false; audioTimestampSet = false;
} }
} }
if (getLatencyMethod != null && !passthrough) { if (getLatencyMethod != null && isInputPcm) {
try { try {
// Compute the audio track latency, excluding the latency due to the buffer (leaving // Compute the audio track latency, excluding the latency due to the buffer (leaving
// latency due to the mixer and audio hardware driver). // latency due to the mixer and audio hardware driver).
@ -1115,11 +1116,11 @@ public final class DefaultAudioSink implements AudioSink {
} }
private long getSubmittedFrames() { private long getSubmittedFrames() {
return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize); return isInputPcm ? (submittedPcmBytes / pcmFrameSize) : submittedEncodedFrames;
} }
private long getWrittenFrames() { private long getWrittenFrames() {
return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize); return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames;
} }
private void resetSyncParams() { private void resetSyncParams() {
@ -1212,20 +1213,10 @@ public final class DefaultAudioSink implements AudioSink {
MODE_STATIC, audioSessionId); MODE_STATIC, audioSessionId);
} }
@C.Encoding private static boolean isEncodingPcm(@C.Encoding int encoding) {
private static int getEncodingForMimeType(String mimeType) { return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT
switch (mimeType) { || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT
case MimeTypes.AUDIO_AC3: || encoding == C.ENCODING_PCM_FLOAT;
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 int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { 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 passthroughEnabled;
private boolean codecNeedsDiscardChannelsWorkaround; private boolean codecNeedsDiscardChannelsWorkaround;
private android.media.MediaFormat passthroughMediaFormat; private android.media.MediaFormat passthroughMediaFormat;
@C.Encoding
private int pcmEncoding; private int pcmEncoding;
private int channelCount; private int channelCount;
private int encoderDelay; private int encoderDelay;
@ -177,6 +178,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
&& mediaCodecSelector.getPassthroughDecoderInfo() != null) { && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; 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; boolean requiresSecureDecryption = false;
DrmInitData drmInitData = format.drmInitData; DrmInitData drmInitData = format.drmInitData;
if (drmInitData != null) { 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. * 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 * This implementation returns true if the {@link AudioSink} indicates that encoded audio output
* supported. * is supported.
* *
* @param mimeType The type of input media. * @param mimeType The type of input media.
* @return Whether passthrough playback is supported. * @return Whether passthrough playback is supported.
*/ */
protected boolean allowPassthrough(String mimeType) { 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 @Override
@ -272,10 +279,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override @Override
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
throws ExoPlaybackException { throws ExoPlaybackException {
boolean passthrough = passthroughMediaFormat != null; @C.Encoding int encoding;
String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) MediaFormat format;
: MimeTypes.AUDIO_RAW; if (passthroughMediaFormat != null) {
MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME));
format = passthroughMediaFormat;
} else {
encoding = pcmEncoding;
format = outputFormat;
}
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int[] channelMap; int[] channelMap;
@ -289,8 +301,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} }
try { try {
audioSink.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap, audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay,
encoderDelay, encoderPadding); encoderPadding);
} catch (AudioSink.ConfigurationException e) { } catch (AudioSink.ConfigurationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex()); 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, protected abstract int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
Format format); 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 @Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) { if (outputStreamEnded) {
@ -329,8 +339,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
if (audioTrackNeedsConfigure) { if (audioTrackNeedsConfigure) {
Format outputFormat = getOutputFormat(); Format outputFormat = getOutputFormat();
audioSink.configure(outputFormat.sampleMimeType, outputFormat.channelCount, audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount,
outputFormat.sampleRate, outputFormat.pcmEncoding, 0, null, encoderDelay, encoderPadding); outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding);
audioTrackNeedsConfigure = false; audioTrackNeedsConfigure = false;
} }

View file

@ -28,13 +28,24 @@ public interface SeekMap {
final class Unseekable implements SeekMap { final class Unseekable implements SeekMap {
private final long durationUs; private final long durationUs;
private final long startPosition;
/** /**
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
* the duration is unknown. * the duration is unknown.
*/ */
public Unseekable(long durationUs) { 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.durationUs = durationUs;
this.startPosition = startPosition;
} }
@Override @Override
@ -49,7 +60,7 @@ public interface SeekMap {
@Override @Override
public long getPosition(long timeUs) { public long getPosition(long timeUs) {
return 0; return startPosition;
} }
} }
@ -78,7 +89,8 @@ public interface SeekMap {
* *
* @param timeUs A seek position in microseconds. * @param timeUs A seek position in microseconds.
* @return The corresponding position (byte offset) in the stream from which data can be provided * @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); long getPosition(long timeUs);

View file

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.flv; package com.google.android.exoplayer2.extractor.flv;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; 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.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; 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. * 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; * Extractor states.
private static final int FLV_TAG_HEADER_SIZE = 11; */
@Retention(RetentionPolicy.SOURCE)
// Parser states. @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_READING_FLV_HEADER = 1;
private static final int STATE_SKIPPING_TO_TAG_HEADER = 2; 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_HEADER = 3;
private static final int STATE_READING_TAG_DATA = 4; 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. // Tag types.
private static final int TAG_TYPE_AUDIO = 8; private static final int TAG_TYPE_AUDIO = 8;
private static final int TAG_TYPE_VIDEO = 9; private static final int TAG_TYPE_VIDEO = 9;
@ -61,33 +70,31 @@ public final class FlvExtractor implements Extractor, SeekMap {
// FLV container identifier. // FLV container identifier.
private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
// Temporary buffers.
private final ParsableByteArray scratch; private final ParsableByteArray scratch;
private final ParsableByteArray headerBuffer; private final ParsableByteArray headerBuffer;
private final ParsableByteArray tagHeaderBuffer; private final ParsableByteArray tagHeaderBuffer;
private final ParsableByteArray tagData; private final ParsableByteArray tagData;
private final ScriptTagPayloadReader metadataReader;
// Extractor outputs.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
private @States int state;
// State variables. private long mediaTagTimestampOffsetUs;
private int parserState;
private int bytesToNextTagHeader; private int bytesToNextTagHeader;
public int tagType; private int tagType;
public int tagDataSize; private int tagDataSize;
public long tagTimestampUs; private long tagTimestampUs;
private boolean outputSeekMap;
// Tags readers.
private AudioTagPayloadReader audioReader; private AudioTagPayloadReader audioReader;
private VideoTagPayloadReader videoReader; private VideoTagPayloadReader videoReader;
private ScriptTagPayloadReader metadataReader;
public FlvExtractor() { public FlvExtractor() {
scratch = new ParsableByteArray(4); scratch = new ParsableByteArray(4);
headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
tagData = new ParsableByteArray(); tagData = new ParsableByteArray();
parserState = STATE_READING_FLV_HEADER; metadataReader = new ScriptTagPayloadReader();
state = STATE_READING_FLV_HEADER;
mediaTagTimestampOffsetUs = C.TIME_UNSET;
} }
@Override @Override
@ -128,7 +135,8 @@ public final class FlvExtractor implements Extractor, SeekMap {
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
parserState = STATE_READING_FLV_HEADER; state = STATE_READING_FLV_HEADER;
mediaTagTimestampOffsetUs = C.TIME_UNSET;
bytesToNextTagHeader = 0; bytesToNextTagHeader = 0;
} }
@ -141,7 +149,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
InterruptedException { InterruptedException {
while (true) { while (true) {
switch (parserState) { switch (state) {
case STATE_READING_FLV_HEADER: case STATE_READING_FLV_HEADER:
if (!readFlvHeader(input)) { if (!readFlvHeader(input)) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
@ -160,6 +168,9 @@ public final class FlvExtractor implements Extractor, SeekMap {
return RESULT_CONTINUE; return RESULT_CONTINUE;
} }
break; break;
default:
// Never happens.
throw new IllegalStateException();
} }
} }
} }
@ -191,15 +202,11 @@ public final class FlvExtractor implements Extractor, SeekMap {
videoReader = new VideoTagPayloadReader( videoReader = new VideoTagPayloadReader(
extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
} }
if (metadataReader == null) {
metadataReader = new ScriptTagPayloadReader(null);
}
extractorOutput.endTracks(); extractorOutput.endTracks();
extractorOutput.seekMap(this);
// We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. // 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; bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4;
parserState = STATE_SKIPPING_TO_TAG_HEADER; state = STATE_SKIPPING_TO_TAG_HEADER;
return true; return true;
} }
@ -213,7 +220,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException {
input.skipFully(bytesToNextTagHeader); input.skipFully(bytesToNextTagHeader);
bytesToNextTagHeader = 0; 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.readUnsignedInt24();
tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L; tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L;
tagHeaderBuffer.skipBytes(3); // streamId tagHeaderBuffer.skipBytes(3); // streamId
parserState = STATE_READING_TAG_DATA; state = STATE_READING_TAG_DATA;
return true; return true;
} }
@ -251,17 +258,24 @@ public final class FlvExtractor implements Extractor, SeekMap {
private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
boolean wasConsumed = true; boolean wasConsumed = true;
if (tagType == TAG_TYPE_AUDIO && audioReader != null) { 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) { } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
videoReader.consume(prepareTagData(input), tagTimestampUs); ensureReadyForMediaOutput();
} else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) { videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs);
} else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) {
metadataReader.consume(prepareTagData(input), tagTimestampUs); metadataReader.consume(prepareTagData(input), tagTimestampUs);
long durationUs = metadataReader.getDurationUs();
if (durationUs != C.TIME_UNSET) {
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
outputSeekMap = true;
}
} else { } else {
input.skipFully(tagDataSize); input.skipFully(tagDataSize);
wasConsumed = false; wasConsumed = false;
} }
bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. 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; return wasConsumed;
} }
@ -277,21 +291,15 @@ public final class FlvExtractor implements Extractor, SeekMap {
return tagData; return tagData;
} }
// SeekMap implementation. private void ensureReadyForMediaOutput() {
if (!outputSeekMap) {
@Override extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
public boolean isSeekable() { outputSeekMap = true;
return false; }
} if (mediaTagTimestampOffsetUs == C.TIME_UNSET) {
mediaTagTimestampOffsetUs =
@Override metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
public long getDurationUs() { }
return metadataReader.getDurationUs();
}
@Override
public long getPosition(long timeUs) {
return 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.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@ -44,11 +43,8 @@ import java.util.Map;
private long durationUs; private long durationUs;
/** public ScriptTagPayloadReader() {
* @param output A {@link TrackOutput} to which samples should be written. super(null);
*/
public ScriptTagPayloadReader(TrackOutput output) {
super(output);
durationUs = C.TIME_UNSET; durationUs = C.TIME_UNSET;
} }

View file

@ -53,7 +53,7 @@ import java.util.Locale;
import java.util.UUID; 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 { public final class MatroskaExtractor implements Extractor {

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp3; package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.Util; 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 static final int BITS_PER_BYTE = 8;
private final long firstFramePosition; private final long firstFramePosition;
private final int frameSize;
private final long dataSize;
private final int bitrate; private final int bitrate;
private final long durationUs; 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.firstFramePosition = firstFramePosition;
this.bitrate = bitrate; this.frameSize = mpegAudioHeader.frameSize;
durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength); 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 @Override
public boolean isSeekable() { public boolean isSeekable() {
return durationUs != C.TIME_UNSET; return dataSize != C.LENGTH_UNSET;
} }
@Override @Override
public long getPosition(long timeUs) { public long getPosition(long timeUs) {
if (durationUs == C.TIME_UNSET) { if (dataSize == C.LENGTH_UNSET) {
return 0; return firstFramePosition;
} }
timeUs = Util.constrainValue(timeUs, 0, durationUs); long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
return firstFramePosition + (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 @Override

View file

@ -38,7 +38,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
/** /**
* Extracts data from an MP3 file. * Extracts data from the MP3 container format.
*/ */
public final class Mp3Extractor implements Extractor { public final class Mp3Extractor implements Extractor {
@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor {
int seekHeader = getSeekFrameHeader(frame, xingBase); int seekHeader = getSeekFrameHeader(frame, xingBase);
Seeker seeker; Seeker seeker;
if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { 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 (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
// If there is a Xing header, read gapless playback metadata at a fixed offset. // If there is a Xing header, read gapless playback metadata at a fixed offset.
input.resetPeekPosition(); input.resetPeekPosition();
@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor {
return getConstantBitrateSeeker(input); return getConstantBitrateSeeker(input);
} }
} else if (seekHeader == SEEK_HEADER_VBRI) { } 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); input.skipFully(synchronizedHeader.frameSize);
} else { // seekerHeader == SEEK_HEADER_UNSET } else { // seekerHeader == SEEK_HEADER_UNSET
// This frame doesn't contain seeking information, so reset the peek position. // 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); input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0); scratch.setPosition(0);
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader);
input.getLength());
} }
/** /**

View file

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.mp3; package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.ParsableByteArray; 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 { /* 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 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 * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it. * 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 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 * @param frame The data in this audio frame, with its position set to immediately after the
* 'VBRI' tag. * '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 * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
* information is not present. * information is not present.
*/ */
public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
long position, long inputLength) { ParsableByteArray frame) {
frame.skipBytes(10); frame.skipBytes(10);
int numFrames = frame.readInt(); int numFrames = frame.readInt();
if (numFrames <= 0) { if (numFrames <= 0) {
@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util;
int entrySize = frame.readUnsignedShort(); int entrySize = frame.readUnsignedShort();
frame.skipBytes(2); frame.skipBytes(2);
// Skip the frame containing the VBRI header. long minPosition = position + mpegAudioHeader.frameSize;
position += mpegAudioHeader.frameSize;
// Read table of contents entries. // Read table of contents entries.
long[] timesUs = new long[entryCount + 1]; long[] timesUs = new long[entryCount];
long[] positions = new long[entryCount + 1]; long[] positions = new long[entryCount];
timesUs[0] = 0L; for (int index = 0; index < entryCount; index++) {
positions[0] = position; timesUs[index] = (index * durationUs) / entryCount;
for (int index = 1; index < timesUs.length; index++) { // 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; int segmentSize;
switch (entrySize) { switch (entrySize) {
case 1: case 1:
@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util;
return null; return null;
} }
position += segmentSize * scale; position += segmentSize * scale;
timesUs[index] = index * durationUs / entryCount; }
positions[index] = if (inputLength != C.LENGTH_UNSET && inputLength != position) {
inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position); Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position);
} }
return new VbriSeeker(timesUs, positions, durationUs); return new VbriSeeker(timesUs, positions, durationUs);
} }

View file

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.mp3; package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.ParsableByteArray; 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 { /* 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 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 * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it. * 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 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 * @param frame The data in this audio frame, with its position set to immediately after the
* 'Xing' or 'Info' tag. * '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 * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
* information is not present. * information is not present.
*/ */
public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
long position, long inputLength) { ParsableByteArray frame) {
int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
int sampleRate = mpegAudioHeader.sampleRate; int sampleRate = mpegAudioHeader.sampleRate;
long firstFramePosition = position + mpegAudioHeader.frameSize;
int flags = frame.readInt(); int flags = frame.readInt();
int frameCount; int frameCount;
@ -54,45 +56,49 @@ import com.google.android.exoplayer2.util.Util;
sampleRate); sampleRate);
if ((flags & 0x06) != 0x06) { if ((flags & 0x06) != 0x06) {
// If the size in bytes or table of contents is missing, the stream is not seekable. // 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(); long dataSize = frame.readUnsignedIntToInt();
frame.skipBytes(1); long[] tableOfContents = new long[100];
long[] tableOfContents = new long[99]; for (int i = 0; i < 100; i++) {
for (int i = 0; i < 99; i++) {
tableOfContents[i] = frame.readUnsignedByte(); tableOfContents[i] = frame.readUnsignedByte();
} }
// TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
// delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
// padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); // 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 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. * Entries are in the range [0, 255], but are stored as long integers for convenience.
*/ */
private final long[] tableOfContents; private final long[] tableOfContents;
private final long sizeBytes;
private final int headerSize;
private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) {
this(firstFramePosition, durationUs, inputLength, null, 0, 0); this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null);
} }
private XingSeeker(long firstFramePosition, long durationUs, long inputLength, private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize,
long[] tableOfContents, long sizeBytes, int headerSize) { long[] tableOfContents) {
this.firstFramePosition = firstFramePosition; this.dataStartPosition = dataStartPosition;
this.xingFrameSize = xingFrameSize;
this.durationUs = durationUs; this.durationUs = durationUs;
this.inputLength = inputLength; this.dataSize = dataSize;
this.tableOfContents = tableOfContents; this.tableOfContents = tableOfContents;
this.sizeBytes = sizeBytes;
this.headerSize = headerSize;
} }
@Override @Override
@ -103,53 +109,45 @@ import com.google.android.exoplayer2.util.Util;
@Override @Override
public long getPosition(long timeUs) { public long getPosition(long timeUs) {
if (!isSeekable()) { if (!isSeekable()) {
return firstFramePosition; return dataStartPosition + xingFrameSize;
} }
float percent = timeUs * 100f / durationUs; double percent = (timeUs * 100d) / durationUs;
float fx; double scaledPosition;
if (percent <= 0f) { if (percent <= 0) {
fx = 0f; scaledPosition = 0;
} else if (percent >= 100f) { } else if (percent >= 100) {
fx = 256f; scaledPosition = 256;
} else { } else {
int a = (int) percent; int prevTableIndex = (int) percent;
float fa, fb; double prevScaledPosition = tableOfContents[prevTableIndex];
if (a == 0) { double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
fa = 0f; // Linearly interpolate between the two scaled positions.
} else { double interpolateFraction = percent - prevTableIndex;
fa = tableOfContents[a - 1]; scaledPosition = prevScaledPosition
} + (interpolateFraction * (nextScaledPosition - prevScaledPosition));
if (a < 99) {
fb = tableOfContents[a];
} else {
fb = 256f;
}
fx = fa + (fb - fa) * (percent - a);
} }
long positionOffset = Math.round((scaledPosition / 256) * dataSize);
long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition; // Ensure returned positions skip the frame containing the XING header.
long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
: firstFramePosition - headerSize + sizeBytes - 1; return dataStartPosition + positionOffset;
return Math.min(position, maximumPosition);
} }
@Override @Override
public long getTimeUs(long position) { public long getTimeUs(long position) {
if (!isSeekable() || position < firstFramePosition) { long positionOffset = position - dataStartPosition;
if (!isSeekable() || positionOffset <= xingFrameSize) {
return 0L; return 0L;
} }
double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; double scaledPosition = (positionOffset * 256d) / dataSize;
int previousTocPosition = int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true);
Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1; long prevTimeUs = getTimeUsForTableIndex(prevTableIndex);
long previousTime = getTimeUsForTocPosition(previousTocPosition); long prevScaledPosition = tableOfContents[prevTableIndex];
long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1);
// Linearly interpolate the time taking into account the next entry. long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1]; // Linearly interpolate between the two table entries.
long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition]; double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0
long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition));
long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs));
* (offsetByte - previousByte) / (nextByte - previousByte));
return previousTime + timeOffset;
} }
@Override @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 * Returns the time in microseconds for a given table index.
* interpreted as a percentage of the stream's duration between 0 and 100. *
* @param tableIndex A table index in the range [0, 100].
* @return The corresponding time in microseconds.
*/ */
private long getTimeUsForTocPosition(int tocPosition) { private long getTimeUsForTableIndex(int tableIndex) {
return durationUs * tocPosition / 100; return (durationUs * tableIndex) / 100;
} }
} }

View file

@ -46,13 +46,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Stack; import java.util.Stack;
import java.util.UUID; 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 { public final class FragmentedMp4Extractor implements Extractor {
@ -73,8 +74,8 @@ public final class FragmentedMp4Extractor implements Extractor {
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, @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_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED,
FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) FLAG_WORKAROUND_IGNORE_EDIT_LISTS})
public @interface Flags {} public @interface Flags {}
/** /**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame. * 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. * messages in the stream will be delivered as samples to this track.
*/ */
public static final int FLAG_ENABLE_EMSG_TRACK = 4; 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 * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
* container. * container.
*/ */
private static final int FLAG_SIDELOADED = 16; private static final int FLAG_SIDELOADED = 8;
/** /**
* Flag to ignore any edit lists in the stream. * 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 String TAG = "FragmentedMp4Extractor";
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); 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; @Flags private final int flags;
private final Track sideloadedTrack; private final Track sideloadedTrack;
// Manifest DRM data. // Sideloaded data.
private final List<Format> closedCaptionFormats;
private final DrmInitData sideloadedDrmInitData; private final DrmInitData sideloadedDrmInitData;
// Track-linked data bundle, accessible as a whole through trackID. // 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 flags Flags that control the extractor's behavior.
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
* @param sideloadedTrack Sideloaded track information, in the case that the extractor * @param sideloadedTrack Sideloaded track information, in the case that the extractor
* will not receive a moov box in the input data. * 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. * @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, public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
Track sideloadedTrack, DrmInitData sideloadedDrmInitData) { 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.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
this.timestampAdjuster = timestampAdjuster; this.timestampAdjuster = timestampAdjuster;
this.sideloadedTrack = sideloadedTrack; this.sideloadedTrack = sideloadedTrack;
this.sideloadedDrmInitData = sideloadedDrmInitData; this.sideloadedDrmInitData = sideloadedDrmInitData;
this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats);
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalPrefix = new ParsableByteArray(5); nalPrefix = new ParsableByteArray(5);
@ -330,7 +345,8 @@ public final class FragmentedMp4Extractor implements Extractor {
currentTrackBundle = null; currentTrackBundle = null;
endOfMdatPosition = atomPosition + atomSize; endOfMdatPosition = atomPosition + atomSize;
if (!haveOutputSeekMap) { 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; haveOutputSeekMap = true;
} }
parserState = STATE_READING_ENCRYPTION_DATA; parserState = STATE_READING_ENCRYPTION_DATA;
@ -483,12 +499,13 @@ public final class FragmentedMp4Extractor implements Extractor {
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
Format.OFFSET_SAMPLE_RELATIVE)); Format.OFFSET_SAMPLE_RELATIVE));
} }
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { if (cea608TrackOutputs == null) {
TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()];
C.TRACK_TYPE_TEXT); for (int i = 0; i < cea608TrackOutputs.length; i++) {
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT);
null)); output.format(closedCaptionFormats.get(i));
cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; cea608TrackOutputs[i] = output;
}
} }
} }
@ -1123,7 +1140,7 @@ public final class FragmentedMp4Extractor implements Extractor {
output.sampleData(nalStartCode, 4); output.sampleData(nalStartCode, 4);
// Write the NAL unit type byte. // Write the NAL unit type byte.
output.sampleData(nalPrefix, 1); output.sampleData(nalPrefix, 1);
processSeiNalUnitPayload = cea608TrackOutputs != null processSeiNalUnitPayload = cea608TrackOutputs.length > 0
&& NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
sampleBytesWritten += 5; sampleBytesWritten += 5;
sampleSize += nalUnitLengthFieldLengthDiff; sampleSize += nalUnitLengthFieldLengthDiff;

View file

@ -41,7 +41,7 @@ import java.util.List;
import java.util.Stack; 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 { public final class Mp4Extractor implements Extractor, SeekMap {

View file

@ -186,7 +186,7 @@ import java.io.IOException;
return start; return start;
} }
long offset = pageSize * (granuleDistance <= 0 ? 2 : 1); long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
long nextPosition = input.getPosition() - offset long nextPosition = input.getPosition() - offset
+ (granuleDistance * (end - start) / (endGranule - startGranule)); + (granuleDistance * (end - start) / (endGranule - startGranule));

View file

@ -118,8 +118,9 @@ import java.util.List;
case 14: case 14:
case 15: case 15:
return 256 << (blockSizeCode - 8); return 256 << (blockSizeCode - 8);
default:
return -1;
} }
return -1;
} }
private class FlacOggSeeker implements OggSeeker, SeekMap { private class FlacOggSeeker implements OggSeeker, SeekMap {

View file

@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
/** /**
* Ogg {@link Extractor}. * Extracts data from the Ogg container format.
*/ */
public class OggExtractor implements Extractor { public class OggExtractor implements Extractor {

View file

@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** /**
* Extracts CEA data from a RawCC file. * Extracts data from the RawCC container format.
*/ */
public final class RawCcExtractor implements Extractor { public final class RawCcExtractor implements Extractor {

View file

@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** /**
* Extracts samples from (E-)AC-3 bitstreams. * Extracts data from (E-)AC-3 bitstreams.
*/ */
public final class Ac3Extractor implements Extractor { 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_HEADER = 1;
private static final int STATE_READING_SAMPLE = 2; 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 ParsableBitArray headerScratchBits;
private final ParsableByteArray headerScratchBytes; private final ParsableByteArray headerScratchBytes;

View file

@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException; 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 { public final class AdtsExtractor implements Extractor {

View file

@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.IOException; 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 { public final class PsExtractor implements Extractor {

View file

@ -45,7 +45,7 @@ import java.util.Collections;
import java.util.List; 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 { 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException; 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. * Factory for {@link WavExtractor} instances.
@ -93,7 +94,7 @@ public final class WavExtractor implements Extractor, SeekMap {
if (!wavHeader.hasDataBounds()) { if (!wavHeader.hasDataBounds()) {
WavHeaderReader.skipToData(input, wavHeader); WavHeaderReader.skipToData(input, wavHeader);
extractorOutput.seekMap(this); extractorOutput.seekMap(wavHeader);
} }
int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true); 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; 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; package com.google.android.exoplayer2.extractor.wav;
import com.google.android.exoplayer2.C; 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. */ /** Header for a WAV file. */
/*package*/ final class WavHeader { /* package */ final class WavHeader implements SeekMap {
/** Number of audio chanels. */ /** Number of audio chanels. */
private final int numChannels; private final int numChannels;
@ -49,12 +51,58 @@ import com.google.android.exoplayer2.C;
this.encoding = encoding; 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() { public long getDurationUs() {
long numFrames = dataSize / blockAlignment; long numFrames = dataSize / blockAlignment;
return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; 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. */ /** Returns the bytes per frame of this WAV. */
public int getBytesPerFrame() { public int getBytesPerFrame() {
return blockAlignment; return blockAlignment;
@ -75,33 +123,8 @@ import com.google.android.exoplayer2.C;
return numChannels; 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. **/ /** Returns the PCM encoding. **/
@C.PcmEncoding public @C.PcmEncoding int getEncoding() {
public int getEncoding() {
return encoding; return encoding;
} }

View file

@ -31,6 +31,8 @@ import java.io.IOException;
/** Integer PCM audio data. */ /** Integer PCM audio data. */
private static final int TYPE_PCM = 0x0001; private static final int TYPE_PCM = 0x0001;
/** Float PCM audio data. */
private static final int TYPE_FLOAT = 0x0003;
/** Extended WAVE format. */ /** Extended WAVE format. */
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
@ -87,14 +89,22 @@ import java.io.IOException;
+ blockAlignment); + blockAlignment);
} }
@C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample); @C.PcmEncoding int encoding;
if (encoding == C.ENCODING_INVALID) { switch (type) {
Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); case TYPE_PCM:
return null; 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) { if (encoding == C.ENCODING_INVALID) {
Log.e(TAG, "Unsupported WAV format type: " + type); Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type);
return null; return null;
} }

View file

@ -20,6 +20,7 @@ import android.annotation.TargetApi;
import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecList; import android.media.MediaCodecList;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@ -120,7 +121,7 @@ public final class MediaCodecUtil {
* exists. * exists.
* @throws DecoderQueryException If there was an error querying the available decoders. * @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 { throws DecoderQueryException {
List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure); List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure);
return decoderInfos.isEmpty() ? null : decoderInfos.get(0); return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
@ -140,27 +141,34 @@ public final class MediaCodecUtil {
public static synchronized List<MediaCodecInfo> getDecoderInfos(String mimeType, public static synchronized List<MediaCodecInfo> getDecoderInfos(String mimeType,
boolean secure) throws DecoderQueryException { boolean secure) throws DecoderQueryException {
CodecKey key = new CodecKey(mimeType, secure); CodecKey key = new CodecKey(mimeType, secure);
List<MediaCodecInfo> decoderInfos = decoderInfosCache.get(key); List<MediaCodecInfo> cachedDecoderInfos = decoderInfosCache.get(key);
if (decoderInfos != null) { if (cachedDecoderInfos != null) {
return decoderInfos; return cachedDecoderInfos;
} }
MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21
? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); ? 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) { 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 // 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. // legacy path. We also try this path on API levels 22 and 23 as a defensive measure.
mediaCodecList = new MediaCodecListCompatV16(); mediaCodecList = new MediaCodecListCompatV16();
decoderInfos = getDecoderInfosInternal(key, mediaCodecList); decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType);
if (!decoderInfos.isEmpty()) { if (!decoderInfos.isEmpty()) {
Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
+ ". Assuming: " + decoderInfos.get(0).name); + ". 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); applyWorkarounds(decoderInfos);
decoderInfos = Collections.unmodifiableList(decoderInfos); List<MediaCodecInfo> unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos);
decoderInfosCache.put(key, decoderInfos); decoderInfosCache.put(key, unmodifiableDecoderInfos);
return decoderInfos; return unmodifiableDecoderInfos;
} }
/** /**
@ -212,10 +220,21 @@ public final class MediaCodecUtil {
// Internal methods. // 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 { try {
List<MediaCodecInfo> decoderInfos = new ArrayList<>(); ArrayList<MediaCodecInfo> decoderInfos = new ArrayList<>();
String mimeType = key.mimeType; String mimeType = key.mimeType;
int numberOfCodecs = mediaCodecList.getCodecCount(); int numberOfCodecs = mediaCodecList.getCodecCount();
boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();
@ -223,7 +242,7 @@ public final class MediaCodecUtil {
for (int i = 0; i < numberOfCodecs; i++) { for (int i = 0; i < numberOfCodecs; i++) {
android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);
String codecName = codecInfo.getName(); String codecName = codecInfo.getName();
if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) { if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit, requestedMimeType)) {
for (String supportedType : codecInfo.getSupportedTypes()) { for (String supportedType : codecInfo.getSupportedTypes()) {
if (supportedType.equalsIgnoreCase(mimeType)) { if (supportedType.equalsIgnoreCase(mimeType)) {
try { try {
@ -265,9 +284,16 @@ public final class MediaCodecUtil {
/** /**
* Returns whether the specified codec is usable for decoding on the current device. * 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, private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name,
boolean secureDecodersExplicit) { boolean secureDecodersExplicit, String requestedMimeType) {
if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) {
return false; return false;
} }
@ -356,6 +382,12 @@ public final class MediaCodecUtil {
return false; 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; return true;
} }

View file

@ -15,309 +15,10 @@
*/ */
package com.google.android.exoplayer2.source; 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 { @Deprecated
public interface AdaptiveMediaSourceEventListener extends 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 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;
}
}
}

View file

@ -112,7 +112,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
if (internalStreams[i] == null) { if (internalStreams[i] == null) {
sampleStreams[i] = null; sampleStreams[i] = null;
} else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { } 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); pendingInitialDiscontinuity);
} }
streams[i] = sampleStreams[i]; streams[i] = sampleStreams[i];
@ -222,9 +222,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
/** /**
* Wraps a {@link SampleStream} and clips its samples. * 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 SampleStream stream;
private final long startUs; private final long startUs;
private final long endUs; private final long endUs;
@ -232,9 +231,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
private boolean pendingDiscontinuity; private boolean pendingDiscontinuity;
private boolean sentEos; private boolean sentEos;
public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, public ClippingSampleStream(SampleStream stream, long startUs, long endUs,
long endUs, boolean pendingDiscontinuity) { boolean pendingDiscontinuity) {
this.mediaPeriod = mediaPeriod;
this.stream = stream; this.stream = stream;
this.startUs = startUs; this.startUs = startUs;
this.endUs = endUs; this.endUs = endUs;
@ -278,9 +276,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding);
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} }
if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ if (endUs != C.TIME_END_OF_SOURCE
&& buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)
&& mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { || (result == C.RESULT_NOTHING_READ
&& getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
buffer.clear(); buffer.clear();
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
sentEos = true; 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.ExoPlayer.ExoPlayerMessage;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; 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.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; 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.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
@ -28,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; 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.source.SampleQueue.UpstreamFormatChangedListener;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
@ -74,11 +76,10 @@ import java.util.Arrays;
private final Uri uri; private final Uri uri;
private final DataSource dataSource; private final DataSource dataSource;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final Handler eventHandler; private final EventDispatcher eventDispatcher;
private final ExtractorMediaSource.EventListener eventListener;
private final Listener listener; private final Listener listener;
private final Allocator allocator; private final Allocator allocator;
private final String customCacheKey; @Nullable private final String customCacheKey;
private final long continueLoadingCheckIntervalBytes; private final long continueLoadingCheckIntervalBytes;
private final Loader loader; private final Loader loader;
private final ExtractorHolder extractorHolder; private final ExtractorHolder extractorHolder;
@ -117,8 +118,7 @@ import java.util.Arrays;
* @param dataSource The data source to read the media. * @param dataSource The data source to read the media.
* @param extractors The extractors to use to read the data source. * @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 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 eventDispatcher A dispatcher to notify of events.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param listener A listener to notify when information about the period changes. * @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 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 * @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 * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
*/ */
public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, public ExtractorMediaPeriod(
int minLoadableRetryCount, Handler eventHandler, Uri uri,
ExtractorMediaSource.EventListener eventListener, Listener listener, DataSource dataSource,
Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) { Extractor[] extractors,
int minLoadableRetryCount,
EventDispatcher eventDispatcher,
Listener listener,
Allocator allocator,
@Nullable String customCacheKey,
int continueLoadingCheckIntervalBytes) {
this.uri = uri; this.uri = uri;
this.dataSource = dataSource; this.dataSource = dataSource;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler; this.eventDispatcher = eventDispatcher;
this.eventListener = eventListener;
this.listener = listener; this.listener = listener;
this.allocator = allocator; this.allocator = allocator;
this.customCacheKey = customCacheKey; this.customCacheKey = customCacheKey;
@ -303,7 +308,8 @@ import java.util.Arrays;
@Override @Override
public long readDiscontinuity() { public long readDiscontinuity() {
if (notifyDiscontinuity) { if (notifyDiscontinuity
&& (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) {
notifyDiscontinuity = false; notifyDiscontinuity = false;
return lastSeekPositionUs; return lastSeekPositionUs;
} }
@ -399,38 +405,75 @@ import java.util.Arrays;
@Override @Override
public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs) { long loadDurationMs) {
copyLengthFromLoader(loadable);
loadingFinished = true;
if (durationUs == C.TIME_UNSET) { if (durationUs == C.TIME_UNSET) {
long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0
: largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); 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); callback.onContinueLoadingRequested(this);
} }
@Override @Override
public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) { long loadDurationMs, boolean released) {
if (released) { eventDispatcher.loadCanceled(
return; loadable.dataSpec,
} C.DATA_TYPE_MEDIA,
copyLengthFromLoader(loadable); C.TRACK_TYPE_UNKNOWN,
for (SampleQueue sampleQueue : sampleQueues) { /* trackFormat= */ null,
sampleQueue.reset(); C.SELECTION_REASON_UNKNOWN,
} /* trackSelectionData= */ null,
if (enabledTrackCount > 0) { /* mediaStartTimeUs= */ 0,
callback.onContinueLoadingRequested(this); durationUs,
elapsedRealtimeMs,
loadDurationMs,
loadable.bytesLoaded);
if (!released) {
copyLengthFromLoader(loadable);
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
if (enabledTrackCount > 0) {
callback.onContinueLoadingRequested(this);
}
} }
} }
@Override @Override
public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs, IOException error) { 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); copyLengthFromLoader(loadable);
notifyLoadError(error); if (isErrorFatal) {
if (isLoadableExceptionFatal(error)) {
return Loader.DONT_RETRY_FATAL; return Loader.DONT_RETRY_FATAL;
} }
int extractedSamplesCount = getExtractedSamplesCount(); int extractedSamplesCount = getExtractedSamplesCount();
@ -606,17 +649,6 @@ import java.util.Arrays;
return e instanceof UnrecognizedInputFormatException; 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 class SampleStreamImpl implements SampleStream {
private final int track; private final int track;
@ -663,7 +695,9 @@ import java.util.Arrays;
private boolean pendingExtractorSeek; private boolean pendingExtractorSeek;
private long seekTimeUs; private long seekTimeUs;
private DataSpec dataSpec;
private long length; private long length;
private long bytesLoaded;
public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder,
ConditionVariable loadCondition) { ConditionVariable loadCondition) {
@ -699,7 +733,8 @@ import java.util.Arrays;
ExtractorInput input = null; ExtractorInput input = null;
try { try {
long position = positionHolder.position; 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) { if (length != C.LENGTH_UNSET) {
length += position; length += position;
} }
@ -723,6 +758,7 @@ import java.util.Arrays;
result = Extractor.RESULT_CONTINUE; result = Extractor.RESULT_CONTINUE;
} else if (input != null) { } else if (input != null) {
positionHolder.position = input.getPosition(); positionHolder.position = input.getPosition();
bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition;
} }
Util.closeQuietly(dataSource); Util.closeQuietly(dataSource);
} }

View file

@ -17,14 +17,19 @@ package com.google.android.exoplayer2.source;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; 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.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; 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. * 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 { public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener {
/** /**
* Listener of {@link ExtractorMediaSource} events. * Listener of {@link ExtractorMediaSource} events.
*
* @deprecated Use {@link MediaSourceEventListener}.
*/ */
@Deprecated
public interface EventListener { public interface EventListener {
/** /**
@ -89,8 +96,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final ExtractorsFactory extractorsFactory; private final ExtractorsFactory extractorsFactory;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final Handler eventHandler; private final EventDispatcher eventDispatcher;
private final EventListener eventListener;
private final String customCacheKey; private final String customCacheKey;
private final int continueLoadingCheckIntervalBytes; private final int continueLoadingCheckIntervalBytes;
@ -98,6 +104,127 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
private long timelineDurationUs; private long timelineDurationUs;
private boolean timelineIsSeekable; 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 uri The {@link Uri} of the media stream.
* @param dataSourceFactory A factory for {@link DataSource}s to read the media. * @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. * 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 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 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, @Deprecated
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { public ExtractorMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory,
Handler eventHandler,
EventListener eventListener) {
this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); 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 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 * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null. * indexing. May be null.
* @deprecated Use {@link Factory} instead.
*/ */
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, @Deprecated
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, public ExtractorMediaSource(
Uri uri,
DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory,
Handler eventHandler,
EventListener eventListener,
String customCacheKey) { String customCacheKey) {
this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES); eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES);
@ -143,16 +282,43 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
* indexing. May be null. * indexing. May be null.
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
* invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
* @deprecated Use {@link Factory} instead.
*/ */
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, @Deprecated
ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, public ExtractorMediaSource(
EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { 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.uri = uri;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory; this.extractorsFactory = extractorsFactory;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler; this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
this.eventListener = eventListener;
this.customCacheKey = customCacheKey; this.customCacheKey = customCacheKey;
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
} }
@ -171,9 +337,16 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
Assertions.checkArgument(id.periodIndex == 0); Assertions.checkArgument(id.periodIndex == 0);
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), return new ExtractorMediaPeriod(
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, uri,
this, allocator, customCacheKey, continueLoadingCheckIntervalBytes); dataSourceFactory.createDataSource(),
extractorsFactory.createExtractors(),
minLoadableRetryCount,
eventDispatcher,
this,
allocator,
customCacheKey,
continueLoadingCheckIntervalBytes);
} }
@Override @Override
@ -208,4 +381,94 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); 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> * player to load and read the media.</li>
* </ul> * </ul>
* All methods are called on the player's internal playback thread, as described in the * 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 { public interface MediaSource {
@ -150,6 +151,8 @@ public interface MediaSource {
/** /**
* Starts preparation of the source. * 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 player The player for which this source is being prepared.
* @param isTopLevelSource Whether this source has been passed directly to * @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. * Throws any pending error encountered while loading or refreshing source information.
* <p>
* Should not be called directly from application code.
*/ */
void maybeThrowSourceInfoRefreshError() throws IOException; 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 * 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 * multiple times with the same period identifier without an intervening call to
* {@link #releasePeriod(MediaPeriod)}. * {@link #releasePeriod(MediaPeriod)}.
* <p>
* Should not be called directly from application code.
* *
* @param id The identifier of the period. * @param id The identifier of the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
@ -178,6 +185,8 @@ public interface MediaSource {
/** /**
* Releases the period. * Releases the period.
* <p>
* Should not be called directly from application code.
* *
* @param mediaPeriod The period to release. * @param mediaPeriod The period to release.
*/ */
@ -186,8 +195,7 @@ public interface MediaSource {
/** /**
* Releases the source. * Releases the source.
* <p> * <p>
* This method should be called when the source is no longer required. It may be called in any * Should not be called directly from application code.
* state.
*/ */
void releaseSource(); 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; 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.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; 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.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -43,14 +41,14 @@ import java.util.Arrays;
*/ */
private static final int INITIAL_SAMPLE_SIZE = 1024; private static final int INITIAL_SAMPLE_SIZE = 1024;
private final Uri uri; private final DataSpec dataSpec;
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final Handler eventHandler; private final EventDispatcher eventDispatcher;
private final EventListener eventListener;
private final int eventSourceId;
private final TrackGroupArray tracks; private final TrackGroupArray tracks;
private final ArrayList<SampleStreamImpl> sampleStreams; private final ArrayList<SampleStreamImpl> sampleStreams;
private final long durationUs;
// Package private to avoid thunk methods. // Package private to avoid thunk methods.
/* package */ final Loader loader; /* package */ final Loader loader;
/* package */ final Format format; /* package */ final Format format;
@ -62,16 +60,20 @@ import java.util.Arrays;
/* package */ int sampleSize; /* package */ int sampleSize;
private int errorCount; private int errorCount;
public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, public SingleSampleMediaPeriod(
int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, DataSpec dataSpec,
int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { DataSource.Factory dataSourceFactory,
this.uri = uri; Format format,
long durationUs,
int minLoadableRetryCount,
EventDispatcher eventDispatcher,
boolean treatLoadErrorsAsEndOfStream) {
this.dataSpec = dataSpec;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.format = format; this.format = format;
this.durationUs = durationUs;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler; this.eventDispatcher = eventDispatcher;
this.eventListener = eventListener;
this.eventSourceId = eventSourceId;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
tracks = new TrackGroupArray(new TrackGroup(format)); tracks = new TrackGroupArray(new TrackGroup(format));
sampleStreams = new ArrayList<>(); sampleStreams = new ArrayList<>();
@ -125,7 +127,9 @@ import java.util.Arrays;
if (loadingFinished || loader.isLoading()) { if (loadingFinished || loader.isLoading()) {
return false; return false;
} }
loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this, loader.startLoading(
new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()),
this,
minLoadableRetryCount); minLoadableRetryCount);
return true; return true;
} }
@ -158,6 +162,18 @@ import java.util.Arrays;
@Override @Override
public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs,
long loadDurationMs) { 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; sampleSize = loadable.sampleSize;
sampleData = loadable.sampleData; sampleData = loadable.sampleData;
loadingFinished = true; loadingFinished = true;
@ -167,34 +183,46 @@ import java.util.Arrays;
@Override @Override
public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
boolean released) { 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 @Override
public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
IOException error) { IOException error) {
notifyLoadError(error);
errorCount++; 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; loadingFinished = true;
return Loader.DONT_RETRY; return Loader.DONT_RETRY;
} }
return Loader.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 final class SampleStreamImpl implements SampleStream {
private static final int STREAM_STATE_SEND_FORMAT = 0; private static final int STREAM_STATE_SEND_FORMAT = 0;
@ -259,14 +287,15 @@ import java.util.Arrays;
/* package */ static final class SourceLoadable implements Loadable { /* package */ static final class SourceLoadable implements Loadable {
private final Uri uri; public final DataSpec dataSpec;
private final DataSource dataSource; private final DataSource dataSource;
private int sampleSize; private int sampleSize;
private byte[] sampleData; private byte[] sampleData;
public SourceLoadable(Uri uri, DataSource dataSource) { public SourceLoadable(DataSpec dataSpec, DataSource dataSource) {
this.uri = uri; this.dataSpec = dataSpec;
this.dataSource = dataSource; this.dataSource = dataSource;
} }
@ -286,7 +315,7 @@ import java.util.Arrays;
sampleSize = 0; sampleSize = 0;
try { try {
// Create and open the input. // Create and open the input.
dataSource.open(new DataSpec(uri)); dataSource.open(dataSpec);
// Load the sample data. // Load the sample data.
int result = 0; int result = 0;
while (result != C.RESULT_END_OF_INPUT) { 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.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Timeline; 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.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
@ -32,7 +35,10 @@ public final class SingleSampleMediaSource implements MediaSource {
/** /**
* Listener of {@link SingleSampleMediaSource} events. * Listener of {@link SingleSampleMediaSource} events.
*
* @deprecated Use {@link MediaSourceEventListener}.
*/ */
@Deprecated
public interface EventListener { 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. * The default minimum number of times to retry loading data prior to failing.
*/ */
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; 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 DataSource.Factory dataSourceFactory;
private final Format format; private final Format format;
private final long durationUs;
private final MediaSourceEventListener.EventDispatcher eventDispatcher;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final Handler eventHandler;
private final EventListener eventListener;
private final int eventSourceId;
private final boolean treatLoadErrorsAsEndOfStream; private final boolean treatLoadErrorsAsEndOfStream;
private final Timeline timeline; private final Timeline timeline;
@ -66,9 +164,11 @@ public final class SingleSampleMediaSource implements MediaSource {
* be obtained. * be obtained.
* @param format The {@link Format} associated with the output track. * @param format The {@link Format} associated with the output track.
* @param durationUs The duration of the media stream in microseconds. * @param durationUs The duration of the media stream in microseconds.
* @deprecated Use {@link Factory} instead.
*/ */
public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, @Deprecated
long durationUs) { public SingleSampleMediaSource(
Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) {
this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); 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 format The {@link Format} associated with the output track.
* @param durationUs The duration of the media stream in microseconds. * @param durationUs The duration of the media stream in microseconds.
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @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, @Deprecated
long durationUs, int minLoadableRetryCount) { public SingleSampleMediaSource(
this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); 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 * @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 * streams, treating them as ended instead. If false, load errors will be propagated normally
* by {@link SampleStream#maybeThrowError()}. * by {@link SampleStream#maybeThrowError()}.
* @deprecated Use {@link Factory} instead.
*/ */
public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, @Deprecated
long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, public SingleSampleMediaSource(
int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { Uri uri,
this.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.dataSourceFactory = dataSourceFactory;
this.format = format; this.format = format;
this.durationUs = durationUs;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.eventSourceId = eventSourceId;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
dataSpec = new DataSpec(uri);
timeline = new SinglePeriodTimeline(durationUs, true); timeline = new SinglePeriodTimeline(durationUs, true);
} }
@ -128,8 +262,14 @@ public final class SingleSampleMediaSource implements MediaSource {
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
Assertions.checkArgument(id.periodIndex == 0); Assertions.checkArgument(id.periodIndex == 0);
return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, return new SingleSampleMediaPeriod(
eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream); dataSpec,
dataSourceFactory,
format,
durationUs,
minLoadableRetryCount,
eventDispatcher,
treatLoadErrorsAsEndOfStream);
} }
@Override @Override
@ -142,4 +282,97 @@ public final class SingleSampleMediaSource implements MediaSource {
// Do nothing. // 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. * @param group The group.
* @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists. * @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists.
*/ */
@SuppressWarnings("ReferenceEquality")
public int indexOf(TrackGroup group) { public int indexOf(TrackGroup group) {
for (int i = 0; i < length; i++) { 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) { if (trackGroups[i] == group) {
return i; return i;
} }
@ -71,6 +74,13 @@ public final class TrackGroupArray {
return C.INDEX_UNSET; return C.INDEX_UNSET;
} }
/**
* Returns whether this track group array is empty.
*/
public boolean isEmpty() {
return length == 0;
}
@Override @Override
public int hashCode() { public int hashCode() {
if (hashCode == 0) { if (hashCode == 0) {

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.ads; package com.google.android.exoplayer2.source.ads;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import java.io.IOException; 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 * Attaches a player that will play ads loaded using this instance. Called on the main thread by
* {@link AdsMediaSource}. * {@link AdsMediaSource}.

View file

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

View file

@ -16,12 +16,11 @@
package com.google.android.exoplayer2.source.chunk; package com.google.android.exoplayer2.source.chunk;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; 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.SampleQueue;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.SequenceableLoader; 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.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List; import java.util.List;
/** /**
@ -185,7 +184,7 @@ public final class Cea608Decoder extends CeaDecoder {
private final ParsableByteArray ccData; private final ParsableByteArray ccData;
private final int packetLength; private final int packetLength;
private final int selectedField; private final int selectedField;
private final LinkedList<CueBuilder> cueBuilders; private final ArrayList<CueBuilder> cueBuilders;
private CueBuilder currentCueBuilder; private CueBuilder currentCueBuilder;
private List<Cue> cues; private List<Cue> cues;
@ -200,7 +199,7 @@ public final class Cea608Decoder extends CeaDecoder {
public Cea608Decoder(String mimeType, int accessibilityChannel) { public Cea608Decoder(String mimeType, int accessibilityChannel) {
ccData = new ParsableByteArray(); ccData = new ParsableByteArray();
cueBuilders = new LinkedList<>(); cueBuilders = new ArrayList<>();
currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
switch (accessibilityChannel) { switch (accessibilityChannel) {
@ -230,8 +229,8 @@ public final class Cea608Decoder extends CeaDecoder {
cues = null; cues = null;
lastCues = null; lastCues = null;
setCaptionMode(CC_MODE_UNKNOWN); setCaptionMode(CC_MODE_UNKNOWN);
setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT);
resetCueBuilders(); resetCueBuilders();
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
repeatableControlSet = false; repeatableControlSet = false;
repeatableControlCc1 = 0; repeatableControlCc1 = 0;
repeatableControlCc2 = 0; repeatableControlCc2 = 0;
@ -434,16 +433,16 @@ public final class Cea608Decoder extends CeaDecoder {
private void handleMiscCode(byte cc2) { private void handleMiscCode(byte cc2) {
switch (cc2) { switch (cc2) {
case CTRL_ROLL_UP_CAPTIONS_2_ROWS: case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
captionRowCount = 2;
setCaptionMode(CC_MODE_ROLL_UP); setCaptionMode(CC_MODE_ROLL_UP);
setCaptionRowCount(2);
return; return;
case CTRL_ROLL_UP_CAPTIONS_3_ROWS: case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
captionRowCount = 3;
setCaptionMode(CC_MODE_ROLL_UP); setCaptionMode(CC_MODE_ROLL_UP);
setCaptionRowCount(3);
return; return;
case CTRL_ROLL_UP_CAPTIONS_4_ROWS: case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
captionRowCount = 4;
setCaptionMode(CC_MODE_ROLL_UP); setCaptionMode(CC_MODE_ROLL_UP);
setCaptionRowCount(4);
return; return;
case CTRL_RESUME_CAPTION_LOADING: case CTRL_RESUME_CAPTION_LOADING:
setCaptionMode(CC_MODE_POP_ON); setCaptionMode(CC_MODE_POP_ON);
@ -451,6 +450,9 @@ public final class Cea608Decoder extends CeaDecoder {
case CTRL_RESUME_DIRECT_CAPTIONING: case CTRL_RESUME_DIRECT_CAPTIONING:
setCaptionMode(CC_MODE_PAINT_ON); setCaptionMode(CC_MODE_PAINT_ON);
return; return;
default:
// Fall through.
break;
} }
if (captionMode == CC_MODE_UNKNOWN) { if (captionMode == CC_MODE_UNKNOWN) {
@ -484,6 +486,9 @@ public final class Cea608Decoder extends CeaDecoder {
case CTRL_DELETE_TO_END_OF_ROW: case CTRL_DELETE_TO_END_OF_ROW:
// TODO: implement // TODO: implement
break; 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() { private void resetCueBuilders() {
currentCueBuilder.reset(captionMode, captionRowCount); currentCueBuilder.reset(captionMode);
cueBuilders.clear(); cueBuilders.clear();
cueBuilders.add(currentCueBuilder); cueBuilders.add(currentCueBuilder);
} }
@ -594,12 +604,14 @@ public final class Cea608Decoder extends CeaDecoder {
public CueBuilder(int captionMode, int captionRowCount) { public CueBuilder(int captionMode, int captionRowCount) {
preambleStyles = new ArrayList<>(); preambleStyles = new ArrayList<>();
midrowStyles = new ArrayList<>(); midrowStyles = new ArrayList<>();
rolledUpCaptions = new LinkedList<>(); rolledUpCaptions = new ArrayList<>();
captionStringBuilder = new SpannableStringBuilder(); 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(); preambleStyles.clear();
midrowStyles.clear(); midrowStyles.clear();
rolledUpCaptions.clear(); rolledUpCaptions.clear();
@ -607,11 +619,13 @@ public final class Cea608Decoder extends CeaDecoder {
row = BASE_ROW; row = BASE_ROW;
indent = 0; indent = 0;
tabOffset = 0; tabOffset = 0;
this.captionMode = captionMode;
this.captionRowCount = captionRowCount;
underlineStartPosition = POSITION_UNSET; underlineStartPosition = POSITION_UNSET;
} }
public void setCaptionRowCount(int captionRowCount) {
this.captionRowCount = captionRowCount;
}
public boolean isEmpty() { public boolean isEmpty() {
return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty()
&& captionStringBuilder.length() == 0; && 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. // The number of empty columns after the end of the text, in the same range.
int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length(); int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length();
int startEndPaddingDelta = startPadding - endPadding; int startEndPaddingDelta = startPadding - endPadding;
if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) { if (captionMode == CC_MODE_POP_ON && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) {
// Treat approximately centered pop-on captions are middle aligned. // 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; position = 0.5f;
positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
} else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) { } 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_DF1 = 0x99; // DefineWindow 1 (+6 bytes)
private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+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_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_DF5 = 0x9D; // DefineWindow 5 (+6 bytes)
private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes)
private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+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_DF1:
case COMMAND_DF2: case COMMAND_DF2:
case COMMAND_DF3: case COMMAND_DF3:
case COMMAND_DS4: case COMMAND_DF4:
case COMMAND_DF5: case COMMAND_DF5:
case COMMAND_DF6: case COMMAND_DF6:
case COMMAND_DF7: case COMMAND_DF7:

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