Commit 2b207804 by ojw28 Committed by GitHub

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

r2.6.1
parents 1b66908f b704a1d6
Showing with 1683 additions and 812 deletions
*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION ***
Before filing an issue: Before filing an issue:
----------------------- -----------------------
- Search existing issues, including issues that are closed. - Search existing issues, including issues that are closed.
......
...@@ -68,7 +68,7 @@ individually. ...@@ -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
......
# 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 @@ ...@@ -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
......
...@@ -17,7 +17,7 @@ buildscript { ...@@ -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:
......
...@@ -17,8 +17,8 @@ project.ext { ...@@ -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 { ...@@ -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
......
...@@ -43,5 +43,7 @@ android { ...@@ -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')
} }
...@@ -15,11 +15,11 @@ ...@@ -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">
......
...@@ -17,15 +17,21 @@ package com.google.android.exoplayer2.imademo; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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};
}
} }
...@@ -16,14 +16,14 @@ ...@@ -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"
......
...@@ -16,14 +16,43 @@ ...@@ -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.
......
...@@ -38,10 +38,10 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame; ...@@ -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; ...@@ -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; ...@@ -320,19 +323,19 @@ import java.util.Locale;
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
} }
// ExtractorMediaSource.EventListener // MediaSourceEventListener
@Override
public void onLoadError(IOException error) {
printInternalError("loadError", error);
}
// AdaptiveMediaSourceEventListener
@Override @Override
public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, public void onLoadStarted(
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, DataSpec dataSpec,
long mediaEndTimeMs, long elapsedRealtimeMs) { int dataType,
int trackType,
Format trackFormat,
int trackSelectionReason,
Object trackSelectionData,
long mediaStartTimeMs,
long mediaEndTimeMs,
long elapsedRealtimeMs) {
// Do nothing. // Do nothing.
} }
...@@ -369,6 +372,23 @@ import java.util.Locale; ...@@ -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; ...@@ -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
......
...@@ -23,6 +23,7 @@ import android.net.Uri; ...@@ -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; ...@@ -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; ...@@ -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, ...@@ -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, ...@@ -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, ...@@ -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, ...@@ -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, ...@@ -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, ...@@ -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, ...@@ -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, ...@@ -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() {
......
...@@ -32,8 +32,8 @@ import android.widget.ExpandableListView; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
......
...@@ -40,6 +40,7 @@ dependencies { ...@@ -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
......
...@@ -18,7 +18,7 @@ ...@@ -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"
......
...@@ -19,10 +19,10 @@ import static org.junit.Assert.assertArrayEquals; ...@@ -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 { ...@@ -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);
} }
......
...@@ -31,13 +31,13 @@ import static org.mockito.Mockito.spy; ...@@ -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 { ...@@ -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,
......
...@@ -21,6 +21,8 @@ import com.google.android.exoplayer2.ExoPlaybackException; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
}
} }
} }
...@@ -15,6 +15,7 @@ ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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);
} }
...@@ -125,6 +133,13 @@ import java.util.List; ...@@ -125,6 +133,13 @@ import java.util.List;
} }
/** /**
* Returns the encoding of output audio.
*/
public @C.Encoding int getEncoding() {
return encoding;
}
/**
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if * 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; ...@@ -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);
......
...@@ -57,8 +57,10 @@ extern "C" { ...@@ -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); ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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, ...@@ -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, ...@@ -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) {
......
...@@ -18,7 +18,7 @@ ...@@ -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"
......
...@@ -25,6 +25,14 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; ...@@ -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
......
...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -100,7 +108,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
Looper.myLooper().quit(); Looper.myLooper().quit();
} }
} }
} }
} }
...@@ -16,6 +16,7 @@ ...@@ -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 { ...@@ -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 {
......
...@@ -52,8 +52,8 @@ public final class ImaAdsMediaSource implements MediaSource { ...@@ -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 { ...@@ -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);
} }
......
...@@ -15,6 +15,7 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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()));
......
...@@ -18,7 +18,7 @@ ...@@ -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"
......
...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; ...@@ -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 { ...@@ -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 { ...@@ -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();
......
...@@ -76,6 +76,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -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 {
......
...@@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)" ...@@ -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 ...@@ -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 ##
......
...@@ -18,7 +18,7 @@ ...@@ -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"
......
...@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -132,7 +140,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
Looper.myLooper().quit(); Looper.myLooper().quit();
} }
} }
} }
} }
...@@ -18,7 +18,7 @@ ...@@ -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"
......
...@@ -27,155 +27,155 @@ track 0: ...@@ -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
...@@ -25,5 +25,9 @@ track 0: ...@@ -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
...@@ -25,5 +25,9 @@ track 0: ...@@ -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
...@@ -25,5 +25,9 @@ track 0: ...@@ -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
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:
......
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:
......
...@@ -23,9 +23,9 @@ import android.test.MoreAsserts; ...@@ -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 { ...@@ -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 { ...@@ -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);
}
} }
...@@ -16,9 +16,13 @@ ...@@ -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; ...@@ -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() {
return getExtractorFactory(0);
} }
private static ExtractorFactory getExtractorFactory(final int flags) { private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
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);
} }
}; };
} }
......
...@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.Timeline.Window; ...@@ -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 { ...@@ -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();
}
} }
} }
...@@ -23,7 +23,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; ...@@ -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 { ...@@ -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.
TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000); // Create all periods and assert period creation of child media sources has been called.
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); testRunner.assertPrepareAndReleaseAllPeriods();
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0));
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0));
} finally {
testRunner.release();
}
} }
/** /**
...@@ -234,7 +238,12 @@ public final class ConcatenatingMediaSourceTest extends TestCase { ...@@ -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) {
......
...@@ -21,7 +21,7 @@ import com.google.android.exoplayer2.Timeline; ...@@ -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; ...@@ -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 { ...@@ -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();
}
} }
} }
...@@ -17,11 +17,11 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -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 { ...@@ -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 { ...@@ -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);
}
} }
...@@ -127,8 +127,8 @@ public final class C { ...@@ -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 { ...@@ -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
...@@ -159,6 +159,10 @@ public final class C { ...@@ -159,6 +159,10 @@ public final class C {
*/ */
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
*/ */
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
...@@ -421,6 +425,11 @@ public final class C { ...@@ -421,6 +425,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.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
......
...@@ -51,9 +51,14 @@ public final class DefaultLoadControl implements LoadControl { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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) {
......
...@@ -1666,11 +1666,11 @@ import java.io.IOException; ...@@ -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; ...@@ -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; ...@@ -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.
......
...@@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { ...@@ -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 { ...@@ -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}
......
...@@ -368,6 +368,8 @@ public interface Player { ...@@ -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);
......
...@@ -91,6 +91,8 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
......
...@@ -15,6 +15,10 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
} }
......
...@@ -25,14 +25,13 @@ import java.nio.ByteBuffer; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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.
......
...@@ -51,8 +51,6 @@ import java.util.Arrays; ...@@ -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;
......
...@@ -51,6 +51,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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());
} }
......
...@@ -200,6 +200,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -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 ...@@ -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;
} }
......
...@@ -28,13 +28,24 @@ public interface SeekMap { ...@@ -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 { ...@@ -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 { ...@@ -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);
......
...@@ -15,6 +15,7 @@ ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
} }
} }
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.flv; ...@@ -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; ...@@ -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;
} }
......
...@@ -53,7 +53,7 @@ import java.util.Locale; ...@@ -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 {
......
...@@ -16,6 +16,7 @@ ...@@ -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; ...@@ -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
......
...@@ -38,7 +38,7 @@ import java.lang.annotation.Retention; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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());
} }
/** /**
......
...@@ -15,6 +15,7 @@ ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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);
} }
......
...@@ -15,6 +15,7 @@ ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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;
} }
} }
...@@ -46,13 +46,14 @@ import java.lang.annotation.Retention; ...@@ -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 { ...@@ -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.
...@@ -94,19 +95,14 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -94,19 +95,14 @@ public final class FragmentedMp4Extractor implements Extractor {
*/ */
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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
......
...@@ -41,7 +41,7 @@ import java.util.List; ...@@ -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 {
......
...@@ -186,7 +186,7 @@ import java.io.IOException; ...@@ -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));
......
...@@ -118,8 +118,9 @@ import java.util.List; ...@@ -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 {
......
...@@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -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 {
......
...@@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -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 {
......
...@@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -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 {
......
...@@ -39,7 +39,7 @@ public final class Ac3Reader implements ElementaryStreamReader { ...@@ -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;
......
...@@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -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 {
......
...@@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; ...@@ -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 {
......
...@@ -45,7 +45,7 @@ import java.util.Collections; ...@@ -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 {
......
...@@ -23,13 +23,14 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; ...@@ -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 { ...@@ -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 { ...@@ -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);
}
} }
...@@ -16,9 +16,11 @@ ...@@ -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; ...@@ -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; ...@@ -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;
} }
......
...@@ -31,6 +31,8 @@ import java.io.IOException; ...@@ -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; ...@@ -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;
} }
......
...@@ -20,6 +20,7 @@ import android.annotation.TargetApi; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
} }
......
...@@ -112,7 +112,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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;
......
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
/**
* Media period that wraps a media source and defers calling its
* {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link #createPeriod()}
* has been called. This is useful if you need to return a media period immediately but the media
* source that should create it is not yet prepared.
*/
public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
public final MediaSource mediaSource;
private final MediaPeriodId id;
private final Allocator allocator;
private MediaPeriod mediaPeriod;
private Callback callback;
private long preparePositionUs;
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
this.id = id;
this.allocator = allocator;
this.mediaSource = mediaSource;
}
/**
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()}
* to release the period.
*/
public void createPeriod() {
mediaPeriod = mediaSource.createPeriod(id, allocator);
if (callback != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
/**
* Releases the period.
*/
public void releasePeriod() {
if (mediaPeriod != null) {
mediaSource.releasePeriod(mediaPeriod);
}
}
@Override
public void prepare(Callback callback, long preparePositionUs) {
this.callback = callback;
this.preparePositionUs = preparePositionUs;
if (mediaPeriod != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
} else {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public TrackGroupArray getTrackGroups() {
return mediaPeriod.getTrackGroups();
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs);
}
@Override
public void discardBuffer(long positionUs) {
mediaPeriod.discardBuffer(positionUs);
}
@Override
public long readDiscontinuity() {
return mediaPeriod.readDiscontinuity();
}
@Override
public long getBufferedPositionUs() {
return mediaPeriod.getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
return mediaPeriod.seekToUs(positionUs);
}
@Override
public long getNextLoadPositionUs() {
return mediaPeriod.getNextLoadPositionUs();
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
callback.onContinueLoadingRequested(this);
}
// MediaPeriod.Callback implementation
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
callback.onPrepared(this);
}
}
...@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; ...@@ -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 ...@@ -758,111 +757,4 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
} }
/**
* Media period used for periods created from unprepared media sources exposed through
* {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes
* available.
*/
private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
public final MediaSource mediaSource;
private final MediaPeriodId id;
private final Allocator allocator;
private MediaPeriod mediaPeriod;
private Callback callback;
private long preparePositionUs;
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
this.id = id;
this.allocator = allocator;
this.mediaSource = mediaSource;
}
public void createPeriod() {
mediaPeriod = mediaSource.createPeriod(id, allocator);
if (callback != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
public void releasePeriod() {
if (mediaPeriod != null) {
mediaSource.releasePeriod(mediaPeriod);
}
}
@Override
public void prepare(Callback callback, long preparePositionUs) {
this.callback = callback;
this.preparePositionUs = preparePositionUs;
if (mediaPeriod != null) {
mediaPeriod.prepare(this, preparePositionUs);
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
} else {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public TrackGroupArray getTrackGroups() {
return mediaPeriod.getTrackGroups();
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
positionUs);
}
@Override
public void discardBuffer(long positionUs) {
mediaPeriod.discardBuffer(positionUs);
}
@Override
public long readDiscontinuity() {
return mediaPeriod.readDiscontinuity();
}
@Override
public long getBufferedPositionUs() {
return mediaPeriod.getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
return mediaPeriod.seekToUs(positionUs);
}
@Override
public long getNextLoadPositionUs() {
return mediaPeriod.getNextLoadPositionUs();
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
callback.onContinueLoadingRequested(this);
}
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
callback.onPrepared(this);
}
}
} }
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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);
} }
......
...@@ -35,7 +35,8 @@ import java.io.IOException; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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();
......
...@@ -15,13 +15,11 @@ ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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) {
......
...@@ -62,8 +62,11 @@ public final class TrackGroupArray { ...@@ -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 { ...@@ -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) {
......
...@@ -16,6 +16,7 @@ ...@@ -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;
...@@ -72,6 +73,15 @@ public interface AdsLoader { ...@@ -72,6 +73,15 @@ public interface AdsLoader {
} }
/** /**
* Sets the supported content types for ad media. Must be called before the first call to
* {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Subsequent calls may be ignored.
*
* @param contentTypes The supported content types for ad media. Each element must be one of
* {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}.
*/
void setSupportedContentTypes(@C.ContentType int... contentTypes);
/**
* Attaches a player that will play ads loaded using this instance. Called on the main thread by * Attaches a player that will play ads loaded using this instance. Called on the main thread by
* {@link AdsMediaSource}. * {@link AdsMediaSource}.
* *
......
...@@ -16,12 +16,11 @@ ...@@ -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;
......
...@@ -33,7 +33,6 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment