mirror of
https://github.com/androidx/media.git
synced 2025-05-21 23:56:32 +08:00
commit
2b20780482
@ -1,5 +1,3 @@
|
||||
*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION ***
|
||||
|
||||
Before filing an issue:
|
||||
-----------------------
|
||||
- Search existing issues, including issues that are closed.
|
||||
|
@ -68,7 +68,7 @@ individually.
|
||||
|
||||
In addition to library modules, ExoPlayer has multiple extension modules that
|
||||
depend on external libraries to provide additional functionality. Some
|
||||
extensions are available from JCenter, whereas others must be built manaully.
|
||||
extensions are available from JCenter, whereas others must be built manually.
|
||||
Browse the [extensions directory][] and their individual READMEs for details.
|
||||
|
||||
More information on the library and extension modules that are available from
|
||||
|
@ -1,5 +1,47 @@
|
||||
# Release notes #
|
||||
|
||||
### 2.6.1 ###
|
||||
|
||||
* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`,
|
||||
`DashMediaSource` and `SingleSampleMediaSource`.
|
||||
* Use the same listener `MediaSourceEventListener` for all MediaSource
|
||||
implementations.
|
||||
* IMA extension:
|
||||
* Support non-ExtractorMediaSource ads
|
||||
([#3302](https://github.com/google/ExoPlayer/issues/3302)).
|
||||
* Skip ads before the ad preceding the player's initial seek position
|
||||
([#3527](https://github.com/google/ExoPlayer/issues/3527)).
|
||||
* Fix ad loading when there is no preroll.
|
||||
* Add an option to turn off hiding controls during ad playback
|
||||
([#3532](https://github.com/google/ExoPlayer/issues/3532)).
|
||||
* Support specifying an ads response instead of an ad tag
|
||||
([#3548](https://github.com/google/ExoPlayer/issues/3548)).
|
||||
* Support overriding the ad load timeout
|
||||
([#3556](https://github.com/google/ExoPlayer/issues/3556)).
|
||||
* DASH: Support time zone designators in ISO8601 UTCTiming elements
|
||||
([#3524](https://github.com/google/ExoPlayer/issues/3524)).
|
||||
* Audio:
|
||||
* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option
|
||||
to use this with `FfmpegAudioRenderer`.
|
||||
* Add support for extracting 32-bit WAVE files
|
||||
([#3379](https://github.com/google/ExoPlayer/issues/3379)).
|
||||
* Support extraction and decoding of Dolby Atmos
|
||||
([#2465](https://github.com/google/ExoPlayer/issues/2465)).
|
||||
* Fix handling of playback parameter changes while paused when followed by a
|
||||
seek.
|
||||
* SimpleExoPlayer: Allow multiple audio and video debug listeners.
|
||||
* DefaultTrackSelector: Support undefined language text track selection when the
|
||||
preferred language is not available
|
||||
([#2980](https://github.com/google/ExoPlayer/issues/2980)).
|
||||
* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and
|
||||
to choose whether size or time constraints are prioritized.
|
||||
* Use surfaceless context for secure `DummySurface`, if available
|
||||
([#3558](https://github.com/google/ExoPlayer/issues/3558)).
|
||||
* FLV: Fix playback of live streams that do not contain an audio track
|
||||
([#3188](https://github.com/google/ExoPlayer/issues/3188)).
|
||||
* CEA-608: Fix handling of row count changes in roll-up mode
|
||||
([#3513](https://github.com/google/ExoPlayer/issues/3513)).
|
||||
|
||||
### 2.6.0 ###
|
||||
|
||||
* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0".
|
||||
@ -142,7 +184,7 @@
|
||||
easy and seamless way of incorporating display ads into ExoPlayer playbacks.
|
||||
You can read more about the IMA extension
|
||||
[here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea).
|
||||
* MediaSession extension: Provides an easy to to connect ExoPlayer with
|
||||
* MediaSession extension: Provides an easy way to connect ExoPlayer with
|
||||
MediaSessionCompat in the Android Support Library.
|
||||
* RTMP extension: An extension for playing streams over RTMP.
|
||||
* Build: Made it easier for application developers to depend on a local checkout
|
||||
|
@ -17,7 +17,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.0'
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
classpath 'com.novoda:bintray-release:0.5.0'
|
||||
}
|
||||
// Workaround for the following test coverage issue. Remove when fixed:
|
||||
|
@ -17,8 +17,8 @@ project.ext {
|
||||
// However, please note that the core media playback functionality provided
|
||||
// by the library requires API level 16 or greater.
|
||||
minSdkVersion = 14
|
||||
compileSdkVersion = 26
|
||||
targetSdkVersion = 26
|
||||
compileSdkVersion = 27
|
||||
targetSdkVersion = 27
|
||||
buildToolsVersion = '26.0.2'
|
||||
testSupportLibraryVersion = '0.5'
|
||||
supportLibraryVersion = '27.0.0'
|
||||
@ -28,7 +28,7 @@ project.ext {
|
||||
junitVersion = '4.12'
|
||||
truthVersion = '0.35'
|
||||
robolectricVersion = '3.4.2'
|
||||
releaseVersion = '2.6.0'
|
||||
releaseVersion = '2.6.1'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
@ -43,5 +43,7 @@ android {
|
||||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile project(modulePrefix + 'library-ui')
|
||||
compile project(modulePrefix + 'library-dash')
|
||||
compile project(modulePrefix + 'library-hls')
|
||||
compile project(modulePrefix + 'extension-ima')
|
||||
}
|
||||
|
@ -15,11 +15,11 @@
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.imademo"
|
||||
android:versionCode="2600"
|
||||
android:versionName="2.6.0">
|
||||
android:versionCode="2601"
|
||||
android:versionName="2.6.1">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
@ -17,15 +17,21 @@ package com.google.android.exoplayer2.imademo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
@ -37,12 +43,12 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* Manages the {@link ExoPlayer}, the IMA plugin and all video playback.
|
||||
*/
|
||||
/* package */ final class PlayerManager {
|
||||
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
|
||||
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
|
||||
|
||||
private final ImaAdsLoader adsLoader;
|
||||
private final DataSource.Factory manifestDataSourceFactory;
|
||||
private final DataSource.Factory mediaDataSourceFactory;
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
private long contentPosition;
|
||||
@ -50,6 +56,14 @@ import com.google.android.exoplayer2.util.Util;
|
||||
public PlayerManager(Context context) {
|
||||
String adTag = context.getString(R.string.ad_tag_url);
|
||||
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
|
||||
manifestDataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
|
||||
mediaDataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
context,
|
||||
Util.getUserAgent(context, context.getString(R.string.application_name)),
|
||||
new DefaultBandwidthMeter());
|
||||
}
|
||||
|
||||
public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) {
|
||||
@ -69,17 +83,21 @@ import com.google.android.exoplayer2.util.Util;
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
|
||||
Util.getUserAgent(context, context.getString(R.string.application_name)));
|
||||
|
||||
// Produces Extractor instances for parsing the content media (i.e. not the ad).
|
||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
|
||||
|
||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||
String contentUrl = context.getString(R.string.content_url);
|
||||
MediaSource contentMediaSource = new ExtractorMediaSource(
|
||||
Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null);
|
||||
MediaSource contentMediaSource =
|
||||
new ExtractorMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(Uri.parse(contentUrl));
|
||||
|
||||
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||
MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory,
|
||||
adsLoader, simpleExoPlayerView.getOverlayFrameLayout());
|
||||
MediaSource mediaSourceWithAds =
|
||||
new AdsMediaSource(
|
||||
contentMediaSource,
|
||||
/* adMediaSourceFactory= */ this,
|
||||
adsLoader,
|
||||
simpleExoPlayerView.getOverlayFrameLayout(),
|
||||
/* eventHandler= */ null,
|
||||
/* eventListener= */ null);
|
||||
|
||||
// Prepare the player with the source.
|
||||
player.seekTo(contentPosition);
|
||||
@ -103,4 +121,32 @@ import com.google.android.exoplayer2.util.Util;
|
||||
adsLoader.release();
|
||||
}
|
||||
|
||||
// AdsMediaSource.MediaSourceFactory implementation.
|
||||
|
||||
@Override
|
||||
public MediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
@ContentType int type = Util.inferContentType(uri);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||
manifestDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_SS:
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
}
|
||||
|
@ -16,14 +16,14 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2600"
|
||||
android:versionName="2.6.0">
|
||||
android:versionCode="2601"
|
||||
android:versionName="2.6.1">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-feature android:name="android.software.leanback" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="26"/>
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
|
||||
|
||||
<application
|
||||
android:label="@string/application_name"
|
||||
|
@ -16,14 +16,43 @@
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Utility methods for demo application.
|
||||
*/
|
||||
/*package*/ final class DemoUtil {
|
||||
/* package */ final class DemoUtil {
|
||||
|
||||
/**
|
||||
* Derives a DRM {@link UUID} from {@code drmScheme}.
|
||||
*
|
||||
* @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or
|
||||
* {@code "clearkey"}.
|
||||
* @return The derived {@link UUID}.
|
||||
* @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}.
|
||||
*/
|
||||
public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException {
|
||||
switch (Util.toLowerInvariant(drmScheme)) {
|
||||
case "widevine":
|
||||
return C.WIDEVINE_UUID;
|
||||
case "playready":
|
||||
return C.PLAYREADY_UUID;
|
||||
case "clearkey":
|
||||
return C.CLEARKEY_UUID;
|
||||
default:
|
||||
try {
|
||||
return UUID.fromString(drmScheme);
|
||||
} catch (RuntimeException e) {
|
||||
throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a track name for display.
|
||||
|
@ -38,10 +38,10 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
|
||||
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
@ -52,12 +52,15 @@ import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Logs player events using {@link Log}.
|
||||
*/
|
||||
/* package */ final class EventLogger implements Player.EventListener, MetadataOutput,
|
||||
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener {
|
||||
/** Logs player events using {@link Log}. */
|
||||
/* package */ final class EventLogger
|
||||
implements Player.EventListener,
|
||||
MetadataOutput,
|
||||
AudioRendererEventListener,
|
||||
VideoRendererEventListener,
|
||||
MediaSourceEventListener,
|
||||
AdsMediaSource.EventListener,
|
||||
DefaultDrmSessionManager.EventListener {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
private static final int MAX_TIMELINE_ITEM_LINES = 3;
|
||||
@ -320,19 +323,19 @@ import java.util.Locale;
|
||||
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
|
||||
}
|
||||
|
||||
// ExtractorMediaSource.EventListener
|
||||
// MediaSourceEventListener
|
||||
|
||||
@Override
|
||||
public void onLoadError(IOException error) {
|
||||
printInternalError("loadError", error);
|
||||
}
|
||||
|
||||
// AdaptiveMediaSourceEventListener
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
|
||||
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
|
||||
long mediaEndTimeMs, long elapsedRealtimeMs) {
|
||||
public void onLoadStarted(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@ -369,6 +372,23 @@ import java.util.Locale;
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// AdsMediaSource.EventListener
|
||||
|
||||
@Override
|
||||
public void onAdLoadError(IOException error) {
|
||||
printInternalError("adLoadError", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdClicked() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdTapped() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
|
||||
private void printInternalError(String type, Exception e) {
|
||||
@ -467,6 +487,9 @@ import java.util.Locale;
|
||||
}
|
||||
}
|
||||
|
||||
// Suppressing reference equality warning because the track group stored in the track selection
|
||||
// must point to the exact track group object to be considered part of it.
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
|
||||
int trackIndex) {
|
||||
return getTrackStatusString(selection != null && selection.getTrackGroup() == group
|
||||
|
@ -23,6 +23,7 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
@ -46,13 +47,13 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
@ -84,7 +85,7 @@ import java.util.UUID;
|
||||
public class PlayerActivity extends Activity implements OnClickListener,
|
||||
PlaybackControlView.VisibilityListener {
|
||||
|
||||
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URL = "drm_license_url";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
|
||||
public static final String DRM_MULTI_SESSION = "drm_multi_session";
|
||||
@ -99,6 +100,9 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
||||
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
|
||||
// For backwards compatibility.
|
||||
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||
|
||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||
static {
|
||||
@ -231,8 +235,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
||||
} else if (view.getParent() == debugRootView) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(),
|
||||
trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag());
|
||||
trackSelectionHelper.showSelectionDialog(
|
||||
this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -257,10 +261,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
||||
lastSeenTrackGroupArray = null;
|
||||
eventLogger = new EventLogger(trackSelector);
|
||||
|
||||
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
|
||||
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
|
||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
||||
if (drmSchemeUuid != null) {
|
||||
if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
|
||||
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
|
||||
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false);
|
||||
@ -269,6 +271,9 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
||||
errorStringId = R.string.error_drm_not_supported;
|
||||
} else {
|
||||
try {
|
||||
String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA
|
||||
: DRM_SCHEME_UUID_EXTRA;
|
||||
UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
|
||||
drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl,
|
||||
keyRequestPropertiesArray, multiSession);
|
||||
} catch (UnsupportedDrmException e) {
|
||||
@ -295,8 +300,8 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.addListener(eventLogger);
|
||||
player.addMetadataOutput(eventLogger);
|
||||
player.setAudioDebugListener(eventLogger);
|
||||
player.setVideoDebugListener(eventLogger);
|
||||
player.addAudioDebugListener(eventLogger);
|
||||
player.addVideoDebugListener(eventLogger);
|
||||
|
||||
simpleExoPlayerView.setPlayer(player);
|
||||
player.setPlayWhenReady(shouldAutoPlay);
|
||||
@ -329,7 +334,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
||||
}
|
||||
MediaSource[] mediaSources = new MediaSource[uris.length];
|
||||
for (int i = 0; i < uris.length; i++) {
|
||||
mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
|
||||
mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger);
|
||||
}
|
||||
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
|
||||
: new ConcatenatingMediaSource(mediaSources);
|
||||
@ -357,21 +362,30 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
|
||||
private MediaSource buildMediaSource(
|
||||
Uri uri,
|
||||
String overrideExtension,
|
||||
@Nullable Handler handler,
|
||||
@Nullable MediaSourceEventListener listener) {
|
||||
@ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
|
||||
: Util.inferContentType("." + overrideExtension);
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource(uri, buildDataSourceFactory(false),
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource(uri, buildDataSourceFactory(false),
|
||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger);
|
||||
return new DashMediaSource.Factory(
|
||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||
buildDataSourceFactory(false))
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
||||
buildDataSourceFactory(false))
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger);
|
||||
return new HlsMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
case C.TYPE_OTHER:
|
||||
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
|
||||
mainHandler, eventLogger);
|
||||
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
|
||||
.createMediaSource(uri, handler, listener);
|
||||
default: {
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
@ -458,7 +472,22 @@ public class PlayerActivity extends Activity implements OnClickListener,
|
||||
// The demo app has a non-null overlay frame layout.
|
||||
simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup);
|
||||
}
|
||||
return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup);
|
||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
||||
new AdsMediaSource.MediaSourceFactory() {
|
||||
@Override
|
||||
public MediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
return PlayerActivity.this.buildMediaSource(
|
||||
uri, /* overrideExtension= */ null, handler, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
|
||||
}
|
||||
};
|
||||
return new AdsMediaSource(
|
||||
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
|
||||
}
|
||||
|
||||
private void releaseAdsLoader() {
|
||||
|
@ -32,8 +32,8 @@ import android.widget.ExpandableListView;
|
||||
import android.widget.ExpandableListView.OnChildClickListener;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
@ -202,7 +202,11 @@ public class SampleChooserActivity extends Activity {
|
||||
break;
|
||||
case "drm_scheme":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
|
||||
drmUuid = getDrmUuid(reader.nextString());
|
||||
try {
|
||||
drmUuid = DemoUtil.getDrmUuid(reader.nextString());
|
||||
} catch (UnsupportedDrmException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
break;
|
||||
case "drm_license_url":
|
||||
Assertions.checkState(!insidePlaylist,
|
||||
@ -270,23 +274,6 @@ public class SampleChooserActivity extends Activity {
|
||||
return group;
|
||||
}
|
||||
|
||||
private UUID getDrmUuid(String typeString) throws ParserException {
|
||||
switch (Util.toLowerInvariant(typeString)) {
|
||||
case "widevine":
|
||||
return C.WIDEVINE_UUID;
|
||||
case "playready":
|
||||
return C.PLAYREADY_UUID;
|
||||
case "clearkey":
|
||||
return C.CLEARKEY_UUID;
|
||||
default:
|
||||
try {
|
||||
return UUID.fromString(typeString);
|
||||
} catch (RuntimeException e) {
|
||||
throw new ParserException("Unsupported drm type: " + typeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SampleAdapter extends BaseExpandableListAdapter {
|
||||
@ -393,7 +380,7 @@ public class SampleChooserActivity extends Activity {
|
||||
|
||||
public void updateIntent(Intent intent) {
|
||||
Assertions.checkNotNull(intent);
|
||||
intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString());
|
||||
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString());
|
||||
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
|
||||
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
|
||||
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession);
|
||||
|
@ -40,6 +40,7 @@ dependencies {
|
||||
compile files('libs/cronet_impl_common_java.jar')
|
||||
compile files('libs/cronet_impl_native_java.jar')
|
||||
androidTestCompile project(modulePrefix + 'library')
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
|
@ -18,7 +18,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer.ext.cronet">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
@ -19,10 +19,10 @@ import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.testutil.MockitoUtil;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
@ -46,9 +46,7 @@ public final class ByteArrayUploadDataProviderTest {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
System.setProperty("dexmaker.dexcache",
|
||||
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
|
||||
initMocks(this);
|
||||
MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this);
|
||||
byteBuffer = ByteBuffer.allocate(TEST_DATA.length);
|
||||
byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA);
|
||||
}
|
||||
|
@ -31,13 +31,13 @@ import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.ConditionVariable;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.MockitoUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
|
||||
@ -107,9 +107,7 @@ public final class CronetDataSourceTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
System.setProperty("dexmaker.dexcache",
|
||||
InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
|
||||
initMocks(this);
|
||||
MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this);
|
||||
dataSourceUnderTest = spy(
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
|
@ -21,6 +21,8 @@ import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.AudioSink;
|
||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
@ -41,6 +43,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
*/
|
||||
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
|
||||
|
||||
private final boolean enableFloatOutput;
|
||||
|
||||
private FfmpegDecoder decoder;
|
||||
|
||||
public FfmpegAudioRenderer() {
|
||||
@ -55,7 +59,23 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(eventHandler, eventListener, audioProcessors);
|
||||
this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioSink The sink to which audio will be output.
|
||||
* @param enableFloatOutput Whether to enable 32-bit float audio format, if supported on the
|
||||
* device/build and if the input format may have bit depth higher than 16-bit. When using
|
||||
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch
|
||||
* adjustment.
|
||||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink, boolean enableFloatOutput) {
|
||||
super(eventHandler, eventListener, null, false, audioSink);
|
||||
this.enableFloatOutput = enableFloatOutput;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -64,7 +84,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
String sampleMimeType = format.sampleMimeType;
|
||||
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) {
|
||||
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
@ -82,7 +102,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
throws FfmpegDecoderException {
|
||||
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
||||
format.sampleMimeType, format.initializationData);
|
||||
format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format));
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@ -90,8 +110,32 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
public Format getOutputFormat() {
|
||||
int channelCount = decoder.getChannelCount();
|
||||
int sampleRate = decoder.getSampleRate();
|
||||
@C.PcmEncoding int encoding = decoder.getEncoding();
|
||||
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
|
||||
Format.NO_VALUE, channelCount, sampleRate, C.ENCODING_PCM_16BIT, null, null, 0, null);
|
||||
Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null);
|
||||
}
|
||||
|
||||
private boolean isOutputSupported(Format inputFormat) {
|
||||
return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT);
|
||||
}
|
||||
|
||||
private boolean shouldUseFloatOutput(Format inputFormat) {
|
||||
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
|
||||
return false;
|
||||
}
|
||||
switch (inputFormat.sampleMimeType) {
|
||||
case MimeTypes.AUDIO_RAW:
|
||||
// For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit.
|
||||
return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT
|
||||
|| inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT
|
||||
|| inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT;
|
||||
case MimeTypes.AUDIO_AC3:
|
||||
// AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding.
|
||||
return false;
|
||||
default:
|
||||
// For all other formats, assume that it's worth using 32-bit float encoding.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||
@ -29,11 +30,15 @@ import java.util.List;
|
||||
/* package */ final class FfmpegDecoder extends
|
||||
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
||||
|
||||
// Space for 64 ms of 6 channel 48 kHz 16-bit PCM audio.
|
||||
private static final int OUTPUT_BUFFER_SIZE = 1536 * 6 * 2 * 2;
|
||||
// Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio.
|
||||
private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2;
|
||||
// Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio.
|
||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
||||
|
||||
private final String codecName;
|
||||
private final byte[] extraData;
|
||||
private final @C.Encoding int encoding;
|
||||
private final int outputBufferSize;
|
||||
|
||||
private long nativeContext; // May be reassigned on resetting the codec.
|
||||
private boolean hasOutputFormat;
|
||||
@ -41,14 +46,17 @@ import java.util.List;
|
||||
private volatile int sampleRate;
|
||||
|
||||
public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||
String mimeType, List<byte[]> initializationData) throws FfmpegDecoderException {
|
||||
String mimeType, List<byte[]> initializationData, boolean outputFloat)
|
||||
throws FfmpegDecoderException {
|
||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
|
||||
}
|
||||
codecName = FfmpegLibrary.getCodecName(mimeType);
|
||||
extraData = getExtraData(mimeType, initializationData);
|
||||
nativeContext = ffmpegInitialize(codecName, extraData);
|
||||
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
|
||||
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
|
||||
nativeContext = ffmpegInitialize(codecName, extraData, outputFloat);
|
||||
if (nativeContext == 0) {
|
||||
throw new FfmpegDecoderException("Initialization failed.");
|
||||
}
|
||||
@ -81,8 +89,8 @@ import java.util.List;
|
||||
}
|
||||
ByteBuffer inputData = inputBuffer.data;
|
||||
int inputSize = inputData.limit();
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, OUTPUT_BUFFER_SIZE);
|
||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, OUTPUT_BUFFER_SIZE);
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
||||
if (result < 0) {
|
||||
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
|
||||
}
|
||||
@ -124,6 +132,13 @@ import java.util.List;
|
||||
return sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoding of output audio.
|
||||
*/
|
||||
public @C.Encoding int getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
|
||||
* not required.
|
||||
@ -153,7 +168,7 @@ import java.util.List;
|
||||
}
|
||||
}
|
||||
|
||||
private native long ffmpegInitialize(String codecName, byte[] extraData);
|
||||
private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat);
|
||||
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
|
||||
ByteBuffer outputData, int outputSize);
|
||||
private native int ffmpegGetChannelCount(long context);
|
||||
|
@ -57,8 +57,10 @@ extern "C" {
|
||||
|
||||
#define ERROR_STRING_BUFFER_LENGTH 256
|
||||
|
||||
// Request a format corresponding to AudioFormat.ENCODING_PCM_16BIT.
|
||||
static const AVSampleFormat OUTPUT_FORMAT = AV_SAMPLE_FMT_S16;
|
||||
// Output format corresponding to AudioFormat.ENCODING_PCM_16BIT.
|
||||
static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
|
||||
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
|
||||
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
|
||||
|
||||
/**
|
||||
* Returns the AVCodec with the specified name, or NULL if it is not available.
|
||||
@ -71,7 +73,7 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName);
|
||||
* Returns the created context.
|
||||
*/
|
||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
||||
jbyteArray extraData);
|
||||
jbyteArray extraData, jboolean outputFloat);
|
||||
|
||||
/**
|
||||
* Decodes the packet into the output buffer, returning the number of bytes
|
||||
@ -107,13 +109,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
|
||||
return getCodecByName(env, codecName) != NULL;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData) {
|
||||
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
|
||||
jboolean outputFloat) {
|
||||
AVCodec *codec = getCodecByName(env, codecName);
|
||||
if (!codec) {
|
||||
LOGE("Codec not found.");
|
||||
return 0L;
|
||||
}
|
||||
return (jlong) createContext(env, codec, extraData);
|
||||
return (jlong) createContext(env, codec, extraData, outputFloat);
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
||||
@ -177,7 +180,8 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
|
||||
LOGE("Unexpected error finding codec %d.", codecId);
|
||||
return 0L;
|
||||
}
|
||||
return (jlong) createContext(env, codec, extraData);
|
||||
return (jlong) createContext(env, codec, extraData,
|
||||
context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
|
||||
}
|
||||
|
||||
avcodec_flush_buffers(context);
|
||||
@ -201,13 +205,14 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
|
||||
}
|
||||
|
||||
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
|
||||
jbyteArray extraData) {
|
||||
jbyteArray extraData, jboolean outputFloat) {
|
||||
AVCodecContext *context = avcodec_alloc_context3(codec);
|
||||
if (!context) {
|
||||
LOGE("Failed to allocate context.");
|
||||
return NULL;
|
||||
}
|
||||
context->request_sample_fmt = OUTPUT_FORMAT;
|
||||
context->request_sample_fmt =
|
||||
outputFloat ? OUTPUT_FORMAT_PCM_FLOAT : OUTPUT_FORMAT_PCM_16BIT;
|
||||
if (extraData) {
|
||||
jsize size = env->GetArrayLength(extraData);
|
||||
context->extradata_size = size;
|
||||
@ -275,7 +280,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||
av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
|
||||
av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0);
|
||||
av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0);
|
||||
av_opt_set_int(resampleContext, "out_sample_fmt", OUTPUT_FORMAT, 0);
|
||||
// The output format is always the requested format.
|
||||
av_opt_set_int(resampleContext, "out_sample_fmt",
|
||||
context->request_sample_fmt, 0);
|
||||
result = avresample_open(resampleContext);
|
||||
if (result < 0) {
|
||||
logError("avresample_open", result);
|
||||
@ -285,7 +292,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||
context->opaque = resampleContext;
|
||||
}
|
||||
int inSampleSize = av_get_bytes_per_sample(sampleFormat);
|
||||
int outSampleSize = av_get_bytes_per_sample(OUTPUT_FORMAT);
|
||||
int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
|
||||
int outSamples = avresample_get_out_samples(resampleContext, sampleCount);
|
||||
int bufferOutSize = outSampleSize * channelCount * outSamples;
|
||||
if (outSize + bufferOutSize > outputSize) {
|
||||
|
@ -18,7 +18,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.flac.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
@ -25,6 +25,14 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
*/
|
||||
public class FlacExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
fail("Flac library not available.");
|
||||
}
|
||||
}
|
||||
|
||||
public void testSample() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(new ExtractorFactory() {
|
||||
@Override
|
||||
|
@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
@ -36,6 +37,14 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka";
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
fail("Flac library not available.");
|
||||
}
|
||||
}
|
||||
|
||||
public void testBasicPlayback() throws ExoPlaybackException {
|
||||
playUri(BEAR_FLAC_URI);
|
||||
}
|
||||
@ -76,12 +85,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
|
||||
player.addListener(this);
|
||||
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
|
||||
uri,
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
|
||||
MatroskaExtractor.FACTORY,
|
||||
null,
|
||||
null);
|
||||
MediaSource mediaSource =
|
||||
new ExtractorMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"))
|
||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
player.prepare(mediaSource);
|
||||
player.setPlayWhenReady(true);
|
||||
Looper.loop();
|
||||
@ -100,7 +108,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import android.os.Handler;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
@ -52,6 +53,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
if (!FlacLibrary.isAvailable()
|
||||
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
|
@ -19,6 +19,7 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebView;
|
||||
@ -49,10 +50,14 @@ import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -66,6 +71,75 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
|
||||
}
|
||||
|
||||
/** Builder for {@link ImaAdsLoader}. */
|
||||
public static final class Builder {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private @Nullable ImaSdkSettings imaSdkSettings;
|
||||
private long vastLoadTimeoutMs;
|
||||
|
||||
/**
|
||||
* Creates a new builder for {@link ImaAdsLoader}.
|
||||
*
|
||||
* @param context The context;
|
||||
*/
|
||||
public Builder(Context context) {
|
||||
this.context = Assertions.checkNotNull(context);
|
||||
vastLoadTimeoutMs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the IMA SDK settings. The provided settings instance's player type and version fields
|
||||
* may be overwritten.
|
||||
*
|
||||
* <p>If this method is not called the default settings will be used.
|
||||
*
|
||||
* @param imaSdkSettings The {@link ImaSdkSettings}.
|
||||
* @return This builder, for convenience.
|
||||
*/
|
||||
public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {
|
||||
this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the VAST load timeout, in milliseconds.
|
||||
*
|
||||
* @param vastLoadTimeoutMs The VAST load timeout, in milliseconds.
|
||||
* @return This builder, for convenience.
|
||||
* @see AdsRequest#setVastLoadTimeout(float)
|
||||
*/
|
||||
public Builder setVastLoadTimeoutMs(long vastLoadTimeoutMs) {
|
||||
Assertions.checkArgument(vastLoadTimeoutMs >= 0);
|
||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link ImaAdsLoader} for the specified ad tag.
|
||||
*
|
||||
* @param adTagUri The URI of a compatible ad tag to load. See
|
||||
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
|
||||
* information on compatible ad tags.
|
||||
* @return The new {@link ImaAdsLoader}.
|
||||
*/
|
||||
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
|
||||
return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response.
|
||||
*
|
||||
* @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of
|
||||
* making a request via an ad tag URL.
|
||||
* @return The new {@link ImaAdsLoader}.
|
||||
*/
|
||||
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
|
||||
return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "ImaAdsLoader";
|
||||
|
||||
@ -77,6 +151,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
|
||||
private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION;
|
||||
|
||||
/** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */
|
||||
private static final long IMA_DURATION_UNSET = -1L;
|
||||
|
||||
/**
|
||||
* Threshold before the end of content at which IMA is notified that content is complete if the
|
||||
* player buffers, in milliseconds.
|
||||
@ -91,9 +168,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:"
|
||||
+ "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}";
|
||||
|
||||
/**
|
||||
* The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}.
|
||||
*/
|
||||
/** The state of ad playback. */
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED})
|
||||
private @interface ImaAdState {}
|
||||
@ -110,13 +185,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
*/
|
||||
private static final int IMA_AD_STATE_PAUSED = 2;
|
||||
|
||||
private final Uri adTagUri;
|
||||
private final @Nullable Uri adTagUri;
|
||||
private final @Nullable String adsResponse;
|
||||
private final long vastLoadTimeoutMs;
|
||||
private final Timeline.Period period;
|
||||
private final List<VideoAdPlayerCallback> adCallbacks;
|
||||
private final ImaSdkFactory imaSdkFactory;
|
||||
private final AdDisplayContainer adDisplayContainer;
|
||||
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
|
||||
|
||||
private Object pendingAdRequestContext;
|
||||
private List<String> supportedMimeTypes;
|
||||
private EventListener eventListener;
|
||||
private Player player;
|
||||
private ViewGroup adUiViewGroup;
|
||||
@ -124,8 +203,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
private VideoProgressUpdate lastAdProgress;
|
||||
|
||||
private AdsManager adsManager;
|
||||
private AdErrorEvent pendingAdErrorEvent;
|
||||
private Timeline timeline;
|
||||
private long contentDurationMs;
|
||||
private int podIndexOffset;
|
||||
private AdPlaybackState adPlaybackState;
|
||||
|
||||
// Fields tracking IMA's state.
|
||||
@ -138,9 +219,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
* Whether IMA has sent an ad event to pause content since the last resume content event.
|
||||
*/
|
||||
private boolean imaPausedContent;
|
||||
/**
|
||||
* The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}.
|
||||
*/
|
||||
/** The current ad playback state. */
|
||||
private @ImaAdState int imaAdState;
|
||||
/**
|
||||
* Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been
|
||||
@ -179,21 +258,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
* Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
|
||||
*/
|
||||
private boolean sentPendingContentPositionMs;
|
||||
/**
|
||||
* Whether {@link #release()} has been called.
|
||||
*/
|
||||
private boolean released;
|
||||
|
||||
/**
|
||||
* Creates a new IMA ads loader.
|
||||
*
|
||||
* <p>If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
|
||||
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
|
||||
* more information.
|
||||
*/
|
||||
public ImaAdsLoader(Context context, Uri adTagUri) {
|
||||
this(context, adTagUri, null);
|
||||
this(context, adTagUri, null, null, C.TIME_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -205,9 +282,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
* more information.
|
||||
* @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to
|
||||
* use the default settings. If set, the player type and version fields may be overwritten.
|
||||
* @deprecated Use {@link ImaAdsLoader.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) {
|
||||
this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET);
|
||||
}
|
||||
|
||||
private ImaAdsLoader(
|
||||
Context context,
|
||||
@Nullable Uri adTagUri,
|
||||
@Nullable ImaSdkSettings imaSdkSettings,
|
||||
@Nullable String adsResponse,
|
||||
long vastLoadTimeoutMs) {
|
||||
Assertions.checkArgument(adTagUri != null || adsResponse != null);
|
||||
this.adTagUri = adTagUri;
|
||||
this.adsResponse = adsResponse;
|
||||
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
|
||||
period = new Timeline.Period();
|
||||
adCallbacks = new ArrayList<>(1);
|
||||
imaSdkFactory = ImaSdkFactory.getInstance();
|
||||
@ -236,8 +327,58 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
return adsLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests ads, if they have not already been requested. Must be called on the main thread.
|
||||
*
|
||||
* <p>Ads will be requested automatically when the player is prepared if this method has not been
|
||||
* called, so it is only necessary to call this method if you want to request ads before preparing
|
||||
* the player
|
||||
*
|
||||
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
|
||||
*/
|
||||
public void requestAds(ViewGroup adUiViewGroup) {
|
||||
if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) {
|
||||
// Ads have already been requested.
|
||||
return;
|
||||
}
|
||||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
||||
pendingAdRequestContext = new Object();
|
||||
AdsRequest request = imaSdkFactory.createAdsRequest();
|
||||
if (adTagUri != null) {
|
||||
request.setAdTagUrl(adTagUri.toString());
|
||||
} else /* adsResponse != null */ {
|
||||
request.setAdsResponse(adsResponse);
|
||||
}
|
||||
if (vastLoadTimeoutMs != C.TIME_UNSET) {
|
||||
request.setVastLoadTimeout(vastLoadTimeoutMs);
|
||||
}
|
||||
request.setAdDisplayContainer(adDisplayContainer);
|
||||
request.setContentProgressProvider(this);
|
||||
request.setUserRequestContext(pendingAdRequestContext);
|
||||
adsLoader.requestAds(request);
|
||||
}
|
||||
|
||||
// AdsLoader implementation.
|
||||
|
||||
@Override
|
||||
public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
|
||||
List<String> supportedMimeTypes = new ArrayList<>();
|
||||
for (@C.ContentType int contentType : contentTypes) {
|
||||
if (contentType == C.TYPE_DASH) {
|
||||
supportedMimeTypes.add(MimeTypes.APPLICATION_MPD);
|
||||
} else if (contentType == C.TYPE_HLS) {
|
||||
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
|
||||
} else if (contentType == C.TYPE_OTHER) {
|
||||
supportedMimeTypes.addAll(Arrays.asList(
|
||||
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
|
||||
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
|
||||
} else if (contentType == C.TYPE_SS) {
|
||||
// IMA does not support SmoothStreaming ad media.
|
||||
}
|
||||
}
|
||||
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) {
|
||||
this.player = player;
|
||||
@ -247,13 +388,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
lastContentProgress = null;
|
||||
adDisplayContainer.setAdContainer(adUiViewGroup);
|
||||
player.addListener(this);
|
||||
maybeNotifyAdError();
|
||||
if (adPlaybackState != null) {
|
||||
// Pass the ad playback state to the player, and resume ads if necessary.
|
||||
eventListener.onAdPlaybackState(adPlaybackState.copy());
|
||||
if (imaPausedContent && player.getPlayWhenReady()) {
|
||||
adsManager.resume();
|
||||
}
|
||||
} else if (adsManager != null) {
|
||||
// Ads have loaded but the ads manager is not initialized.
|
||||
startAdPlayback();
|
||||
} else {
|
||||
requestAds();
|
||||
// Ads haven't loaded yet, so request them.
|
||||
requestAds(adUiViewGroup);
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,7 +420,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
released = true;
|
||||
pendingAdRequestContext = null;
|
||||
if (adsManager != null) {
|
||||
adsManager.destroy();
|
||||
adsManager = null;
|
||||
@ -285,30 +432,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
@Override
|
||||
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
|
||||
AdsManager adsManager = adsManagerLoadedEvent.getAdsManager();
|
||||
if (released) {
|
||||
if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) {
|
||||
adsManager.destroy();
|
||||
return;
|
||||
}
|
||||
pendingAdRequestContext = null;
|
||||
this.adsManager = adsManager;
|
||||
adsManager.addAdErrorListener(this);
|
||||
adsManager.addAdEventListener(this);
|
||||
if (ENABLE_PRELOADING) {
|
||||
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
|
||||
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
|
||||
adsRenderingSettings.setEnablePreloading(true);
|
||||
adsManager.init(adsRenderingSettings);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Initialized with preloading");
|
||||
}
|
||||
} else {
|
||||
adsManager.init();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Initialized without preloading");
|
||||
}
|
||||
if (player != null) {
|
||||
// If a player is attached already, start playback immediately.
|
||||
startAdPlayback();
|
||||
}
|
||||
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
|
||||
adPlaybackState = new AdPlaybackState(adGroupTimesUs);
|
||||
updateAdPlaybackState();
|
||||
}
|
||||
|
||||
// AdEvent.AdEventListener implementation.
|
||||
@ -335,15 +470,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
// The ad position is not always accurate when using preloading. See [Internal: b/62613240].
|
||||
AdPodInfo adPodInfo = ad.getAdPodInfo();
|
||||
int podIndex = adPodInfo.getPodIndex();
|
||||
adGroupIndex = podIndex == -1 ? adPlaybackState.adGroupCount - 1 : podIndex;
|
||||
adGroupIndex =
|
||||
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
|
||||
int adPosition = adPodInfo.getAdPosition();
|
||||
int adCountInAdGroup = adPodInfo.getTotalAds();
|
||||
int adCount = adPodInfo.getTotalAds();
|
||||
adsManager.start();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group "
|
||||
+ adGroupIndex);
|
||||
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
|
||||
}
|
||||
adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup);
|
||||
adPlaybackState.setAdCount(adGroupIndex, adCount);
|
||||
updateAdPlaybackState();
|
||||
break;
|
||||
case CONTENT_PAUSE_REQUESTED:
|
||||
@ -386,19 +521,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
Log.d(TAG, "onAdError " + adErrorEvent);
|
||||
}
|
||||
if (adsManager == null) {
|
||||
// No ads were loaded, so allow playback to start without any ads.
|
||||
pendingAdRequestContext = null;
|
||||
adPlaybackState = new AdPlaybackState(new long[0]);
|
||||
updateAdPlaybackState();
|
||||
}
|
||||
if (eventListener != null) {
|
||||
IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError());
|
||||
eventListener.onLoadError(exception);
|
||||
if (pendingAdErrorEvent == null) {
|
||||
pendingAdErrorEvent = adErrorEvent;
|
||||
}
|
||||
maybeNotifyAdError();
|
||||
}
|
||||
|
||||
// ContentProgressProvider implementation.
|
||||
|
||||
@Override
|
||||
public VideoProgressUpdate getContentProgress() {
|
||||
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
|
||||
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
|
||||
if (player == null) {
|
||||
return lastContentProgress;
|
||||
} else if (pendingContentPositionMs != C.TIME_UNSET) {
|
||||
@ -408,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
|
||||
long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
|
||||
return new VideoProgressUpdate(fakePositionMs, contentDurationMs);
|
||||
} else if (playingAd || contentDurationMs == C.TIME_UNSET) {
|
||||
} else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) {
|
||||
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||
} else {
|
||||
return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
|
||||
@ -421,7 +560,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
public VideoProgressUpdate getAdProgress() {
|
||||
if (player == null) {
|
||||
return lastAdProgress;
|
||||
} else if (!playingAd) {
|
||||
} else if (imaAdState == IMA_AD_STATE_NONE) {
|
||||
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||
} else {
|
||||
long adDuration = player.getDuration();
|
||||
@ -563,6 +702,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onEnded();
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -604,26 +746,74 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void requestAds() {
|
||||
AdsRequest request = imaSdkFactory.createAdsRequest();
|
||||
request.setAdTagUrl(adTagUri.toString());
|
||||
request.setAdDisplayContainer(adDisplayContainer);
|
||||
request.setContentProgressProvider(this);
|
||||
adsLoader.requestAds(request);
|
||||
private void startAdPlayback() {
|
||||
ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
|
||||
AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings();
|
||||
adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING);
|
||||
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
|
||||
|
||||
// Set up the ad playback state, skipping ads based on the start position as required.
|
||||
pendingContentPositionMs = player.getCurrentPosition();
|
||||
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
|
||||
adPlaybackState = new AdPlaybackState(adGroupTimesUs);
|
||||
int adGroupIndexForPosition =
|
||||
getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs));
|
||||
if (adGroupIndexForPosition == 0) {
|
||||
podIndexOffset = 0;
|
||||
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
|
||||
pendingContentPositionMs = C.TIME_UNSET;
|
||||
// There is no preroll and midroll pod indices start at 1.
|
||||
podIndexOffset = -1;
|
||||
} else /* adGroupIndexForPosition > 0 */ {
|
||||
// Skip ad groups before the one at or immediately before the playback position.
|
||||
for (int i = 0; i < adGroupIndexForPosition; i++) {
|
||||
adPlaybackState.playedAdGroup(i);
|
||||
}
|
||||
// Play ads after the midpoint between the ad to play and the one before it, to avoid issues
|
||||
// with rounding one of the two ad times.
|
||||
long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition];
|
||||
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
|
||||
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
|
||||
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
|
||||
|
||||
// We're removing one or more ads, which means that the earliest ad (if any) will be a
|
||||
// midroll/postroll. Midroll pod indices start at 1.
|
||||
podIndexOffset = adGroupIndexForPosition - 1;
|
||||
}
|
||||
|
||||
// Start ad playback.
|
||||
adsManager.init(adsRenderingSettings);
|
||||
updateAdPlaybackState();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeNotifyAdError() {
|
||||
if (eventListener != null && pendingAdErrorEvent != null) {
|
||||
IOException exception =
|
||||
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError());
|
||||
eventListener.onLoadError(exception);
|
||||
pendingAdErrorEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateImaStateForPlayerState() {
|
||||
boolean wasPlayingAd = playingAd;
|
||||
int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup;
|
||||
playingAd = player.isPlayingAd();
|
||||
playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
|
||||
if (!sentContentComplete) {
|
||||
boolean adFinished = (wasPlayingAd && !playingAd)
|
||||
|| playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup();
|
||||
boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup;
|
||||
if (adFinished) {
|
||||
// IMA is waiting for the ad playback to finish so invoke the callback now.
|
||||
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onEnded();
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
|
||||
}
|
||||
}
|
||||
if (!wasPlayingAd && playingAd) {
|
||||
int adGroupIndex = player.getCurrentAdGroupIndex();
|
||||
@ -635,7 +825,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
}
|
||||
}
|
||||
}
|
||||
playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
private void resumeContentInternal() {
|
||||
@ -717,4 +906,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
|
||||
return adGroupTimesUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the ad group that should be played before playing the content at {@code
|
||||
* playbackPositionUs} when starting playback for the first time. This is the latest ad group at
|
||||
* or before the specified playback position. If the first ad is after the playback position,
|
||||
* returns {@link C#INDEX_UNSET}.
|
||||
*/
|
||||
private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) {
|
||||
for (int i = 0; i < adGroupTimesUs.length; i++) {
|
||||
long adGroupTimeUs = adGroupTimesUs[i];
|
||||
// A postroll ad is after any position in the content.
|
||||
if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) {
|
||||
return i == 0 ? C.INDEX_UNSET : (i - 1);
|
||||
}
|
||||
}
|
||||
return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1);
|
||||
}
|
||||
}
|
||||
|
@ -52,8 +52,8 @@ public final class ImaAdsMediaSource implements MediaSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new source that inserts ads linearly with the content specified by
|
||||
* {@code contentMediaSource}.
|
||||
* Constructs a new source that inserts ads linearly with the content specified by {@code
|
||||
* contentMediaSource}.
|
||||
*
|
||||
* @param contentMediaSource The {@link MediaSource} providing the content to play.
|
||||
* @param dataSourceFactory Factory for data sources used to load ad media.
|
||||
@ -62,9 +62,13 @@ public final class ImaAdsMediaSource implements MediaSource {
|
||||
* @param eventHandler A handler for events. May be null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
|
||||
ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler,
|
||||
@Nullable AdsMediaSource.AdsListener eventListener) {
|
||||
public ImaAdsMediaSource(
|
||||
MediaSource contentMediaSource,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
ImaAdsLoader imaAdsLoader,
|
||||
ViewGroup adUiViewGroup,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AdsMediaSource.EventListener eventListener) {
|
||||
adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader,
|
||||
adUiViewGroup, eventHandler, eventListener);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.mediasession;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@ -330,6 +331,7 @@ public final class MediaSessionConnector {
|
||||
private final ExoPlayerEventListener exoPlayerEventListener;
|
||||
private final MediaSessionCallback mediaSessionCallback;
|
||||
private final PlaybackController playbackController;
|
||||
private final String metadataExtrasPrefix;
|
||||
private final Map<String, CommandReceiver> commandMap;
|
||||
|
||||
private Player player;
|
||||
@ -356,15 +358,15 @@ public final class MediaSessionConnector {
|
||||
/**
|
||||
* Creates an instance. Must be called on the same thread that is used to construct the player
|
||||
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
|
||||
* <p>
|
||||
* Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}.
|
||||
*
|
||||
* <p>Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}.
|
||||
*
|
||||
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||
* @param playbackController A {@link PlaybackController} for handling playback actions.
|
||||
*/
|
||||
public MediaSessionConnector(MediaSessionCompat mediaSession,
|
||||
PlaybackController playbackController) {
|
||||
this(mediaSession, playbackController, true);
|
||||
public MediaSessionConnector(
|
||||
MediaSessionCompat mediaSession, PlaybackController playbackController) {
|
||||
this(mediaSession, playbackController, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -372,17 +374,23 @@ public final class MediaSessionConnector {
|
||||
* instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}.
|
||||
*
|
||||
* @param mediaSession The {@link MediaSessionCompat} to connect to.
|
||||
* @param playbackController A {@link PlaybackController} for handling playback actions, or
|
||||
* {@code null} if the connector should handle playback actions directly.
|
||||
* @param playbackController A {@link PlaybackController} for handling playback actions, or {@code
|
||||
* null} if the connector should handle playback actions directly.
|
||||
* @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If
|
||||
* {@code false}, you need to maintain the metadata of the media session yourself (provide at
|
||||
* least the duration to allow clients to show a progress bar).
|
||||
* @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active
|
||||
* queue item to the session metadata.
|
||||
*/
|
||||
public MediaSessionConnector(MediaSessionCompat mediaSession,
|
||||
PlaybackController playbackController, boolean doMaintainMetadata) {
|
||||
public MediaSessionConnector(
|
||||
MediaSessionCompat mediaSession,
|
||||
PlaybackController playbackController,
|
||||
boolean doMaintainMetadata,
|
||||
@Nullable String metadataExtrasPrefix) {
|
||||
this.mediaSession = mediaSession;
|
||||
this.playbackController = playbackController != null ? playbackController
|
||||
: new DefaultPlaybackController();
|
||||
this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : "";
|
||||
this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
|
||||
: Looper.getMainLooper());
|
||||
this.doMaintainMetadata = doMaintainMetadata;
|
||||
@ -553,6 +561,25 @@ public final class MediaSessionConnector {
|
||||
MediaSessionCompat.QueueItem queueItem = queue.get(i);
|
||||
if (queueItem.getQueueId() == activeQueueItemId) {
|
||||
MediaDescriptionCompat description = queueItem.getDescription();
|
||||
Bundle extras = description.getExtras();
|
||||
if (extras != null) {
|
||||
for (String key : extras.keySet()) {
|
||||
Object value = extras.get(key);
|
||||
if (value instanceof String) {
|
||||
builder.putString(metadataExtrasPrefix + key, (String) value);
|
||||
} else if (value instanceof CharSequence) {
|
||||
builder.putText(metadataExtrasPrefix + key, (CharSequence) value);
|
||||
} else if (value instanceof Long) {
|
||||
builder.putLong(metadataExtrasPrefix + key, (Long) value);
|
||||
} else if (value instanceof Integer) {
|
||||
builder.putLong(metadataExtrasPrefix + key, (Integer) value);
|
||||
} else if (value instanceof Bitmap) {
|
||||
builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value);
|
||||
} else if (value instanceof RatingCompat) {
|
||||
builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (description.getTitle() != null) {
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
|
||||
String.valueOf(description.getTitle()));
|
||||
|
@ -18,7 +18,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.opus.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
@ -36,6 +37,14 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm";
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
if (!OpusLibrary.isAvailable()) {
|
||||
fail("Opus library not available.");
|
||||
}
|
||||
}
|
||||
|
||||
public void testBasicPlayback() throws ExoPlaybackException {
|
||||
playUri(BEAR_OPUS_URI);
|
||||
}
|
||||
@ -76,12 +85,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
|
||||
player.addListener(this);
|
||||
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
|
||||
uri,
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"),
|
||||
MatroskaExtractor.FACTORY,
|
||||
null,
|
||||
null);
|
||||
MediaSource mediaSource =
|
||||
new ExtractorMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"))
|
||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
player.prepare(mediaSource);
|
||||
player.setPlayWhenReady(true);
|
||||
Looper.loop();
|
||||
|
@ -76,6 +76,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
if (!OpusLibrary.isAvailable()
|
||||
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
|
@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)"
|
||||
VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
|
||||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in an environment variable:
|
||||
* Download the [Android NDK][] and set its location in an environment variable.
|
||||
Only versions up to NDK 15c are supported currently (see [#3520][]).
|
||||
|
||||
```
|
||||
NDK_PATH="<path to Android NDK>"
|
||||
@ -70,6 +71,7 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||
|
||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||
[#3520]: https://github.com/google/ExoPlayer/issues/3520
|
||||
|
||||
## Notes ##
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.ext.vp9.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Renderer;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
@ -42,6 +43,14 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String TAG = "VpxPlaybackTest";
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
if (!VpxLibrary.isAvailable()) {
|
||||
fail("Vpx library not available.");
|
||||
}
|
||||
}
|
||||
|
||||
public void testBasicPlayback() throws ExoPlaybackException {
|
||||
playUri(BEAR_URI);
|
||||
}
|
||||
@ -105,12 +114,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
||||
player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector);
|
||||
player.addListener(this);
|
||||
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
|
||||
uri,
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"),
|
||||
MatroskaExtractor.FACTORY,
|
||||
null,
|
||||
null);
|
||||
MediaSource mediaSource =
|
||||
new ExtractorMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
|
||||
.setExtractorsFactory(MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer,
|
||||
LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER,
|
||||
new VpxVideoSurfaceView(context)));
|
||||
@ -132,7 +140,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
||||
Looper.myLooper().quit();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.google.android.exoplayer2.core.test">
|
||||
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26"/>
|
||||
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
|
||||
|
||||
<application android:debuggable="true"
|
||||
android:allowBackup="false"
|
||||
|
@ -25,309 +25,313 @@ track 0:
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
sample count = 76
|
||||
sample count = 77
|
||||
sample 0:
|
||||
time = 945782
|
||||
time = 928568
|
||||
flags = 1
|
||||
data = length 384, hash F7E344F4
|
||||
sample 1:
|
||||
time = 952568
|
||||
flags = 1
|
||||
data = length 384, hash 14EF6AFD
|
||||
sample 1:
|
||||
time = 969782
|
||||
sample 2:
|
||||
time = 976568
|
||||
flags = 1
|
||||
data = length 384, hash 61C9B92C
|
||||
sample 2:
|
||||
time = 993782
|
||||
sample 3:
|
||||
time = 1000568
|
||||
flags = 1
|
||||
data = length 384, hash ABE1368
|
||||
sample 3:
|
||||
time = 1017782
|
||||
sample 4:
|
||||
time = 1024568
|
||||
flags = 1
|
||||
data = length 384, hash 6A3B8547
|
||||
sample 4:
|
||||
time = 1041782
|
||||
sample 5:
|
||||
time = 1048568
|
||||
flags = 1
|
||||
data = length 384, hash 30E905FA
|
||||
sample 5:
|
||||
time = 1065782
|
||||
sample 6:
|
||||
time = 1072568
|
||||
flags = 1
|
||||
data = length 384, hash 21A267CD
|
||||
sample 6:
|
||||
time = 1089782
|
||||
sample 7:
|
||||
time = 1096568
|
||||
flags = 1
|
||||
data = length 384, hash D96A2651
|
||||
sample 7:
|
||||
time = 1113782
|
||||
sample 8:
|
||||
time = 1120568
|
||||
flags = 1
|
||||
data = length 384, hash 72340177
|
||||
sample 8:
|
||||
time = 1137782
|
||||
sample 9:
|
||||
time = 1144568
|
||||
flags = 1
|
||||
data = length 384, hash 9345E744
|
||||
sample 9:
|
||||
time = 1161782
|
||||
sample 10:
|
||||
time = 1168568
|
||||
flags = 1
|
||||
data = length 384, hash FDE39E3A
|
||||
sample 10:
|
||||
time = 1185782
|
||||
sample 11:
|
||||
time = 1192568
|
||||
flags = 1
|
||||
data = length 384, hash F0B7465
|
||||
sample 11:
|
||||
time = 1209782
|
||||
sample 12:
|
||||
time = 1216568
|
||||
flags = 1
|
||||
data = length 384, hash 3693AB86
|
||||
sample 12:
|
||||
time = 1233782
|
||||
sample 13:
|
||||
time = 1240568
|
||||
flags = 1
|
||||
data = length 384, hash F39719B1
|
||||
sample 13:
|
||||
time = 1257782
|
||||
sample 14:
|
||||
time = 1264568
|
||||
flags = 1
|
||||
data = length 384, hash DA3958DC
|
||||
sample 14:
|
||||
time = 1281782
|
||||
sample 15:
|
||||
time = 1288568
|
||||
flags = 1
|
||||
data = length 384, hash FDC7599F
|
||||
sample 15:
|
||||
time = 1305782
|
||||
sample 16:
|
||||
time = 1312568
|
||||
flags = 1
|
||||
data = length 384, hash AEFF8471
|
||||
sample 16:
|
||||
time = 1329782
|
||||
sample 17:
|
||||
time = 1336568
|
||||
flags = 1
|
||||
data = length 384, hash 89C92C19
|
||||
sample 17:
|
||||
time = 1353782
|
||||
sample 18:
|
||||
time = 1360568
|
||||
flags = 1
|
||||
data = length 384, hash 5C786A4B
|
||||
sample 18:
|
||||
time = 1377782
|
||||
sample 19:
|
||||
time = 1384568
|
||||
flags = 1
|
||||
data = length 384, hash 5ACA8B
|
||||
sample 19:
|
||||
time = 1401782
|
||||
sample 20:
|
||||
time = 1408568
|
||||
flags = 1
|
||||
data = length 384, hash 7755974C
|
||||
sample 20:
|
||||
time = 1425782
|
||||
sample 21:
|
||||
time = 1432568
|
||||
flags = 1
|
||||
data = length 384, hash 3934B73C
|
||||
sample 21:
|
||||
time = 1449782
|
||||
sample 22:
|
||||
time = 1456568
|
||||
flags = 1
|
||||
data = length 384, hash DDD70A2F
|
||||
sample 22:
|
||||
time = 1473782
|
||||
sample 23:
|
||||
time = 1480568
|
||||
flags = 1
|
||||
data = length 384, hash 8FACE2EF
|
||||
sample 23:
|
||||
time = 1497782
|
||||
sample 24:
|
||||
time = 1504568
|
||||
flags = 1
|
||||
data = length 384, hash 4A602591
|
||||
sample 24:
|
||||
time = 1521782
|
||||
sample 25:
|
||||
time = 1528568
|
||||
flags = 1
|
||||
data = length 384, hash D019AA2D
|
||||
sample 25:
|
||||
time = 1545782
|
||||
sample 26:
|
||||
time = 1552568
|
||||
flags = 1
|
||||
data = length 384, hash 8A680B9D
|
||||
sample 26:
|
||||
time = 1569782
|
||||
sample 27:
|
||||
time = 1576568
|
||||
flags = 1
|
||||
data = length 384, hash B655C959
|
||||
sample 27:
|
||||
time = 1593782
|
||||
sample 28:
|
||||
time = 1600568
|
||||
flags = 1
|
||||
data = length 384, hash 2168336B
|
||||
sample 28:
|
||||
time = 1617782
|
||||
sample 29:
|
||||
time = 1624568
|
||||
flags = 1
|
||||
data = length 384, hash D77F6D31
|
||||
sample 29:
|
||||
time = 1641782
|
||||
sample 30:
|
||||
time = 1648568
|
||||
flags = 1
|
||||
data = length 384, hash 524B4B2F
|
||||
sample 30:
|
||||
time = 1665782
|
||||
sample 31:
|
||||
time = 1672568
|
||||
flags = 1
|
||||
data = length 384, hash 4752DDFC
|
||||
sample 31:
|
||||
time = 1689782
|
||||
sample 32:
|
||||
time = 1696568
|
||||
flags = 1
|
||||
data = length 384, hash E786727F
|
||||
sample 32:
|
||||
time = 1713782
|
||||
sample 33:
|
||||
time = 1720568
|
||||
flags = 1
|
||||
data = length 384, hash 5DA6FB8C
|
||||
sample 33:
|
||||
time = 1737782
|
||||
sample 34:
|
||||
time = 1744568
|
||||
flags = 1
|
||||
data = length 384, hash 92F24269
|
||||
sample 34:
|
||||
time = 1761782
|
||||
sample 35:
|
||||
time = 1768568
|
||||
flags = 1
|
||||
data = length 384, hash CD0A3BA1
|
||||
sample 35:
|
||||
time = 1785782
|
||||
sample 36:
|
||||
time = 1792568
|
||||
flags = 1
|
||||
data = length 384, hash 7D00409F
|
||||
sample 36:
|
||||
time = 1809782
|
||||
sample 37:
|
||||
time = 1816568
|
||||
flags = 1
|
||||
data = length 384, hash D7ADB5FA
|
||||
sample 37:
|
||||
time = 1833782
|
||||
sample 38:
|
||||
time = 1840568
|
||||
flags = 1
|
||||
data = length 384, hash 4A140209
|
||||
sample 38:
|
||||
time = 1857782
|
||||
sample 39:
|
||||
time = 1864568
|
||||
flags = 1
|
||||
data = length 384, hash E801184A
|
||||
sample 39:
|
||||
time = 1881782
|
||||
sample 40:
|
||||
time = 1888568
|
||||
flags = 1
|
||||
data = length 384, hash 53C6CF9C
|
||||
sample 40:
|
||||
time = 1905782
|
||||
sample 41:
|
||||
time = 1912568
|
||||
flags = 1
|
||||
data = length 384, hash 19A8D99F
|
||||
sample 41:
|
||||
time = 1929782
|
||||
sample 42:
|
||||
time = 1936568
|
||||
flags = 1
|
||||
data = length 384, hash E47EB43F
|
||||
sample 42:
|
||||
time = 1953782
|
||||
sample 43:
|
||||
time = 1960568
|
||||
flags = 1
|
||||
data = length 384, hash 4EA329E7
|
||||
sample 43:
|
||||
time = 1977782
|
||||
sample 44:
|
||||
time = 1984568
|
||||
flags = 1
|
||||
data = length 384, hash 1CCAAE62
|
||||
sample 44:
|
||||
time = 2001782
|
||||
sample 45:
|
||||
time = 2008568
|
||||
flags = 1
|
||||
data = length 384, hash ED3F8C66
|
||||
sample 45:
|
||||
time = 2025782
|
||||
sample 46:
|
||||
time = 2032568
|
||||
flags = 1
|
||||
data = length 384, hash D3D646B6
|
||||
sample 46:
|
||||
time = 2049782
|
||||
sample 47:
|
||||
time = 2056568
|
||||
flags = 1
|
||||
data = length 384, hash 68CD1574
|
||||
sample 47:
|
||||
time = 2073782
|
||||
sample 48:
|
||||
time = 2080568
|
||||
flags = 1
|
||||
data = length 384, hash 8CEAB382
|
||||
sample 48:
|
||||
time = 2097782
|
||||
sample 49:
|
||||
time = 2104568
|
||||
flags = 1
|
||||
data = length 384, hash D54B1C48
|
||||
sample 49:
|
||||
time = 2121782
|
||||
sample 50:
|
||||
time = 2128568
|
||||
flags = 1
|
||||
data = length 384, hash FFE2EE90
|
||||
sample 50:
|
||||
time = 2145782
|
||||
sample 51:
|
||||
time = 2152568
|
||||
flags = 1
|
||||
data = length 384, hash BFE8A673
|
||||
sample 51:
|
||||
time = 2169782
|
||||
sample 52:
|
||||
time = 2176568
|
||||
flags = 1
|
||||
data = length 384, hash 978B1C92
|
||||
sample 52:
|
||||
time = 2193782
|
||||
sample 53:
|
||||
time = 2200568
|
||||
flags = 1
|
||||
data = length 384, hash 810CC71E
|
||||
sample 53:
|
||||
time = 2217782
|
||||
sample 54:
|
||||
time = 2224568
|
||||
flags = 1
|
||||
data = length 384, hash 44FE42D9
|
||||
sample 54:
|
||||
time = 2241782
|
||||
sample 55:
|
||||
time = 2248568
|
||||
flags = 1
|
||||
data = length 384, hash 2F5BB02C
|
||||
sample 55:
|
||||
time = 2265782
|
||||
sample 56:
|
||||
time = 2272568
|
||||
flags = 1
|
||||
data = length 384, hash 77DDB90
|
||||
sample 56:
|
||||
time = 2289782
|
||||
sample 57:
|
||||
time = 2296568
|
||||
flags = 1
|
||||
data = length 384, hash 24FB5EDA
|
||||
sample 57:
|
||||
time = 2313782
|
||||
sample 58:
|
||||
time = 2320568
|
||||
flags = 1
|
||||
data = length 384, hash E73203C6
|
||||
sample 58:
|
||||
time = 2337782
|
||||
sample 59:
|
||||
time = 2344568
|
||||
flags = 1
|
||||
data = length 384, hash 14B525F1
|
||||
sample 59:
|
||||
time = 2361782
|
||||
sample 60:
|
||||
time = 2368568
|
||||
flags = 1
|
||||
data = length 384, hash 5E0F4E2E
|
||||
sample 60:
|
||||
time = 2385782
|
||||
sample 61:
|
||||
time = 2392568
|
||||
flags = 1
|
||||
data = length 384, hash 67EE4E31
|
||||
sample 61:
|
||||
time = 2409782
|
||||
sample 62:
|
||||
time = 2416568
|
||||
flags = 1
|
||||
data = length 384, hash 2E04EC4C
|
||||
sample 62:
|
||||
time = 2433782
|
||||
sample 63:
|
||||
time = 2440568
|
||||
flags = 1
|
||||
data = length 384, hash 852CABA7
|
||||
sample 63:
|
||||
time = 2457782
|
||||
sample 64:
|
||||
time = 2464568
|
||||
flags = 1
|
||||
data = length 384, hash 19928903
|
||||
sample 64:
|
||||
time = 2481782
|
||||
sample 65:
|
||||
time = 2488568
|
||||
flags = 1
|
||||
data = length 384, hash 5DA42021
|
||||
sample 65:
|
||||
time = 2505782
|
||||
sample 66:
|
||||
time = 2512568
|
||||
flags = 1
|
||||
data = length 384, hash 45B20B7C
|
||||
sample 66:
|
||||
time = 2529782
|
||||
sample 67:
|
||||
time = 2536568
|
||||
flags = 1
|
||||
data = length 384, hash D108A215
|
||||
sample 67:
|
||||
time = 2553782
|
||||
sample 68:
|
||||
time = 2560568
|
||||
flags = 1
|
||||
data = length 384, hash BD25DB7C
|
||||
sample 68:
|
||||
time = 2577782
|
||||
sample 69:
|
||||
time = 2584568
|
||||
flags = 1
|
||||
data = length 384, hash DA7F9861
|
||||
sample 69:
|
||||
time = 2601782
|
||||
sample 70:
|
||||
time = 2608568
|
||||
flags = 1
|
||||
data = length 384, hash CCD576F
|
||||
sample 70:
|
||||
time = 2625782
|
||||
sample 71:
|
||||
time = 2632568
|
||||
flags = 1
|
||||
data = length 384, hash 405C1EB5
|
||||
sample 71:
|
||||
time = 2649782
|
||||
sample 72:
|
||||
time = 2656568
|
||||
flags = 1
|
||||
data = length 384, hash 6640B74E
|
||||
sample 72:
|
||||
time = 2673782
|
||||
sample 73:
|
||||
time = 2680568
|
||||
flags = 1
|
||||
data = length 384, hash B4E5937A
|
||||
sample 73:
|
||||
time = 2697782
|
||||
sample 74:
|
||||
time = 2704568
|
||||
flags = 1
|
||||
data = length 384, hash CEE17733
|
||||
sample 74:
|
||||
time = 2721782
|
||||
sample 75:
|
||||
time = 2728568
|
||||
flags = 1
|
||||
data = length 384, hash 2A0DA733
|
||||
sample 75:
|
||||
time = 2745782
|
||||
sample 76:
|
||||
time = 2752568
|
||||
flags = 1
|
||||
data = length 384, hash 97F4129B
|
||||
tracksEnded = true
|
||||
|
@ -27,155 +27,155 @@ track 0:
|
||||
initializationData:
|
||||
sample count = 38
|
||||
sample 0:
|
||||
time = 1858196
|
||||
time = 1871586
|
||||
flags = 1
|
||||
data = length 384, hash E801184A
|
||||
sample 1:
|
||||
time = 1882196
|
||||
time = 1895586
|
||||
flags = 1
|
||||
data = length 384, hash 53C6CF9C
|
||||
sample 2:
|
||||
time = 1906196
|
||||
time = 1919586
|
||||
flags = 1
|
||||
data = length 384, hash 19A8D99F
|
||||
sample 3:
|
||||
time = 1930196
|
||||
time = 1943586
|
||||
flags = 1
|
||||
data = length 384, hash E47EB43F
|
||||
sample 4:
|
||||
time = 1954196
|
||||
time = 1967586
|
||||
flags = 1
|
||||
data = length 384, hash 4EA329E7
|
||||
sample 5:
|
||||
time = 1978196
|
||||
time = 1991586
|
||||
flags = 1
|
||||
data = length 384, hash 1CCAAE62
|
||||
sample 6:
|
||||
time = 2002196
|
||||
time = 2015586
|
||||
flags = 1
|
||||
data = length 384, hash ED3F8C66
|
||||
sample 7:
|
||||
time = 2026196
|
||||
time = 2039586
|
||||
flags = 1
|
||||
data = length 384, hash D3D646B6
|
||||
sample 8:
|
||||
time = 2050196
|
||||
time = 2063586
|
||||
flags = 1
|
||||
data = length 384, hash 68CD1574
|
||||
sample 9:
|
||||
time = 2074196
|
||||
time = 2087586
|
||||
flags = 1
|
||||
data = length 384, hash 8CEAB382
|
||||
sample 10:
|
||||
time = 2098196
|
||||
time = 2111586
|
||||
flags = 1
|
||||
data = length 384, hash D54B1C48
|
||||
sample 11:
|
||||
time = 2122196
|
||||
time = 2135586
|
||||
flags = 1
|
||||
data = length 384, hash FFE2EE90
|
||||
sample 12:
|
||||
time = 2146196
|
||||
time = 2159586
|
||||
flags = 1
|
||||
data = length 384, hash BFE8A673
|
||||
sample 13:
|
||||
time = 2170196
|
||||
time = 2183586
|
||||
flags = 1
|
||||
data = length 384, hash 978B1C92
|
||||
sample 14:
|
||||
time = 2194196
|
||||
time = 2207586
|
||||
flags = 1
|
||||
data = length 384, hash 810CC71E
|
||||
sample 15:
|
||||
time = 2218196
|
||||
time = 2231586
|
||||
flags = 1
|
||||
data = length 384, hash 44FE42D9
|
||||
sample 16:
|
||||
time = 2242196
|
||||
time = 2255586
|
||||
flags = 1
|
||||
data = length 384, hash 2F5BB02C
|
||||
sample 17:
|
||||
time = 2266196
|
||||
time = 2279586
|
||||
flags = 1
|
||||
data = length 384, hash 77DDB90
|
||||
sample 18:
|
||||
time = 2290196
|
||||
time = 2303586
|
||||
flags = 1
|
||||
data = length 384, hash 24FB5EDA
|
||||
sample 19:
|
||||
time = 2314196
|
||||
time = 2327586
|
||||
flags = 1
|
||||
data = length 384, hash E73203C6
|
||||
sample 20:
|
||||
time = 2338196
|
||||
time = 2351586
|
||||
flags = 1
|
||||
data = length 384, hash 14B525F1
|
||||
sample 21:
|
||||
time = 2362196
|
||||
time = 2375586
|
||||
flags = 1
|
||||
data = length 384, hash 5E0F4E2E
|
||||
sample 22:
|
||||
time = 2386196
|
||||
time = 2399586
|
||||
flags = 1
|
||||
data = length 384, hash 67EE4E31
|
||||
sample 23:
|
||||
time = 2410196
|
||||
time = 2423586
|
||||
flags = 1
|
||||
data = length 384, hash 2E04EC4C
|
||||
sample 24:
|
||||
time = 2434196
|
||||
time = 2447586
|
||||
flags = 1
|
||||
data = length 384, hash 852CABA7
|
||||
sample 25:
|
||||
time = 2458196
|
||||
time = 2471586
|
||||
flags = 1
|
||||
data = length 384, hash 19928903
|
||||
sample 26:
|
||||
time = 2482196
|
||||
time = 2495586
|
||||
flags = 1
|
||||
data = length 384, hash 5DA42021
|
||||
sample 27:
|
||||
time = 2506196
|
||||
time = 2519586
|
||||
flags = 1
|
||||
data = length 384, hash 45B20B7C
|
||||
sample 28:
|
||||
time = 2530196
|
||||
time = 2543586
|
||||
flags = 1
|
||||
data = length 384, hash D108A215
|
||||
sample 29:
|
||||
time = 2554196
|
||||
time = 2567586
|
||||
flags = 1
|
||||
data = length 384, hash BD25DB7C
|
||||
sample 30:
|
||||
time = 2578196
|
||||
time = 2591586
|
||||
flags = 1
|
||||
data = length 384, hash DA7F9861
|
||||
sample 31:
|
||||
time = 2602196
|
||||
time = 2615586
|
||||
flags = 1
|
||||
data = length 384, hash CCD576F
|
||||
sample 32:
|
||||
time = 2626196
|
||||
time = 2639586
|
||||
flags = 1
|
||||
data = length 384, hash 405C1EB5
|
||||
sample 33:
|
||||
time = 2650196
|
||||
time = 2663586
|
||||
flags = 1
|
||||
data = length 384, hash 6640B74E
|
||||
sample 34:
|
||||
time = 2674196
|
||||
time = 2687586
|
||||
flags = 1
|
||||
data = length 384, hash B4E5937A
|
||||
sample 35:
|
||||
time = 2698196
|
||||
time = 2711586
|
||||
flags = 1
|
||||
data = length 384, hash CEE17733
|
||||
sample 36:
|
||||
time = 2722196
|
||||
time = 2735586
|
||||
flags = 1
|
||||
data = length 384, hash 2A0DA733
|
||||
sample 37:
|
||||
time = 2746196
|
||||
time = 2759586
|
||||
flags = 1
|
||||
data = length 384, hash 97F4129B
|
||||
tracksEnded = true
|
||||
|
@ -25,5 +25,9 @@ track 0:
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
sample count = 0
|
||||
sample count = 1
|
||||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 418, hash B819987
|
||||
tracksEnded = true
|
||||
|
@ -25,5 +25,9 @@ track 0:
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
sample count = 0
|
||||
sample count = 1
|
||||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 418, hash B819987
|
||||
tracksEnded = true
|
||||
|
@ -25,5 +25,9 @@ track 0:
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
sample count = 0
|
||||
sample count = 1
|
||||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 418, hash B819987
|
||||
tracksEnded = true
|
||||
|
@ -1,7 +1,7 @@
|
||||
seekMap:
|
||||
isSeekable = false
|
||||
duration = UNSET TIME
|
||||
getPosition(0) = 0
|
||||
getPosition(0) = 1828
|
||||
numberOfTracks = 2
|
||||
track 0:
|
||||
format:
|
||||
|
@ -1,7 +1,7 @@
|
||||
seekMap:
|
||||
isSeekable = false
|
||||
duration = UNSET TIME
|
||||
getPosition(0) = 0
|
||||
getPosition(0) = 1828
|
||||
numberOfTracks = 3
|
||||
track 0:
|
||||
format:
|
||||
|
@ -27,7 +27,8 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeRenderer;
|
||||
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackSelection;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackSelector;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@ -66,7 +67,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
* Tests playback of a source that exposes a single period.
|
||||
*/
|
||||
public void testPlaySinglePeriodTimeline() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
Object manifest = new Object();
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
@ -85,10 +86,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
* Tests playback of a source that exposes three periods.
|
||||
*/
|
||||
public void testPlayMultiPeriodTimeline() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0));
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(renderer)
|
||||
@ -105,10 +103,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
* source.
|
||||
*/
|
||||
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10));
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
|
||||
final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
|
||||
|
||||
@ -149,7 +144,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
}
|
||||
|
||||
public void testRepreparationGivesFreshSourceInfo() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
Object firstSourceManifest = new Object();
|
||||
MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest,
|
||||
@ -216,10 +211,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
}
|
||||
|
||||
public void testRepeatModeChanges() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000));
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1
|
||||
@ -239,7 +231,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
}
|
||||
|
||||
public void testShuffleModeEnabledChanges() throws Exception {
|
||||
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000));
|
||||
Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
MediaSource[] fakeMediaSources = {
|
||||
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
|
||||
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
|
||||
@ -262,7 +254,6 @@ public final class ExoPlayerTest extends TestCase {
|
||||
}
|
||||
|
||||
public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {
|
||||
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000));
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased")
|
||||
.setRepeatMode(Player.REPEAT_MODE_ALL)
|
||||
@ -272,15 +263,13 @@ public final class ExoPlayerTest extends TestCase {
|
||||
.setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish.
|
||||
.build();
|
||||
new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(fakeTimeline).setRenderers(renderer).setActionSchedule(actionSchedule)
|
||||
.setRenderers(renderer).setActionSchedule(actionSchedule)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
assertTrue(renderer.isEnded);
|
||||
}
|
||||
|
||||
public void testSeekProcessedCallback() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000));
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback")
|
||||
// Initial seek before timeline preparation finished.
|
||||
.pause().seek(10).waitForPlaybackState(Player.STATE_READY)
|
||||
@ -311,4 +300,138 @@ public final class ExoPlayerTest extends TestCase {
|
||||
assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2));
|
||||
}
|
||||
|
||||
public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
MediaSource mediaSource =
|
||||
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
|
||||
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
|
||||
FakeTrackSelector trackSelector = new FakeTrackSelector();
|
||||
|
||||
new ExoPlayerTestRunner.Builder()
|
||||
.setMediaSource(mediaSource)
|
||||
.setRenderers(videoRenderer, audioRenderer)
|
||||
.setTrackSelector(trackSelector)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
|
||||
List<FakeTrackSelection> createdTrackSelections = trackSelector.getSelectedTrackSelections();
|
||||
int numSelectionsEnabled = 0;
|
||||
// Assert that all tracks selection are disabled at the end of the playback.
|
||||
for (FakeTrackSelection trackSelection : createdTrackSelections) {
|
||||
assertFalse(trackSelection.isEnabled);
|
||||
numSelectionsEnabled += trackSelection.enableCount;
|
||||
}
|
||||
// There are 2 renderers, and track selections are made once (1 period).
|
||||
// Track selections are not reused, so there are 2 track selections made.
|
||||
assertEquals(2, createdTrackSelections.size());
|
||||
// There should be 2 track selections enabled in total.
|
||||
assertEquals(2, numSelectionsEnabled);
|
||||
}
|
||||
|
||||
public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception {
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
|
||||
MediaSource mediaSource =
|
||||
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
|
||||
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
|
||||
FakeTrackSelector trackSelector = new FakeTrackSelector();
|
||||
|
||||
new ExoPlayerTestRunner.Builder()
|
||||
.setMediaSource(mediaSource)
|
||||
.setRenderers(videoRenderer, audioRenderer)
|
||||
.setTrackSelector(trackSelector)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
|
||||
List<FakeTrackSelection> createdTrackSelections = trackSelector.getSelectedTrackSelections();
|
||||
int numSelectionsEnabled = 0;
|
||||
// Assert that all tracks selection are disabled at the end of the playback.
|
||||
for (FakeTrackSelection trackSelection : createdTrackSelections) {
|
||||
assertFalse(trackSelection.isEnabled);
|
||||
numSelectionsEnabled += trackSelection.enableCount;
|
||||
}
|
||||
// There are 2 renderers, and track selections are made twice (2 periods).
|
||||
// Track selections are not reused, so there are 4 track selections made.
|
||||
assertEquals(4, createdTrackSelections.size());
|
||||
// There should be 4 track selections enabled in total.
|
||||
assertEquals(4, numSelectionsEnabled);
|
||||
}
|
||||
|
||||
public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade()
|
||||
throws Exception {
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
MediaSource mediaSource =
|
||||
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
|
||||
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
|
||||
final FakeTrackSelector trackSelector = new FakeTrackSelector();
|
||||
ActionSchedule disableTrackAction = new ActionSchedule.Builder("testChangeTrackSelection")
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.executeRunnable(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
trackSelector.setRendererDisabled(0, true);
|
||||
}
|
||||
}).build();
|
||||
|
||||
new ExoPlayerTestRunner.Builder()
|
||||
.setMediaSource(mediaSource)
|
||||
.setRenderers(videoRenderer, audioRenderer)
|
||||
.setTrackSelector(trackSelector)
|
||||
.setActionSchedule(disableTrackAction)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
|
||||
List<FakeTrackSelection> createdTrackSelections = trackSelector.getSelectedTrackSelections();
|
||||
int numSelectionsEnabled = 0;
|
||||
// Assert that all tracks selection are disabled at the end of the playback.
|
||||
for (FakeTrackSelection trackSelection : createdTrackSelections) {
|
||||
assertFalse(trackSelection.isEnabled);
|
||||
numSelectionsEnabled += trackSelection.enableCount;
|
||||
}
|
||||
// There are 2 renderers, and track selections are made twice.
|
||||
// Track selections are not reused, so there are 4 track selections made.
|
||||
assertEquals(4, createdTrackSelections.size());
|
||||
// Initially there are 2 track selections enabled.
|
||||
// The second time one renderer is disabled, so only 1 track selection should be enabled.
|
||||
assertEquals(3, numSelectionsEnabled);
|
||||
}
|
||||
|
||||
public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed()
|
||||
throws Exception {
|
||||
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
|
||||
MediaSource mediaSource =
|
||||
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
|
||||
FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
|
||||
final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true);
|
||||
ActionSchedule disableTrackAction = new ActionSchedule.Builder("testReuseTrackSelection")
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.executeRunnable(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
trackSelector.setRendererDisabled(0, true);
|
||||
}
|
||||
}).build();
|
||||
|
||||
new ExoPlayerTestRunner.Builder()
|
||||
.setMediaSource(mediaSource)
|
||||
.setRenderers(videoRenderer, audioRenderer)
|
||||
.setTrackSelector(trackSelector)
|
||||
.setActionSchedule(disableTrackAction)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
|
||||
List<FakeTrackSelection> createdTrackSelections = trackSelector.getSelectedTrackSelections();
|
||||
int numSelectionsEnabled = 0;
|
||||
// Assert that all tracks selection are disabled at the end of the playback.
|
||||
for (FakeTrackSelection trackSelection : createdTrackSelections) {
|
||||
assertFalse(trackSelection.isEnabled);
|
||||
numSelectionsEnabled += trackSelection.enableCount;
|
||||
}
|
||||
// There are 2 renderers, and track selections are made twice.
|
||||
// TrackSelections are reused, so there are only 2 track selections made for 2 renderers.
|
||||
assertEquals(2, createdTrackSelections.size());
|
||||
// Initially there are 2 track selections enabled.
|
||||
// The second time one renderer is disabled, so only 1 track selection should be enabled.
|
||||
assertEquals(3, numSelectionsEnabled);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ import android.test.MoreAsserts;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.testutil.MockitoUtil;
|
||||
import java.util.HashMap;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Tests {@link OfflineLicenseHelper}.
|
||||
@ -38,7 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
setUpMockito(this);
|
||||
MockitoUtil.setUpMockito(this);
|
||||
when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3});
|
||||
offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback,
|
||||
null);
|
||||
@ -156,14 +156,4 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
|
||||
new byte[] {1, 4, 7, 0, 3, 6}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Mockito for an instrumentation test.
|
||||
*/
|
||||
private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
|
||||
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
|
||||
System.setProperty("dexmaker.dexcache",
|
||||
instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
|
||||
MockitoAnnotations.initMocks(instrumentationTestCase);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,9 +16,13 @@
|
||||
package com.google.android.exoplayer2.extractor.mp4;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Unit test for {@link FragmentedMp4Extractor}.
|
||||
@ -26,26 +30,23 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
|
||||
|
||||
public void testSample() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4",
|
||||
getInstrumentation());
|
||||
ExtractorAsserts.assertBehavior(getExtractorFactory(Collections.<Format>emptyList()),
|
||||
"mp4/sample_fragmented.mp4", getInstrumentation());
|
||||
}
|
||||
|
||||
public void testSampleWithSeiPayloadParsing() throws Exception {
|
||||
// Enabling the CEA-608 track enables SEI payload parsing.
|
||||
ExtractorAsserts.assertBehavior(
|
||||
getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
|
||||
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
|
||||
ExtractorFactory extractorFactory = getExtractorFactory(Collections.singletonList(
|
||||
Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)));
|
||||
ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4",
|
||||
getInstrumentation());
|
||||
}
|
||||
|
||||
private static ExtractorFactory getExtractorFactory() {
|
||||
return getExtractorFactory(0);
|
||||
}
|
||||
|
||||
private static ExtractorFactory getExtractorFactory(final int flags) {
|
||||
private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
|
||||
return new ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FragmentedMp4Extractor(flags, null);
|
||||
return new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import com.google.android.exoplayer2.Timeline.Window;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
|
||||
/**
|
||||
@ -123,9 +123,14 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
|
||||
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
|
||||
*/
|
||||
private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||
return TestUtil.extractTimelineFromMediaSource(
|
||||
new ClippingMediaSource(mediaSource, startMs, endMs));
|
||||
FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
|
||||
ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs);
|
||||
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
|
||||
try {
|
||||
return testRunner.prepareSource();
|
||||
} finally {
|
||||
testRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
@ -208,18 +208,22 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
|
||||
ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly,
|
||||
mediaSourceWithAds);
|
||||
|
||||
// Prepare and assert timeline contains ad groups.
|
||||
Timeline timeline = TestUtil.extractTimelineFromMediaSource(mediaSource);
|
||||
TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1);
|
||||
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
|
||||
try {
|
||||
Timeline timeline = testRunner.prepareSource();
|
||||
TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1);
|
||||
|
||||
// Create all periods and assert period creation of child media sources has been called.
|
||||
TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000);
|
||||
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0));
|
||||
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1));
|
||||
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0));
|
||||
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1));
|
||||
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0));
|
||||
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0));
|
||||
// Create all periods and assert period creation of child media sources has been called.
|
||||
testRunner.assertPrepareAndReleaseAllPeriods();
|
||||
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0));
|
||||
mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1));
|
||||
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0));
|
||||
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1));
|
||||
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0));
|
||||
mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0));
|
||||
} finally {
|
||||
testRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,7 +238,12 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
|
||||
}
|
||||
ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic,
|
||||
new FakeShuffleOrder(mediaSources.length), mediaSources);
|
||||
return TestUtil.extractTimelineFromMediaSource(mediaSource);
|
||||
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
|
||||
try {
|
||||
return testRunner.prepareSource();
|
||||
} finally {
|
||||
testRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static FakeTimeline createFakeTimeline(int periodCount, int windowId) {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -21,7 +21,7 @@ import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
@ -30,12 +30,13 @@ import junit.framework.TestCase;
|
||||
*/
|
||||
public class LoopingMediaSourceTest extends TestCase {
|
||||
|
||||
private final Timeline multiWindowTimeline;
|
||||
private FakeTimeline multiWindowTimeline;
|
||||
|
||||
public LoopingMediaSourceTest() {
|
||||
multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource(
|
||||
new FakeTimeline(new TimelineWindowDefinition(1, 111),
|
||||
new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null));
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
multiWindowTimeline = new FakeTimeline(new TimelineWindowDefinition(1, 111),
|
||||
new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333));
|
||||
}
|
||||
|
||||
public void testSingleLoop() {
|
||||
@ -109,10 +110,14 @@ public class LoopingMediaSourceTest extends TestCase {
|
||||
* the looping timeline.
|
||||
*/
|
||||
private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) {
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||
return TestUtil.extractTimelineFromMediaSource(
|
||||
new LoopingMediaSource(mediaSource, loopCount));
|
||||
FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
|
||||
LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount);
|
||||
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
|
||||
try {
|
||||
return testRunner.prepareSource();
|
||||
} finally {
|
||||
testRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -17,11 +17,11 @@ package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||||
import com.google.android.exoplayer2.testutil.MockitoUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Tests for {@link CachedRegionTracker}.
|
||||
@ -46,7 +46,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
setUpMockito(this);
|
||||
MockitoUtil.setUpMockito(this);
|
||||
|
||||
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
@ -123,14 +123,4 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
|
||||
return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Mockito for an instrumentation test.
|
||||
*/
|
||||
private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
|
||||
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
|
||||
System.setProperty("dexmaker.dexcache",
|
||||
instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
|
||||
MockitoAnnotations.initMocks(instrumentationTestCase);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -127,8 +127,8 @@ public final class C {
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
|
||||
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS,
|
||||
ENCODING_DTS_HD})
|
||||
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3,
|
||||
ENCODING_DTS, ENCODING_DTS_HD})
|
||||
public @interface Encoding {}
|
||||
|
||||
/**
|
||||
@ -136,7 +136,7 @@ public final class C {
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
|
||||
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT})
|
||||
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT})
|
||||
public @interface PcmEncoding {}
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_INVALID
|
||||
@ -158,6 +158,10 @@ public final class C {
|
||||
* PCM encoding with 32 bits per sample.
|
||||
*/
|
||||
public static final int ENCODING_PCM_32BIT = 0x40000000;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_PCM_FLOAT
|
||||
*/
|
||||
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
|
||||
/**
|
||||
* @see AudioFormat#ENCODING_AC3
|
||||
*/
|
||||
@ -420,6 +424,11 @@ public final class C {
|
||||
*/
|
||||
public static final int SELECTION_FLAG_AUTOSELECT = 4;
|
||||
|
||||
/**
|
||||
* Represents an undetermined language as an ISO 639 alpha-3 language code.
|
||||
*/
|
||||
public static final String LANGUAGE_UNDETERMINED = "und";
|
||||
|
||||
/**
|
||||
* Represents a streaming or other media type.
|
||||
*/
|
||||
|
@ -51,9 +51,14 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
*/
|
||||
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
|
||||
|
||||
private static final int ABOVE_HIGH_WATERMARK = 0;
|
||||
private static final int BETWEEN_WATERMARKS = 1;
|
||||
private static final int BELOW_LOW_WATERMARK = 2;
|
||||
/**
|
||||
* The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control
|
||||
* automatically determines its target buffer size.
|
||||
*/
|
||||
public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET;
|
||||
|
||||
/** The default prioritization of buffer time constraints over size constraints. */
|
||||
public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true;
|
||||
|
||||
private final DefaultAllocator allocator;
|
||||
|
||||
@ -61,6 +66,8 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
private final long maxBufferUs;
|
||||
private final long bufferForPlaybackUs;
|
||||
private final long bufferForPlaybackAfterRebufferUs;
|
||||
private final int targetBufferBytesOverwrite;
|
||||
private final boolean prioritizeTimeOverSizeThresholds;
|
||||
private final PriorityTaskManager priorityTaskManager;
|
||||
|
||||
private int targetBufferSize;
|
||||
@ -79,8 +86,14 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
* @param allocator The {@link DefaultAllocator} used by the loader.
|
||||
*/
|
||||
public DefaultLoadControl(DefaultAllocator allocator) {
|
||||
this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||
this(
|
||||
allocator,
|
||||
DEFAULT_MIN_BUFFER_MS,
|
||||
DEFAULT_MAX_BUFFER_MS,
|
||||
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
|
||||
DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DEFAULT_TARGET_BUFFER_BYTES,
|
||||
DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,10 +109,27 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
|
||||
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
|
||||
* buffer depletion rather than a user action.
|
||||
* @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the
|
||||
* target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[],
|
||||
* TrackSelectionArray)}.
|
||||
* @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
|
||||
*/
|
||||
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
|
||||
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
|
||||
this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
|
||||
public DefaultLoadControl(
|
||||
DefaultAllocator allocator,
|
||||
int minBufferMs,
|
||||
int maxBufferMs,
|
||||
int bufferForPlaybackMs,
|
||||
int bufferForPlaybackAfterRebufferMs,
|
||||
int targetBufferBytes,
|
||||
boolean prioritizeTimeOverSizeThresholds) {
|
||||
this(
|
||||
allocator,
|
||||
minBufferMs,
|
||||
maxBufferMs,
|
||||
bufferForPlaybackMs,
|
||||
bufferForPlaybackAfterRebufferMs,
|
||||
targetBufferBytes,
|
||||
prioritizeTimeOverSizeThresholds,
|
||||
null);
|
||||
}
|
||||
|
||||
@ -116,18 +146,30 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
|
||||
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
|
||||
* buffer depletion rather than a user action.
|
||||
* @param priorityTaskManager If not null, registers itself as a task with priority
|
||||
* {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
|
||||
* periods.
|
||||
* @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the
|
||||
* target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[],
|
||||
* TrackSelectionArray)}.
|
||||
* @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
|
||||
* constraints over buffer size constraints.
|
||||
* @param priorityTaskManager If not null, registers itself as a task with priority {@link
|
||||
* C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
|
||||
*/
|
||||
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
|
||||
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
|
||||
public DefaultLoadControl(
|
||||
DefaultAllocator allocator,
|
||||
int minBufferMs,
|
||||
int maxBufferMs,
|
||||
int bufferForPlaybackMs,
|
||||
int bufferForPlaybackAfterRebufferMs,
|
||||
int targetBufferBytes,
|
||||
boolean prioritizeTimeOverSizeThresholds,
|
||||
PriorityTaskManager priorityTaskManager) {
|
||||
this.allocator = allocator;
|
||||
minBufferUs = minBufferMs * 1000L;
|
||||
maxBufferUs = maxBufferMs * 1000L;
|
||||
targetBufferBytesOverwrite = targetBufferBytes;
|
||||
bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
|
||||
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
|
||||
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
|
||||
this.priorityTaskManager = priorityTaskManager;
|
||||
}
|
||||
|
||||
@ -139,12 +181,10 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
@Override
|
||||
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
|
||||
TrackSelectionArray trackSelections) {
|
||||
targetBufferSize = 0;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
if (trackSelections.get(i) != null) {
|
||||
targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType());
|
||||
}
|
||||
}
|
||||
targetBufferSize =
|
||||
targetBufferBytesOverwrite == C.LENGTH_UNSET
|
||||
? calculateTargetBufferSize(renderers, trackSelections)
|
||||
: targetBufferBytesOverwrite;
|
||||
allocator.setTargetBufferSize(targetBufferSize);
|
||||
}
|
||||
|
||||
@ -166,16 +206,28 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
@Override
|
||||
public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) {
|
||||
long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
|
||||
return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs;
|
||||
return minBufferDurationUs <= 0
|
||||
|| bufferedDurationUs >= minBufferDurationUs
|
||||
|| (!prioritizeTimeOverSizeThresholds
|
||||
&& allocator.getTotalBytesAllocated() >= targetBufferSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueLoading(long bufferedDurationUs) {
|
||||
int bufferTimeState = getBufferTimeState(bufferedDurationUs);
|
||||
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
|
||||
boolean wasBuffering = isBuffering;
|
||||
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
|
||||
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
|
||||
if (prioritizeTimeOverSizeThresholds) {
|
||||
isBuffering =
|
||||
bufferedDurationUs < minBufferUs // below low watermark
|
||||
|| (bufferedDurationUs <= maxBufferUs // between watermarks
|
||||
&& isBuffering
|
||||
&& !targetBufferSizeReached);
|
||||
} else {
|
||||
isBuffering =
|
||||
!targetBufferSizeReached
|
||||
&& (bufferedDurationUs < minBufferUs // below low watermark
|
||||
|| (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks
|
||||
}
|
||||
if (priorityTaskManager != null && isBuffering != wasBuffering) {
|
||||
if (isBuffering) {
|
||||
priorityTaskManager.add(C.PRIORITY_PLAYBACK);
|
||||
@ -186,9 +238,23 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
return isBuffering;
|
||||
}
|
||||
|
||||
private int getBufferTimeState(long bufferedDurationUs) {
|
||||
return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK
|
||||
: (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS);
|
||||
/**
|
||||
* Calculate target buffer size in bytes based on the selected tracks. The player will try not to
|
||||
* exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}.
|
||||
*
|
||||
* @param renderers The renderers for which the track were selected.
|
||||
* @param trackSelectionArray The selected tracks.
|
||||
* @return The target buffer size in bytes.
|
||||
*/
|
||||
protected int calculateTargetBufferSize(
|
||||
Renderer[] renderers, TrackSelectionArray trackSelectionArray) {
|
||||
int targetBufferSize = 0;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
if (trackSelectionArray.get(i) != null) {
|
||||
targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType());
|
||||
}
|
||||
}
|
||||
return targetBufferSize;
|
||||
}
|
||||
|
||||
private void reset(boolean resetAllocator) {
|
||||
|
@ -1666,11 +1666,11 @@ import java.io.IOException;
|
||||
// Undo the effect of previous call to associate no-sample renderers with empty tracks
|
||||
// so the mediaPeriod receives back whatever it sent us before.
|
||||
disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams);
|
||||
updatePeriodTrackSelectorResult(trackSelectorResult);
|
||||
// Disable streams on the period and get new streams for updated/newly-enabled tracks.
|
||||
positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
|
||||
sampleStreams, streamResetFlags, positionUs);
|
||||
associateNoSampleRenderersWithEmptySampleStream(sampleStreams);
|
||||
periodTrackSelectorResult = trackSelectorResult;
|
||||
|
||||
// Update whether we have enabled tracks and sanity check the expected streams are non-null.
|
||||
hasEnabledTracks = false;
|
||||
@ -1692,6 +1692,7 @@ import java.io.IOException;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
updatePeriodTrackSelectorResult(null);
|
||||
try {
|
||||
if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
|
||||
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
|
||||
@ -1704,6 +1705,36 @@ import java.io.IOException;
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) {
|
||||
if (periodTrackSelectorResult != null) {
|
||||
disableTrackSelectionsInResult(periodTrackSelectorResult);
|
||||
}
|
||||
periodTrackSelectorResult = trackSelectorResult;
|
||||
if (periodTrackSelectorResult != null) {
|
||||
enableTrackSelectionsInResult(periodTrackSelectorResult);
|
||||
}
|
||||
}
|
||||
|
||||
private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
|
||||
for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
|
||||
boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
|
||||
TrackSelection trackSelection = trackSelectorResult.selections.get(i);
|
||||
if (rendererEnabled && trackSelection != null) {
|
||||
trackSelection.enable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
|
||||
for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
|
||||
boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
|
||||
TrackSelection trackSelection = trackSelectorResult.selections.get(i);
|
||||
if (rendererEnabled && trackSelection != null) {
|
||||
trackSelection.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy
|
||||
* {@link EmptySampleStream} that was associated with it.
|
||||
|
@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo {
|
||||
* The version of the library expressed as a string, for example "1.2.3".
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.6.0";
|
||||
public static final String VERSION = "2.6.1";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.0";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.1";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003.
|
||||
@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo {
|
||||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 2006000;
|
||||
public static final int VERSION_INT = 2006001;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||
|
@ -368,6 +368,8 @@ public interface Player {
|
||||
* @param windowIndex The index of the window.
|
||||
* @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
|
||||
* the window's default position.
|
||||
* @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided
|
||||
* {@code windowIndex} is not within the bounds of the current timeline.
|
||||
*/
|
||||
void seekTo(int windowIndex, long positionMs);
|
||||
|
||||
|
@ -91,6 +91,8 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
private final CopyOnWriteArraySet<VideoListener> videoListeners;
|
||||
private final CopyOnWriteArraySet<TextOutput> textOutputs;
|
||||
private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs;
|
||||
private final CopyOnWriteArraySet<VideoRendererEventListener> videoDebugListeners;
|
||||
private final CopyOnWriteArraySet<AudioRendererEventListener> audioDebugListeners;
|
||||
private final int videoRendererCount;
|
||||
private final int audioRendererCount;
|
||||
|
||||
@ -103,8 +105,6 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
private int videoScalingMode;
|
||||
private SurfaceHolder surfaceHolder;
|
||||
private TextureView textureView;
|
||||
private AudioRendererEventListener audioDebugListener;
|
||||
private VideoRendererEventListener videoDebugListener;
|
||||
private DecoderCounters videoDecoderCounters;
|
||||
private DecoderCounters audioDecoderCounters;
|
||||
private int audioSessionId;
|
||||
@ -117,6 +117,8 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
videoListeners = new CopyOnWriteArraySet<>();
|
||||
textOutputs = new CopyOnWriteArraySet<>();
|
||||
metadataOutputs = new CopyOnWriteArraySet<>();
|
||||
videoDebugListeners = new CopyOnWriteArraySet<>();
|
||||
audioDebugListeners = new CopyOnWriteArraySet<>();
|
||||
Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
|
||||
Handler eventHandler = new Handler(eventLooper);
|
||||
renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener,
|
||||
@ -576,18 +578,64 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
* Sets a listener to receive debug events from the video renderer.
|
||||
*
|
||||
* @param listener The listener.
|
||||
* @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setVideoDebugListener(VideoRendererEventListener listener) {
|
||||
videoDebugListener = listener;
|
||||
videoDebugListeners.clear();
|
||||
if (listener != null) {
|
||||
addVideoDebugListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive debug events from the video renderer.
|
||||
*
|
||||
* @param listener The listener.
|
||||
*/
|
||||
public void addVideoDebugListener(VideoRendererEventListener listener) {
|
||||
videoDebugListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener to receive debug events from the video renderer.
|
||||
*
|
||||
* @param listener The listener.
|
||||
*/
|
||||
public void removeVideoDebugListener(VideoRendererEventListener listener) {
|
||||
videoDebugListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a listener to receive debug events from the audio renderer.
|
||||
*
|
||||
* @param listener The listener.
|
||||
* @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setAudioDebugListener(AudioRendererEventListener listener) {
|
||||
audioDebugListener = listener;
|
||||
audioDebugListeners.clear();
|
||||
if (listener != null) {
|
||||
addAudioDebugListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive debug events from the audio renderer.
|
||||
*
|
||||
* @param listener The listener.
|
||||
*/
|
||||
public void addAudioDebugListener(AudioRendererEventListener listener) {
|
||||
audioDebugListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener to receive debug events from the audio renderer.
|
||||
*
|
||||
* @param listener The listener.
|
||||
*/
|
||||
public void removeAudioDebugListener(AudioRendererEventListener listener) {
|
||||
audioDebugListeners.remove(listener);
|
||||
}
|
||||
|
||||
// ExoPlayer implementation
|
||||
@ -678,7 +726,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
||||
player.setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
|
||||
@ -817,15 +865,15 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
// Internal methods.
|
||||
|
||||
/**
|
||||
* Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}.
|
||||
* Creates the {@link ExoPlayer} implementation used by this instance.
|
||||
*
|
||||
* @param renderers The {@link Renderer}s that will be used by the instance.
|
||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||
* @return A new {@link ExoPlayer} instance.
|
||||
*/
|
||||
protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector,
|
||||
LoadControl loadControl) {
|
||||
protected ExoPlayer createExoPlayerImpl(
|
||||
Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
|
||||
return new ExoPlayerImpl(renderers, trackSelector, loadControl);
|
||||
}
|
||||
|
||||
@ -877,7 +925,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
@Override
|
||||
public void onVideoEnabled(DecoderCounters counters) {
|
||||
videoDecoderCounters = counters;
|
||||
if (videoDebugListener != null) {
|
||||
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
|
||||
videoDebugListener.onVideoEnabled(counters);
|
||||
}
|
||||
}
|
||||
@ -885,7 +933,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
@Override
|
||||
public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
|
||||
long initializationDurationMs) {
|
||||
if (videoDebugListener != null) {
|
||||
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
|
||||
videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
|
||||
initializationDurationMs);
|
||||
}
|
||||
@ -894,14 +942,14 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
@Override
|
||||
public void onVideoInputFormatChanged(Format format) {
|
||||
videoFormat = format;
|
||||
if (videoDebugListener != null) {
|
||||
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
|
||||
videoDebugListener.onVideoInputFormatChanged(format);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsed) {
|
||||
if (videoDebugListener != null) {
|
||||
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
|
||||
videoDebugListener.onDroppedFrames(count, elapsed);
|
||||
}
|
||||
}
|
||||
@ -913,7 +961,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
|
||||
pixelWidthHeightRatio);
|
||||
}
|
||||
if (videoDebugListener != null) {
|
||||
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
|
||||
videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
|
||||
pixelWidthHeightRatio);
|
||||
}
|
||||
@ -926,14 +974,14 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
videoListener.onRenderedFirstFrame();
|
||||
}
|
||||
}
|
||||
if (videoDebugListener != null) {
|
||||
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
|
||||
videoDebugListener.onRenderedFirstFrame(surface);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoDisabled(DecoderCounters counters) {
|
||||
if (videoDebugListener != null) {
|
||||
for (VideoRendererEventListener videoDebugListener : videoDebugListeners) {
|
||||
videoDebugListener.onVideoDisabled(counters);
|
||||
}
|
||||
videoFormat = null;
|
||||
@ -945,7 +993,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
@Override
|
||||
public void onAudioEnabled(DecoderCounters counters) {
|
||||
audioDecoderCounters = counters;
|
||||
if (audioDebugListener != null) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
audioDebugListener.onAudioEnabled(counters);
|
||||
}
|
||||
}
|
||||
@ -953,7 +1001,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
@Override
|
||||
public void onAudioSessionId(int sessionId) {
|
||||
audioSessionId = sessionId;
|
||||
if (audioDebugListener != null) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
audioDebugListener.onAudioSessionId(sessionId);
|
||||
}
|
||||
}
|
||||
@ -961,7 +1009,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
@Override
|
||||
public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
|
||||
long initializationDurationMs) {
|
||||
if (audioDebugListener != null) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,
|
||||
initializationDurationMs);
|
||||
}
|
||||
@ -970,7 +1018,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
@Override
|
||||
public void onAudioInputFormatChanged(Format format) {
|
||||
audioFormat = format;
|
||||
if (audioDebugListener != null) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
audioDebugListener.onAudioInputFormatChanged(format);
|
||||
}
|
||||
}
|
||||
@ -978,14 +1026,14 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
@Override
|
||||
public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs,
|
||||
long elapsedSinceLastFeedMs) {
|
||||
if (audioDebugListener != null) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDisabled(DecoderCounters counters) {
|
||||
if (audioDebugListener != null) {
|
||||
for (AudioRendererEventListener audioDebugListener : audioDebugListeners) {
|
||||
audioDebugListener.onAudioDisabled(counters);
|
||||
}
|
||||
audioFormat = null;
|
||||
|
@ -15,6 +15,10 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.audio;
|
||||
|
||||
import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE0;
|
||||
import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE1;
|
||||
import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
@ -181,7 +185,14 @@ public final class Ac3Util {
|
||||
channelCount += 2;
|
||||
}
|
||||
}
|
||||
return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE,
|
||||
String mimeType = MimeTypes.AUDIO_E_AC3;
|
||||
if (data.bytesLeft() > 0) {
|
||||
nextByte = data.readUnsignedByte();
|
||||
if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a
|
||||
mimeType = MimeTypes.AUDIO_ATMOS;
|
||||
}
|
||||
}
|
||||
return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE,
|
||||
Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
|
||||
}
|
||||
|
||||
@ -198,29 +209,176 @@ public final class Ac3Util {
|
||||
boolean isEac3 = data.readBits(5) == 16;
|
||||
data.setPosition(initialPosition);
|
||||
String mimeType;
|
||||
int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED;
|
||||
int streamType = STREAM_TYPE_UNDEFINED;
|
||||
int sampleRate;
|
||||
int acmod;
|
||||
int frameSize;
|
||||
int sampleCount;
|
||||
boolean lfeon;
|
||||
int channelCount;
|
||||
if (isEac3) {
|
||||
mimeType = MimeTypes.AUDIO_E_AC3;
|
||||
// Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2.
|
||||
data.skipBits(16); // syncword
|
||||
streamType = data.readBits(2);
|
||||
data.skipBits(3); // substreamid
|
||||
frameSize = (data.readBits(11) + 1) * 2;
|
||||
int fscod = data.readBits(2);
|
||||
int audioBlocks;
|
||||
int numblkscod;
|
||||
if (fscod == 3) {
|
||||
numblkscod = 3;
|
||||
sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)];
|
||||
audioBlocks = 6;
|
||||
} else {
|
||||
int numblkscod = data.readBits(2);
|
||||
numblkscod = data.readBits(2);
|
||||
audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod];
|
||||
sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
|
||||
}
|
||||
sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks;
|
||||
acmod = data.readBits(3);
|
||||
lfeon = data.readBit();
|
||||
channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
|
||||
data.skipBits(5 + 5); // bsid, dialnorm
|
||||
if (data.readBit()) { // compre
|
||||
data.skipBits(8); // compr
|
||||
}
|
||||
if (acmod == 0) {
|
||||
data.skipBits(5); // dialnorm2
|
||||
if (data.readBit()) { // compr2e
|
||||
data.skipBits(8); // compr2
|
||||
}
|
||||
}
|
||||
if (streamType == STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape
|
||||
data.skipBits(16); // chanmap
|
||||
}
|
||||
if (data.readBit()) { // mixmdate
|
||||
if (acmod > 2) {
|
||||
data.skipBits(2); // dmixmod
|
||||
}
|
||||
if ((acmod & 0x01) != 0 && acmod > 2) {
|
||||
data.skipBits(3 + 3); // ltrtcmixlev, lorocmixlev
|
||||
}
|
||||
if ((acmod & 0x04) != 0) {
|
||||
data.skipBits(6); // ltrtsurmixlev, lorosurmixlev
|
||||
}
|
||||
if (lfeon && data.readBit()) { // lfemixlevcode
|
||||
data.skipBits(5); // lfemixlevcod
|
||||
}
|
||||
if (streamType == STREAM_TYPE_TYPE0) {
|
||||
if (data.readBit()) { // pgmscle
|
||||
data.skipBits(6); //pgmscl
|
||||
}
|
||||
if (acmod == 0 && data.readBit()) { // pgmscl2e
|
||||
data.skipBits(6); // pgmscl2
|
||||
}
|
||||
if (data.readBit()) { // extpgmscle
|
||||
data.skipBits(6); // extpgmscl
|
||||
}
|
||||
int mixdef = data.readBits(2);
|
||||
if (mixdef == 1) {
|
||||
data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl
|
||||
} else if (mixdef == 2) {
|
||||
data.skipBits(12); // mixdata
|
||||
} else if (mixdef == 3) {
|
||||
int mixdeflen = data.readBits(5);
|
||||
if (data.readBit()) { // mixdata2e
|
||||
data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl
|
||||
if (data.readBit()) { // extpgmlscle
|
||||
data.skipBits(4); // extpgmlscl
|
||||
}
|
||||
if (data.readBit()) { // extpgmcscle
|
||||
data.skipBits(4); // extpgmcscl
|
||||
}
|
||||
if (data.readBit()) { // extpgmrscle
|
||||
data.skipBits(4); // extpgmrscl
|
||||
}
|
||||
if (data.readBit()) { // extpgmlsscle
|
||||
data.skipBits(4); // extpgmlsscl
|
||||
}
|
||||
if (data.readBit()) { // extpgmrsscle
|
||||
data.skipBits(4); // extpgmrsscl
|
||||
}
|
||||
if (data.readBit()) { // extpgmlfescle
|
||||
data.skipBits(4); // extpgmlfescl
|
||||
}
|
||||
if (data.readBit()) { // dmixscle
|
||||
data.skipBits(4); // dmixscl
|
||||
}
|
||||
if (data.readBit()) { // addche
|
||||
if (data.readBit()) { // extpgmaux1scle
|
||||
data.skipBits(4); // extpgmaux1scl
|
||||
}
|
||||
if (data.readBit()) { // extpgmaux2scle
|
||||
data.skipBits(4); // extpgmaux2scl
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.readBit()) { // mixdata3e
|
||||
data.skipBits(5); // spchdat
|
||||
if (data.readBit()) { // addspchdate
|
||||
data.skipBits(5 + 2); // spchdat1, spchan1att
|
||||
if (data.readBit()) { // addspdat1e
|
||||
data.skipBits(5 + 3); // spchdat2, spchan2att
|
||||
}
|
||||
}
|
||||
}
|
||||
data.skipBits(8 * (mixdeflen + 2)); // mixdata
|
||||
data.byteAlign(); // mixdatafill
|
||||
}
|
||||
if (acmod < 2) {
|
||||
if (data.readBit()) { // paninfoe
|
||||
data.skipBits(8 + 6); // panmean, paninfo
|
||||
}
|
||||
if (acmod == 0) {
|
||||
if (data.readBit()) { // paninfo2e
|
||||
data.skipBits(8 + 6); // panmean2, paninfo2
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.readBit()) { // frmmixcfginfoe
|
||||
if (numblkscod == 0) {
|
||||
data.skipBits(5); // blkmixcfginfo[0]
|
||||
} else {
|
||||
for (int blk = 0; blk < audioBlocks; blk++) {
|
||||
if (data.readBit()) { // blkmixcfginfoe
|
||||
data.skipBits(5); // blkmixcfginfo[blk]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.readBit()) { // infomdate
|
||||
data.skipBits(3 + 1 + 1); // bsmod, copyrightb, origbs
|
||||
if (acmod == 2) {
|
||||
data.skipBits(2 + 2); // dsurmod, dheadphonmod
|
||||
}
|
||||
if (acmod >= 6) {
|
||||
data.skipBits(2); // dsurexmod
|
||||
}
|
||||
if (data.readBit()) { // audioprodie
|
||||
data.skipBits(5 + 2 + 1); // mixlevel, roomtyp, adconvtyp
|
||||
}
|
||||
if (acmod == 0 && data.readBit()) { // audioprodi2e
|
||||
data.skipBits(5 + 2 + 1); // mixlevel2, roomtyp2, adconvtyp2
|
||||
}
|
||||
if (fscod < 3) {
|
||||
data.skipBit(); // sourcefscod
|
||||
}
|
||||
}
|
||||
if (streamType == 0 && numblkscod != 3) {
|
||||
data.skipBit(); // convsync
|
||||
}
|
||||
if (streamType == 2 && (numblkscod == 3 || data.readBit())) { // blkid
|
||||
data.skipBits(6); // frmsizecod
|
||||
}
|
||||
mimeType = MimeTypes.AUDIO_E_AC3;
|
||||
if (data.readBit()) { // addbsie
|
||||
int addbsil = data.readBits(6);
|
||||
if (addbsil == 1 && data.readBits(8) == 1) { // addbsi
|
||||
mimeType = MimeTypes.AUDIO_ATMOS;
|
||||
}
|
||||
}
|
||||
} else /* is AC-3 */ {
|
||||
mimeType = MimeTypes.AUDIO_AC3;
|
||||
data.skipBits(16 + 16); // syncword, crc1
|
||||
@ -240,9 +398,9 @@ public final class Ac3Util {
|
||||
}
|
||||
sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
|
||||
sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
|
||||
lfeon = data.readBit();
|
||||
channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
|
||||
}
|
||||
boolean lfeon = data.readBit();
|
||||
int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0);
|
||||
return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize,
|
||||
sampleCount);
|
||||
}
|
||||
|
@ -25,14 +25,13 @@ import java.nio.ByteBuffer;
|
||||
* A sink that consumes audio data.
|
||||
* <p>
|
||||
* Before starting playback, specify the input audio format by calling
|
||||
* {@link #configure(String, int, int, int, int, int[], int, int)}.
|
||||
* {@link #configure(int, int, int, int, int[], int, int)}.
|
||||
* <p>
|
||||
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
|
||||
* when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
|
||||
* <p>
|
||||
* Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format
|
||||
* changes. The sink will be reinitialized on the next call to
|
||||
* {@link #handleBuffer(ByteBuffer, long)}.
|
||||
* Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format changes.
|
||||
* The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
|
||||
* <p>
|
||||
* Call {@link #reset()} to prepare the sink to receive audio data from a new playback position.
|
||||
* <p>
|
||||
@ -76,7 +75,7 @@ public interface AudioSink {
|
||||
*
|
||||
* @param bufferSize The size of the sink's buffer, in bytes.
|
||||
* @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for
|
||||
* PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the
|
||||
* PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the
|
||||
* buffered media can have a variable bitrate so the duration may be unknown.
|
||||
* @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds.
|
||||
*/
|
||||
@ -166,13 +165,12 @@ public interface AudioSink {
|
||||
void setListener(Listener listener);
|
||||
|
||||
/**
|
||||
* Returns whether it's possible to play audio in the specified format using encoded audio
|
||||
* passthrough.
|
||||
* Returns whether it's possible to play audio in the specified encoding.
|
||||
*
|
||||
* @param mimeType The format mime type.
|
||||
* @return Whether it's possible to play audio in the format using encoded audio passthrough.
|
||||
* @param encoding The audio encoding.
|
||||
* @return Whether it's possible to play audio in the specified encoding.
|
||||
*/
|
||||
boolean isPassthroughSupported(String mimeType);
|
||||
boolean isEncodingSupported(@C.Encoding int encoding);
|
||||
|
||||
/**
|
||||
* Returns the playback position in the stream starting at zero, in microseconds, or
|
||||
@ -186,12 +184,9 @@ public interface AudioSink {
|
||||
/**
|
||||
* Configures (or reconfigures) the sink.
|
||||
*
|
||||
* @param inputMimeType The MIME type of audio data provided in the input buffers.
|
||||
* @param inputEncoding The encoding of audio data provided in the input buffers.
|
||||
* @param inputChannelCount The number of channels.
|
||||
* @param inputSampleRate The sample rate in Hz.
|
||||
* @param inputPcmEncoding For PCM formats, the encoding used. One of
|
||||
* {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT}
|
||||
* and {@link C#ENCODING_PCM_32BIT}.
|
||||
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
|
||||
* suitable buffer size.
|
||||
* @param outputChannels A mapping from input to output channels that is applied to this sink's
|
||||
@ -205,9 +200,9 @@ public interface AudioSink {
|
||||
* immediately preceding the next call to {@link #reset()} or this method.
|
||||
* @throws ConfigurationException If an error occurs configuring the sink.
|
||||
*/
|
||||
void configure(String inputMimeType, int inputChannelCount, int inputSampleRate,
|
||||
@C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels,
|
||||
int trimStartSamples, int trimEndSamples) throws ConfigurationException;
|
||||
void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate,
|
||||
int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples,
|
||||
int trimEndSamples) throws ConfigurationException;
|
||||
|
||||
/**
|
||||
* Starts or resumes consuming audio if initialized.
|
||||
@ -228,8 +223,7 @@ public interface AudioSink {
|
||||
* Returns whether the data was handled in full. If the data was not handled in full then the same
|
||||
* {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
|
||||
* except in the case of an intervening call to {@link #reset()} (or to
|
||||
* {@link #configure(String, int, int, int, int, int[], int, int)} that causes the sink to be
|
||||
* reset).
|
||||
* {@link #configure(int, int, int, int, int[], int, int)} that causes the sink to be reset).
|
||||
*
|
||||
* @param buffer The buffer containing audio data.
|
||||
* @param presentationTimeUs The presentation timestamp of the buffer in microseconds.
|
||||
|
@ -51,8 +51,6 @@ import java.util.Arrays;
|
||||
/**
|
||||
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
|
||||
* to start using the new channel map.
|
||||
*
|
||||
* @see AudioSink#configure(String, int, int, int, int, int[], int, int)
|
||||
*/
|
||||
public void setChannelMap(int[] outputChannels) {
|
||||
pendingOutputChannels = outputChannels;
|
||||
|
@ -29,15 +29,14 @@ import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback
|
||||
@ -174,7 +173,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
private final ConditionVariable releasingConditionVariable;
|
||||
private final long[] playheadOffsets;
|
||||
private final AudioTrackUtil audioTrackUtil;
|
||||
private final LinkedList<PlaybackParametersCheckpoint> playbackParametersCheckpoints;
|
||||
private final ArrayDeque<PlaybackParametersCheckpoint> playbackParametersCheckpoints;
|
||||
|
||||
@Nullable private Listener listener;
|
||||
/**
|
||||
@ -182,13 +181,13 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
*/
|
||||
private AudioTrack keepSessionIdAudioTrack;
|
||||
private AudioTrack audioTrack;
|
||||
private boolean isInputPcm;
|
||||
private int inputSampleRate;
|
||||
private int sampleRate;
|
||||
private int channelConfig;
|
||||
private @C.Encoding int encoding;
|
||||
private @C.Encoding int outputEncoding;
|
||||
private AudioAttributes audioAttributes;
|
||||
private boolean passthrough;
|
||||
private boolean processingEnabled;
|
||||
private int bufferSize;
|
||||
private long bufferSizeUs;
|
||||
|
||||
@ -277,7 +276,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
drainingAudioProcessorIndex = C.INDEX_UNSET;
|
||||
this.audioProcessors = new AudioProcessor[0];
|
||||
outputBuffers = new ByteBuffer[0];
|
||||
playbackParametersCheckpoints = new LinkedList<>();
|
||||
playbackParametersCheckpoints = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -286,9 +285,15 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPassthroughSupported(String mimeType) {
|
||||
return audioCapabilities != null
|
||||
&& audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType));
|
||||
public boolean isEncodingSupported(@C.Encoding int encoding) {
|
||||
if (isEncodingPcm(encoding)) {
|
||||
// AudioTrack supports 16-bit integer PCM output in all platform API versions, and float
|
||||
// output from platform API version 21 only. Other integer PCM encodings are resampled by this
|
||||
// sink to 16-bit PCM.
|
||||
return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21;
|
||||
} else {
|
||||
return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -331,18 +336,20 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(String inputMimeType, int inputChannelCount, int inputSampleRate,
|
||||
@C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels,
|
||||
int trimStartSamples, int trimEndSamples) throws ConfigurationException {
|
||||
public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate,
|
||||
int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples,
|
||||
int trimEndSamples) throws ConfigurationException {
|
||||
boolean flush = false;
|
||||
this.inputSampleRate = inputSampleRate;
|
||||
int channelCount = inputChannelCount;
|
||||
int sampleRate = inputSampleRate;
|
||||
@C.Encoding int encoding;
|
||||
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(inputMimeType);
|
||||
boolean flush = false;
|
||||
if (!passthrough) {
|
||||
encoding = inputPcmEncoding;
|
||||
pcmFrameSize = Util.getPcmFrameSize(inputPcmEncoding, channelCount);
|
||||
isInputPcm = isEncodingPcm(inputEncoding);
|
||||
if (isInputPcm) {
|
||||
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
|
||||
}
|
||||
@C.Encoding int encoding = inputEncoding;
|
||||
boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT;
|
||||
if (processingEnabled) {
|
||||
trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples);
|
||||
channelMappingAudioProcessor.setChannelMap(outputChannels);
|
||||
for (AudioProcessor audioProcessor : availableAudioProcessors) {
|
||||
@ -357,11 +364,6 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
encoding = audioProcessor.getOutputEncoding();
|
||||
}
|
||||
}
|
||||
if (flush) {
|
||||
resetAudioProcessors();
|
||||
}
|
||||
} else {
|
||||
encoding = getEncodingForMimeType(inputMimeType);
|
||||
}
|
||||
|
||||
int channelConfig;
|
||||
@ -411,11 +413,11 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
|
||||
// Workaround for Nexus Player not reporting support for mono passthrough.
|
||||
// (See [Internal: b/34268671].)
|
||||
if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) {
|
||||
if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) {
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
}
|
||||
|
||||
if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate
|
||||
if (!flush && isInitialized() && outputEncoding == encoding && this.sampleRate == sampleRate
|
||||
&& this.channelConfig == channelConfig) {
|
||||
// We already have an audio track with the correct sample rate, channel config and encoding.
|
||||
return;
|
||||
@ -423,16 +425,24 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
|
||||
reset();
|
||||
|
||||
this.encoding = encoding;
|
||||
this.passthrough = passthrough;
|
||||
this.processingEnabled = processingEnabled;
|
||||
this.sampleRate = sampleRate;
|
||||
this.channelConfig = channelConfig;
|
||||
outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT;
|
||||
outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount);
|
||||
|
||||
outputEncoding = encoding;
|
||||
if (isInputPcm) {
|
||||
outputPcmFrameSize = Util.getPcmFrameSize(outputEncoding, channelCount);
|
||||
}
|
||||
if (specifiedBufferSize != 0) {
|
||||
bufferSize = specifiedBufferSize;
|
||||
} else if (passthrough) {
|
||||
} else if (isInputPcm) {
|
||||
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding);
|
||||
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
|
||||
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
|
||||
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
|
||||
int maxAppBufferSize = (int) Math.max(minBufferSize,
|
||||
durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
|
||||
bufferSize = Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize);
|
||||
} else {
|
||||
// TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into
|
||||
// account. [Internal: b/25181305]
|
||||
if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) {
|
||||
@ -442,21 +452,9 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
// DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.
|
||||
bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND);
|
||||
}
|
||||
} else {
|
||||
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding);
|
||||
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
|
||||
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
|
||||
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
|
||||
int maxAppBufferSize = (int) Math.max(minBufferSize,
|
||||
durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
|
||||
bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize
|
||||
: multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize
|
||||
: multipliedBufferSize;
|
||||
}
|
||||
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize);
|
||||
|
||||
// The old playback parameters may no longer be applicable so try to reset them now.
|
||||
setPlaybackParameters(playbackParameters);
|
||||
bufferSizeUs =
|
||||
isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private void resetAudioProcessors() {
|
||||
@ -487,6 +485,13 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
releasingConditionVariable.block();
|
||||
|
||||
audioTrack = initializeAudioTrack();
|
||||
|
||||
// The old playback parameters may no longer be applicable so try to reset them now.
|
||||
setPlaybackParameters(playbackParameters);
|
||||
|
||||
// Flush and reset active audio processors.
|
||||
resetAudioProcessors();
|
||||
|
||||
int audioSessionId = audioTrack.getAudioSessionId();
|
||||
if (enablePreV21AudioSessionWorkaround) {
|
||||
if (Util.SDK_INT < 21) {
|
||||
@ -574,7 +579,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (passthrough && framesPerEncodedSample == 0) {
|
||||
if (!isInputPcm && framesPerEncodedSample == 0) {
|
||||
// If this is the first encoded sample, calculate the sample size in frames.
|
||||
framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer);
|
||||
}
|
||||
@ -618,20 +623,19 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
}
|
||||
|
||||
if (passthrough) {
|
||||
submittedEncodedFrames += framesPerEncodedSample;
|
||||
} else {
|
||||
if (isInputPcm) {
|
||||
submittedPcmBytes += buffer.remaining();
|
||||
} else {
|
||||
submittedEncodedFrames += framesPerEncodedSample;
|
||||
}
|
||||
|
||||
inputBuffer = buffer;
|
||||
}
|
||||
|
||||
if (passthrough) {
|
||||
// Passthrough buffers are not processed.
|
||||
writeBuffer(inputBuffer, presentationTimeUs);
|
||||
} else {
|
||||
if (processingEnabled) {
|
||||
processBuffers(presentationTimeUs);
|
||||
} else {
|
||||
writeBuffer(inputBuffer, presentationTimeUs);
|
||||
}
|
||||
|
||||
if (!inputBuffer.hasRemaining()) {
|
||||
@ -679,10 +683,9 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs)
|
||||
throws WriteException {
|
||||
private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (outputBuffer != null) {
|
||||
Assertions.checkArgument(outputBuffer == buffer);
|
||||
@ -701,7 +704,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
int bytesRemaining = buffer.remaining();
|
||||
int bytesWritten = 0;
|
||||
if (Util.SDK_INT < 21) { // passthrough == false
|
||||
if (Util.SDK_INT < 21) { // isInputPcm == true
|
||||
// Work out how many bytes we can write without the risk of blocking.
|
||||
int bytesPending =
|
||||
(int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize));
|
||||
@ -728,17 +731,15 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
throw new WriteException(bytesWritten);
|
||||
}
|
||||
|
||||
if (!passthrough) {
|
||||
if (isInputPcm) {
|
||||
writtenPcmBytes += bytesWritten;
|
||||
}
|
||||
if (bytesWritten == bytesRemaining) {
|
||||
if (passthrough) {
|
||||
if (!isInputPcm) {
|
||||
writtenEncodedFrames += framesPerEncodedSample;
|
||||
}
|
||||
outputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -758,7 +759,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
private boolean drainAudioProcessorsToEndOfStream() throws WriteException {
|
||||
boolean audioProcessorNeedsEndOfStream = false;
|
||||
if (drainingAudioProcessorIndex == C.INDEX_UNSET) {
|
||||
drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0;
|
||||
drainingAudioProcessorIndex = processingEnabled ? 0 : audioProcessors.length;
|
||||
audioProcessorNeedsEndOfStream = true;
|
||||
}
|
||||
while (drainingAudioProcessorIndex < audioProcessors.length) {
|
||||
@ -799,8 +800,8 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
|
||||
@Override
|
||||
public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||
if (passthrough) {
|
||||
// The playback parameters are always the default in passthrough mode.
|
||||
if (isInitialized() && !processingEnabled) {
|
||||
// The playback parameters are always the default if processing is disabled.
|
||||
this.playbackParameters = PlaybackParameters.DEFAULT;
|
||||
return this.playbackParameters;
|
||||
}
|
||||
@ -1076,7 +1077,7 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
audioTimestampSet = false;
|
||||
}
|
||||
}
|
||||
if (getLatencyMethod != null && !passthrough) {
|
||||
if (getLatencyMethod != null && isInputPcm) {
|
||||
try {
|
||||
// Compute the audio track latency, excluding the latency due to the buffer (leaving
|
||||
// latency due to the mixer and audio hardware driver).
|
||||
@ -1115,11 +1116,11 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
}
|
||||
|
||||
private long getSubmittedFrames() {
|
||||
return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize);
|
||||
return isInputPcm ? (submittedPcmBytes / pcmFrameSize) : submittedEncodedFrames;
|
||||
}
|
||||
|
||||
private long getWrittenFrames() {
|
||||
return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize);
|
||||
return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames;
|
||||
}
|
||||
|
||||
private void resetSyncParams() {
|
||||
@ -1212,20 +1213,10 @@ public final class DefaultAudioSink implements AudioSink {
|
||||
MODE_STATIC, audioSessionId);
|
||||
}
|
||||
|
||||
@C.Encoding
|
||||
private static int getEncodingForMimeType(String mimeType) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.AUDIO_AC3:
|
||||
return C.ENCODING_AC3;
|
||||
case MimeTypes.AUDIO_E_AC3:
|
||||
return C.ENCODING_E_AC3;
|
||||
case MimeTypes.AUDIO_DTS:
|
||||
return C.ENCODING_DTS;
|
||||
case MimeTypes.AUDIO_DTS_HD:
|
||||
return C.ENCODING_DTS_HD;
|
||||
default:
|
||||
return C.ENCODING_INVALID;
|
||||
}
|
||||
private static boolean isEncodingPcm(@C.Encoding int encoding) {
|
||||
return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT
|
||||
|| encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT
|
||||
|| encoding == C.ENCODING_PCM_FLOAT;
|
||||
}
|
||||
|
||||
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
|
||||
|
@ -51,6 +51,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
private boolean passthroughEnabled;
|
||||
private boolean codecNeedsDiscardChannelsWorkaround;
|
||||
private android.media.MediaFormat passthroughMediaFormat;
|
||||
@C.Encoding
|
||||
private int pcmEncoding;
|
||||
private int channelCount;
|
||||
private int encoderDelay;
|
||||
@ -177,6 +178,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
&& mediaCodecSelector.getPassthroughDecoderInfo() != null) {
|
||||
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
|
||||
}
|
||||
if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.isEncodingSupported(format.pcmEncoding))
|
||||
|| !audioSink.isEncodingSupported(C.ENCODING_PCM_16BIT)) {
|
||||
// Assume the decoder outputs 16-bit PCM, unless the input is raw.
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
}
|
||||
boolean requiresSecureDecryption = false;
|
||||
DrmInitData drmInitData = format.drmInitData;
|
||||
if (drmInitData != null) {
|
||||
@ -219,14 +225,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
|
||||
/**
|
||||
* Returns whether encoded audio passthrough should be used for playing back the input format.
|
||||
* This implementation returns true if the {@link AudioSink} indicates that passthrough is
|
||||
* supported.
|
||||
* This implementation returns true if the {@link AudioSink} indicates that encoded audio output
|
||||
* is supported.
|
||||
*
|
||||
* @param mimeType The type of input media.
|
||||
* @return Whether passthrough playback is supported.
|
||||
*/
|
||||
protected boolean allowPassthrough(String mimeType) {
|
||||
return audioSink.isPassthroughSupported(mimeType);
|
||||
@C.Encoding int encoding = MimeTypes.getEncoding(mimeType);
|
||||
return encoding != C.ENCODING_INVALID && audioSink.isEncodingSupported(encoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -272,10 +279,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
@Override
|
||||
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
|
||||
throws ExoPlaybackException {
|
||||
boolean passthrough = passthroughMediaFormat != null;
|
||||
String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME)
|
||||
: MimeTypes.AUDIO_RAW;
|
||||
MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
|
||||
@C.Encoding int encoding;
|
||||
MediaFormat format;
|
||||
if (passthroughMediaFormat != null) {
|
||||
encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME));
|
||||
format = passthroughMediaFormat;
|
||||
} else {
|
||||
encoding = pcmEncoding;
|
||||
format = outputFormat;
|
||||
}
|
||||
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||
int[] channelMap;
|
||||
@ -289,8 +301,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
}
|
||||
|
||||
try {
|
||||
audioSink.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap,
|
||||
encoderDelay, encoderPadding);
|
||||
audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay,
|
||||
encoderPadding);
|
||||
} catch (AudioSink.ConfigurationException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
|
@ -200,6 +200,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
protected abstract int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
Format format);
|
||||
|
||||
/**
|
||||
* Returns whether the audio sink can accept audio in the specified encoding.
|
||||
*
|
||||
* @param encoding The audio encoding.
|
||||
* @return Whether the audio sink can accept audio in the specified encoding.
|
||||
*/
|
||||
protected final boolean supportsOutputEncoding(@C.Encoding int encoding) {
|
||||
return audioSink.isEncodingSupported(encoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
if (outputStreamEnded) {
|
||||
@ -329,8 +339,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
|
||||
if (audioTrackNeedsConfigure) {
|
||||
Format outputFormat = getOutputFormat();
|
||||
audioSink.configure(outputFormat.sampleMimeType, outputFormat.channelCount,
|
||||
outputFormat.sampleRate, outputFormat.pcmEncoding, 0, null, encoderDelay, encoderPadding);
|
||||
audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount,
|
||||
outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding);
|
||||
audioTrackNeedsConfigure = false;
|
||||
}
|
||||
|
||||
|
@ -28,13 +28,24 @@ public interface SeekMap {
|
||||
final class Unseekable implements SeekMap {
|
||||
|
||||
private final long durationUs;
|
||||
private final long startPosition;
|
||||
|
||||
/**
|
||||
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
|
||||
* the duration is unknown.
|
||||
*/
|
||||
public Unseekable(long durationUs) {
|
||||
this(durationUs, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
|
||||
* the duration is unknown.
|
||||
* @param startPosition The position (byte offset) of the start of the media.
|
||||
*/
|
||||
public Unseekable(long durationUs, long startPosition) {
|
||||
this.durationUs = durationUs;
|
||||
this.startPosition = startPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -49,7 +60,7 @@ public interface SeekMap {
|
||||
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
return 0;
|
||||
return startPosition;
|
||||
}
|
||||
|
||||
}
|
||||
@ -78,7 +89,8 @@ public interface SeekMap {
|
||||
*
|
||||
* @param timeUs A seek position in microseconds.
|
||||
* @return The corresponding position (byte offset) in the stream from which data can be provided
|
||||
* to the extractor, or 0 if {@code #isSeekable()} returns false.
|
||||
* to the extractor. If {@link #isSeekable()} returns false then the returned value will be
|
||||
* independent of {@code timeUs}, and will indicate the start of the media in the stream.
|
||||
*/
|
||||
long getPosition(long timeUs);
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor.flv;
|
||||
|
||||
import android.support.annotation.IntDef;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
@ -25,11 +26,13 @@ import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Facilitates the extraction of data from the FLV container format.
|
||||
* Extracts data from the FLV container format.
|
||||
*/
|
||||
public final class FlvExtractor implements Extractor, SeekMap {
|
||||
public final class FlvExtractor implements Extractor {
|
||||
|
||||
/**
|
||||
* Factory for {@link FlvExtractor} instances.
|
||||
@ -43,16 +46,22 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
|
||||
};
|
||||
|
||||
// Header sizes.
|
||||
private static final int FLV_HEADER_SIZE = 9;
|
||||
private static final int FLV_TAG_HEADER_SIZE = 11;
|
||||
|
||||
// Parser states.
|
||||
/**
|
||||
* Extractor states.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({STATE_READING_FLV_HEADER, STATE_SKIPPING_TO_TAG_HEADER, STATE_READING_TAG_HEADER,
|
||||
STATE_READING_TAG_DATA})
|
||||
private @interface States {}
|
||||
private static final int STATE_READING_FLV_HEADER = 1;
|
||||
private static final int STATE_SKIPPING_TO_TAG_HEADER = 2;
|
||||
private static final int STATE_READING_TAG_HEADER = 3;
|
||||
private static final int STATE_READING_TAG_DATA = 4;
|
||||
|
||||
// Header sizes.
|
||||
private static final int FLV_HEADER_SIZE = 9;
|
||||
private static final int FLV_TAG_HEADER_SIZE = 11;
|
||||
|
||||
// Tag types.
|
||||
private static final int TAG_TYPE_AUDIO = 8;
|
||||
private static final int TAG_TYPE_VIDEO = 9;
|
||||
@ -61,33 +70,31 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
// FLV container identifier.
|
||||
private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
|
||||
|
||||
// Temporary buffers.
|
||||
private final ParsableByteArray scratch;
|
||||
private final ParsableByteArray headerBuffer;
|
||||
private final ParsableByteArray tagHeaderBuffer;
|
||||
private final ParsableByteArray tagData;
|
||||
private final ScriptTagPayloadReader metadataReader;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
|
||||
// State variables.
|
||||
private int parserState;
|
||||
private @States int state;
|
||||
private long mediaTagTimestampOffsetUs;
|
||||
private int bytesToNextTagHeader;
|
||||
public int tagType;
|
||||
public int tagDataSize;
|
||||
public long tagTimestampUs;
|
||||
|
||||
// Tags readers.
|
||||
private int tagType;
|
||||
private int tagDataSize;
|
||||
private long tagTimestampUs;
|
||||
private boolean outputSeekMap;
|
||||
private AudioTagPayloadReader audioReader;
|
||||
private VideoTagPayloadReader videoReader;
|
||||
private ScriptTagPayloadReader metadataReader;
|
||||
|
||||
public FlvExtractor() {
|
||||
scratch = new ParsableByteArray(4);
|
||||
headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
|
||||
tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
|
||||
tagData = new ParsableByteArray();
|
||||
parserState = STATE_READING_FLV_HEADER;
|
||||
metadataReader = new ScriptTagPayloadReader();
|
||||
state = STATE_READING_FLV_HEADER;
|
||||
mediaTagTimestampOffsetUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -128,7 +135,8 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
|
||||
@Override
|
||||
public void seek(long position, long timeUs) {
|
||||
parserState = STATE_READING_FLV_HEADER;
|
||||
state = STATE_READING_FLV_HEADER;
|
||||
mediaTagTimestampOffsetUs = C.TIME_UNSET;
|
||||
bytesToNextTagHeader = 0;
|
||||
}
|
||||
|
||||
@ -141,7 +149,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
|
||||
InterruptedException {
|
||||
while (true) {
|
||||
switch (parserState) {
|
||||
switch (state) {
|
||||
case STATE_READING_FLV_HEADER:
|
||||
if (!readFlvHeader(input)) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
@ -160,6 +168,9 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
return RESULT_CONTINUE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Never happens.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -191,15 +202,11 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
videoReader = new VideoTagPayloadReader(
|
||||
extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
|
||||
}
|
||||
if (metadataReader == null) {
|
||||
metadataReader = new ScriptTagPayloadReader(null);
|
||||
}
|
||||
extractorOutput.endTracks();
|
||||
extractorOutput.seekMap(this);
|
||||
|
||||
// We need to skip any additional content in the FLV header, plus the 4 byte previous tag size.
|
||||
bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4;
|
||||
parserState = STATE_SKIPPING_TO_TAG_HEADER;
|
||||
state = STATE_SKIPPING_TO_TAG_HEADER;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -213,7 +220,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||
input.skipFully(bytesToNextTagHeader);
|
||||
bytesToNextTagHeader = 0;
|
||||
parserState = STATE_READING_TAG_HEADER;
|
||||
state = STATE_READING_TAG_HEADER;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -236,7 +243,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
tagTimestampUs = tagHeaderBuffer.readUnsignedInt24();
|
||||
tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L;
|
||||
tagHeaderBuffer.skipBytes(3); // streamId
|
||||
parserState = STATE_READING_TAG_DATA;
|
||||
state = STATE_READING_TAG_DATA;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -251,17 +258,24 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
|
||||
boolean wasConsumed = true;
|
||||
if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
|
||||
audioReader.consume(prepareTagData(input), tagTimestampUs);
|
||||
ensureReadyForMediaOutput();
|
||||
audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs);
|
||||
} else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
|
||||
videoReader.consume(prepareTagData(input), tagTimestampUs);
|
||||
} else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
|
||||
ensureReadyForMediaOutput();
|
||||
videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs);
|
||||
} else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) {
|
||||
metadataReader.consume(prepareTagData(input), tagTimestampUs);
|
||||
long durationUs = metadataReader.getDurationUs();
|
||||
if (durationUs != C.TIME_UNSET) {
|
||||
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
|
||||
outputSeekMap = true;
|
||||
}
|
||||
} else {
|
||||
input.skipFully(tagDataSize);
|
||||
wasConsumed = false;
|
||||
}
|
||||
bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
|
||||
parserState = STATE_SKIPPING_TO_TAG_HEADER;
|
||||
state = STATE_SKIPPING_TO_TAG_HEADER;
|
||||
return wasConsumed;
|
||||
}
|
||||
|
||||
@ -277,21 +291,15 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
||||
return tagData;
|
||||
}
|
||||
|
||||
// SeekMap implementation.
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
return metadataReader.getDurationUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
return 0;
|
||||
private void ensureReadyForMediaOutput() {
|
||||
if (!outputSeekMap) {
|
||||
extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
||||
outputSeekMap = true;
|
||||
}
|
||||
if (mediaTagTimestampOffsetUs == C.TIME_UNSET) {
|
||||
mediaTagTimestampOffsetUs =
|
||||
metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.flv;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
@ -44,11 +43,8 @@ import java.util.Map;
|
||||
|
||||
private long durationUs;
|
||||
|
||||
/**
|
||||
* @param output A {@link TrackOutput} to which samples should be written.
|
||||
*/
|
||||
public ScriptTagPayloadReader(TrackOutput output) {
|
||||
super(output);
|
||||
public ScriptTagPayloadReader() {
|
||||
super(null);
|
||||
durationUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Extracts data from a Matroska or WebM file.
|
||||
* Extracts data from the Matroska and WebM container formats.
|
||||
*/
|
||||
public final class MatroskaExtractor implements Extractor {
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.extractor.mp3;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
@ -26,27 +27,46 @@ import com.google.android.exoplayer2.util.Util;
|
||||
private static final int BITS_PER_BYTE = 8;
|
||||
|
||||
private final long firstFramePosition;
|
||||
private final int frameSize;
|
||||
private final long dataSize;
|
||||
private final int bitrate;
|
||||
private final long durationUs;
|
||||
|
||||
public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) {
|
||||
/**
|
||||
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
|
||||
* @param firstFramePosition The position of the first frame in the stream.
|
||||
* @param mpegAudioHeader The MPEG audio header associated with the first frame.
|
||||
*/
|
||||
public ConstantBitrateSeeker(long inputLength, long firstFramePosition,
|
||||
MpegAudioHeader mpegAudioHeader) {
|
||||
this.firstFramePosition = firstFramePosition;
|
||||
this.bitrate = bitrate;
|
||||
durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength);
|
||||
this.frameSize = mpegAudioHeader.frameSize;
|
||||
this.bitrate = mpegAudioHeader.bitrate;
|
||||
if (inputLength == C.LENGTH_UNSET) {
|
||||
dataSize = C.LENGTH_UNSET;
|
||||
durationUs = C.TIME_UNSET;
|
||||
} else {
|
||||
dataSize = inputLength - firstFramePosition;
|
||||
durationUs = getTimeUs(inputLength);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return durationUs != C.TIME_UNSET;
|
||||
return dataSize != C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
if (durationUs == C.TIME_UNSET) {
|
||||
return 0;
|
||||
if (dataSize == C.LENGTH_UNSET) {
|
||||
return firstFramePosition;
|
||||
}
|
||||
timeUs = Util.constrainValue(timeUs, 0, durationUs);
|
||||
return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
|
||||
long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
|
||||
// Constrain to nearest preceding frame offset.
|
||||
positionOffset = (positionOffset / frameSize) * frameSize;
|
||||
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize);
|
||||
// Add data start position.
|
||||
return firstFramePosition + positionOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -38,7 +38,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Extracts data from an MP3 file.
|
||||
* Extracts data from the MP3 container format.
|
||||
*/
|
||||
public final class Mp3Extractor implements Extractor {
|
||||
|
||||
@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor {
|
||||
int seekHeader = getSeekFrameHeader(frame, xingBase);
|
||||
Seeker seeker;
|
||||
if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) {
|
||||
seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
|
||||
seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
|
||||
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
|
||||
// If there is a Xing header, read gapless playback metadata at a fixed offset.
|
||||
input.resetPeekPosition();
|
||||
@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor {
|
||||
return getConstantBitrateSeeker(input);
|
||||
}
|
||||
} else if (seekHeader == SEEK_HEADER_VBRI) {
|
||||
seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
|
||||
seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
|
||||
input.skipFully(synchronizedHeader.frameSize);
|
||||
} else { // seekerHeader == SEEK_HEADER_UNSET
|
||||
// This frame doesn't contain seeking information, so reset the peek position.
|
||||
@ -393,8 +393,7 @@ public final class Mp3Extractor implements Extractor {
|
||||
input.peekFully(scratch.data, 0, 4);
|
||||
scratch.setPosition(0);
|
||||
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
|
||||
return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate,
|
||||
input.getLength());
|
||||
return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor.mp3;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
@ -25,21 +26,23 @@ import com.google.android.exoplayer2.util.Util;
|
||||
*/
|
||||
/* package */ final class VbriSeeker implements Mp3Extractor.Seeker {
|
||||
|
||||
private static final String TAG = "VbriSeeker";
|
||||
|
||||
/**
|
||||
* Returns a {@link VbriSeeker} for seeking in the stream, if required information is present.
|
||||
* Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
|
||||
* caller should reset it.
|
||||
*
|
||||
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
|
||||
* @param position The position of the start of this frame in the stream.
|
||||
* @param mpegAudioHeader The MPEG audio header associated with the frame.
|
||||
* @param frame The data in this audio frame, with its position set to immediately after the
|
||||
* 'VBRI' tag.
|
||||
* @param position The position (byte offset) of the start of this frame in the stream.
|
||||
* @param inputLength The length of the stream in bytes.
|
||||
* @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
|
||||
* information is not present.
|
||||
*/
|
||||
public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
|
||||
long position, long inputLength) {
|
||||
public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
|
||||
ParsableByteArray frame) {
|
||||
frame.skipBytes(10);
|
||||
int numFrames = frame.readInt();
|
||||
if (numFrames <= 0) {
|
||||
@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util;
|
||||
int entrySize = frame.readUnsignedShort();
|
||||
frame.skipBytes(2);
|
||||
|
||||
// Skip the frame containing the VBRI header.
|
||||
position += mpegAudioHeader.frameSize;
|
||||
|
||||
long minPosition = position + mpegAudioHeader.frameSize;
|
||||
// Read table of contents entries.
|
||||
long[] timesUs = new long[entryCount + 1];
|
||||
long[] positions = new long[entryCount + 1];
|
||||
timesUs[0] = 0L;
|
||||
positions[0] = position;
|
||||
for (int index = 1; index < timesUs.length; index++) {
|
||||
long[] timesUs = new long[entryCount];
|
||||
long[] positions = new long[entryCount];
|
||||
for (int index = 0; index < entryCount; index++) {
|
||||
timesUs[index] = (index * durationUs) / entryCount;
|
||||
// Ensure positions do not fall within the frame containing the VBRI header. This constraint
|
||||
// will normally only apply to the first entry in the table.
|
||||
positions[index] = Math.max(position, minPosition);
|
||||
int segmentSize;
|
||||
switch (entrySize) {
|
||||
case 1:
|
||||
@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util;
|
||||
return null;
|
||||
}
|
||||
position += segmentSize * scale;
|
||||
timesUs[index] = index * durationUs / entryCount;
|
||||
positions[index] =
|
||||
inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position);
|
||||
}
|
||||
if (inputLength != C.LENGTH_UNSET && inputLength != position) {
|
||||
Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position);
|
||||
}
|
||||
return new VbriSeeker(timesUs, positions, durationUs);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor.mp3;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
@ -25,24 +26,25 @@ import com.google.android.exoplayer2.util.Util;
|
||||
*/
|
||||
/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
|
||||
|
||||
private static final String TAG = "XingSeeker";
|
||||
|
||||
/**
|
||||
* Returns a {@link XingSeeker} for seeking in the stream, if required information is present.
|
||||
* Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
|
||||
* caller should reset it.
|
||||
*
|
||||
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
|
||||
* @param position The position of the start of this frame in the stream.
|
||||
* @param mpegAudioHeader The MPEG audio header associated with the frame.
|
||||
* @param frame The data in this audio frame, with its position set to immediately after the
|
||||
* 'Xing' or 'Info' tag.
|
||||
* @param position The position (byte offset) of the start of this frame in the stream.
|
||||
* @param inputLength The length of the stream in bytes.
|
||||
* @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
|
||||
* information is not present.
|
||||
*/
|
||||
public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
|
||||
long position, long inputLength) {
|
||||
public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
|
||||
ParsableByteArray frame) {
|
||||
int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
|
||||
int sampleRate = mpegAudioHeader.sampleRate;
|
||||
long firstFramePosition = position + mpegAudioHeader.frameSize;
|
||||
|
||||
int flags = frame.readInt();
|
||||
int frameCount;
|
||||
@ -54,45 +56,49 @@ import com.google.android.exoplayer2.util.Util;
|
||||
sampleRate);
|
||||
if ((flags & 0x06) != 0x06) {
|
||||
// If the size in bytes or table of contents is missing, the stream is not seekable.
|
||||
return new XingSeeker(firstFramePosition, durationUs, inputLength);
|
||||
return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs);
|
||||
}
|
||||
|
||||
long sizeBytes = frame.readUnsignedIntToInt();
|
||||
frame.skipBytes(1);
|
||||
long[] tableOfContents = new long[99];
|
||||
for (int i = 0; i < 99; i++) {
|
||||
long dataSize = frame.readUnsignedIntToInt();
|
||||
long[] tableOfContents = new long[100];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
tableOfContents[i] = frame.readUnsignedByte();
|
||||
}
|
||||
|
||||
// TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
|
||||
// delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
|
||||
// padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte();
|
||||
return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents,
|
||||
sizeBytes, mpegAudioHeader.frameSize);
|
||||
|
||||
if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) {
|
||||
Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize));
|
||||
}
|
||||
return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs, dataSize,
|
||||
tableOfContents);
|
||||
}
|
||||
|
||||
private final long firstFramePosition;
|
||||
private final long dataStartPosition;
|
||||
private final int xingFrameSize;
|
||||
private final long durationUs;
|
||||
private final long inputLength;
|
||||
/**
|
||||
* Data size, including the XING frame.
|
||||
*/
|
||||
private final long dataSize;
|
||||
/**
|
||||
* Entries are in the range [0, 255], but are stored as long integers for convenience.
|
||||
*/
|
||||
private final long[] tableOfContents;
|
||||
private final long sizeBytes;
|
||||
private final int headerSize;
|
||||
|
||||
private XingSeeker(long firstFramePosition, long durationUs, long inputLength) {
|
||||
this(firstFramePosition, durationUs, inputLength, null, 0, 0);
|
||||
private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) {
|
||||
this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null);
|
||||
}
|
||||
|
||||
private XingSeeker(long firstFramePosition, long durationUs, long inputLength,
|
||||
long[] tableOfContents, long sizeBytes, int headerSize) {
|
||||
this.firstFramePosition = firstFramePosition;
|
||||
private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize,
|
||||
long[] tableOfContents) {
|
||||
this.dataStartPosition = dataStartPosition;
|
||||
this.xingFrameSize = xingFrameSize;
|
||||
this.durationUs = durationUs;
|
||||
this.inputLength = inputLength;
|
||||
this.dataSize = dataSize;
|
||||
this.tableOfContents = tableOfContents;
|
||||
this.sizeBytes = sizeBytes;
|
||||
this.headerSize = headerSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -103,53 +109,45 @@ import com.google.android.exoplayer2.util.Util;
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
if (!isSeekable()) {
|
||||
return firstFramePosition;
|
||||
return dataStartPosition + xingFrameSize;
|
||||
}
|
||||
float percent = timeUs * 100f / durationUs;
|
||||
float fx;
|
||||
if (percent <= 0f) {
|
||||
fx = 0f;
|
||||
} else if (percent >= 100f) {
|
||||
fx = 256f;
|
||||
double percent = (timeUs * 100d) / durationUs;
|
||||
double scaledPosition;
|
||||
if (percent <= 0) {
|
||||
scaledPosition = 0;
|
||||
} else if (percent >= 100) {
|
||||
scaledPosition = 256;
|
||||
} else {
|
||||
int a = (int) percent;
|
||||
float fa, fb;
|
||||
if (a == 0) {
|
||||
fa = 0f;
|
||||
} else {
|
||||
fa = tableOfContents[a - 1];
|
||||
}
|
||||
if (a < 99) {
|
||||
fb = tableOfContents[a];
|
||||
} else {
|
||||
fb = 256f;
|
||||
}
|
||||
fx = fa + (fb - fa) * (percent - a);
|
||||
int prevTableIndex = (int) percent;
|
||||
double prevScaledPosition = tableOfContents[prevTableIndex];
|
||||
double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
|
||||
// Linearly interpolate between the two scaled positions.
|
||||
double interpolateFraction = percent - prevTableIndex;
|
||||
scaledPosition = prevScaledPosition
|
||||
+ (interpolateFraction * (nextScaledPosition - prevScaledPosition));
|
||||
}
|
||||
|
||||
long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition;
|
||||
long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1
|
||||
: firstFramePosition - headerSize + sizeBytes - 1;
|
||||
return Math.min(position, maximumPosition);
|
||||
long positionOffset = Math.round((scaledPosition / 256) * dataSize);
|
||||
// Ensure returned positions skip the frame containing the XING header.
|
||||
positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
|
||||
return dataStartPosition + positionOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeUs(long position) {
|
||||
if (!isSeekable() || position < firstFramePosition) {
|
||||
long positionOffset = position - dataStartPosition;
|
||||
if (!isSeekable() || positionOffset <= xingFrameSize) {
|
||||
return 0L;
|
||||
}
|
||||
double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes;
|
||||
int previousTocPosition =
|
||||
Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1;
|
||||
long previousTime = getTimeUsForTocPosition(previousTocPosition);
|
||||
|
||||
// Linearly interpolate the time taking into account the next entry.
|
||||
long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1];
|
||||
long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition];
|
||||
long nextTime = getTimeUsForTocPosition(previousTocPosition + 1);
|
||||
long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime)
|
||||
* (offsetByte - previousByte) / (nextByte - previousByte));
|
||||
return previousTime + timeOffset;
|
||||
double scaledPosition = (positionOffset * 256d) / dataSize;
|
||||
int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true);
|
||||
long prevTimeUs = getTimeUsForTableIndex(prevTableIndex);
|
||||
long prevScaledPosition = tableOfContents[prevTableIndex];
|
||||
long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1);
|
||||
long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
|
||||
// Linearly interpolate between the two table entries.
|
||||
double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0
|
||||
: ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition));
|
||||
return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -158,11 +156,13 @@ import com.google.android.exoplayer2.util.Util;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time in microseconds corresponding to a table of contents position, which is
|
||||
* interpreted as a percentage of the stream's duration between 0 and 100.
|
||||
* Returns the time in microseconds for a given table index.
|
||||
*
|
||||
* @param tableIndex A table index in the range [0, 100].
|
||||
* @return The corresponding time in microseconds.
|
||||
*/
|
||||
private long getTimeUsForTocPosition(int tocPosition) {
|
||||
return durationUs * tocPosition / 100;
|
||||
private long getTimeUsForTableIndex(int tableIndex) {
|
||||
return (durationUs * tableIndex) / 100;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -46,13 +46,14 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Facilitates the extraction of data from the fragmented mp4 container format.
|
||||
* Extracts data from the FMP4 container format.
|
||||
*/
|
||||
public final class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
@ -73,8 +74,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
|
||||
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK,
|
||||
FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS})
|
||||
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED,
|
||||
FLAG_WORKAROUND_IGNORE_EDIT_LISTS})
|
||||
public @interface Flags {}
|
||||
/**
|
||||
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
|
||||
@ -93,20 +94,15 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
* messages in the stream will be delivered as samples to this track.
|
||||
*/
|
||||
public static final int FLAG_ENABLE_EMSG_TRACK = 4;
|
||||
/**
|
||||
* Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages
|
||||
* contained within SEI NAL units in the stream will be delivered as samples to this track.
|
||||
*/
|
||||
public static final int FLAG_ENABLE_CEA608_TRACK = 8;
|
||||
/**
|
||||
* Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
|
||||
* container.
|
||||
*/
|
||||
private static final int FLAG_SIDELOADED = 16;
|
||||
private static final int FLAG_SIDELOADED = 8;
|
||||
/**
|
||||
* Flag to ignore any edit lists in the stream.
|
||||
*/
|
||||
public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 32;
|
||||
public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 16;
|
||||
|
||||
private static final String TAG = "FragmentedMp4Extractor";
|
||||
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
|
||||
@ -124,7 +120,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
@Flags private final int flags;
|
||||
private final Track sideloadedTrack;
|
||||
|
||||
// Manifest DRM data.
|
||||
// Sideloaded data.
|
||||
private final List<Format> closedCaptionFormats;
|
||||
private final DrmInitData sideloadedDrmInitData;
|
||||
|
||||
// Track-linked data bundle, accessible as a whole through trackID.
|
||||
@ -193,15 +190,33 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
* @param flags Flags that control the extractor's behavior.
|
||||
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
|
||||
* @param sideloadedTrack Sideloaded track information, in the case that the extractor
|
||||
* will not receive a moov box in the input data.
|
||||
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks.
|
||||
* will not receive a moov box in the input data. Null if a moov box is expected.
|
||||
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
|
||||
* pssh boxes (if present) will be used.
|
||||
*/
|
||||
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
|
||||
Track sideloadedTrack, DrmInitData sideloadedDrmInitData) {
|
||||
this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData,
|
||||
Collections.<Format>emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param flags Flags that control the extractor's behavior.
|
||||
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
|
||||
* @param sideloadedTrack Sideloaded track information, in the case that the extractor
|
||||
* will not receive a moov box in the input data. Null if a moov box is expected.
|
||||
* @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
|
||||
* pssh boxes (if present) will be used.
|
||||
* @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
|
||||
* caption channels to expose.
|
||||
*/
|
||||
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
|
||||
Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List<Format> closedCaptionFormats) {
|
||||
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
this.sideloadedTrack = sideloadedTrack;
|
||||
this.sideloadedDrmInitData = sideloadedDrmInitData;
|
||||
this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats);
|
||||
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
|
||||
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
|
||||
nalPrefix = new ParsableByteArray(5);
|
||||
@ -330,7 +345,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
currentTrackBundle = null;
|
||||
endOfMdatPosition = atomPosition + atomSize;
|
||||
if (!haveOutputSeekMap) {
|
||||
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
|
||||
// This must be the first mdat in the stream.
|
||||
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition));
|
||||
haveOutputSeekMap = true;
|
||||
}
|
||||
parserState = STATE_READING_ENCRYPTION_DATA;
|
||||
@ -483,12 +499,13 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
|
||||
Format.OFFSET_SAMPLE_RELATIVE));
|
||||
}
|
||||
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) {
|
||||
TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1,
|
||||
C.TRACK_TYPE_TEXT);
|
||||
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0,
|
||||
null));
|
||||
cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput};
|
||||
if (cea608TrackOutputs == null) {
|
||||
cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()];
|
||||
for (int i = 0; i < cea608TrackOutputs.length; i++) {
|
||||
TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT);
|
||||
output.format(closedCaptionFormats.get(i));
|
||||
cea608TrackOutputs[i] = output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1123,7 +1140,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
output.sampleData(nalStartCode, 4);
|
||||
// Write the NAL unit type byte.
|
||||
output.sampleData(nalPrefix, 1);
|
||||
processSeiNalUnitPayload = cea608TrackOutputs != null
|
||||
processSeiNalUnitPayload = cea608TrackOutputs.length > 0
|
||||
&& NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
|
||||
sampleBytesWritten += 5;
|
||||
sampleSize += nalUnitLengthFieldLengthDiff;
|
||||
|
@ -41,7 +41,7 @@ import java.util.List;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Extracts data from an unfragmented MP4 file.
|
||||
* Extracts data from the MP4 container format.
|
||||
*/
|
||||
public final class Mp4Extractor implements Extractor, SeekMap {
|
||||
|
||||
|
@ -186,7 +186,7 @@ import java.io.IOException;
|
||||
return start;
|
||||
}
|
||||
|
||||
long offset = pageSize * (granuleDistance <= 0 ? 2 : 1);
|
||||
long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
|
||||
long nextPosition = input.getPosition() - offset
|
||||
+ (granuleDistance * (end - start) / (endGranule - startGranule));
|
||||
|
||||
|
@ -118,8 +118,9 @@ import java.util.List;
|
||||
case 14:
|
||||
case 15:
|
||||
return 256 << (blockSizeCode - 8);
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private class FlacOggSeeker implements OggSeeker, SeekMap {
|
||||
|
@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Ogg {@link Extractor}.
|
||||
* Extracts data from the Ogg container format.
|
||||
*/
|
||||
public class OggExtractor implements Extractor {
|
||||
|
||||
|
@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Extracts CEA data from a RawCC file.
|
||||
* Extracts data from the RawCC container format.
|
||||
*/
|
||||
public final class RawCcExtractor implements Extractor {
|
||||
|
||||
|
@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Extracts samples from (E-)AC-3 bitstreams.
|
||||
* Extracts data from (E-)AC-3 bitstreams.
|
||||
*/
|
||||
public final class Ac3Extractor implements Extractor {
|
||||
|
||||
|
@ -39,7 +39,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
|
||||
private static final int STATE_READING_HEADER = 1;
|
||||
private static final int STATE_READING_SAMPLE = 2;
|
||||
|
||||
private static final int HEADER_SIZE = 8;
|
||||
private static final int HEADER_SIZE = 128;
|
||||
|
||||
private final ParsableBitArray headerScratchBits;
|
||||
private final ParsableByteArray headerScratchBytes;
|
||||
|
@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Extracts samples from AAC bit streams with ADTS framing.
|
||||
* Extracts data from AAC bit streams with ADTS framing.
|
||||
*/
|
||||
public final class AdtsExtractor implements Extractor {
|
||||
|
||||
|
@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Facilitates the extraction of data from the MPEG-2 PS container format.
|
||||
* Extracts data from the MPEG-2 PS container format.
|
||||
*/
|
||||
public final class PsExtractor implements Extractor {
|
||||
|
||||
|
@ -45,7 +45,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Facilitates the extraction of data from the MPEG-2 TS container format.
|
||||
* Extracts data from the MPEG-2 TS container format.
|
||||
*/
|
||||
public final class TsExtractor implements Extractor {
|
||||
|
||||
|
@ -23,13 +23,14 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.io.IOException;
|
||||
|
||||
/** {@link Extractor} to extract samples from a WAV byte stream. */
|
||||
public final class WavExtractor implements Extractor, SeekMap {
|
||||
/**
|
||||
* Extracts data from WAV byte streams.
|
||||
*/
|
||||
public final class WavExtractor implements Extractor {
|
||||
|
||||
/**
|
||||
* Factory for {@link WavExtractor} instances.
|
||||
@ -93,7 +94,7 @@ public final class WavExtractor implements Extractor, SeekMap {
|
||||
|
||||
if (!wavHeader.hasDataBounds()) {
|
||||
WavHeaderReader.skipToData(input, wavHeader);
|
||||
extractorOutput.seekMap(this);
|
||||
extractorOutput.seekMap(wavHeader);
|
||||
}
|
||||
|
||||
int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true);
|
||||
@ -113,20 +114,4 @@ public final class WavExtractor implements Extractor, SeekMap {
|
||||
return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
// SeekMap implementation.
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
return wavHeader.getDurationUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
return wavHeader.getPosition(timeUs);
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,11 @@
|
||||
package com.google.android.exoplayer2.extractor.wav;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** Header for a WAV file. */
|
||||
/*package*/ final class WavHeader {
|
||||
/* package */ final class WavHeader implements SeekMap {
|
||||
|
||||
/** Number of audio chanels. */
|
||||
private final int numChannels;
|
||||
@ -49,12 +51,58 @@ import com.google.android.exoplayer2.C;
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
/** Returns the duration in microseconds of this WAV. */
|
||||
// Setting bounds.
|
||||
|
||||
/**
|
||||
* Sets the data start position and size in bytes of sample data in this WAV.
|
||||
*
|
||||
* @param dataStartPosition The data start position in bytes.
|
||||
* @param dataSize The data size in bytes.
|
||||
*/
|
||||
public void setDataBounds(long dataStartPosition, long dataSize) {
|
||||
this.dataStartPosition = dataStartPosition;
|
||||
this.dataSize = dataSize;
|
||||
}
|
||||
|
||||
/** Returns whether the data start position and size have been set. */
|
||||
public boolean hasDataBounds() {
|
||||
return dataStartPosition != 0 && dataSize != 0;
|
||||
}
|
||||
|
||||
// SeekMap implementation.
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
long numFrames = dataSize / blockAlignment;
|
||||
return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPosition(long timeUs) {
|
||||
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
|
||||
// Constrain to nearest preceding frame offset.
|
||||
positionOffset = (positionOffset / blockAlignment) * blockAlignment;
|
||||
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment);
|
||||
// Add data start position.
|
||||
return dataStartPosition + positionOffset;
|
||||
}
|
||||
|
||||
// Misc getters.
|
||||
|
||||
/**
|
||||
* Returns the time in microseconds for the given position in bytes.
|
||||
*
|
||||
* @param position The position in bytes.
|
||||
*/
|
||||
public long getTimeUs(long position) {
|
||||
return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
|
||||
}
|
||||
|
||||
/** Returns the bytes per frame of this WAV. */
|
||||
public int getBytesPerFrame() {
|
||||
return blockAlignment;
|
||||
@ -75,33 +123,8 @@ import com.google.android.exoplayer2.C;
|
||||
return numChannels;
|
||||
}
|
||||
|
||||
/** Returns the position in bytes in this WAV for the given time in microseconds. */
|
||||
public long getPosition(long timeUs) {
|
||||
long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
|
||||
// Round down to nearest frame.
|
||||
long position = (unroundedPosition / blockAlignment) * blockAlignment;
|
||||
return Math.min(position, dataSize - blockAlignment) + dataStartPosition;
|
||||
}
|
||||
|
||||
/** Returns the time in microseconds for the given position in bytes in this WAV. */
|
||||
public long getTimeUs(long position) {
|
||||
return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
|
||||
}
|
||||
|
||||
/** Returns true if the data start position and size have been set. */
|
||||
public boolean hasDataBounds() {
|
||||
return dataStartPosition != 0 && dataSize != 0;
|
||||
}
|
||||
|
||||
/** Sets the start position and size in bytes of sample data in this WAV. */
|
||||
public void setDataBounds(long dataStartPosition, long dataSize) {
|
||||
this.dataStartPosition = dataStartPosition;
|
||||
this.dataSize = dataSize;
|
||||
}
|
||||
|
||||
/** Returns the PCM encoding. **/
|
||||
@C.PcmEncoding
|
||||
public int getEncoding() {
|
||||
public @C.PcmEncoding int getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,8 @@ import java.io.IOException;
|
||||
|
||||
/** Integer PCM audio data. */
|
||||
private static final int TYPE_PCM = 0x0001;
|
||||
/** Float PCM audio data. */
|
||||
private static final int TYPE_FLOAT = 0x0003;
|
||||
/** Extended WAVE format. */
|
||||
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
|
||||
|
||||
@ -87,14 +89,22 @@ import java.io.IOException;
|
||||
+ blockAlignment);
|
||||
}
|
||||
|
||||
@C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample);
|
||||
if (encoding == C.ENCODING_INVALID) {
|
||||
Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample);
|
||||
return null;
|
||||
@C.PcmEncoding int encoding;
|
||||
switch (type) {
|
||||
case TYPE_PCM:
|
||||
case TYPE_WAVE_FORMAT_EXTENSIBLE:
|
||||
encoding = Util.getPcmEncoding(bitsPerSample);
|
||||
break;
|
||||
case TYPE_FLOAT:
|
||||
encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unsupported WAV format type: " + type);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) {
|
||||
Log.e(TAG, "Unsupported WAV format type: " + type);
|
||||
if (encoding == C.ENCODING_INVALID) {
|
||||
Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import android.annotation.TargetApi;
|
||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||
import android.media.MediaCodecList;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
@ -120,7 +121,7 @@ public final class MediaCodecUtil {
|
||||
* exists.
|
||||
* @throws DecoderQueryException If there was an error querying the available decoders.
|
||||
*/
|
||||
public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
|
||||
public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
|
||||
throws DecoderQueryException {
|
||||
List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure);
|
||||
return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
|
||||
@ -140,27 +141,34 @@ public final class MediaCodecUtil {
|
||||
public static synchronized List<MediaCodecInfo> getDecoderInfos(String mimeType,
|
||||
boolean secure) throws DecoderQueryException {
|
||||
CodecKey key = new CodecKey(mimeType, secure);
|
||||
List<MediaCodecInfo> decoderInfos = decoderInfosCache.get(key);
|
||||
if (decoderInfos != null) {
|
||||
return decoderInfos;
|
||||
List<MediaCodecInfo> cachedDecoderInfos = decoderInfosCache.get(key);
|
||||
if (cachedDecoderInfos != null) {
|
||||
return cachedDecoderInfos;
|
||||
}
|
||||
MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21
|
||||
? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16();
|
||||
decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
|
||||
ArrayList<MediaCodecInfo> decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType);
|
||||
if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) {
|
||||
// Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the
|
||||
// legacy path. We also try this path on API levels 22 and 23 as a defensive measure.
|
||||
mediaCodecList = new MediaCodecListCompatV16();
|
||||
decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
|
||||
decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType);
|
||||
if (!decoderInfos.isEmpty()) {
|
||||
Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
|
||||
+ ". Assuming: " + decoderInfos.get(0).name);
|
||||
}
|
||||
}
|
||||
if (MimeTypes.AUDIO_ATMOS.equals(mimeType)) {
|
||||
// E-AC3 decoders can decode Atmos streams, but in 2-D rather than 3-D.
|
||||
CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure);
|
||||
ArrayList<MediaCodecInfo> eac3DecoderInfos =
|
||||
getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType);
|
||||
decoderInfos.addAll(eac3DecoderInfos);
|
||||
}
|
||||
applyWorkarounds(decoderInfos);
|
||||
decoderInfos = Collections.unmodifiableList(decoderInfos);
|
||||
decoderInfosCache.put(key, decoderInfos);
|
||||
return decoderInfos;
|
||||
List<MediaCodecInfo> unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos);
|
||||
decoderInfosCache.put(key, unmodifiableDecoderInfos);
|
||||
return unmodifiableDecoderInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -212,10 +220,21 @@ public final class MediaCodecUtil {
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private static List<MediaCodecInfo> getDecoderInfosInternal(
|
||||
CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
|
||||
/**
|
||||
* Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by
|
||||
* {@code mediaCodecList}.
|
||||
*
|
||||
* @param key The codec key.
|
||||
* @param mediaCodecList The codec list.
|
||||
* @param requestedMimeType The originally requested MIME type, which may differ from the codec
|
||||
* key MIME type if the codec key is being considered as a fallback.
|
||||
* @return The codec information for usable codecs matching the specified key.
|
||||
* @throws DecoderQueryException If there was an error querying the available decoders.
|
||||
*/
|
||||
private static ArrayList<MediaCodecInfo> getDecoderInfosInternal(CodecKey key,
|
||||
MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException {
|
||||
try {
|
||||
List<MediaCodecInfo> decoderInfos = new ArrayList<>();
|
||||
ArrayList<MediaCodecInfo> decoderInfos = new ArrayList<>();
|
||||
String mimeType = key.mimeType;
|
||||
int numberOfCodecs = mediaCodecList.getCodecCount();
|
||||
boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();
|
||||
@ -223,7 +242,7 @@ public final class MediaCodecUtil {
|
||||
for (int i = 0; i < numberOfCodecs; i++) {
|
||||
android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);
|
||||
String codecName = codecInfo.getName();
|
||||
if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) {
|
||||
if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit, requestedMimeType)) {
|
||||
for (String supportedType : codecInfo.getSupportedTypes()) {
|
||||
if (supportedType.equalsIgnoreCase(mimeType)) {
|
||||
try {
|
||||
@ -265,9 +284,16 @@ public final class MediaCodecUtil {
|
||||
|
||||
/**
|
||||
* Returns whether the specified codec is usable for decoding on the current device.
|
||||
*
|
||||
* @param info The codec information.
|
||||
* @param name The name of the codec
|
||||
* @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.
|
||||
* @param requestedMimeType The originally requested MIME type, which may differ from the codec
|
||||
* key MIME type if the codec key is being considered as a fallback.
|
||||
* @return Whether the specified codec is usable for decoding on the current device.
|
||||
*/
|
||||
private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name,
|
||||
boolean secureDecodersExplicit) {
|
||||
boolean secureDecodersExplicit, String requestedMimeType) {
|
||||
if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) {
|
||||
return false;
|
||||
}
|
||||
@ -356,6 +382,12 @@ public final class MediaCodecUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
// MTK E-AC3 decoder doesn't support decoding Atmos streams in 2-D. See [Internal: b/69400041].
|
||||
if (MimeTypes.AUDIO_ATMOS.equals(requestedMimeType)
|
||||
&& "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -15,309 +15,10 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Interface for callbacks to be notified of adaptive {@link MediaSource} events.
|
||||
* Interface for callbacks to be notified of {@link MediaSource} events.
|
||||
*
|
||||
* @deprecated Use {@link MediaSourceEventListener}.
|
||||
*/
|
||||
public interface AdaptiveMediaSourceEventListener {
|
||||
|
||||
/**
|
||||
* Called when a load begins.
|
||||
*
|
||||
* @param dataSpec Defines the data being loaded.
|
||||
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
|
||||
* being loaded.
|
||||
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
|
||||
* to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does
|
||||
* not belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
|
||||
* the load is not for media data.
|
||||
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
|
||||
* load is not for media data.
|
||||
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began.
|
||||
*/
|
||||
void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
|
||||
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
|
||||
long mediaEndTimeMs, long elapsedRealtimeMs);
|
||||
|
||||
/**
|
||||
* Called when a load ends.
|
||||
*
|
||||
* @param dataSpec Defines the data being loaded.
|
||||
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
|
||||
* being loaded.
|
||||
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
|
||||
* to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does
|
||||
* not belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
|
||||
* the load is not for media data.
|
||||
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
|
||||
* load is not for media data.
|
||||
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended.
|
||||
* @param loadDurationMs The duration of the load.
|
||||
* @param bytesLoaded The number of bytes that were loaded.
|
||||
*/
|
||||
void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
|
||||
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
|
||||
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded);
|
||||
|
||||
/**
|
||||
* Called when a load is canceled.
|
||||
*
|
||||
* @param dataSpec Defines the data being loaded.
|
||||
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
|
||||
* being loaded.
|
||||
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
|
||||
* to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does
|
||||
* not belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
|
||||
* the load is not for media data.
|
||||
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
|
||||
* load is not for media data.
|
||||
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was
|
||||
* canceled.
|
||||
* @param loadDurationMs The duration of the load up to the point at which it was canceled.
|
||||
* @param bytesLoaded The number of bytes that were loaded prior to cancelation.
|
||||
*/
|
||||
void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
|
||||
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
|
||||
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded);
|
||||
|
||||
/**
|
||||
* Called when a load error occurs.
|
||||
* <p>
|
||||
* The error may or may not have resulted in the load being canceled, as indicated by the
|
||||
* {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will
|
||||
* <em>not</em> be called in addition to this method.
|
||||
* <p>
|
||||
* This method being called does not indicate that playback has failed, or that it will fail. The
|
||||
* player may be able to recover from the error and continue. Hence applications should
|
||||
* <em>not</em> implement this method to display a user visible error or initiate an application
|
||||
* level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
|
||||
* such behavior). This method is called to provide the application with an opportunity to log the
|
||||
* error if it wishes to do so.
|
||||
*
|
||||
* @param dataSpec Defines the data being loaded.
|
||||
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
|
||||
* being loaded.
|
||||
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
|
||||
* to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does
|
||||
* not belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
|
||||
* the load is not for media data.
|
||||
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
|
||||
* load is not for media data.
|
||||
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error
|
||||
* occurred.
|
||||
* @param loadDurationMs The duration of the load up to the point at which the error occurred.
|
||||
* @param bytesLoaded The number of bytes that were loaded prior to the error.
|
||||
* @param error The load error.
|
||||
* @param wasCanceled Whether the load was canceled as a result of the error.
|
||||
*/
|
||||
void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
|
||||
int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
|
||||
long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded,
|
||||
IOException error, boolean wasCanceled);
|
||||
|
||||
/**
|
||||
* Called when data is removed from the back of a media buffer, typically so that it can be
|
||||
* re-buffered in a different format.
|
||||
*
|
||||
* @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
|
||||
* @param mediaStartTimeMs The start time of the media being discarded.
|
||||
* @param mediaEndTimeMs The end time of the media being discarded.
|
||||
*/
|
||||
void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs);
|
||||
|
||||
/**
|
||||
* Called when a downstream format change occurs (i.e. when the format of the media being read
|
||||
* from one or more {@link SampleStream}s provided by the source changes).
|
||||
*
|
||||
* @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does
|
||||
* not belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaTimeMs The media time at which the change occurred.
|
||||
*/
|
||||
void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason,
|
||||
Object trackSelectionData, long mediaTimeMs);
|
||||
|
||||
/**
|
||||
* Dispatches events to a {@link AdaptiveMediaSourceEventListener}.
|
||||
*/
|
||||
final class EventDispatcher {
|
||||
|
||||
private final Handler handler;
|
||||
private final AdaptiveMediaSourceEventListener listener;
|
||||
private final long mediaTimeOffsetMs;
|
||||
|
||||
public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) {
|
||||
this(handler, listener, 0);
|
||||
}
|
||||
|
||||
public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener,
|
||||
long mediaTimeOffsetMs) {
|
||||
this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
|
||||
this.listener = listener;
|
||||
this.mediaTimeOffsetMs = mediaTimeOffsetMs;
|
||||
}
|
||||
|
||||
public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) {
|
||||
return new EventDispatcher(handler, listener, mediaTimeOffsetMs);
|
||||
}
|
||||
|
||||
public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
|
||||
loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
|
||||
null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs);
|
||||
}
|
||||
|
||||
public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType,
|
||||
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
|
||||
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) {
|
||||
if (listener != null) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
|
||||
trackSelectionData, adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
|
||||
long loadDurationMs, long bytesLoaded) {
|
||||
loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
|
||||
null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded);
|
||||
}
|
||||
|
||||
public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType,
|
||||
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
|
||||
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
|
||||
final long loadDurationMs, final long bytesLoaded) {
|
||||
if (listener != null) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat,
|
||||
trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
|
||||
long loadDurationMs, long bytesLoaded) {
|
||||
loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
|
||||
null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded);
|
||||
}
|
||||
|
||||
public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType,
|
||||
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
|
||||
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
|
||||
final long loadDurationMs, final long bytesLoaded) {
|
||||
if (listener != null) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat,
|
||||
trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
|
||||
long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) {
|
||||
loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
|
||||
null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded,
|
||||
error, wasCanceled);
|
||||
}
|
||||
|
||||
public void loadError(final DataSpec dataSpec, final int dataType, final int trackType,
|
||||
final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
|
||||
final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
|
||||
final long loadDurationMs, final long bytesLoaded, final IOException error,
|
||||
final boolean wasCanceled) {
|
||||
if (listener != null) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
|
||||
trackSelectionData, adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded,
|
||||
error, wasCanceled);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs,
|
||||
final long mediaEndTimeUs) {
|
||||
if (listener != null) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void downstreamFormatChanged(final int trackType, final Format trackFormat,
|
||||
final int trackSelectionReason, final Object trackSelectionData,
|
||||
final long mediaTimeUs) {
|
||||
if (listener != null) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason,
|
||||
trackSelectionData, adjustMediaTime(mediaTimeUs));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private long adjustMediaTime(long mediaTimeUs) {
|
||||
long mediaTimeMs = C.usToMs(mediaTimeUs);
|
||||
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@Deprecated
|
||||
public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {}
|
||||
|
@ -112,7 +112,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
||||
if (internalStreams[i] == null) {
|
||||
sampleStreams[i] = null;
|
||||
} else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) {
|
||||
sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs,
|
||||
sampleStreams[i] = new ClippingSampleStream(internalStreams[i], startUs, endUs,
|
||||
pendingInitialDiscontinuity);
|
||||
}
|
||||
streams[i] = sampleStreams[i];
|
||||
@ -222,9 +222,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
||||
/**
|
||||
* Wraps a {@link SampleStream} and clips its samples.
|
||||
*/
|
||||
private static final class ClippingSampleStream implements SampleStream {
|
||||
private final class ClippingSampleStream implements SampleStream {
|
||||
|
||||
private final MediaPeriod mediaPeriod;
|
||||
private final SampleStream stream;
|
||||
private final long startUs;
|
||||
private final long endUs;
|
||||
@ -232,9 +231,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
||||
private boolean pendingDiscontinuity;
|
||||
private boolean sentEos;
|
||||
|
||||
public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs,
|
||||
long endUs, boolean pendingDiscontinuity) {
|
||||
this.mediaPeriod = mediaPeriod;
|
||||
public ClippingSampleStream(SampleStream stream, long startUs, long endUs,
|
||||
boolean pendingDiscontinuity) {
|
||||
this.stream = stream;
|
||||
this.startUs = startUs;
|
||||
this.endUs = endUs;
|
||||
@ -278,9 +276,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
||||
formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding);
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ
|
||||
&& buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ
|
||||
&& mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
|
||||
if (endUs != C.TIME_END_OF_SOURCE
|
||||
&& ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)
|
||||
|| (result == C.RESULT_NOTHING_READ
|
||||
&& getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
|
||||
buffer.clear();
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
sentEos = true;
|
||||
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Media period that wraps a media source and defers calling its
|
||||
* {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link #createPeriod()}
|
||||
* has been called. This is useful if you need to return a media period immediately but the media
|
||||
* source that should create it is not yet prepared.
|
||||
*/
|
||||
public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
|
||||
|
||||
public final MediaSource mediaSource;
|
||||
|
||||
private final MediaPeriodId id;
|
||||
private final Allocator allocator;
|
||||
|
||||
private MediaPeriod mediaPeriod;
|
||||
private Callback callback;
|
||||
private long preparePositionUs;
|
||||
|
||||
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
|
||||
this.id = id;
|
||||
this.allocator = allocator;
|
||||
this.mediaSource = mediaSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then
|
||||
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()}
|
||||
* to release the period.
|
||||
*/
|
||||
public void createPeriod() {
|
||||
mediaPeriod = mediaSource.createPeriod(id, allocator);
|
||||
if (callback != null) {
|
||||
mediaPeriod.prepare(this, preparePositionUs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the period.
|
||||
*/
|
||||
public void releasePeriod() {
|
||||
if (mediaPeriod != null) {
|
||||
mediaSource.releasePeriod(mediaPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Callback callback, long preparePositionUs) {
|
||||
this.callback = callback;
|
||||
this.preparePositionUs = preparePositionUs;
|
||||
if (mediaPeriod != null) {
|
||||
mediaPeriod.prepare(this, preparePositionUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowPrepareError() throws IOException {
|
||||
if (mediaPeriod != null) {
|
||||
mediaPeriod.maybeThrowPrepareError();
|
||||
} else {
|
||||
mediaSource.maybeThrowSourceInfoRefreshError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackGroupArray getTrackGroups() {
|
||||
return mediaPeriod.getTrackGroups();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
|
||||
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
|
||||
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
|
||||
positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void discardBuffer(long positionUs) {
|
||||
mediaPeriod.discardBuffer(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readDiscontinuity() {
|
||||
return mediaPeriod.readDiscontinuity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
return mediaPeriod.getBufferedPositionUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long seekToUs(long positionUs) {
|
||||
return mediaPeriod.seekToUs(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextLoadPositionUs() {
|
||||
return mediaPeriod.getNextLoadPositionUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean continueLoading(long positionUs) {
|
||||
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContinueLoadingRequested(MediaPeriod source) {
|
||||
callback.onContinueLoadingRequested(this);
|
||||
}
|
||||
|
||||
// MediaPeriod.Callback implementation
|
||||
|
||||
@Override
|
||||
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||
callback.onPrepared(this);
|
||||
}
|
||||
|
||||
}
|
@ -27,7 +27,6 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
|
||||
import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
@ -758,111 +757,4 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Media period used for periods created from unprepared media sources exposed through
|
||||
* {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes
|
||||
* available.
|
||||
*/
|
||||
private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
|
||||
|
||||
public final MediaSource mediaSource;
|
||||
|
||||
private final MediaPeriodId id;
|
||||
private final Allocator allocator;
|
||||
|
||||
private MediaPeriod mediaPeriod;
|
||||
private Callback callback;
|
||||
private long preparePositionUs;
|
||||
|
||||
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
|
||||
this.id = id;
|
||||
this.allocator = allocator;
|
||||
this.mediaSource = mediaSource;
|
||||
}
|
||||
|
||||
public void createPeriod() {
|
||||
mediaPeriod = mediaSource.createPeriod(id, allocator);
|
||||
if (callback != null) {
|
||||
mediaPeriod.prepare(this, preparePositionUs);
|
||||
}
|
||||
}
|
||||
|
||||
public void releasePeriod() {
|
||||
if (mediaPeriod != null) {
|
||||
mediaSource.releasePeriod(mediaPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Callback callback, long preparePositionUs) {
|
||||
this.callback = callback;
|
||||
this.preparePositionUs = preparePositionUs;
|
||||
if (mediaPeriod != null) {
|
||||
mediaPeriod.prepare(this, preparePositionUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowPrepareError() throws IOException {
|
||||
if (mediaPeriod != null) {
|
||||
mediaPeriod.maybeThrowPrepareError();
|
||||
} else {
|
||||
mediaSource.maybeThrowSourceInfoRefreshError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackGroupArray getTrackGroups() {
|
||||
return mediaPeriod.getTrackGroups();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
|
||||
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
|
||||
return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags,
|
||||
positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void discardBuffer(long positionUs) {
|
||||
mediaPeriod.discardBuffer(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readDiscontinuity() {
|
||||
return mediaPeriod.readDiscontinuity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
return mediaPeriod.getBufferedPositionUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long seekToUs(long positionUs) {
|
||||
return mediaPeriod.seekToUs(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextLoadPositionUs() {
|
||||
return mediaPeriod.getNextLoadPositionUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean continueLoading(long positionUs) {
|
||||
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContinueLoadingRequested(MediaPeriod source) {
|
||||
callback.onContinueLoadingRequested(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||
callback.onPrepared(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
@ -28,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
@ -74,11 +76,10 @@ import java.util.Arrays;
|
||||
private final Uri uri;
|
||||
private final DataSource dataSource;
|
||||
private final int minLoadableRetryCount;
|
||||
private final Handler eventHandler;
|
||||
private final ExtractorMediaSource.EventListener eventListener;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final Listener listener;
|
||||
private final Allocator allocator;
|
||||
private final String customCacheKey;
|
||||
@Nullable private final String customCacheKey;
|
||||
private final long continueLoadingCheckIntervalBytes;
|
||||
private final Loader loader;
|
||||
private final ExtractorHolder extractorHolder;
|
||||
@ -117,8 +118,7 @@ import java.util.Arrays;
|
||||
* @param dataSource The data source to read the media.
|
||||
* @param extractors The extractors to use to read the data source.
|
||||
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
|
||||
* @param eventHandler A handler for events. May be null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param eventDispatcher A dispatcher to notify of events.
|
||||
* @param listener A listener to notify when information about the period changes.
|
||||
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
|
||||
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
|
||||
@ -126,15 +126,20 @@ import java.util.Arrays;
|
||||
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
|
||||
* invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
||||
*/
|
||||
public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors,
|
||||
int minLoadableRetryCount, Handler eventHandler,
|
||||
ExtractorMediaSource.EventListener eventListener, Listener listener,
|
||||
Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) {
|
||||
public ExtractorMediaPeriod(
|
||||
Uri uri,
|
||||
DataSource dataSource,
|
||||
Extractor[] extractors,
|
||||
int minLoadableRetryCount,
|
||||
EventDispatcher eventDispatcher,
|
||||
Listener listener,
|
||||
Allocator allocator,
|
||||
@Nullable String customCacheKey,
|
||||
int continueLoadingCheckIntervalBytes) {
|
||||
this.uri = uri;
|
||||
this.dataSource = dataSource;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
this.eventDispatcher = eventDispatcher;
|
||||
this.listener = listener;
|
||||
this.allocator = allocator;
|
||||
this.customCacheKey = customCacheKey;
|
||||
@ -303,7 +308,8 @@ import java.util.Arrays;
|
||||
|
||||
@Override
|
||||
public long readDiscontinuity() {
|
||||
if (notifyDiscontinuity) {
|
||||
if (notifyDiscontinuity
|
||||
&& (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) {
|
||||
notifyDiscontinuity = false;
|
||||
return lastSeekPositionUs;
|
||||
}
|
||||
@ -399,38 +405,75 @@ import java.util.Arrays;
|
||||
@Override
|
||||
public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs,
|
||||
long loadDurationMs) {
|
||||
copyLengthFromLoader(loadable);
|
||||
loadingFinished = true;
|
||||
if (durationUs == C.TIME_UNSET) {
|
||||
long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
|
||||
durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0
|
||||
: largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
|
||||
listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable());
|
||||
}
|
||||
eventDispatcher.loadCompleted(
|
||||
loadable.dataSpec,
|
||||
C.DATA_TYPE_MEDIA,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
/* trackFormat= */ null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
/* trackSelectionData= */ null,
|
||||
/* mediaStartTimeUs= */ 0,
|
||||
durationUs,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
loadable.bytesLoaded);
|
||||
copyLengthFromLoader(loadable);
|
||||
loadingFinished = true;
|
||||
callback.onContinueLoadingRequested(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
|
||||
long loadDurationMs, boolean released) {
|
||||
if (released) {
|
||||
return;
|
||||
}
|
||||
copyLengthFromLoader(loadable);
|
||||
for (SampleQueue sampleQueue : sampleQueues) {
|
||||
sampleQueue.reset();
|
||||
}
|
||||
if (enabledTrackCount > 0) {
|
||||
callback.onContinueLoadingRequested(this);
|
||||
eventDispatcher.loadCanceled(
|
||||
loadable.dataSpec,
|
||||
C.DATA_TYPE_MEDIA,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
/* trackFormat= */ null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
/* trackSelectionData= */ null,
|
||||
/* mediaStartTimeUs= */ 0,
|
||||
durationUs,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
loadable.bytesLoaded);
|
||||
if (!released) {
|
||||
copyLengthFromLoader(loadable);
|
||||
for (SampleQueue sampleQueue : sampleQueues) {
|
||||
sampleQueue.reset();
|
||||
}
|
||||
if (enabledTrackCount > 0) {
|
||||
callback.onContinueLoadingRequested(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs,
|
||||
long loadDurationMs, IOException error) {
|
||||
boolean isErrorFatal = isLoadableExceptionFatal(error);
|
||||
eventDispatcher.loadError(
|
||||
loadable.dataSpec,
|
||||
C.DATA_TYPE_MEDIA,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
/* trackFormat= */ null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
/* trackSelectionData= */ null,
|
||||
/* mediaStartTimeUs= */ 0,
|
||||
durationUs,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
loadable.bytesLoaded,
|
||||
error,
|
||||
/* wasCanceled= */ isErrorFatal);
|
||||
copyLengthFromLoader(loadable);
|
||||
notifyLoadError(error);
|
||||
if (isLoadableExceptionFatal(error)) {
|
||||
if (isErrorFatal) {
|
||||
return Loader.DONT_RETRY_FATAL;
|
||||
}
|
||||
int extractedSamplesCount = getExtractedSamplesCount();
|
||||
@ -606,17 +649,6 @@ import java.util.Arrays;
|
||||
return e instanceof UnrecognizedInputFormatException;
|
||||
}
|
||||
|
||||
private void notifyLoadError(final IOException error) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onLoadError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final class SampleStreamImpl implements SampleStream {
|
||||
|
||||
private final int track;
|
||||
@ -663,7 +695,9 @@ import java.util.Arrays;
|
||||
|
||||
private boolean pendingExtractorSeek;
|
||||
private long seekTimeUs;
|
||||
private DataSpec dataSpec;
|
||||
private long length;
|
||||
private long bytesLoaded;
|
||||
|
||||
public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder,
|
||||
ConditionVariable loadCondition) {
|
||||
@ -699,7 +733,8 @@ import java.util.Arrays;
|
||||
ExtractorInput input = null;
|
||||
try {
|
||||
long position = positionHolder.position;
|
||||
length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey));
|
||||
dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey);
|
||||
length = dataSource.open(dataSpec);
|
||||
if (length != C.LENGTH_UNSET) {
|
||||
length += position;
|
||||
}
|
||||
@ -723,6 +758,7 @@ import java.util.Arrays;
|
||||
result = Extractor.RESULT_CONTINUE;
|
||||
} else if (input != null) {
|
||||
positionHolder.position = input.getPosition();
|
||||
bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition;
|
||||
}
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
|
@ -17,14 +17,19 @@ package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -40,10 +45,12 @@ import java.io.IOException;
|
||||
* Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
|
||||
*/
|
||||
public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener {
|
||||
|
||||
/**
|
||||
* Listener of {@link ExtractorMediaSource} events.
|
||||
*
|
||||
* @deprecated Use {@link MediaSourceEventListener}.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface EventListener {
|
||||
|
||||
/**
|
||||
@ -89,8 +96,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final ExtractorsFactory extractorsFactory;
|
||||
private final int minLoadableRetryCount;
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final String customCacheKey;
|
||||
private final int continueLoadingCheckIntervalBytes;
|
||||
|
||||
@ -98,6 +104,127 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
|
||||
private long timelineDurationUs;
|
||||
private boolean timelineIsSeekable;
|
||||
|
||||
/** Factory for {@link ExtractorMediaSource}s. */
|
||||
public static final class Factory implements AdsMediaSource.MediaSourceFactory {
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
private @Nullable ExtractorsFactory extractorsFactory;
|
||||
private @Nullable String customCacheKey;
|
||||
private int minLoadableRetryCount;
|
||||
private int continueLoadingCheckIntervalBytes;
|
||||
private boolean isCreateCalled;
|
||||
|
||||
/**
|
||||
* Creates a new factory for {@link ExtractorMediaSource}s.
|
||||
*
|
||||
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
|
||||
*/
|
||||
public Factory(DataSource.Factory dataSourceFactory) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA;
|
||||
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the factory for {@link Extractor}s to process the media stream. The default value is an
|
||||
* instance of {@link DefaultExtractorsFactory}.
|
||||
*
|
||||
* @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
|
||||
* possible formats are known, pass a factory that instantiates extractors for those
|
||||
* formats.
|
||||
* @return This factory, for convenience.
|
||||
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||
*/
|
||||
public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) {
|
||||
Assertions.checkState(!isCreateCalled);
|
||||
this.extractorsFactory = extractorsFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom key that uniquely identifies the original stream. Used for cache indexing.
|
||||
* The default value is {@code null}.
|
||||
*
|
||||
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for
|
||||
* cache indexing.
|
||||
* @return This factory, for convenience.
|
||||
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||
*/
|
||||
public Factory setCustomCacheKey(String customCacheKey) {
|
||||
Assertions.checkState(!isCreateCalled);
|
||||
this.customCacheKey = customCacheKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum number of times to retry if a loading error occurs. The default value is
|
||||
* {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}.
|
||||
*
|
||||
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
|
||||
* @return This factory, for convenience.
|
||||
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||
*/
|
||||
public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {
|
||||
Assertions.checkState(!isCreateCalled);
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of bytes that should be loaded between each invocation of {@link
|
||||
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is
|
||||
* {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}.
|
||||
*
|
||||
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between
|
||||
* each invocation of {@link
|
||||
* MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
||||
* @return This factory, for convenience.
|
||||
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||
*/
|
||||
public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) {
|
||||
Assertions.checkState(!isCreateCalled);
|
||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events
|
||||
* will not be delivered.
|
||||
*
|
||||
* @param uri The {@link Uri}.
|
||||
* @return The new {@link ExtractorMediaSource}.
|
||||
*/
|
||||
public ExtractorMediaSource createMediaSource(Uri uri) {
|
||||
return createMediaSource(uri, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link ExtractorMediaSource} using the current parameters.
|
||||
*
|
||||
* @param uri The {@link Uri}.
|
||||
* @param eventHandler A handler for events.
|
||||
* @param eventListener A listener of events.
|
||||
* @return The new {@link ExtractorMediaSource}.
|
||||
*/
|
||||
@Override
|
||||
public ExtractorMediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) {
|
||||
isCreateCalled = true;
|
||||
if (extractorsFactory == null) {
|
||||
extractorsFactory = new DefaultExtractorsFactory();
|
||||
}
|
||||
return new ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory,
|
||||
minLoadableRetryCount, eventHandler, eventListener, customCacheKey,
|
||||
continueLoadingCheckIntervalBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedTypes() {
|
||||
return new int[] {C.TYPE_OTHER};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The {@link Uri} of the media stream.
|
||||
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
|
||||
@ -106,9 +233,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
|
||||
* Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
|
||||
* @param eventHandler A handler for events. May be null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @deprecated Use {@link Factory} instead.
|
||||
*/
|
||||
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) {
|
||||
@Deprecated
|
||||
public ExtractorMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory,
|
||||
Handler eventHandler,
|
||||
EventListener eventListener) {
|
||||
this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null);
|
||||
}
|
||||
|
||||
@ -122,9 +255,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
|
||||
* indexing. May be null.
|
||||
* @deprecated Use {@link Factory} instead.
|
||||
*/
|
||||
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener,
|
||||
@Deprecated
|
||||
public ExtractorMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory,
|
||||
Handler eventHandler,
|
||||
EventListener eventListener,
|
||||
String customCacheKey) {
|
||||
this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
|
||||
eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES);
|
||||
@ -143,16 +282,43 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
|
||||
* indexing. May be null.
|
||||
* @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each
|
||||
* invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}.
|
||||
* @deprecated Use {@link Factory} instead.
|
||||
*/
|
||||
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler,
|
||||
EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) {
|
||||
@Deprecated
|
||||
public ExtractorMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory,
|
||||
int minLoadableRetryCount,
|
||||
Handler eventHandler,
|
||||
EventListener eventListener,
|
||||
String customCacheKey,
|
||||
int continueLoadingCheckIntervalBytes) {
|
||||
this(
|
||||
uri,
|
||||
dataSourceFactory,
|
||||
extractorsFactory,
|
||||
minLoadableRetryCount,
|
||||
eventHandler,
|
||||
eventListener == null ? null : new EventListenerWrapper(eventListener),
|
||||
customCacheKey,
|
||||
continueLoadingCheckIntervalBytes);
|
||||
}
|
||||
|
||||
private ExtractorMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory,
|
||||
int minLoadableRetryCount,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable MediaSourceEventListener eventListener,
|
||||
@Nullable String customCacheKey,
|
||||
int continueLoadingCheckIntervalBytes) {
|
||||
this.uri = uri;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.extractorsFactory = extractorsFactory;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
this.customCacheKey = customCacheKey;
|
||||
this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes;
|
||||
}
|
||||
@ -171,9 +337,16 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
Assertions.checkArgument(id.periodIndex == 0);
|
||||
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
|
||||
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
|
||||
this, allocator, customCacheKey, continueLoadingCheckIntervalBytes);
|
||||
return new ExtractorMediaPeriod(
|
||||
uri,
|
||||
dataSourceFactory.createDataSource(),
|
||||
extractorsFactory.createExtractors(),
|
||||
minLoadableRetryCount,
|
||||
eventDispatcher,
|
||||
this,
|
||||
allocator,
|
||||
customCacheKey,
|
||||
continueLoadingCheckIntervalBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -208,4 +381,94 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
|
||||
this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in
|
||||
* {@link MediaSourceEventListener}.
|
||||
*/
|
||||
private static final class EventListenerWrapper implements MediaSourceEventListener {
|
||||
private final EventListener eventListener;
|
||||
|
||||
public EventListenerWrapper(EventListener eventListener) {
|
||||
this.eventListener = Assertions.checkNotNull(eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded,
|
||||
IOException error,
|
||||
boolean wasCanceled) {
|
||||
eventListener.onLoadError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaTimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,8 @@ import java.io.IOException;
|
||||
* player to load and read the media.</li>
|
||||
* </ul>
|
||||
* All methods are called on the player's internal playback thread, as described in the
|
||||
* {@link ExoPlayer} Javadoc.
|
||||
* {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances
|
||||
* should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once.
|
||||
*/
|
||||
public interface MediaSource {
|
||||
|
||||
@ -150,6 +151,8 @@ public interface MediaSource {
|
||||
|
||||
/**
|
||||
* Starts preparation of the source.
|
||||
* <p>
|
||||
* Should not be called directly from application code.
|
||||
*
|
||||
* @param player The player for which this source is being prepared.
|
||||
* @param isTopLevelSource Whether this source has been passed directly to
|
||||
@ -162,6 +165,8 @@ public interface MediaSource {
|
||||
|
||||
/**
|
||||
* Throws any pending error encountered while loading or refreshing source information.
|
||||
* <p>
|
||||
* Should not be called directly from application code.
|
||||
*/
|
||||
void maybeThrowSourceInfoRefreshError() throws IOException;
|
||||
|
||||
@ -169,6 +174,8 @@ public interface MediaSource {
|
||||
* Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called
|
||||
* multiple times with the same period identifier without an intervening call to
|
||||
* {@link #releasePeriod(MediaPeriod)}.
|
||||
* <p>
|
||||
* Should not be called directly from application code.
|
||||
*
|
||||
* @param id The identifier of the period.
|
||||
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
|
||||
@ -178,6 +185,8 @@ public interface MediaSource {
|
||||
|
||||
/**
|
||||
* Releases the period.
|
||||
* <p>
|
||||
* Should not be called directly from application code.
|
||||
*
|
||||
* @param mediaPeriod The period to release.
|
||||
*/
|
||||
@ -186,8 +195,7 @@ public interface MediaSource {
|
||||
/**
|
||||
* Releases the source.
|
||||
* <p>
|
||||
* This method should be called when the source is no longer required. It may be called in any
|
||||
* state.
|
||||
* Should not be called directly from application code.
|
||||
*/
|
||||
void releaseSource();
|
||||
|
||||
|
@ -0,0 +1,491 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Interface for callbacks to be notified of {@link MediaSource} events. */
|
||||
public interface MediaSourceEventListener {
|
||||
/**
|
||||
* Called when a load begins.
|
||||
*
|
||||
* @param dataSpec Defines the data being loaded.
|
||||
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
|
||||
* being loaded.
|
||||
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to
|
||||
* media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
|
||||
* belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
|
||||
* the load is not for media data.
|
||||
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
|
||||
* load is not for media data.
|
||||
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began.
|
||||
*/
|
||||
void onLoadStarted(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs);
|
||||
|
||||
/**
|
||||
* Called when a load ends.
|
||||
*
|
||||
* @param dataSpec Defines the data being loaded.
|
||||
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
|
||||
* being loaded.
|
||||
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to
|
||||
* media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
|
||||
* belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
|
||||
* the load is not for media data.
|
||||
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
|
||||
* load is not for media data.
|
||||
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended.
|
||||
* @param loadDurationMs The duration of the load.
|
||||
* @param bytesLoaded The number of bytes that were loaded.
|
||||
*/
|
||||
void onLoadCompleted(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded);
|
||||
|
||||
/**
|
||||
* Called when a load is canceled.
|
||||
*
|
||||
* @param dataSpec Defines the data being loaded.
|
||||
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
|
||||
* being loaded.
|
||||
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to
|
||||
* media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
|
||||
* belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
|
||||
* the load is not for media data.
|
||||
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
|
||||
* load is not for media data.
|
||||
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was
|
||||
* canceled.
|
||||
* @param loadDurationMs The duration of the load up to the point at which it was canceled.
|
||||
* @param bytesLoaded The number of bytes that were loaded prior to cancelation.
|
||||
*/
|
||||
void onLoadCanceled(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded);
|
||||
|
||||
/**
|
||||
* Called when a load error occurs.
|
||||
*
|
||||
* <p>The error may or may not have resulted in the load being canceled, as indicated by the
|
||||
* {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will
|
||||
* <em>not</em> be called in addition to this method.
|
||||
*
|
||||
* <p>This method being called does not indicate that playback has failed, or that it will fail.
|
||||
* The player may be able to recover from the error and continue. Hence applications should
|
||||
* <em>not</em> implement this method to display a user visible error or initiate an application
|
||||
* level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
|
||||
* such behavior). This method is called to provide the application with an opportunity to log the
|
||||
* error if it wishes to do so.
|
||||
*
|
||||
* @param dataSpec Defines the data being loaded.
|
||||
* @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
|
||||
* being loaded.
|
||||
* @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to
|
||||
* media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
|
||||
* belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
|
||||
* the load is not for media data.
|
||||
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
|
||||
* load is not for media data.
|
||||
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error
|
||||
* occurred.
|
||||
* @param loadDurationMs The duration of the load up to the point at which the error occurred.
|
||||
* @param bytesLoaded The number of bytes that were loaded prior to the error.
|
||||
* @param error The load error.
|
||||
* @param wasCanceled Whether the load was canceled as a result of the error.
|
||||
*/
|
||||
void onLoadError(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded,
|
||||
IOException error,
|
||||
boolean wasCanceled);
|
||||
|
||||
/**
|
||||
* Called when data is removed from the back of a media buffer, typically so that it can be
|
||||
* re-buffered in a different format.
|
||||
*
|
||||
* @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
|
||||
* @param mediaStartTimeMs The start time of the media being discarded.
|
||||
* @param mediaEndTimeMs The end time of the media being discarded.
|
||||
*/
|
||||
void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs);
|
||||
|
||||
/**
|
||||
* Called when a downstream format change occurs (i.e. when the format of the media being read
|
||||
* from one or more {@link SampleStream}s provided by the source changes).
|
||||
*
|
||||
* @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
|
||||
* @param trackFormat The format of the track to which the data belongs. Null if the data does not
|
||||
* belong to a track.
|
||||
* @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
|
||||
* data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
|
||||
* @param trackSelectionData Optional data associated with the selection of the track to which the
|
||||
* data belongs. Null if the data does not belong to a track.
|
||||
* @param mediaTimeMs The media time at which the change occurred.
|
||||
*/
|
||||
void onDownstreamFormatChanged(
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaTimeMs);
|
||||
|
||||
/** Dispatches events to a {@link MediaSourceEventListener}. */
|
||||
final class EventDispatcher {
|
||||
|
||||
@Nullable private final Handler handler;
|
||||
@Nullable private final MediaSourceEventListener listener;
|
||||
private final long mediaTimeOffsetMs;
|
||||
|
||||
public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
|
||||
this(handler, listener, 0);
|
||||
}
|
||||
|
||||
public EventDispatcher(
|
||||
@Nullable Handler handler,
|
||||
@Nullable MediaSourceEventListener listener,
|
||||
long mediaTimeOffsetMs) {
|
||||
this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
|
||||
this.listener = listener;
|
||||
this.mediaTimeOffsetMs = mediaTimeOffsetMs;
|
||||
}
|
||||
|
||||
public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) {
|
||||
return new EventDispatcher(handler, listener, mediaTimeOffsetMs);
|
||||
}
|
||||
|
||||
public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
|
||||
loadStarted(
|
||||
dataSpec,
|
||||
dataType,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
null,
|
||||
C.TIME_UNSET,
|
||||
C.TIME_UNSET,
|
||||
elapsedRealtimeMs);
|
||||
}
|
||||
|
||||
public void loadStarted(
|
||||
final DataSpec dataSpec,
|
||||
final int dataType,
|
||||
final int trackType,
|
||||
final Format trackFormat,
|
||||
final int trackSelectionReason,
|
||||
final Object trackSelectionData,
|
||||
final long mediaStartTimeUs,
|
||||
final long mediaEndTimeUs,
|
||||
final long elapsedRealtimeMs) {
|
||||
if (listener != null && handler != null) {
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onLoadStarted(
|
||||
dataSpec,
|
||||
dataType,
|
||||
trackType,
|
||||
trackFormat,
|
||||
trackSelectionReason,
|
||||
trackSelectionData,
|
||||
adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs),
|
||||
elapsedRealtimeMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void loadCompleted(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded) {
|
||||
loadCompleted(
|
||||
dataSpec,
|
||||
dataType,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
null,
|
||||
C.TIME_UNSET,
|
||||
C.TIME_UNSET,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
bytesLoaded);
|
||||
}
|
||||
|
||||
public void loadCompleted(
|
||||
final DataSpec dataSpec,
|
||||
final int dataType,
|
||||
final int trackType,
|
||||
final Format trackFormat,
|
||||
final int trackSelectionReason,
|
||||
final Object trackSelectionData,
|
||||
final long mediaStartTimeUs,
|
||||
final long mediaEndTimeUs,
|
||||
final long elapsedRealtimeMs,
|
||||
final long loadDurationMs,
|
||||
final long bytesLoaded) {
|
||||
if (listener != null && handler != null) {
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onLoadCompleted(
|
||||
dataSpec,
|
||||
dataType,
|
||||
trackType,
|
||||
trackFormat,
|
||||
trackSelectionReason,
|
||||
trackSelectionData,
|
||||
adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs),
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
bytesLoaded);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void loadCanceled(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded) {
|
||||
loadCanceled(
|
||||
dataSpec,
|
||||
dataType,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
null,
|
||||
C.TIME_UNSET,
|
||||
C.TIME_UNSET,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
bytesLoaded);
|
||||
}
|
||||
|
||||
public void loadCanceled(
|
||||
final DataSpec dataSpec,
|
||||
final int dataType,
|
||||
final int trackType,
|
||||
final Format trackFormat,
|
||||
final int trackSelectionReason,
|
||||
final Object trackSelectionData,
|
||||
final long mediaStartTimeUs,
|
||||
final long mediaEndTimeUs,
|
||||
final long elapsedRealtimeMs,
|
||||
final long loadDurationMs,
|
||||
final long bytesLoaded) {
|
||||
if (listener != null && handler != null) {
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onLoadCanceled(
|
||||
dataSpec,
|
||||
dataType,
|
||||
trackType,
|
||||
trackFormat,
|
||||
trackSelectionReason,
|
||||
trackSelectionData,
|
||||
adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs),
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
bytesLoaded);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void loadError(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded,
|
||||
IOException error,
|
||||
boolean wasCanceled) {
|
||||
loadError(
|
||||
dataSpec,
|
||||
dataType,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
null,
|
||||
C.TIME_UNSET,
|
||||
C.TIME_UNSET,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
bytesLoaded,
|
||||
error,
|
||||
wasCanceled);
|
||||
}
|
||||
|
||||
public void loadError(
|
||||
final DataSpec dataSpec,
|
||||
final int dataType,
|
||||
final int trackType,
|
||||
final Format trackFormat,
|
||||
final int trackSelectionReason,
|
||||
final Object trackSelectionData,
|
||||
final long mediaStartTimeUs,
|
||||
final long mediaEndTimeUs,
|
||||
final long elapsedRealtimeMs,
|
||||
final long loadDurationMs,
|
||||
final long bytesLoaded,
|
||||
final IOException error,
|
||||
final boolean wasCanceled) {
|
||||
if (listener != null && handler != null) {
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onLoadError(
|
||||
dataSpec,
|
||||
dataType,
|
||||
trackType,
|
||||
trackFormat,
|
||||
trackSelectionReason,
|
||||
trackSelectionData,
|
||||
adjustMediaTime(mediaStartTimeUs),
|
||||
adjustMediaTime(mediaEndTimeUs),
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
bytesLoaded,
|
||||
error,
|
||||
wasCanceled);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void upstreamDiscarded(
|
||||
final int trackType, final long mediaStartTimeUs, final long mediaEndTimeUs) {
|
||||
if (listener != null && handler != null) {
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onUpstreamDiscarded(
|
||||
trackType, adjustMediaTime(mediaStartTimeUs), adjustMediaTime(mediaEndTimeUs));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void downstreamFormatChanged(
|
||||
final int trackType,
|
||||
final Format trackFormat,
|
||||
final int trackSelectionReason,
|
||||
final Object trackSelectionData,
|
||||
final long mediaTimeUs) {
|
||||
if (listener != null && handler != null) {
|
||||
handler.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onDownstreamFormatChanged(
|
||||
trackType,
|
||||
trackFormat,
|
||||
trackSelectionReason,
|
||||
trackSelectionData,
|
||||
adjustMediaTime(mediaTimeUs));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private long adjustMediaTime(long mediaTimeUs) {
|
||||
long mediaTimeMs = C.usToMs(mediaTimeUs);
|
||||
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,13 +15,11 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
@ -43,14 +41,14 @@ import java.util.Arrays;
|
||||
*/
|
||||
private static final int INITIAL_SAMPLE_SIZE = 1024;
|
||||
|
||||
private final Uri uri;
|
||||
private final DataSpec dataSpec;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final int minLoadableRetryCount;
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
private final int eventSourceId;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final TrackGroupArray tracks;
|
||||
private final ArrayList<SampleStreamImpl> sampleStreams;
|
||||
private final long durationUs;
|
||||
|
||||
// Package private to avoid thunk methods.
|
||||
/* package */ final Loader loader;
|
||||
/* package */ final Format format;
|
||||
@ -62,16 +60,20 @@ import java.util.Arrays;
|
||||
/* package */ int sampleSize;
|
||||
private int errorCount;
|
||||
|
||||
public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format,
|
||||
int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
|
||||
int eventSourceId, boolean treatLoadErrorsAsEndOfStream) {
|
||||
this.uri = uri;
|
||||
public SingleSampleMediaPeriod(
|
||||
DataSpec dataSpec,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
Format format,
|
||||
long durationUs,
|
||||
int minLoadableRetryCount,
|
||||
EventDispatcher eventDispatcher,
|
||||
boolean treatLoadErrorsAsEndOfStream) {
|
||||
this.dataSpec = dataSpec;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.format = format;
|
||||
this.durationUs = durationUs;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
this.eventSourceId = eventSourceId;
|
||||
this.eventDispatcher = eventDispatcher;
|
||||
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
|
||||
tracks = new TrackGroupArray(new TrackGroup(format));
|
||||
sampleStreams = new ArrayList<>();
|
||||
@ -125,7 +127,9 @@ import java.util.Arrays;
|
||||
if (loadingFinished || loader.isLoading()) {
|
||||
return false;
|
||||
}
|
||||
loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this,
|
||||
loader.startLoading(
|
||||
new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()),
|
||||
this,
|
||||
minLoadableRetryCount);
|
||||
return true;
|
||||
}
|
||||
@ -158,6 +162,18 @@ import java.util.Arrays;
|
||||
@Override
|
||||
public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs,
|
||||
long loadDurationMs) {
|
||||
eventDispatcher.loadCompleted(
|
||||
loadable.dataSpec,
|
||||
C.DATA_TYPE_MEDIA,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
format,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
/* trackSelectionData= */ null,
|
||||
/* mediaStartTimeUs= */ 0,
|
||||
durationUs,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
loadable.sampleSize);
|
||||
sampleSize = loadable.sampleSize;
|
||||
sampleData = loadable.sampleData;
|
||||
loadingFinished = true;
|
||||
@ -167,34 +183,46 @@ import java.util.Arrays;
|
||||
@Override
|
||||
public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
|
||||
boolean released) {
|
||||
// Do nothing.
|
||||
eventDispatcher.loadCanceled(
|
||||
loadable.dataSpec,
|
||||
C.DATA_TYPE_MEDIA,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
/* trackFormat= */ null,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
/* trackSelectionData= */ null,
|
||||
/* mediaStartTimeUs= */ 0,
|
||||
durationUs,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
loadable.sampleSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
|
||||
IOException error) {
|
||||
notifyLoadError(error);
|
||||
errorCount++;
|
||||
if (treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount) {
|
||||
boolean cancel = treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount;
|
||||
eventDispatcher.loadError(
|
||||
loadable.dataSpec,
|
||||
C.DATA_TYPE_MEDIA,
|
||||
C.TRACK_TYPE_UNKNOWN,
|
||||
format,
|
||||
C.SELECTION_REASON_UNKNOWN,
|
||||
/* trackSelectionData= */ null,
|
||||
/* mediaStartTimeUs= */ 0,
|
||||
durationUs,
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
loadable.sampleSize,
|
||||
error,
|
||||
/* wasCanceled= */ cancel);
|
||||
if (cancel) {
|
||||
loadingFinished = true;
|
||||
return Loader.DONT_RETRY;
|
||||
}
|
||||
return Loader.RETRY;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void notifyLoadError(final IOException e) {
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onLoadError(eventSourceId, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final class SampleStreamImpl implements SampleStream {
|
||||
|
||||
private static final int STREAM_STATE_SEND_FORMAT = 0;
|
||||
@ -259,14 +287,15 @@ import java.util.Arrays;
|
||||
|
||||
/* package */ static final class SourceLoadable implements Loadable {
|
||||
|
||||
private final Uri uri;
|
||||
public final DataSpec dataSpec;
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
private int sampleSize;
|
||||
private byte[] sampleData;
|
||||
|
||||
public SourceLoadable(Uri uri, DataSource dataSource) {
|
||||
this.uri = uri;
|
||||
public SourceLoadable(DataSpec dataSpec, DataSource dataSource) {
|
||||
this.dataSpec = dataSpec;
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@ -286,7 +315,7 @@ import java.util.Arrays;
|
||||
sampleSize = 0;
|
||||
try {
|
||||
// Create and open the input.
|
||||
dataSource.open(new DataSpec(uri));
|
||||
dataSource.open(dataSpec);
|
||||
// Load the sample data.
|
||||
int result = 0;
|
||||
while (result != C.RESULT_END_OF_INPUT) {
|
||||
|
@ -17,11 +17,14 @@ package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -32,7 +35,10 @@ public final class SingleSampleMediaSource implements MediaSource {
|
||||
|
||||
/**
|
||||
* Listener of {@link SingleSampleMediaSource} events.
|
||||
*
|
||||
* @deprecated Use {@link MediaSourceEventListener}.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface EventListener {
|
||||
|
||||
/**
|
||||
@ -45,18 +51,110 @@ public final class SingleSampleMediaSource implements MediaSource {
|
||||
|
||||
}
|
||||
|
||||
/** Factory for {@link SingleSampleMediaSource}. */
|
||||
public static final class Factory {
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
|
||||
private int minLoadableRetryCount;
|
||||
private boolean treatLoadErrorsAsEndOfStream;
|
||||
private boolean isCreateCalled;
|
||||
|
||||
/**
|
||||
* Creates a factory for {@link SingleSampleMediaSource}s.
|
||||
*
|
||||
* @param dataSourceFactory The factory from which the {@link DataSource} to read the media will
|
||||
* be obtained.
|
||||
*/
|
||||
public Factory(DataSource.Factory dataSourceFactory) {
|
||||
this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory);
|
||||
this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum number of times to retry if a loading error occurs. The default value is
|
||||
* {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}.
|
||||
*
|
||||
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
|
||||
* @return This factory, for convenience.
|
||||
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||
*/
|
||||
public Factory setMinLoadableRetryCount(int minLoadableRetryCount) {
|
||||
Assertions.checkState(!isCreateCalled);
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether load errors will be treated as end-of-stream signal (load errors will not be
|
||||
* propagated). The default value is false.
|
||||
*
|
||||
* @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample
|
||||
* streams, treating them as ended instead. If false, load errors will be propagated
|
||||
* normally by {@link SampleStream#maybeThrowError()}.
|
||||
* @return This factory, for convenience.
|
||||
* @throws IllegalStateException If one of the {@code create} methods has already been called.
|
||||
*/
|
||||
public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) {
|
||||
Assertions.checkState(!isCreateCalled);
|
||||
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events
|
||||
* will not be delivered.
|
||||
*
|
||||
* @param uri The {@link Uri}.
|
||||
* @param format The {@link Format} of the media stream.
|
||||
* @param durationUs The duration of the media stream in microseconds.
|
||||
* @return The new {@link ExtractorMediaSource}.
|
||||
*/
|
||||
public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) {
|
||||
return createMediaSource(uri, format, durationUs, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link SingleSampleMediaSource} using the current parameters.
|
||||
*
|
||||
* @param uri The {@link Uri}.
|
||||
* @param format The {@link Format} of the media stream.
|
||||
* @param durationUs The duration of the media stream in microseconds.
|
||||
* @param eventHandler A handler for events.
|
||||
* @param eventListener A listener of events., Format format, long durationUs
|
||||
* @return The newly built {@link SingleSampleMediaSource}.
|
||||
*/
|
||||
public SingleSampleMediaSource createMediaSource(
|
||||
Uri uri,
|
||||
Format format,
|
||||
long durationUs,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable MediaSourceEventListener eventListener) {
|
||||
isCreateCalled = true;
|
||||
return new SingleSampleMediaSource(
|
||||
uri,
|
||||
dataSourceFactory,
|
||||
format,
|
||||
durationUs,
|
||||
minLoadableRetryCount,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
treatLoadErrorsAsEndOfStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The default minimum number of times to retry loading data prior to failing.
|
||||
*/
|
||||
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
|
||||
|
||||
private final Uri uri;
|
||||
private final DataSpec dataSpec;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final Format format;
|
||||
private final long durationUs;
|
||||
private final MediaSourceEventListener.EventDispatcher eventDispatcher;
|
||||
private final int minLoadableRetryCount;
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
private final int eventSourceId;
|
||||
private final boolean treatLoadErrorsAsEndOfStream;
|
||||
private final Timeline timeline;
|
||||
|
||||
@ -66,9 +164,11 @@ public final class SingleSampleMediaSource implements MediaSource {
|
||||
* be obtained.
|
||||
* @param format The {@link Format} associated with the output track.
|
||||
* @param durationUs The duration of the media stream in microseconds.
|
||||
* @deprecated Use {@link Factory} instead.
|
||||
*/
|
||||
public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
|
||||
long durationUs) {
|
||||
@Deprecated
|
||||
public SingleSampleMediaSource(
|
||||
Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) {
|
||||
this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
|
||||
}
|
||||
|
||||
@ -79,10 +179,16 @@ public final class SingleSampleMediaSource implements MediaSource {
|
||||
* @param format The {@link Format} associated with the output track.
|
||||
* @param durationUs The duration of the media stream in microseconds.
|
||||
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
|
||||
* @deprecated Use {@link Factory} instead.
|
||||
*/
|
||||
public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
|
||||
long durationUs, int minLoadableRetryCount) {
|
||||
this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false);
|
||||
@Deprecated
|
||||
public SingleSampleMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
Format format,
|
||||
long durationUs,
|
||||
int minLoadableRetryCount) {
|
||||
this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,18 +204,46 @@ public final class SingleSampleMediaSource implements MediaSource {
|
||||
* @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample
|
||||
* streams, treating them as ended instead. If false, load errors will be propagated normally
|
||||
* by {@link SampleStream#maybeThrowError()}.
|
||||
* @deprecated Use {@link Factory} instead.
|
||||
*/
|
||||
public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
|
||||
long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
|
||||
int eventSourceId, boolean treatLoadErrorsAsEndOfStream) {
|
||||
this.uri = uri;
|
||||
@Deprecated
|
||||
public SingleSampleMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
Format format,
|
||||
long durationUs,
|
||||
int minLoadableRetryCount,
|
||||
Handler eventHandler,
|
||||
EventListener eventListener,
|
||||
int eventSourceId,
|
||||
boolean treatLoadErrorsAsEndOfStream) {
|
||||
this(
|
||||
uri,
|
||||
dataSourceFactory,
|
||||
format,
|
||||
durationUs,
|
||||
minLoadableRetryCount,
|
||||
eventHandler,
|
||||
eventListener == null ? null : new EventListenerWrapper(eventListener, eventSourceId),
|
||||
treatLoadErrorsAsEndOfStream);
|
||||
}
|
||||
|
||||
private SingleSampleMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
Format format,
|
||||
long durationUs,
|
||||
int minLoadableRetryCount,
|
||||
Handler eventHandler,
|
||||
MediaSourceEventListener eventListener,
|
||||
boolean treatLoadErrorsAsEndOfStream) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.format = format;
|
||||
this.durationUs = durationUs;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
this.eventSourceId = eventSourceId;
|
||||
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
|
||||
this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
dataSpec = new DataSpec(uri);
|
||||
timeline = new SinglePeriodTimeline(durationUs, true);
|
||||
}
|
||||
|
||||
@ -128,8 +262,14 @@ public final class SingleSampleMediaSource implements MediaSource {
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
Assertions.checkArgument(id.periodIndex == 0);
|
||||
return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount,
|
||||
eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream);
|
||||
return new SingleSampleMediaPeriod(
|
||||
dataSpec,
|
||||
dataSourceFactory,
|
||||
format,
|
||||
durationUs,
|
||||
minLoadableRetryCount,
|
||||
eventDispatcher,
|
||||
treatLoadErrorsAsEndOfStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -142,4 +282,97 @@ public final class SingleSampleMediaSource implements MediaSource {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in
|
||||
* {@link MediaSourceEventListener}.
|
||||
*/
|
||||
private static final class EventListenerWrapper implements MediaSourceEventListener {
|
||||
|
||||
private final EventListener eventListener;
|
||||
private final int eventSourceId;
|
||||
|
||||
public EventListenerWrapper(EventListener eventListener, int eventSourceId) {
|
||||
this.eventListener = Assertions.checkNotNull(eventListener);
|
||||
this.eventSourceId = eventSourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(
|
||||
DataSpec dataSpec,
|
||||
int dataType,
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaStartTimeMs,
|
||||
long mediaEndTimeMs,
|
||||
long elapsedRealtimeMs,
|
||||
long loadDurationMs,
|
||||
long bytesLoaded,
|
||||
IOException error,
|
||||
boolean wasCanceled) {
|
||||
eventListener.onLoadError(eventSourceId, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(
|
||||
int trackType,
|
||||
Format trackFormat,
|
||||
int trackSelectionReason,
|
||||
Object trackSelectionData,
|
||||
long mediaTimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,8 +62,11 @@ public final class TrackGroupArray {
|
||||
* @param group The group.
|
||||
* @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists.
|
||||
*/
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public int indexOf(TrackGroup group) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
// Suppressed reference equality warning because this is looking for the index of a specific
|
||||
// TrackGroup object, not the index of a potential equal TrackGroup.
|
||||
if (trackGroups[i] == group) {
|
||||
return i;
|
||||
}
|
||||
@ -71,6 +74,13 @@ public final class TrackGroupArray {
|
||||
return C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this track group array is empty.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return length == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (hashCode == 0) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.source.ads;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -71,6 +72,15 @@ public interface AdsLoader {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the supported content types for ad media. Must be called before the first call to
|
||||
* {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Subsequent calls may be ignored.
|
||||
*
|
||||
* @param contentTypes The supported content types for ad media. Each element must be one of
|
||||
* {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}.
|
||||
*/
|
||||
void setSupportedContentTypes(@C.ContentType int... contentTypes);
|
||||
|
||||
/**
|
||||
* Attaches a player that will play ads loaded using this instance. Called on the main thread by
|
||||
* {@link AdsMediaSource}.
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.ads;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.Nullable;
|
||||
@ -23,16 +24,19 @@ import android.view.ViewGroup;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.DeferredMediaPeriod;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -40,10 +44,33 @@ import java.util.Map;
|
||||
*/
|
||||
public final class AdsMediaSource implements MediaSource {
|
||||
|
||||
/**
|
||||
* Listener for events relating to ad loading.
|
||||
*/
|
||||
public interface AdsListener {
|
||||
/** Factory for creating {@link MediaSource}s to play ad media. */
|
||||
public interface MediaSourceFactory {
|
||||
|
||||
/**
|
||||
* Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}.
|
||||
*
|
||||
* @param uri The URI of the media or manifest to play.
|
||||
* @param handler A handler for listener events. May be null if delivery of events is not
|
||||
* required.
|
||||
* @param listener A listener for events. May be null if delivery of events is not required.
|
||||
* @return The new media source.
|
||||
*/
|
||||
MediaSource createMediaSource(
|
||||
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener);
|
||||
|
||||
/**
|
||||
* Returns the content types supported by media sources created by this factory. Each element
|
||||
* should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link
|
||||
* C#TYPE_OTHER}.
|
||||
*
|
||||
* @return The content types supported by media sources created by this factory.
|
||||
*/
|
||||
int[] getSupportedTypes();
|
||||
}
|
||||
|
||||
/** Listener for ads media source events. */
|
||||
public interface EventListener extends MediaSourceEventListener {
|
||||
|
||||
/**
|
||||
* Called if there was an error loading ads. The media source will load the content without ads
|
||||
@ -69,17 +96,15 @@ public final class AdsMediaSource implements MediaSource {
|
||||
private static final String TAG = "AdsMediaSource";
|
||||
|
||||
private final MediaSource contentMediaSource;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final MediaSourceFactory adMediaSourceFactory;
|
||||
private final AdsLoader adsLoader;
|
||||
private final ViewGroup adUiViewGroup;
|
||||
@Nullable private final Handler eventHandler;
|
||||
@Nullable private final EventListener eventListener;
|
||||
private final Handler mainHandler;
|
||||
private final ComponentListener componentListener;
|
||||
private final Map<MediaPeriod, MediaSource> adMediaSourceByMediaPeriod;
|
||||
private final Map<MediaSource, List<DeferredMediaPeriod>> deferredMediaPeriodByAdMediaSource;
|
||||
private final Timeline.Period period;
|
||||
@Nullable
|
||||
private final Handler eventHandler;
|
||||
@Nullable
|
||||
private final AdsListener eventListener;
|
||||
|
||||
private Handler playerHandler;
|
||||
private ExoPlayer player;
|
||||
@ -94,22 +119,31 @@ public final class AdsMediaSource implements MediaSource {
|
||||
private MediaSource.Listener listener;
|
||||
|
||||
/**
|
||||
* Constructs a new source that inserts ads linearly with the content specified by
|
||||
* {@code contentMediaSource}.
|
||||
* Constructs a new source that inserts ads linearly with the content specified by {@code
|
||||
* contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}.
|
||||
*
|
||||
* @param contentMediaSource The {@link MediaSource} providing the content to play.
|
||||
* @param dataSourceFactory Factory for data sources used to load ad media.
|
||||
* @param adsLoader The loader for ads.
|
||||
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
|
||||
*/
|
||||
public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
|
||||
AdsLoader adsLoader, ViewGroup adUiViewGroup) {
|
||||
this(contentMediaSource, dataSourceFactory, adsLoader, adUiViewGroup, null, null);
|
||||
public AdsMediaSource(
|
||||
MediaSource contentMediaSource,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
AdsLoader adsLoader,
|
||||
ViewGroup adUiViewGroup) {
|
||||
this(
|
||||
contentMediaSource,
|
||||
dataSourceFactory,
|
||||
adsLoader,
|
||||
adUiViewGroup,
|
||||
/* eventHandler= */ null,
|
||||
/* eventListener= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new source that inserts ads linearly with the content specified by
|
||||
* {@code contentMediaSource}.
|
||||
* Constructs a new source that inserts ads linearly with the content specified by {@code
|
||||
* contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}.
|
||||
*
|
||||
* @param contentMediaSource The {@link MediaSource} providing the content to play.
|
||||
* @param dataSourceFactory Factory for data sources used to load ad media.
|
||||
@ -118,21 +152,53 @@ public final class AdsMediaSource implements MediaSource {
|
||||
* @param eventHandler A handler for events. May be null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory,
|
||||
AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler,
|
||||
@Nullable AdsListener eventListener) {
|
||||
public AdsMediaSource(
|
||||
MediaSource contentMediaSource,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
AdsLoader adsLoader,
|
||||
ViewGroup adUiViewGroup,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable EventListener eventListener) {
|
||||
this(
|
||||
contentMediaSource,
|
||||
new ExtractorMediaSource.Factory(dataSourceFactory),
|
||||
adsLoader,
|
||||
adUiViewGroup,
|
||||
eventHandler,
|
||||
eventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new source that inserts ads linearly with the content specified by {@code
|
||||
* contentMediaSource}.
|
||||
*
|
||||
* @param contentMediaSource The {@link MediaSource} providing the content to play.
|
||||
* @param adMediaSourceFactory Factory for media sources used to load ad media.
|
||||
* @param adsLoader The loader for ads.
|
||||
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
|
||||
* @param eventHandler A handler for events. May be null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public AdsMediaSource(
|
||||
MediaSource contentMediaSource,
|
||||
MediaSourceFactory adMediaSourceFactory,
|
||||
AdsLoader adsLoader,
|
||||
ViewGroup adUiViewGroup,
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable EventListener eventListener) {
|
||||
this.contentMediaSource = contentMediaSource;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.adMediaSourceFactory = adMediaSourceFactory;
|
||||
this.adsLoader = adsLoader;
|
||||
this.adUiViewGroup = adUiViewGroup;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
mainHandler = new Handler(Looper.getMainLooper());
|
||||
componentListener = new ComponentListener();
|
||||
adMediaSourceByMediaPeriod = new HashMap<>();
|
||||
deferredMediaPeriodByAdMediaSource = new HashMap<>();
|
||||
period = new Timeline.Period();
|
||||
adGroupMediaSources = new MediaSource[0][];
|
||||
adDurationsUs = new long[0][];
|
||||
adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -173,9 +239,9 @@ public final class AdsMediaSource implements MediaSource {
|
||||
final int adGroupIndex = id.adGroupIndex;
|
||||
final int adIndexInAdGroup = id.adIndexInAdGroup;
|
||||
if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
|
||||
MediaSource adMediaSource = new ExtractorMediaSource(
|
||||
adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory,
|
||||
new DefaultExtractorsFactory(), mainHandler, componentListener);
|
||||
Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup];
|
||||
final MediaSource adMediaSource =
|
||||
adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener);
|
||||
int oldAdCount = adGroupMediaSources[id.adGroupIndex].length;
|
||||
if (adIndexInAdGroup >= oldAdCount) {
|
||||
int adCount = adIndexInAdGroup + 1;
|
||||
@ -185,30 +251,37 @@ public final class AdsMediaSource implements MediaSource {
|
||||
Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET);
|
||||
}
|
||||
adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
|
||||
adMediaSource.prepareSource(player, false, new Listener() {
|
||||
deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList<DeferredMediaPeriod>());
|
||||
adMediaSource.prepareSource(player, false, new MediaSource.Listener() {
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
|
||||
Object manifest) {
|
||||
onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline);
|
||||
@Nullable Object manifest) {
|
||||
onAdSourceInfoRefreshed(adMediaSource, adGroupIndex, adIndexInAdGroup, timeline);
|
||||
}
|
||||
});
|
||||
}
|
||||
MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
|
||||
MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator);
|
||||
adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource);
|
||||
return mediaPeriod;
|
||||
DeferredMediaPeriod deferredMediaPeriod =
|
||||
new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator);
|
||||
List<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
|
||||
if (mediaPeriods == null) {
|
||||
deferredMediaPeriod.createPeriod();
|
||||
} else {
|
||||
// Keep track of the deferred media period so it can be populated with the real media period
|
||||
// when the source's info becomes available.
|
||||
mediaPeriods.add(deferredMediaPeriod);
|
||||
}
|
||||
return deferredMediaPeriod;
|
||||
} else {
|
||||
return contentMediaSource.createPeriod(id, allocator);
|
||||
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator);
|
||||
mediaPeriod.createPeriod();
|
||||
return mediaPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) {
|
||||
adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod);
|
||||
} else {
|
||||
contentMediaSource.releasePeriod(mediaPeriod);
|
||||
}
|
||||
((DeferredMediaPeriod) mediaPeriod).releasePeriod();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -263,9 +336,17 @@ public final class AdsMediaSource implements MediaSource {
|
||||
maybeUpdateSourceInfo();
|
||||
}
|
||||
|
||||
private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) {
|
||||
private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex,
|
||||
int adIndexInAdGroup, Timeline timeline) {
|
||||
Assertions.checkArgument(timeline.getPeriodCount() == 1);
|
||||
adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs();
|
||||
if (deferredMediaPeriodByAdMediaSource.containsKey(mediaSource)) {
|
||||
List<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
|
||||
for (int i = 0; i < mediaPeriods.size(); i++) {
|
||||
mediaPeriods.get(i).createPeriod();
|
||||
}
|
||||
deferredMediaPeriodByAdMediaSource.remove(mediaSource);
|
||||
}
|
||||
maybeUpdateSourceInfo();
|
||||
}
|
||||
|
||||
@ -280,11 +361,8 @@ public final class AdsMediaSource implements MediaSource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for component events. All methods are called on the main thread.
|
||||
*/
|
||||
private final class ComponentListener implements AdsLoader.EventListener,
|
||||
ExtractorMediaSource.EventListener {
|
||||
/** Listener for component events. All methods are called on the main thread. */
|
||||
private final class ComponentListener implements AdsLoader.EventListener {
|
||||
|
||||
@Override
|
||||
public void onAdPlaybackState(final AdPlaybackState adPlaybackState) {
|
||||
|
@ -16,12 +16,11 @@
|
||||
package com.google.android.exoplayer2.source.chunk;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.SampleQueue;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
import com.google.android.exoplayer2.source.SequenceableLoader;
|
||||
|
@ -33,7 +33,6 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -185,7 +184,7 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
private final ParsableByteArray ccData;
|
||||
private final int packetLength;
|
||||
private final int selectedField;
|
||||
private final LinkedList<CueBuilder> cueBuilders;
|
||||
private final ArrayList<CueBuilder> cueBuilders;
|
||||
|
||||
private CueBuilder currentCueBuilder;
|
||||
private List<Cue> cues;
|
||||
@ -200,7 +199,7 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
|
||||
public Cea608Decoder(String mimeType, int accessibilityChannel) {
|
||||
ccData = new ParsableByteArray();
|
||||
cueBuilders = new LinkedList<>();
|
||||
cueBuilders = new ArrayList<>();
|
||||
currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
|
||||
packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
|
||||
switch (accessibilityChannel) {
|
||||
@ -230,8 +229,8 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
cues = null;
|
||||
lastCues = null;
|
||||
setCaptionMode(CC_MODE_UNKNOWN);
|
||||
setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT);
|
||||
resetCueBuilders();
|
||||
captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
|
||||
repeatableControlSet = false;
|
||||
repeatableControlCc1 = 0;
|
||||
repeatableControlCc2 = 0;
|
||||
@ -434,16 +433,16 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
private void handleMiscCode(byte cc2) {
|
||||
switch (cc2) {
|
||||
case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
|
||||
captionRowCount = 2;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
setCaptionRowCount(2);
|
||||
return;
|
||||
case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
|
||||
captionRowCount = 3;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
setCaptionRowCount(3);
|
||||
return;
|
||||
case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
|
||||
captionRowCount = 4;
|
||||
setCaptionMode(CC_MODE_ROLL_UP);
|
||||
setCaptionRowCount(4);
|
||||
return;
|
||||
case CTRL_RESUME_CAPTION_LOADING:
|
||||
setCaptionMode(CC_MODE_POP_ON);
|
||||
@ -451,6 +450,9 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
case CTRL_RESUME_DIRECT_CAPTIONING:
|
||||
setCaptionMode(CC_MODE_PAINT_ON);
|
||||
return;
|
||||
default:
|
||||
// Fall through.
|
||||
break;
|
||||
}
|
||||
|
||||
if (captionMode == CC_MODE_UNKNOWN) {
|
||||
@ -484,6 +486,9 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
case CTRL_DELETE_TO_END_OF_ROW:
|
||||
// TODO: implement
|
||||
break;
|
||||
default:
|
||||
// Fall through.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -515,8 +520,13 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
private void setCaptionRowCount(int captionRowCount) {
|
||||
this.captionRowCount = captionRowCount;
|
||||
currentCueBuilder.setCaptionRowCount(captionRowCount);
|
||||
}
|
||||
|
||||
private void resetCueBuilders() {
|
||||
currentCueBuilder.reset(captionMode, captionRowCount);
|
||||
currentCueBuilder.reset(captionMode);
|
||||
cueBuilders.clear();
|
||||
cueBuilders.add(currentCueBuilder);
|
||||
}
|
||||
@ -594,12 +604,14 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
public CueBuilder(int captionMode, int captionRowCount) {
|
||||
preambleStyles = new ArrayList<>();
|
||||
midrowStyles = new ArrayList<>();
|
||||
rolledUpCaptions = new LinkedList<>();
|
||||
rolledUpCaptions = new ArrayList<>();
|
||||
captionStringBuilder = new SpannableStringBuilder();
|
||||
reset(captionMode, captionRowCount);
|
||||
reset(captionMode);
|
||||
setCaptionRowCount(captionRowCount);
|
||||
}
|
||||
|
||||
public void reset(int captionMode, int captionRowCount) {
|
||||
public void reset(int captionMode) {
|
||||
this.captionMode = captionMode;
|
||||
preambleStyles.clear();
|
||||
midrowStyles.clear();
|
||||
rolledUpCaptions.clear();
|
||||
@ -607,11 +619,13 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
row = BASE_ROW;
|
||||
indent = 0;
|
||||
tabOffset = 0;
|
||||
this.captionMode = captionMode;
|
||||
this.captionRowCount = captionRowCount;
|
||||
underlineStartPosition = POSITION_UNSET;
|
||||
}
|
||||
|
||||
public void setCaptionRowCount(int captionRowCount) {
|
||||
this.captionRowCount = captionRowCount;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty()
|
||||
&& captionStringBuilder.length() == 0;
|
||||
@ -726,8 +740,10 @@ public final class Cea608Decoder extends CeaDecoder {
|
||||
// The number of empty columns after the end of the text, in the same range.
|
||||
int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length();
|
||||
int startEndPaddingDelta = startPadding - endPadding;
|
||||
if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) {
|
||||
// Treat approximately centered pop-on captions are middle aligned.
|
||||
if (captionMode == CC_MODE_POP_ON && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) {
|
||||
// Treat approximately centered pop-on captions as middle aligned. We also treat captions
|
||||
// that are wider than they should be in this way. See
|
||||
// https://github.com/google/ExoPlayer/issues/3534.
|
||||
position = 0.5f;
|
||||
positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
|
||||
} else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) {
|
||||
|
@ -104,7 +104,7 @@ public final class Cea708Decoder extends CeaDecoder {
|
||||
private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes)
|
||||
private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes)
|
||||
private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes)
|
||||
private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes)
|
||||
private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes)
|
||||
private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes)
|
||||
private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes)
|
||||
private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes)
|
||||
@ -464,7 +464,7 @@ public final class Cea708Decoder extends CeaDecoder {
|
||||
case COMMAND_DF1:
|
||||
case COMMAND_DF2:
|
||||
case COMMAND_DF3:
|
||||
case COMMAND_DS4:
|
||||
case COMMAND_DF4:
|
||||
case COMMAND_DF5:
|
||||
case COMMAND_DF6:
|
||||
case COMMAND_DF7:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user