Merge pull request #2570 from google/dev-v2

r2.3.0
This commit is contained in:
ojw28 2017-03-17 20:42:16 +00:00 committed by GitHub
commit 86230c4510
211 changed files with 6440 additions and 2584 deletions

View file

@ -1,5 +1,52 @@
# Release notes #
### r2.3.0 ###
* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio
rendering. You can read more about the GVR extension
[here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g).
* DASH improvements:
* Support embedded CEA-608 closed captions
([#2362](https://github.com/google/ExoPlayer/issues/2362)).
* Support embedded EMSG events
([#2176](https://github.com/google/ExoPlayer/issues/2176)).
* Support mspr:pro manifest element
([#2386](https://github.com/google/ExoPlayer/issues/2386)).
* Correct handling of empty segment indices at the start of live events
([#1865](https://github.com/google/ExoPlayer/issues/1865)).
* HLS improvements:
* Respect initial track selection
([#2353](https://github.com/google/ExoPlayer/issues/2353)).
* Reduced frequency of media playlist requests when playback position is close
to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)).
* Exposed the master playlist through ExoPlayer.getCurrentManifest()
([#2537](https://github.com/google/ExoPlayer/issues/2537)).
* Support CLOSED-CAPTIONS #EXT-X-MEDIA type
([#341](https://github.com/google/ExoPlayer/issues/341)).
* Fixed handling of negative values in #EXT-X-SUPPORT
([#2495](https://github.com/google/ExoPlayer/issues/2495)).
* Fixed potential endless buffering state for streams with WebVTT subtitles
([#2424](https://github.com/google/ExoPlayer/issues/2424)).
* MPEG-TS improvements:
* Support for multiple programs.
* Support for multiple closed captions and caption service descriptors
([#2161](https://github.com/google/ExoPlayer/issues/2161)).
* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable
constant bitrate seeking in MP3 files that would otherwise be unseekable
([#2445](https://github.com/google/ExoPlayer/issues/2445)).
* ID3: Better handle malformed ID3 data
([#2486](https://github.com/google/ExoPlayer/issues/2486)).
* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector.
* DRM: Add support for CENC ClearKey on API level 21+
([#2361](https://github.com/google/ExoPlayer/issues/2361)).
* DRM: Support dynamic setting of key request headers
([#1924](https://github.com/google/ExoPlayer/issues/1924)).
* SmoothStreaming: Fixed handling of start_time placeholder
([#2447](https://github.com/google/ExoPlayer/issues/2447)).
* FLAC extension: Fix proguard configuration
([#2427](https://github.com/google/ExoPlayer/issues/2427)).
* Misc bugfixes.
### r2.2.0 ###
* Demo app: Automatic recovery from BehindLiveWindowException, plus improved
@ -246,6 +293,12 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later,
however it can be assumed that all such changes are included in the most recent
V2 release.
### r1.5.15 ###
* SmoothStreaming: Fixed handling of start_time placeholder
([#2447](https://github.com/google/ExoPlayer/issues/2447)).
* Misc bugfixes.
### r1.5.14 ###
* Fixed cache failures when using an encrypted cache content index.

View file

@ -11,16 +11,13 @@
// 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.
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.1'
classpath 'com.novoda:bintray-release:0.3.4'
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.novoda:bintray-release:0.4.0'
}
}
@ -29,13 +26,24 @@ allprojects {
jcenter()
}
project.ext {
compileSdkVersion=24
targetSdkVersion=24
buildToolsVersion='23.0.3'
releaseRepoName = 'exoplayer'
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality
// provided by the library requires API level 16 or greater.
minSdkVersion=9
compileSdkVersion=25
targetSdkVersion=25
buildToolsVersion='25'
releaseRepoName = getBintrayRepo()
releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.2.0'
releaseVersion = 'r2.3.0'
releaseWebsite = 'https://github.com/google/ExoPlayer'
}
}
def getBintrayRepo() {
boolean publicRepo = hasProperty('publicRepo') &&
property('publicRepo').toBoolean()
return publicRepo ? 'exoplayer' : 'exoplayer-test'
}

View file

@ -33,6 +33,11 @@ android {
}
}
lintOptions {
// The demo app does not have translations.
disable 'MissingTranslation'
}
productFlavors {
noExtensions
withExtensions

View file

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2200"
android:versionName="2.2.0">
android:versionCode="2300"
android:versionName="2.3.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

View file

@ -277,6 +277,18 @@
}
]
},
{
"name": "ClearKey DASH",
"samples": [
{
"name": "Big Buck Bunny (CENC ClearKey)",
"uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd",
"extension": "mpd",
"drm_scheme": "cenc",
"drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json"
}
]
},
{
"name": "SmoothStreaming",
"samples": [

View file

@ -101,9 +101,6 @@ import java.util.Locale;
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
if (timeline == null) {
return;
}
int periodCount = timeline.getPeriodCount();
int windowCount = timeline.getWindowCount();
Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);

View file

@ -55,7 +55,7 @@ import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
@ -70,8 +70,6 @@ import com.google.android.exoplayer2.util.Util;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
@ -112,7 +110,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private DefaultTrackSelector trackSelector;
private TrackSelectionHelper trackSelectionHelper;
private DebugTextViewHelper debugViewHelper;
private boolean playerNeedsSource;
private boolean needRetrySource;
private boolean shouldAutoPlay;
private int resumeWindow;
@ -231,7 +229,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private void initializePlayer() {
Intent intent = getIntent();
if (player == null) {
boolean needNewPlayer = player == null;
if (needNewPlayer) {
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
@ -239,19 +238,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (drmSchemeUuid != null) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
Map<String, String> keyRequestProperties;
if (keyRequestPropertiesArray == null || keyRequestPropertiesArray.length < 2) {
keyRequestProperties = null;
} else {
keyRequestProperties = new HashMap<>();
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
keyRequestProperties.put(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]);
}
}
try {
drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl,
keyRequestProperties);
keyRequestPropertiesArray);
} catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
@ -267,7 +256,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(),
@ -284,9 +273,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.setPlayWhenReady(shouldAutoPlay);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
playerNeedsSource = true;
}
if (playerNeedsSource) {
if (needNewPlayer || needRetrySource) {
String action = intent.getAction();
Uri[] uris;
String[] extensions;
@ -322,14 +310,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.seekTo(resumeWindow, resumePosition);
}
player.prepare(mediaSource, !haveResumePosition, false);
playerNeedsSource = false;
needRetrySource = false;
updateButtonVisibilities();
}
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
: uri.getLastPathSegment());
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
: Util.inferContentType("." + overrideExtension);
switch (type) {
case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false),
@ -349,12 +337,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(UUID uuid,
String licenseUrl, Map<String, String> keyRequestProperties) throws UnsupportedDrmException {
String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) {
return null;
}
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false), keyRequestProperties);
buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]);
}
}
return new DefaultDrmSessionManager<>(uuid,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
}
@ -425,7 +419,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override
public void onPositionDiscontinuity() {
if (playerNeedsSource) {
if (needRetrySource) {
// This will only occur if the user has performed a seek whilst in the error state. Update the
// resume position so that if the user then retries, playback will resume from the position to
// which they seeked.
@ -466,7 +460,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (errorString != null) {
showToast(errorString);
}
playerNeedsSource = true;
needRetrySource = true;
if (isBehindLiveWindow(e)) {
clearResumePosition();
initializePlayer();
@ -498,7 +492,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private void updateButtonVisibilities() {
debugRootView.removeAllViews();
retryButton.setVisibility(playerNeedsSource ? View.VISIBLE : View.GONE);
retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE);
debugRootView.addView(retryButton);
if (player == null) {

View file

@ -262,11 +262,13 @@ public class SampleChooserActivity extends Activity {
}
private UUID getDrmUuid(String typeString) throws ParserException {
switch (typeString.toLowerCase()) {
switch (Util.toLowerInvariant(typeString)) {
case "widevine":
return C.WIDEVINE_UUID;
case "playready":
return C.PLAYREADY_UUID;
case "cenc":
return C.CLEARKEY_UUID;
default:
try {
return UUID.fromString(typeString);

View file

@ -51,7 +51,7 @@ import java.util.Locale;
private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory();
private final MappingTrackSelector selector;
private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory;
private final TrackSelection.Factory adaptiveTrackSelectionFactory;
private MappedTrackInfo trackInfo;
private int rendererIndex;
@ -67,13 +67,13 @@ import java.util.Locale;
/**
* @param selector The track selector.
* @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s,
* or null if the selection helper should not support adaptive video.
* @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null
* if the selection helper should not support adaptive tracks.
*/
public TrackSelectionHelper(MappingTrackSelector selector,
TrackSelection.Factory adaptiveVideoTrackSelectionFactory) {
TrackSelection.Factory adaptiveTrackSelectionFactory) {
this.selector = selector;
this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory;
this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
}
/**
@ -92,7 +92,7 @@ import java.util.Locale;
trackGroups = trackInfo.getTrackGroups(rendererIndex);
trackGroupsAdaptive = new boolean[trackGroups.length];
for (int i = 0; i < trackGroups.length; i++) {
trackGroupsAdaptive[i] = adaptiveVideoTrackSelectionFactory != null
trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null
&& trackInfo.getAdaptiveSupport(rendererIndex, i, false)
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED
&& trackGroups.get(i).length > 1;
@ -271,7 +271,7 @@ import java.util.Locale;
private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) {
TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveVideoTrackSelectionFactory);
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory);
override = new SelectionOverride(factory, group, tracks);
}
@ -301,15 +301,18 @@ import java.util.Locale;
private static String buildTrackName(Format format) {
String trackName;
if (MimeTypes.isVideo(format.sampleMimeType)) {
trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format),
buildBitrateString(format)), buildTrackIdString(format));
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(
buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} else if (MimeTypes.isAudio(format.sampleMimeType)) {
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
buildAudioPropertyString(format)), buildBitrateString(format)),
buildTrackIdString(format));
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(
buildLanguageString(format), buildAudioPropertyString(format)),
buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} else {
trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format),
buildBitrateString(format)), buildTrackIdString(format));
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
}
return trackName.length() == 0 ? "unknown" : trackName;
}
@ -342,4 +345,8 @@ import java.util.Locale;
return format.id == null ? "" : ("id:" + format.id);
}
private static String buildSampleMimeTypeString(Format format) {
return format.sampleMimeType == null ? "" : format.sampleMimeType;
}
}

View file

@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

View file

@ -118,7 +118,8 @@ public final class CronetDataSourceTest {
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects
mockClock));
mockClock,
null));
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))

View file

@ -32,7 +32,6 @@ import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -98,7 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
private final Map<String, String> requestProperties;
private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final ConditionVariable operation;
private final Clock clock;
@ -136,7 +136,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) {
this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS, false);
DEFAULT_READ_TIMEOUT_MILLIS, false, null);
}
/**
@ -149,17 +149,20 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
* @param defaultRequestProperties The default request properties to be used.
*/
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects) {
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
RequestProperties defaultRequestProperties) {
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock());
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties);
}
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock) {
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock,
RequestProperties defaultRequestProperties) {
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = Assertions.checkNotNull(executor);
this.contentTypePredicate = contentTypePredicate;
@ -168,7 +171,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
this.clock = Assertions.checkNotNull(clock);
requestProperties = new HashMap<>();
this.defaultRequestProperties = defaultRequestProperties;
requestProperties = new RequestProperties();
operation = new ConditionVariable();
}
@ -176,23 +180,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
@Override
public void setRequestProperty(String name, String value) {
synchronized (requestProperties) {
requestProperties.put(name, value);
}
requestProperties.set(name, value);
}
@Override
public void clearRequestProperty(String name) {
synchronized (requestProperties) {
requestProperties.remove(name);
}
requestProperties.remove(name);
}
@Override
public void clearAllRequestProperties() {
synchronized (requestProperties) {
requestProperties.clear();
}
requestProperties.clear();
}
@Override
@ -421,16 +419,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(),
this, executor);
// Set the headers.
synchronized (requestProperties) {
if (dataSpec.postBody != null && dataSpec.postBody.length != 0
&& !requestProperties.containsKey(CONTENT_TYPE)) {
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec,
Status.IDLE);
}
for (Entry<String, String> headerEntry : requestProperties.entrySet()) {
requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
boolean isContentTypeHeaderSet = false;
if (defaultRequestProperties != null) {
for (Entry<String, String> headerEntry : defaultRequestProperties.getSnapshot().entrySet()) {
String key = headerEntry.getKey();
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
requestBuilder.addHeader(key, headerEntry.getValue());
}
}
Map<String, String> requestPropertiesSnapshot = requestProperties.getSnapshot();
for (Entry<String, String> headerEntry : requestPropertiesSnapshot.entrySet()) {
String key = headerEntry.getKey();
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
requestBuilder.addHeader(key, headerEntry.getValue());
}
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec,
Status.IDLE);
}
// Set the Range header.
if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder();

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
@ -68,9 +69,10 @@ public final class CronetDataSourceFactory extends BaseFactory {
}
@Override
protected CronetDataSource createDataSourceInternal() {
protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) {
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects);
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, null, defaultRequestProperties);
}
}

View file

@ -31,9 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
NDK_PATH="<path to Android NDK>"
```
* Fetch and build FFmpeg.
For example, to fetch and build for armv7a:
* Fetch and build FFmpeg. For example, to fetch and build for armv7a:
```
cd "${FFMPEG_EXT_PATH}/jni" && \
@ -69,15 +67,14 @@ make -j4 && \
make install-libs
```
* Build the JNI native libraries.
* Build the JNI native libraries. Repeat this step for any other architectures
you need to support.
```
cd "${FFMPEG_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4
```
Repeat these steps for any other architectures you need to support.
* In your project, you can add a dependency on the extension by using a rule
like this:

View file

@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}

View file

@ -19,7 +19,7 @@ import android.os.Handler;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
@ -43,21 +43,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* @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.
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
super(eventHandler, eventListener);
}
/**
* @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 audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@Override

View file

@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}

View file

@ -5,7 +5,10 @@
native <methods>;
}
# Some members of this class are being accessed from native methods. Keep them unobfuscated.
# Some members of these classes are being accessed from native methods. Keep them unobfuscated.
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*;
}
-keep class com.google.android.exoplayer2.util.FlacStreamInfo {
*;
}

View file

@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = extractorOutput.track(0);
trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
extractorOutput.endTracks();
try {
decoderJni = new FlacDecoderJni();

View file

@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import android.os.Handler;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
@ -38,21 +38,11 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @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.
*/
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
super(eventHandler, eventListener);
}
/**
* @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 audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@Override

View file

@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \
LOCAL_SRC_FILES := $(FLAC_SOURCES)
LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM
LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC
LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H
LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions
LOCAL_LDLIBS := -llog -lz -lm

View file

@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) {
}
FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points;
for (unsigned i = mSeekTable->num_points - 1; i >= 0; i--) {
for (unsigned i = mSeekTable->num_points; i > 0; ) {
i--;
if (points[i].sample_number <= sample) {
return firstFrameOffset + points[i].stream_offset;
}

38
extensions/gvr/README.md Normal file
View file

@ -0,0 +1,38 @@
# ExoPlayer GVR Extension #
## Description ##
The GVR extension wraps the [Google VR SDK for Android][]. It provides a
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
of surround sound and ambisonic soundfields.
## Using the extension ##
The easiest way to use the extension is to add it as a gradle dependency. You
need to make sure you have the jcenter repository included in the `build.gradle`
file in the root of your project:
```gradle
repositories {
jcenter()
}
```
Next, include the following in your module's `build.gradle` file:
```gradle
compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
```
where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used.
## Using GvrAudioProcessor ##
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
return a GvrAudioProcessor.
* If constructing renderers directly, pass a GvrAudioProcessor to
MediaCodecAudioRenderer's constructor.
[Google VR SDK for Android]: https://developers.google.com/vr/android/
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround

View file

@ -0,0 +1,35 @@
// 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.
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
}
}
dependencies {
compile project(':library')
compile 'com.google.vr:sdk-audio:1.30.0'
}
ext {
releaseArtifact = 'extension-gvr'
releaseDescription = 'Google VR extension for ExoPlayer.'
}
apply from: '../../publish.gradle'

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest package="com.google.android.exoplayer2.ext.gvr"/>

View file

@ -0,0 +1,173 @@
/*
* 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.ext.gvr;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.vr.sdk.audio.GvrAudioSurround;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of
* surround sound and ambisonic soundfields.
*/
public final class GvrAudioProcessor implements AudioProcessor {
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
private static final int OUTPUT_CHANNEL_COUNT = 2;
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
private int sampleRateHz;
private int channelCount;
private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer;
private boolean inputEnded;
private float w;
private float x;
private float y;
private float z;
/**
* Creates a new GVR audio processor.
*/
public GvrAudioProcessor() {
// Use the identity for the initial orientation.
w = 1f;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
}
/**
* Updates the listener head orientation. May be called on any thread. See
* {@code GvrAudioSurround.updateNativeOrientation}.
*/
public synchronized void updateOrientation(float w, float x, float y, float z) {
this.w = w;
this.x = x;
this.y = y;
this.z = z;
if (gvrAudioSurround != null) {
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
}
}
@Override
public synchronized boolean configure(int sampleRateHz, int channelCount,
@C.Encoding int encoding) throws UnhandledFormatException {
if (encoding != C.ENCODING_PCM_16BIT) {
maybeReleaseGvrAudioSurround();
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
maybeReleaseGvrAudioSurround();
int surroundFormat;
switch (channelCount) {
case 2:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
break;
case 4:
surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
break;
case 6:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
break;
case 9:
surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
break;
case 16:
surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
break;
default:
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
FRAMES_PER_OUTPUT_BUFFER);
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
if (buffer == null) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder());
}
return true;
}
@Override
public boolean isActive() {
return gvrAudioSurround != null;
}
@Override
public int getOutputChannelCount() {
return OUTPUT_CHANNEL_COUNT;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public void queueInput(ByteBuffer input) {
int position = input.position();
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
input.position(position + readBytes);
}
@Override
public void queueEndOfStream() {
inputEnded = true;
gvrAudioSurround.triggerProcessing();
}
@Override
public ByteBuffer getOutput() {
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes);
return buffer;
}
@Override
public boolean isEnded() {
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
}
@Override
public void flush() {
gvrAudioSurround.flush();
inputEnded = false;
}
@Override
public synchronized void release() {
buffer = null;
maybeReleaseGvrAudioSurround();
}
private void maybeReleaseGvrAudioSurround() {
if (this.gvrAudioSurround != null) {
GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround;
this.gvrAudioSurround = null;
gvrAudioSurround.release();
}
}
}

View file

@ -12,31 +12,31 @@
// See the License for the specific language governing permissions and
// limitations under the License.
apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
lintOptions {
// See: https://github.com/square/okio/issues/58
warning 'InvalidPackage'
}
}
dependencies {
compile project(':library')
compile('com.squareup.okhttp3:okhttp:3.4.1') {
compile('com.squareup.okhttp3:okhttp:3.6.0') {
exclude group: 'org.json'
}
}
publish {
artifactId = 'extension-okhttp'
description = 'An OkHttp extension for ExoPlayer.'
repoName = releaseRepoName
userOrg = releaseUserOrg
groupId = releaseGroupId
version = releaseVersion
website = releaseWebsite
ext {
releaseArtifact = 'extension-okhttp'
releaseDescription = 'OkHttp extension for ExoPlayer.'
}
apply from: '../../publish.gradle'

View file

@ -27,7 +27,6 @@ import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@ -51,7 +50,8 @@ public class OkHttpDataSource implements HttpDataSource {
private final Predicate<String> contentTypePredicate;
private final TransferListener<? super OkHttpDataSource> listener;
private final CacheControl cacheControl;
private final HashMap<String, String> requestProperties;
private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private DataSpec dataSpec;
private Response response;
@ -87,7 +87,7 @@ public class OkHttpDataSource implements HttpDataSource {
*/
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) {
this(callFactory, userAgent, contentTypePredicate, listener, null);
this(callFactory, userAgent, contentTypePredicate, listener, null, null);
}
/**
@ -99,16 +99,19 @@ public class OkHttpDataSource implements HttpDataSource {
* {@link #open(DataSpec)}.
* @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
* the server as HTTP headers on every request.
*/
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
CacheControl cacheControl) {
CacheControl cacheControl, RequestProperties defaultRequestProperties) {
this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate;
this.listener = listener;
this.cacheControl = cacheControl;
this.requestProperties = new HashMap<>();
this.defaultRequestProperties = defaultRequestProperties;
this.requestProperties = new RequestProperties();
}
@Override
@ -125,24 +128,18 @@ public class OkHttpDataSource implements HttpDataSource {
public void setRequestProperty(String name, String value) {
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
synchronized (requestProperties) {
requestProperties.put(name, value);
}
requestProperties.set(name, value);
}
@Override
public void clearRequestProperty(String name) {
Assertions.checkNotNull(name);
synchronized (requestProperties) {
requestProperties.remove(name);
}
requestProperties.remove(name);
}
@Override
public void clearAllRequestProperties() {
synchronized (requestProperties) {
requestProperties.clear();
}
requestProperties.clear();
}
@Override
@ -268,11 +265,14 @@ public class OkHttpDataSource implements HttpDataSource {
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
synchronized (requestProperties) {
for (Map.Entry<String, String> property : requestProperties.entrySet()) {
builder.addHeader(property.getKey(), property.getValue());
if (defaultRequestProperties != null) {
for (Map.Entry<String, String> property : defaultRequestProperties.getSnapshot().entrySet()) {
builder.header(property.getKey(), property.getValue());
}
}
for (Map.Entry<String, String> property : requestProperties.getSnapshot().entrySet()) {
builder.header(property.getKey(), property.getValue());
}
if (!(position == 0 && length == C.LENGTH_UNSET)) {
String rangeRequest = "bytes=" + position + "-";
if (length != C.LENGTH_UNSET) {

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.okhttp;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
@ -59,8 +60,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
}
@Override
protected OkHttpDataSource createDataSourceInternal() {
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
protected OkHttpDataSource createDataSourceInternal(
HttpDataSource.RequestProperties defaultRequestProperties) {
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl,
defaultRequestProperties);
}
}

View file

@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
@ -32,4 +32,3 @@ android {
dependencies {
compile project(':library')
}

View file

@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.opus;
import android.os.Handler;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
@ -40,35 +40,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
super(eventHandler, eventListener);
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
/**
* @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 audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
}
/**
* @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 audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys) {
super(eventHandler, eventListener, audioCapabilities, drmSessionManager,
playClearSamplesWithoutKeys);
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,
audioProcessors);
}
@Override

View file

@ -213,7 +213,7 @@ import java.util.List;
SimpleOutputBuffer outputBuffer, int sampleRate);
private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native void opusClose(long decoder);
private native void opusReset(long decoder);

View file

@ -40,6 +40,18 @@ git clone https://chromium.googlesource.com/webm/libvpx libvpx && \
git clone https://chromium.googlesource.com/libyuv/libyuv libyuv
```
* Checkout the appropriate branches of libvpx and libyuv (the scripts and
makefiles bundled in this repo are known to work only at these versions of the
libraries - we will update this periodically as newer versions of
libvpx/libyuv are released):
```
cd "${VP9_EXT_PATH}/jni/libvpx" && \
git checkout tags/v1.6.1 -b v1.6.1 && \
cd "${VP9_EXT_PATH}/jni/libyuv" && \
git checkout e2611a73
```
* Run a script that generates necessary configuration files for libvpx:
```
@ -79,5 +91,7 @@ dependencies {
`generate_libvpx_android_configs.sh`
* Clean and re-build the project.
* If you want to use your own version of libvpx or libyuv, place it in
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively.
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
please note that `generate_libvpx_android_configs.sh` and the makefiles need
to be modified to work with arbitrary versions of libvpx and libyuv.

View file

@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}

View file

@ -19,6 +19,7 @@ import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
@ -38,8 +39,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
private static final String BEAR_URI = "asset:///bear-vp9.webm";
private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm";
private static final String ROADTRIP_10BIT_URI = "asset:///roadtrip-vp92-10bit.webm";
private static final String INVALID_BITSTREAM_URI = "asset:///invalid-bitstream.webm";
private static final String TAG = "VpxPlaybackTest";
public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_URI);
}
@ -48,6 +52,15 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
playUri(BEAR_ODD_DIMENSIONS_URI);
}
public void test10BitProfile2Playback() throws ExoPlaybackException {
if (VpxLibrary.isHighBitDepthSupported()) {
Log.d(TAG, "High Bit Depth supported.");
playUri(ROADTRIP_10BIT_URI);
return;
}
Log.d(TAG, "High Bit Depth not supported.");
}
public void testInvalidBitstream() {
try {
playUri(INVALID_BITSTREAM_URI);

View file

@ -65,6 +65,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private final boolean playClearSamplesWithoutKeys;
private final EventDispatcher eventDispatcher;
private final FormatHolder formatHolder;
private final DecoderInputBuffer flagsOnlyBuffer;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private DecoderCounters decoderCounters;
@ -149,6 +150,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
joiningDeadlineMs = -1;
clearLastReportedVideoSize();
formatHolder = new FormatHolder();
flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
}
@ -165,10 +167,22 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return;
}
// Try and read a format if we don't have one already.
if (format == null && !readFormat()) {
// We can't make progress without one.
return;
if (format == null) {
// We don't have a format yet, so try and read one.
flagsOnlyBuffer.clear();
int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
} else if (result == C.RESULT_BUFFER_READ) {
// End of stream read having not read a format.
Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
inputStreamEnded = true;
outputStreamEnded = true;
return;
} else {
// We still don't have a format and can't make progress without one.
return;
}
}
if (isRendererAvailable()) {
@ -327,7 +341,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
// We've already read an encrypted sample into buffer, and are waiting for keys.
result = C.RESULT_BUFFER_READ;
} else {
result = readSource(formatHolder, inputBuffer);
result = readSource(formatHolder, inputBuffer, false);
}
if (result == C.RESULT_NOTHING_READ) {
@ -485,15 +499,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
private boolean readFormat() throws ExoPlaybackException {
int result = readSource(formatHolder, null);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
return true;
}
return false;
}
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
Format oldFormat = format;
format = newFormat;

View file

@ -141,7 +141,7 @@ import java.nio.ByteBuffer;
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
private native int vpxGetErrorCode(long context);

View file

@ -57,6 +57,16 @@ public final class VpxLibrary {
return isAvailable() ? vpxGetBuildConfig() : null;
}
/**
* Returns true if the underlying libvpx library supports high bit depth.
*/
public static boolean isHighBitDepthSupported() {
String config = getBuildConfig();
int indexHbd = config != null
? config.indexOf("--enable-vp9-highbitdepth") : -1;
return indexHbd >= 0;
}
private static native String vpxGetVersion();
private static native String vpxGetBuildConfig();
public static native boolean vpxIsSecureDecodeSupported();

View file

@ -26,6 +26,7 @@ import java.nio.ByteBuffer;
public static final int COLORSPACE_UNKNOWN = 0;
public static final int COLORSPACE_BT601 = 1;
public static final int COLORSPACE_BT709 = 2;
public static final int COLORSPACE_BT2020 = 3;
private final VpxDecoder owner;

View file

@ -42,6 +42,12 @@ import javax.microedition.khronos.opengles.GL10;
1.793f, -0.533f, 0.0f,
};
private static final float[] kColorConversion2020 = {
1.168f, 1.168f, 1.168f,
0.0f, -0.188f, 2.148f,
1.683f, -0.652f, 0.0f,
};
private static final String VERTEX_SHADER =
"varying vec2 interp_tc;\n"
+ "attribute vec4 in_pos;\n"
@ -59,12 +65,13 @@ import javax.microedition.khronos.opengles.GL10;
+ "uniform sampler2D v_tex;\n"
+ "uniform mat3 mColorConversion;\n"
+ "void main() {\n"
+ " vec3 yuv;"
+ " vec3 yuv;\n"
+ " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n"
+ " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n"
+ " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n"
+ " gl_FragColor = vec4(mColorConversion * yuv, 1.0);"
+ " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n"
+ "}\n";
private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer(
-1.0f, 1.0f,
-1.0f, -1.0f,
@ -156,8 +163,18 @@ import javax.microedition.khronos.opengles.GL10;
}
VpxOutputBuffer outputBuffer = renderedOutputBuffer;
// Set color matrix. Assume BT709 if the color space is unknown.
float[] colorConversion = outputBuffer.colorspace == VpxOutputBuffer.COLORSPACE_BT601
? kColorConversion601 : kColorConversion709;
float[] colorConversion = kColorConversion709;
switch (outputBuffer.colorspace) {
case VpxOutputBuffer.COLORSPACE_BT601:
colorConversion = kColorConversion601;
break;
case VpxOutputBuffer.COLORSPACE_BT2020:
colorConversion = kColorConversion2020;
break;
case VpxOutputBuffer.COLORSPACE_BT709:
default:
break; // Do nothing
}
GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0);
for (int i = 0; i < 3; i++) {

View file

@ -40,7 +40,7 @@ config[0]+=" --enable-neon-asm"
arch[1]="armeabi"
config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
config[1]+=" --disable-neon-asm --disable-media"
config[1]+=" --disable-neon-asm"
arch[2]="mips"
config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
@ -78,12 +78,12 @@ convert_asm() {
for i in $(seq 0 ${limit}); do
while read file; do
case "${file}" in
*.asm.s)
*.asm.[sS])
# Some files may already have been processed (there are duplicated
# .asm.s files for vp8 in the armeabi/armeabi-v7a configurations).
file="libvpx/${file}"
if [[ ! -e "${file}" ]]; then
asm_file="${file%.s}"
asm_file="${file%.[sS]}"
cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}"
remove_trailing_whitespace "${file}"
rm "${asm_file}"
@ -105,7 +105,11 @@ for i in $(seq 0 ${limit}); do
echo "configure ${config[${i}]} ${common_params}"
../../libvpx/configure ${config[${i}]} ${common_params}
rm -f libvpx_srcs.txt
make libvpx_srcs.txt
for f in ${allowed_files}; do
# the build system supports multiple different configurations. avoid
# failing out when, for example, vp8_rtcd.h is not part of a configuration
make "${f}" || true
done
# remove files that aren't needed
rm -rf !(${allowed_files// /|})

View file

@ -35,16 +35,22 @@ LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \
$(filter %.c, $(libvpx_codec_srcs))))
# include assembly files if they exist
# "%.asm.s" covers neon assembly and "%.asm" covers x86 assembly
# "%.asm.[sS]" covers neon assembly and "%.asm" covers x86 assembly
LOCAL_SRC_FILES += $(addprefix libvpx/, \
$(filter %.asm.s %.asm, $(libvpx_codec_srcs)))
LOCAL_SRC_FILES += $(addprefix libvpx/, \
$(filter %.asm.S %.asm, $(libvpx_codec_srcs)))
ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),)
# append .neon to *_neon.c and *.s
# append .neon to *_neon.c and *.[sS]
LOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES))
LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES))
LOCAL_SRC_FILES := $(subst .S,.S.neon,$(LOCAL_SRC_FILES))
endif
# remove duplicates
LOCAL_SRC_FILES := $(sort $(LOCAL_SRC_FILES))
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \
$(LOCAL_PATH)/libvpx/vpx

View file

@ -74,8 +74,11 @@ DECODER_FUNC(jlong, vpxInit) {
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
cfg.threads = android_getCpuCount();
errorCode = 0;
if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) {
LOGE("ERROR: Fail to initialize libvpx decoder.");
vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo,
&cfg, 0);
if (err) {
LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err);
errorCode = err;
return 0;
}
@ -160,6 +163,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
const int kColorspaceUnknown = 0;
const int kColorspaceBT601 = 1;
const int kColorspaceBT709 = 2;
const int kColorspaceBT2020 = 3;
int colorspace = kColorspaceUnknown;
switch (img->cs) {
@ -169,6 +173,9 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
case VPX_CS_BT_709:
colorspace = kColorspaceBT709;
break;
case VPX_CS_BT_2020:
colorspace = kColorspaceBT2020;
break;
default:
break;
}
@ -186,14 +193,55 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
jbyte* const data =
reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(dataObject));
// TODO: This copy can be eliminated by using external frame buffers. NOLINT
// This is insignificant for smaller videos but takes ~1.5ms for 1080p
// clips. So this should eventually be gotten rid of.
const uint64_t y_length = img->stride[VPX_PLANE_Y] * img->d_h;
const uint64_t uv_length = img->stride[VPX_PLANE_U] * ((img->d_h + 1) / 2);
memcpy(data, img->planes[VPX_PLANE_Y], y_length);
memcpy(data + y_length, img->planes[VPX_PLANE_U], uv_length);
memcpy(data + y_length + uv_length, img->planes[VPX_PLANE_V], uv_length);
const int32_t uvHeight = (img->d_h + 1) / 2;
const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h;
const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight;
int sample = 0;
if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420.
// Note: The stride for BT2020 is twice of what we use so this is wasting
// memory. The long term goal however is to upload half-float/short so
// it's not important to optimize the stride at this time.
// Y
for (int y = 0; y < img->d_h; y++) {
const uint16_t* srcBase = reinterpret_cast<uint16_t*>(
img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y);
int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y;
for (int x = 0; x < img->d_w; x++) {
// Lightweight dither. Carryover the remainder of each 10->8 bit
// conversion to the next pixel.
sample += *srcBase++;
*destBase++ = sample >> 2;
sample = sample & 3; // Remainder.
}
}
// UV
const int32_t uvWidth = (img->d_w + 1) / 2;
for (int y = 0; y < uvHeight; y++) {
const uint16_t* srcUBase = reinterpret_cast<uint16_t*>(
img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y);
const uint16_t* srcVBase = reinterpret_cast<uint16_t*>(
img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y);
int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y;
int8_t* destVBase = data + yLength + uvLength
+ img->stride[VPX_PLANE_V] * y;
for (int x = 0; x < uvWidth; x++) {
// Lightweight dither. Carryover the remainder of each 10->8 bit
// conversion to the next pixel.
sample += *srcUBase++;
*destUBase++ = sample >> 2;
sample = (*srcVBase++) + (sample & 3); // srcV + previousRemainder.
*destVBase++ = sample >> 2;
sample = sample & 3; // Remainder.
}
}
} else {
// TODO: This copy can be eliminated by using external frame buffers. This
// is insignificant for smaller videos but takes ~1.5ms for 1080p clips.
// So this should eventually be gotten rid of.
memcpy(data, img->planes[VPX_PLANE_Y], yLength);
memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength);
memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength);
}
}
return 0;
}

View file

@ -1,6 +1,6 @@
#Mon Oct 24 14:40:37 BST 2016
#Mon Mar 13 11:17:14 GMT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip

View file

@ -14,19 +14,13 @@
import com.android.builder.core.BuilderConstants
apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
// Important: ExoPlayerLib specifies a minSdkVersion of 9 because
// various components provided by the library may be of use on older
// devices. However, please note that the core video playback
// functionality provided by the library requires API level 16 or
// greater.
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
@ -47,10 +41,10 @@ android {
}
dependencies {
compile 'com.android.support:support-annotations:25.2.0'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'org.mockito:mockito-core:1.9.5'
compile 'com.android.support:support-annotations:25.0.1'
}
android.libraryVariants.all { variant ->
@ -86,12 +80,8 @@ android.libraryVariants.all { variant ->
}
}
publish {
artifactId = 'exoplayer'
description = 'The ExoPlayer library.'
repoName = releaseRepoName
userOrg = releaseUserOrg
groupId = releaseGroupId
version = releaseVersion
website = releaseWebsite
ext {
releaseArtifact = 'exoplayer'
releaseDescription = 'The ExoPlayer library.'
}
apply from: '../publish.gradle'

View file

@ -0,0 +1,382 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
numberOfTracks = 3
track 0:
format:
bitrate = -1
id = 1
containerMimeType = null
sampleMimeType = video/avc
maxInputSize = -1
width = 1080
height = 720
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample count = 30
sample 0:
time = 66000
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
time = 199000
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
time = 132000
flags = 0
data = length 1295, hash C0DA5090
sample 3:
time = 100000
flags = 0
data = length 469, hash D6E0A200
sample 4:
time = 166000
flags = 0
data = length 564, hash E5F56C5B
sample 5:
time = 332000
flags = 0
data = length 6075, hash 8756E49E
sample 6:
time = 266000
flags = 0
data = length 847, hash DCC2B618
sample 7:
time = 233000
flags = 0
data = length 455, hash B9CCE047
sample 8:
time = 299000
flags = 0
data = length 467, hash 69806D94
sample 9:
time = 466000
flags = 0
data = length 4549, hash 3944F501
sample 10:
time = 399000
flags = 0
data = length 1087, hash 491BF106
sample 11:
time = 367000
flags = 0
data = length 380, hash 5FED016A
sample 12:
time = 433000
flags = 0
data = length 455, hash 8A0610
sample 13:
time = 599000
flags = 0
data = length 5190, hash B9031D8
sample 14:
time = 533000
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
time = 500000
flags = 0
data = length 653, hash 8494F326
sample 16:
time = 566000
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
time = 733000
flags = 0
data = length 4884, hash D16B6A96
sample 18:
time = 666000
flags = 0
data = length 997, hash 164FF210
sample 19:
time = 633000
flags = 0
data = length 640, hash F664125B
sample 20:
time = 700000
flags = 0
data = length 491, hash B5930C7C
sample 21:
time = 866000
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
time = 800000
flags = 0
data = length 838, hash 294A3451
sample 23:
time = 767000
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
time = 833000
flags = 0
data = length 329, hash A654FFA1
sample 25:
time = 1000000
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
time = 933000
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
time = 900000
flags = 0
data = length 415, hash B31BBC3B
sample 28:
time = 967000
flags = 0
data = length 415, hash 850DFEA3
sample 29:
time = 1033000
flags = 0
data = length 619, hash AB5E56CA
track 1:
format:
bitrate = -1
id = 2
containerMimeType = null
sampleMimeType = audio/mp4a-latm
maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 1
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = und
drmInitData = -
initializationData:
data = length 5, hash 2B7623A
sample count = 46
sample 0:
time = 0
flags = 1
data = length 18, hash 96519432
sample 1:
time = 23000
flags = 1
data = length 4, hash EE9DF
sample 2:
time = 46000
flags = 1
data = length 4, hash EEDBF
sample 3:
time = 69000
flags = 1
data = length 157, hash E2F078F4
sample 4:
time = 92000
flags = 1
data = length 371, hash B9471F94
sample 5:
time = 116000
flags = 1
data = length 373, hash 2AB265CB
sample 6:
time = 139000
flags = 1
data = length 402, hash 1295477C
sample 7:
time = 162000
flags = 1
data = length 455, hash 2D8146C8
sample 8:
time = 185000
flags = 1
data = length 434, hash F2C5D287
sample 9:
time = 208000
flags = 1
data = length 450, hash 84143FCD
sample 10:
time = 232000
flags = 1
data = length 429, hash EF769D50
sample 11:
time = 255000
flags = 1
data = length 450, hash EC3DE692
sample 12:
time = 278000
flags = 1
data = length 447, hash 3E519E13
sample 13:
time = 301000
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
time = 325000
flags = 1
data = length 447, hash A439EA97
sample 15:
time = 348000
flags = 1
data = length 456, hash 1E9034C6
sample 16:
time = 371000
flags = 1
data = length 398, hash 99DB7345
sample 17:
time = 394000
flags = 1
data = length 474, hash 3F05F10A
sample 18:
time = 417000
flags = 1
data = length 416, hash C105EE09
sample 19:
time = 441000
flags = 1
data = length 454, hash 5FDBE458
sample 20:
time = 464000
flags = 1
data = length 438, hash 41A93AC3
sample 21:
time = 487000
flags = 1
data = length 443, hash 10FDA652
sample 22:
time = 510000
flags = 1
data = length 412, hash 1F791E25
sample 23:
time = 534000
flags = 1
data = length 482, hash A6D983D
sample 24:
time = 557000
flags = 1
data = length 386, hash BED7392F
sample 25:
time = 580000
flags = 1
data = length 463, hash 5309F8C9
sample 26:
time = 603000
flags = 1
data = length 394, hash 21C7321F
sample 27:
time = 626000
flags = 1
data = length 489, hash 71B4730D
sample 28:
time = 650000
flags = 1
data = length 403, hash D9C6DE89
sample 29:
time = 673000
flags = 1
data = length 447, hash 9B14B73B
sample 30:
time = 696000
flags = 1
data = length 439, hash 4760D35B
sample 31:
time = 719000
flags = 1
data = length 463, hash 1601F88D
sample 32:
time = 743000
flags = 1
data = length 423, hash D4AE6773
sample 33:
time = 766000
flags = 1
data = length 497, hash A3C674D3
sample 34:
time = 789000
flags = 1
data = length 419, hash D3734A1F
sample 35:
time = 812000
flags = 1
data = length 474, hash DFB41F9
sample 36:
time = 835000
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
time = 859000
flags = 1
data = length 445, hash D15B0E39
sample 38:
time = 882000
flags = 1
data = length 453, hash 77ED81E4
sample 39:
time = 905000
flags = 1
data = length 545, hash 3321AEB9
sample 40:
time = 928000
flags = 1
data = length 317, hash F557D0E
sample 41:
time = 952000
flags = 1
data = length 537, hash ED58CF7B
sample 42:
time = 975000
flags = 1
data = length 458, hash 51CDAA10
sample 43:
time = 998000
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
time = 1021000
flags = 1
data = length 446, hash D6735B8A
sample 45:
time = 1044000
flags = 1
data = length 10, hash A453EEBE
track 3:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = application/cea-608
maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 0
tracksEnded = true

View file

@ -6,7 +6,7 @@ numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
id = 0
containerMimeType = null
sampleMimeType = audio/ac3
maxInputSize = -1

View file

@ -6,7 +6,7 @@ numberOfTracks = 2
track 0:
format:
bitrate = -1
id = null
id = 0
containerMimeType = null
sampleMimeType = audio/mp4a-latm
maxInputSize = -1
@ -606,7 +606,7 @@ track 0:
track 1:
format:
bitrate = -1
id = null
id = 1
containerMimeType = null
sampleMimeType = application/id3
maxInputSize = -1

View file

@ -6,7 +6,7 @@ numberOfTracks = 2
track 192:
format:
bitrate = -1
id = null
id = 192
containerMimeType = null
sampleMimeType = audio/mpeg-L2
maxInputSize = 4096
@ -45,7 +45,7 @@ track 192:
track 224:
format:
bitrate = -1
id = null
id = 224
containerMimeType = null
sampleMimeType = video/mpeg2
maxInputSize = -1

View file

@ -6,7 +6,7 @@ numberOfTracks = 2
track 256:
format:
bitrate = -1
id = null
id = 1/256
containerMimeType = null
sampleMimeType = video/mpeg2
maxInputSize = -1
@ -38,7 +38,7 @@ track 256:
track 257:
format:
bitrate = -1
id = null
id = 1/257
containerMimeType = null
sampleMimeType = audio/mpeg-L2
maxInputSize = 4096

View file

@ -460,6 +460,11 @@ public final class ExoPlayerTest extends TestCase {
return 0;
}
@Override
public void discardBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
assertTrue(preparedPeriod);
@ -513,8 +518,9 @@ public final class ExoPlayerTest extends TestCase {
}
@Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
if (buffer == null || !readFormat) {
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
boolean formatRequired) {
if (formatRequired || !readFormat) {
formatHolder.format = format;
readFormat = true;
return C.RESULT_FORMAT_READ;
@ -571,7 +577,7 @@ public final class ExoPlayerTest extends TestCase {
FormatHolder formatHolder = new FormatHolder();
DecoderInputBuffer buffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
int result = readSource(formatHolder, buffer);
int result = readSource(formatHolder, buffer, false);
if (result == C.RESULT_FORMAT_READ) {
formatReadCount++;
assertEquals(expectedFormat, formatHolder.format);

View file

@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.HashMap;
import org.mockito.Mock;
@ -217,7 +218,11 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
}
private static Representation newRepresentations(DrmInitData drmInitData) {
Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData);
Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4,
MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0);
if (drmInitData != null) {
format = format.copyWithDrmInitData(drmInitData);
}
return Representation.newInstance("", 0, format, "", new SingleSegmentBase());
}

View file

@ -25,21 +25,32 @@ import com.google.android.exoplayer2.testutil.TestUtil;
*/
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
private static final TestUtil.ExtractorFactory EXTRACTOR_FACTORY =
new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new FragmentedMp4Extractor();
}
};
public void testSample() throws Exception {
TestUtil.assertOutput(EXTRACTOR_FACTORY, "mp4/sample_fragmented.mp4", getInstrumentation());
TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
}
public void testSampleWithSeiPayloadParsing() throws Exception {
// Enabling the CEA-608 track enables SEI payload parsing.
TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
}
public void testAtomWithZeroSize() throws Exception {
TestUtil.assertThrows(EXTRACTOR_FACTORY, "mp4/sample_fragmented_zero_size_atom.mp4",
TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
getInstrumentation(), ParserException.class);
}
private static TestUtil.ExtractorFactory getExtractorFactory() {
return getExtractorFactory(0);
}
private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) {
return new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new FragmentedMp4Extractor(flags, null);
}
};
}
}

View file

@ -69,8 +69,8 @@ public class AdtsReaderTest extends TestCase {
@Override
protected void setUp() throws Exception {
FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
adtsOutput = fakeExtractorOutput.track(0);
id3Output = fakeExtractorOutput.track(1);
adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO);
id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA);
adtsReader = new AdtsReader(true);
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
adtsReader.createTracks(fakeExtractorOutput, idGenerator);

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -74,7 +75,8 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomPesReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
factory);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
.setSimulateIOErrors(false)
@ -92,13 +94,14 @@ public final class TsExtractorTest extends InstrumentationTestCase {
TrackOutput trackOutput = reader.getTrackOutput();
assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */));
assertEquals(
Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, "und", null, 0),
Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0),
((FakeTrackOutput) trackOutput).format);
}
public void testCustomInitialSectionReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
factory);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
.setSimulateIOErrors(false)
@ -178,8 +181,9 @@ public final class TsExtractorTest extends InstrumentationTestCase {
@Override
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
output = extractorOutput.track(idGenerator.getNextId());
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
idGenerator.generateNewId();
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN);
output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0,
language, null, 0));
}

View file

@ -0,0 +1,197 @@
/*
* 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.dash.manifest;
import android.net.Uri;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import junit.framework.TestCase;
/**
* Unit tests for {@link DashManifest}.
*/
public class DashManifestTest extends TestCase {
private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", "");
private static final List<SchemeValuePair> DUMMY_ACCESSIBILITY_DESCRIPTORS =
Collections.emptyList();
private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase();
private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0);
public void testCopy() throws Exception {
Representation[][][] representations = newRepresentations(3, 2, 3);
DashManifest sourceManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0]),
newAdaptationSet(3, representations[0][1])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0]),
newAdaptationSet(6, representations[1][1])),
newPeriod("7", 7,
newAdaptationSet(8, representations[2][0]),
newAdaptationSet(9, representations[2][1])));
List<RepresentationKey> keys = Arrays.asList(
new RepresentationKey(0, 0, 0),
new RepresentationKey(0, 0, 1),
new RepresentationKey(0, 1, 2),
new RepresentationKey(1, 0, 1),
new RepresentationKey(1, 1, 0),
new RepresentationKey(1, 1, 2),
new RepresentationKey(2, 0, 1),
new RepresentationKey(2, 0, 2),
new RepresentationKey(2, 1, 0));
// Keys don't need to be in any particular order
Collections.shuffle(keys, new Random(0));
DashManifest copyManifest = sourceManifest.copy(keys);
DashManifest expectedManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
newAdaptationSet(3, representations[0][1][2])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0][1]),
newAdaptationSet(6, representations[1][1][0], representations[1][1][2])),
newPeriod("7", 7,
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
newAdaptationSet(9, representations[2][1][0])));
assertManifestEquals(expectedManifest, copyManifest);
}
public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception {
Representation[][][] representations = newRepresentations(2, 1, 1);
DashManifest sourceManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0])));
DashManifest copyManifest = sourceManifest.copy(Arrays.asList(
new RepresentationKey(0, 0, 0),
new RepresentationKey(1, 0, 0)));
DashManifest expectedManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0])));
assertManifestEquals(expectedManifest, copyManifest);
}
public void testCopySkipPeriod() throws Exception {
Representation[][][] representations = newRepresentations(3, 2, 3);
DashManifest sourceManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0]),
newAdaptationSet(3, representations[0][1])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0]),
newAdaptationSet(6, representations[1][1])),
newPeriod("7", 7,
newAdaptationSet(8, representations[2][0]),
newAdaptationSet(9, representations[2][1])));
DashManifest copyManifest = sourceManifest.copy(Arrays.asList(
new RepresentationKey(0, 0, 0),
new RepresentationKey(0, 0, 1),
new RepresentationKey(0, 1, 2),
new RepresentationKey(2, 0, 1),
new RepresentationKey(2, 0, 2),
new RepresentationKey(2, 1, 0)));
DashManifest expectedManifest = newDashManifest(7,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
newAdaptationSet(3, representations[0][1][2])),
newPeriod("7", 4,
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
newAdaptationSet(9, representations[2][1][0])));
assertManifestEquals(expectedManifest, copyManifest);
}
private static void assertManifestEquals(DashManifest expected, DashManifest actual) {
assertEquals(expected.availabilityStartTime, actual.availabilityStartTime);
assertEquals(expected.duration, actual.duration);
assertEquals(expected.minBufferTime, actual.minBufferTime);
assertEquals(expected.dynamic, actual.dynamic);
assertEquals(expected.minUpdatePeriod, actual.minUpdatePeriod);
assertEquals(expected.timeShiftBufferDepth, actual.timeShiftBufferDepth);
assertEquals(expected.suggestedPresentationDelay, actual.suggestedPresentationDelay);
assertEquals(expected.utcTiming, actual.utcTiming);
assertEquals(expected.location, actual.location);
assertEquals(expected.getPeriodCount(), actual.getPeriodCount());
for (int i = 0; i < expected.getPeriodCount(); i++) {
Period expectedPeriod = expected.getPeriod(i);
Period actualPeriod = actual.getPeriod(i);
assertEquals(expectedPeriod.id, actualPeriod.id);
assertEquals(expectedPeriod.startMs, actualPeriod.startMs);
List<AdaptationSet> expectedAdaptationSets = expectedPeriod.adaptationSets;
List<AdaptationSet> actualAdaptationSets = actualPeriod.adaptationSets;
assertEquals(expectedAdaptationSets.size(), actualAdaptationSets.size());
for (int j = 0; j < expectedAdaptationSets.size(); j++) {
AdaptationSet expectedAdaptationSet = expectedAdaptationSets.get(j);
AdaptationSet actualAdaptationSet = actualAdaptationSets.get(j);
assertEquals(expectedAdaptationSet.id, actualAdaptationSet.id);
assertEquals(expectedAdaptationSet.type, actualAdaptationSet.type);
assertEquals(expectedAdaptationSet.accessibilityDescriptors,
actualAdaptationSet.accessibilityDescriptors);
assertEquals(expectedAdaptationSet.representations, actualAdaptationSet.representations);
}
}
}
private static Representation[][][] newRepresentations(int periodCount, int adaptationSetCounts,
int representationCounts) {
Representation[][][] representations = new Representation[periodCount][][];
for (int i = 0; i < periodCount; i++) {
representations[i] = new Representation[adaptationSetCounts][];
for (int j = 0; j < adaptationSetCounts; j++) {
representations[i][j] = new Representation[representationCounts];
for (int k = 0; k < representationCounts; k++) {
representations[i][j][k] = newRepresentation();
}
}
}
return representations;
}
private static Representation newRepresentation() {
return Representation.newInstance("", 0, DUMMY_FORMAT, "", DUMMY_SEGMENT_BASE);
}
private static DashManifest newDashManifest(int duration, Period... periods) {
return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY,
Arrays.asList(periods));
}
private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) {
return new Period(id, startMs, Arrays.asList(adaptationSets));
}
private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {
return new AdaptationSet(++seed, ++seed, Arrays.asList(representations),
DUMMY_ACCESSIBILITY_DESCRIPTORS);
}
}

View file

@ -19,6 +19,7 @@ import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
@ -53,12 +54,14 @@ public class HlsMasterPlaylistParserTest extends TestCase {
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n";
public void testParseMasterPlaylist() throws IOException{
HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
assertNotNull(playlist);
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n";
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
public void testParseMasterPlaylist() throws IOException{
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants);
@ -98,18 +101,28 @@ public class HlsMasterPlaylistParserTest extends TestCase {
public void testPlaylistWithInvalidHeader() throws IOException {
try {
parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
fail("Expected exception not thrown.");
} catch (ParserException e) {
// Expected due to invalid header.
}
}
private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException {
public void testPlaylistWithClosedCaption() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITH_CC);
assertEquals(1, playlist.muxedCaptionFormats.size());
Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0);
assertEquals(MimeTypes.APPLICATION_CEA708, closedCaptionFormat.sampleMimeType);
assertEquals(4, closedCaptionFormat.accessibilityChannel);
assertEquals("es", closedCaptionFormat.language);
}
private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)
throws IOException {
Uri playlistUri = Uri.parse(uri);
ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
return new HlsPlaylistParser().parse(playlistUri, inputStream);
return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
}
}

View file

@ -36,6 +36,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
String playlistString = "#EXTM3U\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-START:TIME-OFFSET=-25"
+ "#EXT-X-TARGETDURATION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
@ -73,6 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType);
assertEquals(mediaPlaylist.durationUs - 25000000, mediaPlaylist.startOffsetUs);
assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(3, mediaPlaylist.version);

View file

@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.FakeDataSource.Builder;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
@ -42,13 +42,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testMaxCacheFileSize() throws Exception {
@ -126,9 +126,15 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
MoreAsserts.assertEmpty(simpleCache.getKeys());
}
public void testReadOnlyCache() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
assertReadDataContentLength(cacheDataSource, false, false);
assertEquals(0, cacheDir.list().length);
}
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
throws IOException {
// Read all data from upstream and cache
// Read all data from upstream and write to cache
CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength);
assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength);
@ -184,14 +190,21 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
Builder builder = new Builder();
return createCacheDataSource(setReadException, simulateUnknownLength, flags,
new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE));
}
private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength, @CacheDataSource.Flags int flags,
CacheDataSink cacheWriteDataSink) {
FakeDataSource.Builder builder = new FakeDataSource.Builder();
if (setReadException) {
builder.appendReadError(new IOException("Shouldn't read from upstream"));
}
builder.setSimulateUnknownLength(simulateUnknownLength);
builder.appendReadData(TEST_DATA);
FakeDataSource upstream = builder.build();
return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE);
FakeDataSource upstream =
builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build();
return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
flags, null);
}
}

View file

@ -4,7 +4,7 @@ import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@ -36,13 +36,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
@Override
public void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testAddGetRemove() throws Exception {

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import org.mockito.Mock;
@ -49,13 +50,13 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testGetRegion_noSpansInCache() {

View file

@ -16,7 +16,7 @@
package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -48,13 +48,13 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testCacheFile() throws Exception {

View file

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileInputStream;
@ -39,12 +38,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testCommittingOneFile() throws Exception {
@ -192,6 +191,41 @@ public class SimpleCacheTest extends InstrumentationTestCase {
assertEquals(0, cacheDir.listFiles().length);
}
public void testGetCachedBytes() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
// No cached bytes, returns -'length'
assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 0, 100));
// Position value doesn't affect the return value
assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 20, 100));
addCache(simpleCache, KEY_1, 0, 15);
// Returns the length of a single span
assertEquals(15, simpleCache.getCachedBytes(KEY_1, 0, 100));
// Value is capped by the 'length'
assertEquals(10, simpleCache.getCachedBytes(KEY_1, 0, 10));
addCache(simpleCache, KEY_1, 15, 35);
// Returns the length of two adjacent spans
assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100));
addCache(simpleCache, KEY_1, 60, 10);
// Not adjacent span doesn't affect return value
assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100));
// Returns length of hole up to the next cached span
assertEquals(-5, simpleCache.getCachedBytes(KEY_1, 55, 100));
simpleCache.releaseHoleSpan(cacheSpan);
}
private SimpleCache getSimpleCache() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
}

View file

@ -17,7 +17,6 @@
package com.google.android.exoplayer2.util;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -34,14 +33,14 @@ public class AtomicFileTest extends InstrumentationTestCase {
@Override
public void setUp() throws Exception {
tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext());
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
file = new File(tempFolder, "atomicFile");
atomicFile = new AtomicFile(file);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(tempFolder);
Util.recursiveDelete(tempFolder);
}
public void testDelete() throws Exception {

View file

@ -142,8 +142,10 @@ public class UtilTest extends TestCase {
public void testParseXsDateTime() throws Exception {
assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42"));
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00,000Z"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800"));
}
public void testUnescapeInvalidFileName() {

View file

@ -254,6 +254,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
return index;
}
/**
* Use {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} instead.
*/
@Deprecated
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) {
return readSource(formatHolder, buffer, false);
}
/**
* Reads from the enabled upstream source. If the upstream source has been read to the end then
* {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
@ -262,13 +270,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
* caller requires that the format of the stream be read even if it's not changing.
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
* @param formatRequired Whether the caller requires that the format of the stream be read even if
* it's not changing. A sample will never be read if set to true, however it is still possible
* for the end of stream or nothing to be read.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}.
*/
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) {
int result = stream.readData(formatHolder, buffer);
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer,
boolean formatRequired) {
int result = stream.readData(formatHolder, buffer, formatRequired);
if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) {
readEndOfStream = true;

View file

@ -443,9 +443,16 @@ public final class C {
*/
public static final UUID UUID_NIL = new UUID(0L, 0L);
/**
* UUID for the ClearKey DRM scheme.
* <p>
* ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.
*/
public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);
/**
* UUID for the Widevine DRM scheme.
* <p></p>
* <p>
* Widevine is supported on Android devices running Android 4.3 (API Level 18) and up.
*/
public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
@ -477,7 +484,7 @@ public final class C {
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
* should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the
* underlying {@link android.media.AudioTrack}. The message object should not be modified by the
* caller after it has been passed
* caller after it has been passed.
*/
public static final int MSG_SET_PLAYBACK_PARAMS = 3;
@ -515,7 +522,13 @@ public final class C {
* The stereo mode for 360/3D/VR videos.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT})
@IntDef({
Format.NO_VALUE,
STEREO_MODE_MONO,
STEREO_MODE_TOP_BOTTOM,
STEREO_MODE_LEFT_RIGHT,
STEREO_MODE_STEREO_MESH
})
public @interface StereoMode {}
/**
* Indicates Monoscopic stereo layout, used with 360/3D/VR videos.
@ -529,6 +542,16 @@ public final class C {
* Indicates Left-Right stereo layout, used with 360/3D/VR videos.
*/
public static final int STEREO_MODE_LEFT_RIGHT = 2;
/**
* Indicates a stereo layout where the left and right eyes have separate meshes,
* used with 360/3D/VR videos.
*/
public static final int STEREO_MODE_STEREO_MESH = 3;
/**
* Priority for media playback.
*/
public static final int PRIORITY_PLAYBACK = 0;
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving

View file

@ -51,11 +51,6 @@ public final class DefaultLoadControl implements LoadControl {
*/
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
/**
* Priority for media loading.
*/
public static final int LOADING_PRIORITY = 0;
private static final int ABOVE_HIGH_WATERMARK = 0;
private static final int BETWEEN_WATERMARKS = 1;
private static final int BELOW_LOW_WATERMARK = 2;
@ -122,7 +117,7 @@ public final class DefaultLoadControl implements LoadControl {
* 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 #LOADING_PRIORITY} during loading periods, and unregisters itself during draining
* {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
* periods.
*/
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
@ -183,9 +178,9 @@ public final class DefaultLoadControl implements LoadControl {
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) {
priorityTaskManager.add(LOADING_PRIORITY);
priorityTaskManager.add(C.PRIORITY_PLAYBACK);
} else {
priorityTaskManager.remove(LOADING_PRIORITY);
priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
}
}
return isBuffering;
@ -199,7 +194,7 @@ public final class DefaultLoadControl implements LoadControl {
private void reset(boolean resetAllocator) {
targetBufferSize = 0;
if (priorityTaskManager != null && isBuffering) {
priorityTaskManager.remove(LOADING_PRIORITY);
priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
}
isBuffering = false;
if (resetAllocator) {

View file

@ -56,8 +56,7 @@ public final class ExoPlaybackException extends Exception {
* The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
* {@link #TYPE_UNEXPECTED}.
*/
@Type
public final int type;
@Type public final int type;
/**
* If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.

View file

@ -455,6 +455,8 @@ import java.io.IOException;
TraceUtil.beginSection("doSomeWork");
updatePlaybackPositions();
playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs);
boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true;
for (Renderer renderer : enabledRenderers) {

View file

@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
String VERSION = "2.2.0";
String VERSION = "2.3.0";
/**
* The version of the library, expressed as an integer.
@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006).
*/
int VERSION_INT = 2002000;
int VERSION_INT = 2003000;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View file

@ -120,7 +120,7 @@ public final class Format implements Parcelable {
/**
* The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo
* modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link
* C#STEREO_MODE_LEFT_RIGHT}.
* C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}.
*/
@C.StereoMode
public final int stereoMode;
@ -438,16 +438,19 @@ public final class Format implements Parcelable {
drmInitData, metadata);
}
public Format copyWithManifestFormatInfo(Format manifestFormat,
boolean preferManifestDrmInitData) {
public Format copyWithManifestFormatInfo(Format manifestFormat) {
if (this == manifestFormat) {
// No need to copy from ourselves.
return this;
}
String id = manifestFormat.id;
String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
@C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
String language = this.language == null ? manifestFormat.language : this.language;
DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null)
|| this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData;
DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData
: this.drmInitData;
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
@ -672,9 +675,6 @@ public final class Format implements Parcelable {
dest.writeParcelable(metadata, 0);
}
/**
* {@link Creator} implementation.
*/
public static final Creator<Format> CREATOR = new Creator<Format>() {
@Override

View file

@ -28,6 +28,7 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.decoder.DecoderCounters;
@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer {
buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
componentListener, allowedVideoJoiningTimeMs, out);
buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
componentListener, out);
componentListener, buildAudioProcessors(), out);
buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out);
@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer {
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
* not be used for DRM protected playbacks.
* not be used for DRM protected playbacks.
* @param extensionRendererMode The extension renderer mode.
* @param eventListener An event listener.
* @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers
@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer {
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
* not be used for DRM protected playbacks.
* not be used for DRM protected playbacks.
* @param extensionRendererMode The extension renderer mode.
* @param eventListener An event listener.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
* before output. May be empty.
* @param out An array to which the built renderers should be appended.
*/
protected void buildAudioRenderers(Context context, Handler mainHandler,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
@ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener,
ArrayList<Renderer> out) {
AudioProcessor[] audioProcessors, ArrayList<Renderer> out) {
out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
mainHandler, eventListener, AudioCapabilities.getCapabilities(context)));
mainHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;
@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibopusAudioRenderer.");
} catch (ClassNotFoundException e) {
@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibflacAudioRenderer.");
} catch (ClassNotFoundException e) {
@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded FfmpegAudioRenderer.");
} catch (ClassNotFoundException e) {
@ -787,6 +793,13 @@ public class SimpleExoPlayer implements ExoPlayer {
// Do nothing.
}
/**
* Builds an array of {@link AudioProcessor}s that will process PCM audio before output.
*/
protected AudioProcessor[] buildAudioProcessors() {
return new AudioProcessor[0];
}
// Internal methods.
private void removeSurfaceCallbacks() {

View file

@ -0,0 +1,123 @@
/*
* 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.audio;
import com.google.android.exoplayer2.C;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Interface for audio processors.
*/
public interface AudioProcessor {
/**
* Exception thrown when a processor can't be configured for a given input audio format.
*/
final class UnhandledFormatException extends Exception {
public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) {
super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding "
+ encoding);
}
}
/**
* An empty, direct {@link ByteBuffer}.
*/
ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
/**
* Configures the processor to process input audio with the specified format. After calling this
* method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the
* processor will not accept any buffers until it is reconfigured. Returns {@code true} if the
* processor must be flushed, or if the value returned by {@link #isActive()} has changed as a
* result of the call. If it's active, {@link #getOutputChannelCount()} and
* {@link #getOutputEncoding()} return the processor's output format.
*
* @param sampleRateHz The sample rate of input audio in Hz.
* @param channelCount The number of interleaved channels in input audio.
* @param encoding The encoding of input audio.
* @return {@code true} if the processor must be flushed or the value returned by
* {@link #isActive()} has changed as a result of the call.
* @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
*/
boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
throws UnhandledFormatException;
/**
* Returns whether the processor is configured and active.
*/
boolean isActive();
/**
* Returns the number of audio channels in the data output by the processor.
*/
int getOutputChannelCount();
/**
* Returns the audio encoding used in the data output by the processor.
*/
@C.Encoding
int getOutputEncoding();
/**
* Queues audio data between the position and limit of the input {@code buffer} for processing.
* {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as
* read-only. Its position will be advanced by the number of bytes consumed (which may be zero).
* The caller retains ownership of the provided buffer. Calling this method invalidates any
* previous buffer returned by {@link #getOutput()}.
*
* @param buffer The input buffer to process.
*/
void queueInput(ByteBuffer buffer);
/**
* Queues an end of stream signal. After this method has been called,
* {@link #queueInput(ByteBuffer)} may not be called until after the next call to
* {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple
* calls may be required to read all of the remaining output data. {@link #isEnded()} will return
* {@code true} once all remaining output data has been read.
*/
void queueEndOfStream();
/**
* Returns a buffer containing processed output data between its position and limit. The buffer
* will always be a direct byte buffer with native byte order. Calling this method invalidates any
* previously returned buffer. The buffer will be empty if no output is available.
*
* @return A buffer containing processed output data between its position and limit.
*/
ByteBuffer getOutput();
/**
* Returns whether this processor will return no more output from {@link #getOutput()} until it
* has been {@link #flush()}ed and more input has been queued.
*/
boolean isEnded();
/**
* Clears any state in preparation for receiving a new stream of input buffers.
*/
void flush();
/**
* Releases any resources associated with this instance.
*/
void release();
}

View file

@ -25,13 +25,13 @@ import android.os.ConditionVariable;
import android.os.SystemClock;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
/**
* Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles
@ -54,8 +54,8 @@ import java.nio.ByteOrder;
* safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling
* {@link #configure(String, int, int, int, int)}.
* <p>
* Call {@link #handleEndOfStream()} to play out all data when no more input buffers will be
* provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call
* Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will
* be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call
* {@link #release()} when the instance is no longer required.
*/
public final class AudioTrack {
@ -91,6 +91,21 @@ public final class AudioTrack {
}
/**
* Thrown when a failure occurs configuring the track.
*/
public static final class ConfigurationException extends Exception {
public ConfigurationException(Throwable cause) {
super(cause);
}
public ConfigurationException(String message) {
super(message);
}
}
/**
* Thrown when a failure occurs initializing an {@link android.media.AudioTrack}.
*/
@ -255,6 +270,8 @@ public final class AudioTrack {
public static boolean failOnSpuriousAudioTimestamp = false;
private final AudioCapabilities audioCapabilities;
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
private final AudioProcessor[] availableAudioProcessors;
private final Listener listener;
private final ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets;
@ -268,14 +285,13 @@ public final class AudioTrack {
private android.media.AudioTrack audioTrack;
private int sampleRate;
private int channelConfig;
@C.Encoding
private int encoding;
@C.Encoding
private int outputEncoding;
@C.StreamType
private int streamType;
@C.Encoding
private int sourceEncoding;
@C.Encoding
private int targetEncoding;
private boolean passthrough;
private int pcmFrameSize;
private int bufferSize;
private long bufferSizeUs;
@ -290,8 +306,12 @@ public final class AudioTrack {
private long lastTimestampSampleTimeUs;
private Method getLatencyMethod;
private int pcmFrameSize;
private long submittedPcmBytes;
private long submittedEncodedFrames;
private int outputPcmFrameSize;
private long writtenPcmBytes;
private long writtenEncodedFrames;
private int framesPerEncodedSample;
private int startMediaTimeState;
private long startMediaTimeUs;
@ -299,12 +319,14 @@ public final class AudioTrack {
private long latencyUs;
private float volume;
private byte[] temporaryBuffer;
private int temporaryBufferOffset;
private ByteBuffer currentSourceBuffer;
private ByteBuffer resampledBuffer;
private boolean useResampledBuffer;
private AudioProcessor[] audioProcessors;
private ByteBuffer[] outputBuffers;
private ByteBuffer inputBuffer;
private ByteBuffer outputBuffer;
private byte[] preV21OutputBuffer;
private int preV21OutputBufferOffset;
private int drainingAudioProcessorIndex;
private boolean handledEndOfStream;
private boolean playing;
private int audioSessionId;
@ -313,11 +335,20 @@ public final class AudioTrack {
private long lastFeedElapsedRealtimeMs;
/**
* @param audioCapabilities The current audio capabilities.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
* output. May be empty.
* @param listener Listener for audio track events.
*/
public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) {
public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors,
Listener listener) {
this.audioCapabilities = audioCapabilities;
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
availableAudioProcessors = new AudioProcessor[audioProcessors.length + 2];
availableAudioProcessors[0] = new ResamplingAudioProcessor();
availableAudioProcessors[1] = channelMappingAudioProcessor;
System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length);
this.listener = listener;
releasingConditionVariable = new ConditionVariable(true);
if (Util.SDK_INT >= 18) {
@ -340,6 +371,9 @@ public final class AudioTrack {
startMediaTimeState = START_NOT_SET;
streamType = C.STREAM_TYPE_DEFAULT;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
drainingAudioProcessorIndex = C.INDEX_UNSET;
this.audioProcessors = new AudioProcessor[0];
outputBuffers = new ByteBuffer[0];
}
/**
@ -414,9 +448,70 @@ public final class AudioTrack {
* {@link C#ENCODING_PCM_32BIT}.
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
* suitable buffer size automatically.
* @throws ConfigurationException If an error occurs configuring the track.
*/
public void configure(String mimeType, int channelCount, int sampleRate,
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize) {
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException {
configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null);
}
/**
* Configures (or reconfigures) the audio track.
*
* @param mimeType The mime type.
* @param channelCount The number of channels.
* @param sampleRate The sample rate in Hz.
* @param pcmEncoding 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 automatically.
* @param outputChannels A mapping from input to output channels that is applied to this track's
* input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the
* input unchanged. Otherwise, the element at index {@code i} specifies index of the input
* channel to map to output channel {@code i} when preprocessing input buffers. After the
* map is applied the audio data will have {@code outputChannels.length} channels.
* @throws ConfigurationException If an error occurs configuring the track.
*/
public void configure(String mimeType, int channelCount, int sampleRate,
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels)
throws ConfigurationException {
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
@C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding;
boolean flush = false;
if (!passthrough) {
pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount);
// Reconfigure the audio processors.
channelMappingAudioProcessor.setChannelMap(outputChannels);
ArrayList<AudioProcessor> newAudioProcessors = new ArrayList<>();
for (AudioProcessor audioProcessor : availableAudioProcessors) {
try {
flush |= audioProcessor.configure(sampleRate, channelCount, encoding);
} catch (AudioProcessor.UnhandledFormatException e) {
throw new ConfigurationException(e);
}
if (audioProcessor.isActive()) {
newAudioProcessors.add(audioProcessor);
channelCount = audioProcessor.getOutputChannelCount();
encoding = audioProcessor.getOutputEncoding();
} else {
audioProcessor.flush();
}
}
if (flush) {
int count = newAudioProcessors.size();
audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]);
outputBuffers = new ByteBuffer[count];
for (int i = 0; i < count; i++) {
AudioProcessor audioProcessor = audioProcessors[i];
audioProcessor.flush();
outputBuffers[i] = audioProcessor.getOutput();
}
}
}
int channelConfig;
switch (channelCount) {
case 1:
@ -444,7 +539,7 @@ public final class AudioTrack {
channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
break;
default:
throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
throw new ConfigurationException("Unsupported channel count: " + channelCount);
}
// Workaround for overly strict channel configuration checks on nVidia Shield.
@ -462,25 +557,13 @@ public final class AudioTrack {
}
}
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
// 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) {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
}
@C.Encoding int sourceEncoding;
if (passthrough) {
sourceEncoding = getEncodingForMimeType(mimeType);
} else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT
|| pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) {
sourceEncoding = pcmEncoding;
} else {
throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding);
}
if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate
if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate
&& this.channelConfig == channelConfig) {
// We already have an audio track with the correct sample rate, channel config and encoding.
return;
@ -488,38 +571,38 @@ public final class AudioTrack {
reset();
this.sourceEncoding = sourceEncoding;
this.encoding = encoding;
this.passthrough = passthrough;
this.sampleRate = sampleRate;
this.channelConfig = channelConfig;
targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT;
pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels.
outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT;
outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount);
if (specifiedBufferSize != 0) {
bufferSize = specifiedBufferSize;
} else if (passthrough) {
// TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into
// account. [Internal: b/25181305]
if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) {
if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) {
// AC-3 allows bitrates up to 640 kbit/s.
bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND);
} else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ {
} else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ {
// 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 =
android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding);
android.media.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) * pcmFrameSize;
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
int maxAppBufferSize = (int) Math.max(minBufferSize,
durationUsToFrames(MAX_BUFFER_DURATION_US) * pcmFrameSize);
durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize
: multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize
: multipliedBufferSize;
}
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize));
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize);
}
private void initialize() throws InitializationException {
@ -531,15 +614,15 @@ public final class AudioTrack {
releasingConditionVariable.block();
if (tunneling) {
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding,
bufferSize, audioSessionId);
} else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, MODE_STREAM);
outputEncoding, bufferSize, MODE_STREAM);
} else {
// Re-attach to the same audio session.
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, MODE_STREAM, audioSessionId);
outputEncoding, bufferSize, MODE_STREAM, audioSessionId);
}
checkAudioTrackInitialized();
@ -594,25 +677,26 @@ public final class AudioTrack {
}
/**
* Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current
* position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is
* advanced by the number of bytes that were successfully written.
* {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is
* discontinuous with the last buffer handled since the track was reset.
* Attempts to process data from a {@link ByteBuffer}, starting from its current position and
* ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the
* number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if
* {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset.
* <p>
* Returns whether the data was written in full. If the data was not written in full then the same
* Returns whether the data was handled in full. If the data was not handled in full then the same
* {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
* except in the case of an interleaving call to {@link #reset()} (or an interleaving call to
* {@link #configure(String, int, int, int, int)} that caused the track to be reset).
*
* @param buffer The buffer containing audio data to play back.
* @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
* @return Whether the buffer was consumed fully.
* @param buffer The buffer containing audio data.
* @param presentationTimeUs The presentation timestamp of the buffer in microseconds.
* @return Whether the buffer was handled fully.
* @throws InitializationException If an error occurs initializing the track.
* @throws WriteException If an error occurs writing the audio data.
*/
@SuppressWarnings("ReferenceEquality")
public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
throws InitializationException, WriteException {
Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer);
if (!isInitialized()) {
initialize();
if (playing) {
@ -620,27 +704,12 @@ public final class AudioTrack {
}
}
boolean hadData = hasData;
hasData = hasPendingData();
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
}
boolean result = writeBuffer(buffer, presentationTimeUs);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
return result;
}
@SuppressWarnings("ReferenceEquality")
private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
boolean isNewSourceBuffer = currentSourceBuffer == null;
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
currentSourceBuffer = buffer;
if (needsPassthroughWorkarounds()) {
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its
// buffer empties. See [Internal: b/18899620].
if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
// We force an underrun to pause the track, so don't notify the listener in this case.
hasData = false;
return false;
}
@ -653,27 +722,25 @@ public final class AudioTrack {
}
}
if (isNewSourceBuffer) {
// We're seeing this buffer for the first time.
boolean hadData = hasData;
hasData = hasPendingData();
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
}
if (!currentSourceBuffer.hasRemaining()) {
if (inputBuffer == null) {
// We are seeing this buffer for the first time.
if (!buffer.hasRemaining()) {
// The buffer is empty.
currentSourceBuffer = null;
return true;
}
useResampledBuffer = targetEncoding != sourceEncoding;
if (useResampledBuffer) {
Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT);
// Resample the buffer to get the data in the target encoding.
resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer);
buffer = resampledBuffer;
}
if (passthrough && framesPerEncodedSample == 0) {
// If this is the first encoded sample, calculate the sample size in frames.
framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer);
framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer);
}
if (startMediaTimeState == START_NOT_SET) {
startMediaTimeUs = Math.max(0, presentationTimeUs);
startMediaTimeState = START_IN_SYNC;
@ -695,66 +762,172 @@ public final class AudioTrack {
listener.onPositionDiscontinuity();
}
}
if (Util.SDK_INT < 21) {
// Copy {@code buffer} into {@code temporaryBuffer}.
int bytesRemaining = buffer.remaining();
if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) {
temporaryBuffer = new byte[bytesRemaining];
}
int originalPosition = buffer.position();
buffer.get(temporaryBuffer, 0, bytesRemaining);
buffer.position(originalPosition);
temporaryBufferOffset = 0;
if (passthrough) {
submittedEncodedFrames += framesPerEncodedSample;
} else {
submittedPcmBytes += buffer.remaining();
}
inputBuffer = buffer;
}
buffer = useResampledBuffer ? resampledBuffer : buffer;
if (passthrough) {
// Passthrough buffers are not processed.
writeBuffer(inputBuffer, presentationTimeUs);
} else {
processBuffers(presentationTimeUs);
}
if (!inputBuffer.hasRemaining()) {
inputBuffer = null;
return true;
}
return false;
}
private void processBuffers(long avSyncPresentationTimeUs) throws WriteException {
int count = audioProcessors.length;
int index = count;
while (index >= 0) {
ByteBuffer input = index > 0 ? outputBuffers[index - 1]
: (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER);
if (index == count) {
writeBuffer(input, avSyncPresentationTimeUs);
} else {
AudioProcessor audioProcessor = audioProcessors[index];
audioProcessor.queueInput(input);
ByteBuffer output = audioProcessor.getOutput();
outputBuffers[index] = output;
if (output.hasRemaining()) {
// Handle the output as input to the next audio processor or the AudioTrack.
index++;
continue;
}
}
if (input.hasRemaining()) {
// The input wasn't consumed and no output was produced, so give up for now.
return;
}
// Get more input from upstream.
index--;
}
}
@SuppressWarnings("ReferenceEquality")
private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs)
throws WriteException {
if (!buffer.hasRemaining()) {
return true;
}
if (outputBuffer != null) {
Assertions.checkArgument(outputBuffer == buffer);
} else {
outputBuffer = buffer;
if (Util.SDK_INT < 21) {
int bytesRemaining = buffer.remaining();
if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) {
preV21OutputBuffer = new byte[bytesRemaining];
}
int originalPosition = buffer.position();
buffer.get(preV21OutputBuffer, 0, bytesRemaining);
buffer.position(originalPosition);
preV21OutputBufferOffset = 0;
}
}
int bytesRemaining = buffer.remaining();
int bytesWritten = 0;
if (Util.SDK_INT < 21) { // passthrough == false
// Work out how many bytes we can write without the risk of blocking.
int bytesPending =
(int) (submittedPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * pcmFrameSize));
(int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize));
int bytesToWrite = bufferSize - bytesPending;
if (bytesToWrite > 0) {
bytesToWrite = Math.min(bytesRemaining, bytesToWrite);
bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite);
if (bytesWritten >= 0) {
temporaryBufferOffset += bytesWritten;
bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite);
if (bytesWritten > 0) {
preV21OutputBufferOffset += bytesWritten;
buffer.position(buffer.position() + bytesWritten);
}
buffer.position(buffer.position() + bytesWritten);
}
} else if (tunneling) {
Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET);
bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining,
avSyncPresentationTimeUs);
} else {
bytesWritten = tunneling
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
}
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
if (bytesWritten < 0) {
throw new WriteException(bytesWritten);
}
if (!passthrough) {
submittedPcmBytes += bytesWritten;
writtenPcmBytes += bytesWritten;
}
if (bytesWritten == bytesRemaining) {
if (passthrough) {
submittedEncodedFrames += framesPerEncodedSample;
writtenEncodedFrames += framesPerEncodedSample;
}
currentSourceBuffer = null;
outputBuffer = null;
return true;
}
return false;
}
/**
* Ensures that the last data passed to {@link #handleBuffer(ByteBuffer, long)} is played in full.
* Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended.
*
* @throws WriteException If an error occurs draining data to the track.
*/
public void handleEndOfStream() {
if (isInitialized()) {
audioTrackUtil.handleEndOfStream(getSubmittedFrames());
bytesUntilNextAvSync = 0;
public void playToEndOfStream() throws WriteException {
if (handledEndOfStream || !isInitialized()) {
return;
}
// Drain the audio processors.
boolean audioProcessorNeedsEndOfStream = false;
if (drainingAudioProcessorIndex == C.INDEX_UNSET) {
drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0;
audioProcessorNeedsEndOfStream = true;
}
while (drainingAudioProcessorIndex < audioProcessors.length) {
AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex];
if (audioProcessorNeedsEndOfStream) {
audioProcessor.queueEndOfStream();
}
processBuffers(C.TIME_UNSET);
if (!audioProcessor.isEnded()) {
return;
}
audioProcessorNeedsEndOfStream = true;
drainingAudioProcessorIndex++;
}
// Finish writing any remaining output to the track.
if (outputBuffer != null) {
writeBuffer(outputBuffer, C.TIME_UNSET);
if (outputBuffer != null) {
return;
}
}
// Drain the track.
audioTrackUtil.handleEndOfStream(getWrittenFrames());
bytesUntilNextAvSync = 0;
handledEndOfStream = true;
}
/**
* Returns whether all buffers passed to {@link #handleBuffer(ByteBuffer, long)} have been
* completely processed and played.
*/
public boolean isEnded() {
return !isInitialized() || (handledEndOfStream && !hasPendingData());
}
/**
@ -762,7 +935,7 @@ public final class AudioTrack {
*/
public boolean hasPendingData() {
return isInitialized()
&& (getSubmittedFrames() > audioTrackUtil.getPlaybackHeadPosition()
&& (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition()
|| overrideHasPendingData());
}
@ -815,9 +988,14 @@ public final class AudioTrack {
/**
* Enables tunneling. The audio track is reset if tunneling was previously disabled or if the
* audio session id has changed. Enabling tunneling requires platform API version 21 onwards.
* <p>
* If this instance has {@link AudioProcessor}s and tunneling is enabled, care must be taken that
* audio processors do not output buffers with a different duration than their input, and buffer
* processors must produce output corresponding to their last input immediately after that input
* is queued.
*
* @param tunnelingAudioSessionId The audio session id to use.
* @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21.
* @throws IllegalStateException Thrown if enabling tunneling on platform API version &lt; 21.
*/
public void enableTunnelingV21(int tunnelingAudioSessionId) {
Assertions.checkState(Util.SDK_INT >= 21);
@ -884,8 +1062,18 @@ public final class AudioTrack {
if (isInitialized()) {
submittedPcmBytes = 0;
submittedEncodedFrames = 0;
writtenPcmBytes = 0;
writtenEncodedFrames = 0;
framesPerEncodedSample = 0;
currentSourceBuffer = null;
inputBuffer = null;
outputBuffer = null;
for (int i = 0; i < audioProcessors.length; i++) {
AudioProcessor audioProcessor = audioProcessors[i];
audioProcessor.flush();
outputBuffers[i] = audioProcessor.getOutput();
}
handledEndOfStream = false;
drainingAudioProcessorIndex = C.INDEX_UNSET;
avSyncHeader = null;
bytesUntilNextAvSync = 0;
startMediaTimeState = START_NOT_SET;
@ -920,6 +1108,9 @@ public final class AudioTrack {
public void release() {
reset();
releaseKeepSessionIdAudioTrack();
for (AudioProcessor audioProcessor : availableAudioProcessors) {
audioProcessor.release();
}
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
playing = false;
}
@ -1063,10 +1254,6 @@ public final class AudioTrack {
return audioTrack != null;
}
private long pcmBytesToFrames(long byteCount) {
return byteCount / pcmFrameSize;
}
private long framesToDurationUs(long frameCount) {
return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
}
@ -1076,7 +1263,11 @@ public final class AudioTrack {
}
private long getSubmittedFrames() {
return passthrough ? submittedEncodedFrames : pcmBytesToFrames(submittedPcmBytes);
return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize);
}
private long getWrittenFrames() {
return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize);
}
private void resetSyncParams() {
@ -1094,7 +1285,7 @@ public final class AudioTrack {
*/
private boolean needsPassthroughWorkarounds() {
return Util.SDK_INT < 23
&& (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3);
&& (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3);
}
/**
@ -1129,82 +1320,6 @@ public final class AudioTrack {
sessionId);
}
/**
* Converts the provided buffer into 16-bit PCM.
*
* @param buffer The buffer containing the data to convert.
* @param sourceEncoding The data encoding.
* @param out A buffer into which the output should be written, if its capacity is sufficient.
* @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the
* capacity was insufficient for the output.
*/
private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding,
ByteBuffer out) {
int offset = buffer.position();
int limit = buffer.limit();
int size = limit - offset;
int resampledSize;
switch (sourceEncoding) {
case C.ENCODING_PCM_8BIT:
resampledSize = size * 2;
break;
case C.ENCODING_PCM_24BIT:
resampledSize = (size / 3) * 2;
break;
case C.ENCODING_PCM_32BIT:
resampledSize = size / 2;
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
// Never happens.
throw new IllegalStateException();
}
ByteBuffer resampledBuffer = out;
if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) {
resampledBuffer = ByteBuffer.allocateDirect(resampledSize);
}
resampledBuffer.position(0);
resampledBuffer.limit(resampledSize);
// Samples are little endian.
switch (sourceEncoding) {
case C.ENCODING_PCM_8BIT:
// 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
for (int i = offset; i < limit; i++) {
resampledBuffer.put((byte) 0);
resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128));
}
break;
case C.ENCODING_PCM_24BIT:
// 24->16 bit resampling. Drop the least significant byte.
for (int i = offset; i < limit; i += 3) {
resampledBuffer.put(buffer.get(i + 1));
resampledBuffer.put(buffer.get(i + 2));
}
break;
case C.ENCODING_PCM_32BIT:
// 32->16 bit resampling. Drop the two least significant bytes.
for (int i = offset; i < limit; i += 4) {
resampledBuffer.put(buffer.get(i + 2));
resampledBuffer.put(buffer.get(i + 3));
}
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
// Never happens.
throw new IllegalStateException();
}
resampledBuffer.position(0);
return resampledBuffer;
}
@C.Encoding
private static int getEncodingForMimeType(String mimeType) {
switch (mimeType) {

View file

@ -0,0 +1,155 @@
/*
* 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.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.Encoding;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* An {@link AudioProcessor} that applies a mapping from input channels onto specified output
* channels. This can be used to reorder, duplicate or discard channels.
*/
/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor {
private int channelCount;
private int sampleRateHz;
private int[] pendingOutputChannels;
private boolean active;
private int[] outputChannels;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/**
* Creates a new processor that applies a channel mapping.
*/
public ChannelMappingAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
}
/**
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
* to start using the new channel map.
*
* @see AudioTrack#configure(String, int, int, int, int, int[])
*/
public void setChannelMap(int[] outputChannels) {
pendingOutputChannels = outputChannels;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
throws UnhandledFormatException {
boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels);
outputChannels = pendingOutputChannels;
if (outputChannels == null) {
active = false;
return outputChannelsChanged;
}
if (encoding != C.ENCODING_PCM_16BIT) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz
&& this.channelCount == channelCount) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
active = channelCount != outputChannels.length;
for (int i = 0; i < outputChannels.length; i++) {
int channelIndex = outputChannels[i];
if (channelIndex >= channelCount) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
active |= (channelIndex != i);
}
return true;
}
@Override
public boolean isActive() {
return active;
}
@Override
public int getOutputChannelCount() {
return outputChannels == null ? channelCount : outputChannels.length;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
int position = inputBuffer.position();
int limit = inputBuffer.limit();
int frameCount = (limit - position) / (2 * channelCount);
int outputSize = frameCount * outputChannels.length * 2;
if (buffer.capacity() < outputSize) {
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
while (position < limit) {
for (int channelIndex : outputChannels) {
buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
}
position += channelCount * 2;
}
inputBuffer.position(limit);
buffer.flip();
outputBuffer = buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
}
@Override
public void release() {
flush();
buffer = EMPTY_BUFFER;
}
}

View file

@ -47,8 +47,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private final AudioTrack audioTrack;
private boolean passthroughEnabled;
private boolean codecNeedsDiscardChannelsWorkaround;
private android.media.MediaFormat passthroughMediaFormat;
private int pcmEncoding;
private int channelCount;
private long currentPositionUs;
private boolean allowPositionDiscontinuity;
@ -121,13 +123,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before
* output.
*/
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
AudioProcessor... audioProcessors) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
}
@ -185,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) {
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
if (passthroughEnabled) {
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
passthroughMediaFormat = format.getFrameworkMediaFormatV16();
@ -216,17 +222,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
// output 16-bit PCM.
pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
: C.ENCODING_PCM_16BIT;
channelCount = newFormat.channelCount;
}
@Override
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
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;
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0);
int[] channelMap;
if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) {
channelMap = new int[this.channelCount];
for (int i = 0; i < this.channelCount; i++) {
channelMap[i] = i;
}
} else {
channelMap = null;
}
try {
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap);
} catch (AudioTrack.ConfigurationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
}
/**
@ -304,7 +326,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
public boolean isEnded() {
return super.isEnded() && !audioTrack.hasPendingData();
return super.isEnded() && audioTrack.isEnded();
}
@Override
@ -353,8 +375,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
@Override
protected void onOutputStreamEnded() {
audioTrack.handleEndOfStream();
protected void renderToEndOfStream() throws ExoPlaybackException {
try {
audioTrack.playToEndOfStream();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
}
@Override
@ -376,6 +402,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
}
/**
* Returns whether the decoder is known to output six audio channels when provided with input with
* fewer than six channels.
* <p>
* See [Internal: b/35655036].
*/
private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) {
// The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7.
return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName)
&& "samsung".equals(Util.MANUFACTURER)
&& (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte")
|| Util.DEVICE.startsWith("heroqlte"));
}
private final class AudioTrackListener implements AudioTrack.Listener {
@Override

View file

@ -0,0 +1,176 @@
/*
* 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.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}.
*/
/* package */ final class ResamplingAudioProcessor implements AudioProcessor {
private int sampleRateHz;
private int channelCount;
@C.PcmEncoding
private int encoding;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/**
* Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}.
*/
public ResamplingAudioProcessor() {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
encoding = C.ENCODING_INVALID;
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
throws UnhandledFormatException {
if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount
&& this.encoding == encoding) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.encoding = encoding;
if (encoding == C.ENCODING_PCM_16BIT) {
buffer = EMPTY_BUFFER;
}
return true;
}
@Override
public boolean isActive() {
return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT;
}
@Override
public int getOutputChannelCount() {
return channelCount;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
// Prepare the output buffer.
int position = inputBuffer.position();
int limit = inputBuffer.limit();
int size = limit - position;
int resampledSize;
switch (encoding) {
case C.ENCODING_PCM_8BIT:
resampledSize = size * 2;
break;
case C.ENCODING_PCM_24BIT:
resampledSize = (size / 3) * 2;
break;
case C.ENCODING_PCM_32BIT:
resampledSize = size / 2;
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
throw new IllegalStateException();
}
if (buffer.capacity() < resampledSize) {
buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
// Resample the little endian input and update the input/output buffers.
switch (encoding) {
case C.ENCODING_PCM_8BIT:
// 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
for (int i = position; i < limit; i++) {
buffer.put((byte) 0);
buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));
}
break;
case C.ENCODING_PCM_24BIT:
// 24->16 bit resampling. Drop the least significant byte.
for (int i = position; i < limit; i += 3) {
buffer.put(inputBuffer.get(i + 1));
buffer.put(inputBuffer.get(i + 2));
}
break;
case C.ENCODING_PCM_32BIT:
// 32->16 bit resampling. Drop the two least significant bytes.
for (int i = position; i < limit; i += 4) {
buffer.put(inputBuffer.get(i + 2));
buffer.put(inputBuffer.get(i + 3));
}
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
// Never happens.
throw new IllegalStateException();
}
inputBuffer.position(inputBuffer.limit());
buffer.flip();
outputBuffer = buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
}
@Override
public void release() {
flush();
buffer = EMPTY_BUFFER;
}
}

View file

@ -34,6 +34,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
@ -67,12 +68,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
*/
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private final boolean playClearSamplesWithoutKeys;
private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private final FormatHolder formatHolder;
private final DecoderInputBuffer flagsOnlyBuffer;
private DecoderCounters decoderCounters;
private Format inputFormat;
@ -83,8 +84,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private DrmSession<ExoMediaCrypto> drmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession;
@ReinitializationState
private int decoderReinitializationState;
@ReinitializationState private int decoderReinitializationState;
private boolean decoderReceivedBuffers;
private boolean audioTrackNeedsConfigure;
@ -102,10 +102,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* @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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public SimpleDecoderAudioRenderer(Handler eventHandler,
AudioRendererEventListener eventListener) {
this(eventHandler, eventListener, null);
AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) {
this(eventHandler, eventListener, null, null, false, audioProcessors);
}
/**
@ -133,16 +134,19 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public SimpleDecoderAudioRenderer(Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
AudioProcessor... audioProcessors) {
super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
this.drmSessionManager = drmSessionManager;
formatHolder = new FormatHolder();
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
formatHolder = new FormatHolder();
flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
audioTrackNeedsConfigure = true;
}
@ -174,13 +178,31 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) {
try {
audioTrack.playToEndOfStream();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
return;
}
// Try and read a format if we don't have one already.
if (inputFormat == null && !readFormat()) {
// We can't make progress without one.
return;
if (inputFormat == null) {
// We don't have a format yet, so try and read one.
flagsOnlyBuffer.clear();
int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
} else if (result == C.RESULT_BUFFER_READ) {
// End of stream read having not read a format.
Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
inputStreamEnded = true;
processEndOfStream();
return;
} else {
// We still don't have a format and can't make progress without one.
return;
}
}
// If we don't have a decoder yet, we need to instantiate one.
@ -193,8 +215,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
while (drainOutputBuffer()) {}
while (feedInputBuffer()) {}
TraceUtil.endSection();
} catch (AudioTrack.InitializationException | AudioTrack.WriteException
| AudioDecoderException e) {
} catch (AudioDecoderException | AudioTrack.ConfigurationException
| AudioTrack.InitializationException | AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
decoderCounters.ensureUpdated();
@ -255,7 +277,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,
AudioTrack.InitializationException, AudioTrack.WriteException {
AudioTrack.ConfigurationException, AudioTrack.InitializationException,
AudioTrack.WriteException {
if (outputBuffer == null) {
outputBuffer = decoder.dequeueOutputBuffer();
if (outputBuffer == null) {
@ -274,8 +297,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} else {
outputBuffer.release();
outputBuffer = null;
outputStreamEnded = true;
audioTrack.handleEndOfStream();
processEndOfStream();
}
return false;
}
@ -324,7 +346,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
// We've already read an encrypted sample into buffer, and are waiting for keys.
result = C.RESULT_BUFFER_READ;
} else {
result = readSource(formatHolder, inputBuffer);
result = readSource(formatHolder, inputBuffer, false);
}
if (result == C.RESULT_NOTHING_READ) {
@ -365,6 +387,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
}
private void processEndOfStream() throws ExoPlaybackException {
outputStreamEnded = true;
try {
audioTrack.playToEndOfStream();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
}
private void flushDecoder() throws ExoPlaybackException {
waitingForKeys = false;
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
@ -383,7 +414,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
public boolean isEnded() {
return outputStreamEnded && !audioTrack.hasPendingData();
return outputStreamEnded && audioTrack.isEnded();
}
@Override
@ -513,15 +544,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
decoderReceivedBuffers = false;
}
private boolean readFormat() throws ExoPlaybackException {
int result = readSource(formatHolder, null);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
return true;
}
return false;
}
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
Format oldFormat = inputFormat;
inputFormat = newFormat;

View file

@ -61,8 +61,7 @@ public class DecoderInputBuffer extends Buffer {
*/
public long timeUs;
@BufferReplacementMode
private final int bufferReplacementMode;
@BufferReplacementMode private final int bufferReplacementMode;
/**
* @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One

View file

@ -35,6 +35,7 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
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;
@ -103,6 +104,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
public static final int MODE_RELEASE = 3;
private static final String TAG = "OfflineDrmSessionMngr";
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private static final int MSG_PROVISION = 0;
private static final int MSG_KEYS = 1;
@ -280,6 +282,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
* required.
*
* <p>{@code mode} must be one of these:
* <ul>
* <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
* requested otherwise the offline license is restored.
* <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
@ -288,6 +291,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
* requested otherwise the offline license is renewed.
* <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license
* is released.
* </ul>
*
* @param mode The mode to be set.
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
@ -337,6 +341,12 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
schemeInitData = psshData;
}
}
if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
&& (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
|| MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
// Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
schemeMimeType = CENC_SCHEME_MIME_TYPE;
}
}
state = STATE_OPENING;
openInternal(true);

View file

@ -31,7 +31,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
/** Wraps the exception which is the cause of the error state. */
class DrmSessionException extends Exception {
DrmSessionException(Exception e) {
public DrmSessionException(Exception e) {
super(e);
}
@ -70,8 +70,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
* @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
* {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
*/
@State
int getState();
@State int getState();
/**
* Returns a {@link ExoMediaCrypto} for the open session.

View file

@ -24,6 +24,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.HashMap;
@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
}
/**
* @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request
* properties can be set by calling {@link #setKeyRequestProperty(String, String)}.
* @param defaultUrl The default license URL.
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
* @param keyRequestProperties Request properties to set when making key requests, or null.
*/
@Deprecated
public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory,
Map<String, String> keyRequestProperties) {
this.dataSourceFactory = dataSourceFactory;
this.defaultUrl = defaultUrl;
this.keyRequestProperties = keyRequestProperties;
this.keyRequestProperties = new HashMap<>();
if (keyRequestProperties != null) {
this.keyRequestProperties.putAll(keyRequestProperties);
}
}
/**
* Sets a header for key requests made by the callback.
*
* @param name The name of the header field.
* @param value The value of the field.
*/
public void setKeyRequestProperty(String name, String value) {
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
synchronized (keyRequestProperties) {
keyRequestProperties.put(name, value);
}
}
/**
* Clears a header for key requests made by the callback.
*
* @param name The name of the header field.
*/
public void clearKeyRequestProperty(String name) {
Assertions.checkNotNull(name);
synchronized (keyRequestProperties) {
keyRequestProperties.remove(name);
}
}
/**
* Clears all headers for key requests made by the callback.
*/
public void clearAllKeyRequestProperties() {
synchronized (keyRequestProperties) {
keyRequestProperties.clear();
}
}
@Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
return executePost(url, new byte[0], null);
return executePost(dataSourceFactory, url, new byte[0], null);
}
@Override
@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
if (C.PLAYREADY_UUID.equals(uuid)) {
requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
}
if (keyRequestProperties != null) {
synchronized (keyRequestProperties) {
requestProperties.putAll(keyRequestProperties);
}
return executePost(url, request.getData(), requestProperties);
return executePost(dataSourceFactory, url, request.getData(), requestProperties);
}
private byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
throws IOException {
private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url,
byte[] data, Map<String, String> requestProperties) throws IOException {
HttpDataSource dataSource = dataSourceFactory.createDataSource();
if (requestProperties != null) {
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {

View file

@ -17,7 +17,6 @@
package com.google.android.exoplayer2.drm;
import android.media.MediaDrm;
import android.net.Uri;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
@ -27,24 +26,14 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
import com.google.android.exoplayer2.source.chunk.InitializationChunk;
import com.google.android.exoplayer2.source.dash.DashUtil;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.util.HashMap;
@ -58,28 +47,6 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
private final DefaultDrmSessionManager<T> drmSessionManager;
private final HandlerThread handlerThread;
/**
* Helper method to download a DASH manifest.
*
* @param dataSource The {@link HttpDataSource} from which the manifest should be read.
* @param manifestUriString The URI of the manifest to be read.
* @return An instance of {@link DashManifest}.
* @throws IOException If an error occurs reading data from the stream.
* @see DashManifestParser
*/
public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString)
throws IOException {
DataSourceInputStream inputStream = new DataSourceInputStream(
dataSource, new DataSpec(Uri.parse(manifestUriString)));
try {
inputStream.open();
DashManifestParser parser = new DashManifestParser();
return parser.parse(dataSource.getUri(), inputStream);
} finally {
inputStream.close();
}
}
/**
* Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
* you're done with the helper instance.
@ -93,7 +60,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
return newWidevineInstance(
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null);
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null);
}
/**
@ -174,7 +141,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
*/
public byte[] download(HttpDataSource dataSource, String manifestUriString)
throws IOException, InterruptedException, DrmSessionException {
return download(dataSource, downloadManifest(dataSource, manifestUriString));
return download(dataSource, DashUtil.loadManifest(dataSource, manifestUriString));
}
/**
@ -210,11 +177,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
Representation representation = adaptationSet.representations.get(0);
DrmInitData drmInitData = representation.format.drmInitData;
if (drmInitData == null) {
InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation);
if (initializationChunk == null) {
return null;
}
Format sampleFormat = initializationChunk.getSampleFormat();
Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation);
if (sampleFormat != null) {
drmInitData = sampleFormat.drmInitData;
}
@ -288,28 +251,4 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
return session;
}
private static InitializationChunk loadInitializationChunk(final DataSource dataSource,
final Representation representation) throws IOException, InterruptedException {
RangedUri rangedUri = representation.getInitializationUri();
if (rangedUri == null) {
return null;
}
DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start,
rangedUri.length, representation.getCacheKey());
InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec,
representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */,
newWrappedExtractor(representation.format));
initializationChunk.load();
return initializationChunk;
}
private static ChunkExtractorWrapper newWrappedExtractor(final Format format) {
final String mimeType = format.containerMimeType;
final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM)
|| mimeType.startsWith(MimeTypes.AUDIO_WEBM);
final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor();
return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */,
false /* resendFormatOnInit */);
}
}

View file

@ -43,8 +43,7 @@ public final class UnsupportedDrmException extends Exception {
/**
* Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
*/
@Reason
public final int reason;
@Reason public final int reason;
/**
* @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;
@ -27,6 +28,8 @@ import java.util.Arrays;
*/
public final class DefaultExtractorInput implements ExtractorInput {
private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024;
private static final int PEEK_MAX_FREE_SPACE = 512 * 1024;
private static final byte[] SCRATCH_SPACE = new byte[4096];
private final DataSource dataSource;
@ -46,7 +49,7 @@ public final class DefaultExtractorInput implements ExtractorInput {
this.dataSource = dataSource;
this.position = position;
this.streamLength = length;
peekBuffer = new byte[8 * 1024];
peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
}
@Override
@ -176,7 +179,9 @@ public final class DefaultExtractorInput implements ExtractorInput {
private void ensureSpaceForPeek(int length) {
int requiredLength = peekBufferPosition + length;
if (requiredLength > peekBuffer.length) {
peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength));
int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2,
requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE);
peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity);
}
}
@ -218,7 +223,12 @@ public final class DefaultExtractorInput implements ExtractorInput {
private void updatePeekBuffer(int bytesConsumed) {
peekBufferLength -= bytesConsumed;
peekBufferPosition = 0;
System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength);
byte[] newPeekBuffer = peekBuffer;
if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) {
newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
}
System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength);
peekBuffer = newPeekBuffer;
}
/**

View file

@ -70,6 +70,8 @@ public final class DefaultTrackOutput implements TrackOutput {
private Format downstreamFormat;
// Accessed only by the loading thread (or the consuming thread when there is no loading thread).
private boolean pendingFormatAdjustment;
private Format lastUnadjustedFormat;
private long sampleOffsetUs;
private long totalBytesWritten;
private Allocation lastAllocation;
@ -265,40 +267,42 @@ public final class DefaultTrackOutput implements TrackOutput {
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
* caller requires that the format of the stream be read even if it's not changing.
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
* @param formatRequired Whether the caller requires that the format of the stream be read even if
* it's not changing. A sample will never be read if set to true, however it is still possible
* for the end of stream or nothing to be read.
* @param loadingFinished True if an empty queue should be considered the end of the stream.
* @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
* be set if the buffer's timestamp is less than this value.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}.
*/
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished,
long decodeOnlyUntilUs) {
switch (infoQueue.readData(formatHolder, buffer, downstreamFormat, extrasHolder)) {
case C.RESULT_NOTHING_READ:
if (loadingFinished) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
}
return C.RESULT_NOTHING_READ;
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
boolean loadingFinished, long decodeOnlyUntilUs) {
int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
downstreamFormat, extrasHolder);
switch (result) {
case C.RESULT_FORMAT_READ:
downstreamFormat = formatHolder.format;
return C.RESULT_FORMAT_READ;
case C.RESULT_BUFFER_READ:
if (buffer.timeUs < decodeOnlyUntilUs) {
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
if (!buffer.isEndOfStream()) {
if (buffer.timeUs < decodeOnlyUntilUs) {
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
// Read encryption data if the sample is encrypted.
if (buffer.isEncrypted()) {
readEncryptionData(buffer, extrasHolder);
}
// Write the sample data into the holder.
buffer.ensureSpaceForWrite(extrasHolder.size);
readData(extrasHolder.offset, buffer.data, extrasHolder.size);
// Advance the read head.
dropDownstreamTo(extrasHolder.nextOffset);
}
// Read encryption data if the sample is encrypted.
if (buffer.isEncrypted()) {
readEncryptionData(buffer, extrasHolder);
}
// Write the sample data into the holder.
buffer.ensureSpaceForWrite(extrasHolder.size);
readData(extrasHolder.offset, buffer.data, extrasHolder.size);
// Advance the read head.
dropDownstreamTo(extrasHolder.nextOffset);
return C.RESULT_BUFFER_READ;
case C.RESULT_NOTHING_READ:
return C.RESULT_NOTHING_READ;
default:
throw new IllegalStateException();
}
@ -445,23 +449,24 @@ public final class DefaultTrackOutput implements TrackOutput {
}
/**
* Like {@link #format(Format)}, but with an offset that will be added to the timestamps of
* samples subsequently queued to the buffer. The offset is also used to adjust
* {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently
* passed to {@link #format(Format)}.
* Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
* subsequently queued to the buffer.
*
* @param format The format.
* @param sampleOffsetUs The timestamp offset in microseconds.
*/
public void formatWithOffset(Format format, long sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs;
format(format);
public void setSampleOffsetUs(long sampleOffsetUs) {
if (this.sampleOffsetUs != sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs;
pendingFormatAdjustment = true;
}
}
@Override
public void format(Format format) {
Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
boolean formatChanged = infoQueue.format(adjustedFormat);
lastUnadjustedFormat = format;
pendingFormatAdjustment = false;
if (upstreamFormatChangeListener != null && formatChanged) {
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
}
@ -518,6 +523,9 @@ public final class DefaultTrackOutput implements TrackOutput {
@Override
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
byte[] encryptionKey) {
if (pendingFormatAdjustment) {
format(lastUnadjustedFormat);
}
if (!startWriteOperation()) {
infoQueue.commitSampleTimestamp(timeUs);
return;
@ -754,23 +762,34 @@ public final class DefaultTrackOutput implements TrackOutput {
* and the absolute position of the first byte that may still be required after the current
* sample has been read. May be null if the caller requires that the format of the stream be
* read even if it's not changing.
* @param formatRequired Whether the caller requires that the format of the stream be read even
* if it's not changing. A sample will never be read if set to true, however it is still
* possible for the end of stream or nothing to be read.
* @param loadingFinished True if an empty queue should be considered the end of the stream.
* @param downstreamFormat The current downstream {@link Format}. If the format of the next
* sample is different to the current downstream format then a format will be read.
* @param extrasHolder The holder into which extra sample information should be written.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
* or {@link C#RESULT_BUFFER_READ}.
*/
@SuppressWarnings("ReferenceEquality")
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
Format downstreamFormat, BufferExtrasHolder extrasHolder) {
boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
BufferExtrasHolder extrasHolder) {
if (queueSize == 0) {
if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
if (loadingFinished) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
} else if (upstreamFormat != null
&& (formatRequired || upstreamFormat != downstreamFormat)) {
formatHolder.format = upstreamFormat;
return C.RESULT_FORMAT_READ;
} else {
return C.RESULT_NOTHING_READ;
}
return C.RESULT_NOTHING_READ;
}
if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
formatHolder.format = formats[relativeReadIndex];
return C.RESULT_FORMAT_READ;
}

View file

@ -102,4 +102,5 @@ public interface Extractor {
* Releases all kept resources.
*/
void release();
}

View file

@ -23,17 +23,18 @@ public interface ExtractorOutput {
/**
* Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
* <p>
* The same {@link TrackOutput} is returned if multiple calls are made with the same
* {@code trackId}.
* The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
*
* @param trackId A track identifier.
* @param id A track identifier.
* @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C}
* {@code TRACK_TYPE_*} constants.
* @return The {@link TrackOutput} for the given track identifier.
*/
TrackOutput track(int trackId);
TrackOutput track(int id, int type);
/**
* Called when all tracks have been identified, meaning no new {@code trackId} values will be
* passed to {@link #track(int)}.
* passed to {@link #track(int, int)}.
*/
void endTracks();

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -26,6 +27,18 @@ import java.util.regex.Pattern;
*/
public final class GaplessInfoHolder {
/**
* A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed
* to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback
* information are decoded.
*/
public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() {
@Override
public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) {
return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2);
}
};
private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.flv;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap {
boolean hasAudio = (flags & 0x04) != 0;
boolean hasVideo = (flags & 0x01) != 0;
if (hasAudio && audioReader == null) {
audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO));
audioReader = new AudioTagPayloadReader(
extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO));
}
if (hasVideo && videoReader == null) {
videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO));
videoReader = new VideoTagPayloadReader(
extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
}
if (metadataReader == null) {
metadataReader = new ScriptTagPayloadReader(null);

View file

@ -673,6 +673,9 @@ public final class MatroskaExtractor implements Extractor {
case 3:
currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;
break;
case 15:
currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH;
break;
default:
break;
}
@ -1462,6 +1465,7 @@ public final class MatroskaExtractor implements Extractor {
throw new ParserException("Unrecognized codec identifier.");
}
int type;
Format format;
@C.SelectionFlags int selectionFlags = 0;
selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
@ -1469,10 +1473,12 @@ public final class MatroskaExtractor implements Extractor {
// TODO: Consider reading the name elements of the tracks and, if present, incorporating them
// into the trackId passed when creating the formats.
if (MimeTypes.isAudio(mimeType)) {
type = C.TRACK_TYPE_AUDIO;
format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,
initializationData, drmInitData, selectionFlags, language);
} else if (MimeTypes.isVideo(mimeType)) {
type = C.TRACK_TYPE_VIDEO;
if (displayUnit == Track.DISPLAY_UNIT_PIXELS) {
displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth;
displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight;
@ -1485,17 +1491,19 @@ public final class MatroskaExtractor implements Extractor {
Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData,
Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData);
} else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, selectionFlags, language, drmInitData);
} else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
|| MimeTypes.APPLICATION_PGS.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, initializationData, language, drmInitData);
} else {
throw new ParserException("Unexpected MIME type.");
}
this.output = output.track(number);
this.output = output.track(number, type);
this.output.format(format);
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp3;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
@ -33,6 +34,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Extracts data from an MP3 file.
@ -51,6 +54,23 @@ public final class Mp3Extractor implements Extractor {
};
/**
* Flags controlling the behavior of the extractor.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA})
public @interface Flags {}
/**
* Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
* otherwise not be possible.
*/
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
/**
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
* required.
*/
public static final int FLAG_DISABLE_ID3_METADATA = 2;
/**
* The maximum number of bytes to search when synchronizing, before giving up.
*/
@ -72,6 +92,7 @@ public final class Mp3Extractor implements Extractor {
private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
@Flags private final int flags;
private final long forcedFirstSampleTimestampUs;
private final ParsableByteArray scratch;
private final MpegAudioHeader synchronizedHeader;
@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor {
* Constructs a new {@link Mp3Extractor}.
*/
public Mp3Extractor() {
this(C.TIME_UNSET);
this(0);
}
/**
* Constructs a new {@link Mp3Extractor}.
*
* @param flags Flags that control the extractor's behavior.
*/
public Mp3Extractor(@Flags int flags) {
this(flags, C.TIME_UNSET);
}
/**
* Constructs a new {@link Mp3Extractor}.
*
* @param flags Flags that control the extractor's behavior.
* @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or
* {@link C#TIME_UNSET} if forcing is not required.
*/
public Mp3Extractor(long forcedFirstSampleTimestampUs) {
public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) {
this.flags = flags;
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
scratch = new ParsableByteArray(SCRATCH_LENGTH);
synchronizedHeader = new MpegAudioHeader();
@ -118,7 +150,7 @@ public final class Mp3Extractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = extractorOutput.track(0);
trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
extractorOutput.endTracks();
}
@ -151,7 +183,8 @@ public final class Mp3Extractor implements Extractor {
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata));
gaplessInfoHolder.encoderPadding, null, null, 0, null,
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
}
return readSample(input);
}
@ -284,7 +317,11 @@ public final class Mp3Extractor implements Extractor {
byte[] id3Data = new byte[tagLength];
System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
metadata = new Id3Decoder().decode(id3Data, tagLength);
// We need to parse enough ID3 metadata to retrieve any gapless playback information even
// if ID3 metadata parsing is disabled.
Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0
? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata);
}
@ -350,7 +387,8 @@ public final class Mp3Extractor implements Extractor {
}
}
if (seeker == null) {
if (seeker == null || (!seeker.isSeekable()
&& (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
// Repopulate the synchronized header in case we had to skip an invalid seeking header, which
// would give an invalid CBR bitrate.
input.resetPeekPosition();

View file

@ -332,6 +332,9 @@ import java.util.List;
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
}
// Omit any sample at the end point of an edit for audio tracks.
boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;
// Count the number of samples after applying edits.
int editedSampleCount = 0;
int nextSampleIndex = 0;
@ -342,7 +345,8 @@ import java.util.List;
long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale,
track.movieTimescale);
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false);
int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample,
false);
editedSampleCount += endIndex - startIndex;
copyMetadata |= nextSampleIndex != startIndex;
nextSampleIndex = endIndex;
@ -365,7 +369,7 @@ import java.util.List;
long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale,
track.movieTimescale);
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false);
int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false);
if (copyMetadata) {
int count = endIndex - startIndex;
System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
@ -716,6 +720,9 @@ import java.util.List;
case 2:
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
break;
case 3:
stereoMode = C.STEREO_MODE_STEREO_MESH;
break;
default:
break;
}

View file

@ -106,7 +106,6 @@ public final class FragmentedMp4Extractor implements Extractor {
private static final String TAG = "FragmentedMp4Extractor";
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
@ -118,8 +117,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static final int STATE_READING_SAMPLE_CONTINUE = 4;
// Workarounds.
@Flags
private final int flags;
@Flags private final int flags;
private final Track sideloadedTrack;
// Track-linked data bundle, accessible as a whole through trackID.
@ -127,8 +125,8 @@ public final class FragmentedMp4Extractor implements Extractor {
// Temporary arrays.
private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalLength;
private final ParsableByteArray nalPayload;
private final ParsableByteArray nalPrefix;
private final ParsableByteArray nalBuffer;
private final ParsableByteArray encryptionSignalByte;
// Adjusts sample timestamps.
@ -154,17 +152,25 @@ public final class FragmentedMp4Extractor implements Extractor {
private int sampleSize;
private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining;
private boolean processSeiNalUnitPayload;
// Extractor output.
private ExtractorOutput extractorOutput;
private TrackOutput eventMessageTrackOutput;
private TrackOutput cea608TrackOutput;
private TrackOutput[] cea608TrackOutputs;
// Whether extractorOutput.seekMap has been called.
private boolean haveOutputSeekMap;
public FragmentedMp4Extractor() {
this(0, null);
this(0);
}
/**
* @param flags Flags that control the extractor's behavior.
*/
public FragmentedMp4Extractor(@Flags int flags) {
this(flags, null);
}
/**
@ -172,24 +178,24 @@ public final class FragmentedMp4Extractor implements Extractor {
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
*/
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) {
this(flags, null, timestampAdjuster);
this(flags, timestampAdjuster, null);
}
/**
* @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 timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
*/
public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack,
TimestampAdjuster timestampAdjuster) {
this.sideloadedTrack = sideloadedTrack;
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
Track sideloadedTrack) {
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
this.timestampAdjuster = timestampAdjuster;
this.sideloadedTrack = sideloadedTrack;
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
nalPayload = new ParsableByteArray(1);
nalPrefix = new ParsableByteArray(5);
nalBuffer = new ParsableByteArray();
encryptionSignalByte = new ParsableByteArray(1);
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<>();
@ -209,7 +215,7 @@ public final class FragmentedMp4Extractor implements Extractor {
public void init(ExtractorOutput output) {
extractorOutput = output;
if (sideloadedTrack != null) {
TrackBundle bundle = new TrackBundle(output.track(0));
TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type));
bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
trackBundles.put(0, bundle);
maybeInitExtraTracks();
@ -420,7 +426,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// We need to create the track bundles.
for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i);
TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i));
TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type));
trackBundle.init(track, defaultSampleValuesArray.get(track.id));
trackBundles.put(track.id, trackBundle);
durationUs = Math.max(durationUs, track.durationUs);
@ -449,14 +455,16 @@ public final class FragmentedMp4Extractor implements Extractor {
private void maybeInitExtraTracks() {
if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) {
eventMessageTrackOutput = extractorOutput.track(trackBundles.size());
eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA);
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
Format.OFFSET_SAMPLE_RELATIVE));
}
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) {
cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1);
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,
null, Format.NO_VALUE, 0, null, null));
cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput};
}
}
@ -1059,49 +1067,49 @@ public final class FragmentedMp4Extractor implements Extractor {
if (track.nalUnitLengthFieldLength != 0) {
// Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
// they're only 1 or 2 bytes long.
byte[] nalLengthData = nalLength.data;
nalLengthData[0] = 0;
nalLengthData[1] = 0;
nalLengthData[2] = 0;
int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength;
byte[] nalPrefixData = nalPrefix.data;
nalPrefixData[0] = 0;
nalPrefixData[1] = 0;
nalPrefixData[2] = 0;
int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1;
int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;
// NAL units are length delimited, but the decoder requires start code delimited units.
// Loop until we've written the sample to the track output, replacing length delimiters with
// start codes as we encounter them.
while (sampleBytesWritten < sampleSize) {
if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next one.
input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
nalLength.setPosition(0);
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
// Read the NAL length so that we know where we find the next one, and its type.
input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength);
nalPrefix.setPosition(0);
sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1;
// Write a start code for the current NAL unit.
nalStartCode.setPosition(0);
output.sampleData(nalStartCode, 4);
sampleBytesWritten += 4;
// Write the NAL unit type byte.
output.sampleData(nalPrefix, 1);
processSeiNalUnitPayload = cea608TrackOutputs != null
&& NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
sampleBytesWritten += 5;
sampleSize += nalUnitLengthFieldLengthDiff;
if (cea608TrackOutput != null) {
byte[] nalPayloadData = nalPayload.data;
// Peek the NAL unit type byte.
input.peekFully(nalPayloadData, 0, 1);
if ((nalPayloadData[0] & 0x1F) == NAL_UNIT_TYPE_SEI) {
// Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte.
nalPayload.reset(sampleCurrentNalBytesRemaining);
input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining);
// Write the SEI unit straight to the output.
output.sampleData(nalPayload, sampleCurrentNalBytesRemaining);
sampleBytesWritten += sampleCurrentNalBytesRemaining;
sampleCurrentNalBytesRemaining = 0;
// Unescape and process the SEI unit.
int unescapedLength = NalUnitUtil.unescapeStream(nalPayloadData, nalPayload.limit());
nalPayload.setPosition(1); // Skip the NAL unit type byte.
nalPayload.setLimit(unescapedLength);
CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalPayload,
cea608TrackOutput);
}
}
} else {
// Write the payload of the NAL unit.
int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
int writtenBytes;
if (processSeiNalUnitPayload) {
// Read and write the payload of the SEI NAL unit.
nalBuffer.reset(sampleCurrentNalBytesRemaining);
input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining);
output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining);
writtenBytes = sampleCurrentNalBytesRemaining;
// Unescape and process the SEI NAL unit.
int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit());
// If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte.
nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0);
nalBuffer.setLimit(unescapedLength);
CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer,
cea608TrackOutputs);
} else {
// Write the payload of the NAL unit.
writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
}
sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes;
}

View file

@ -83,8 +83,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private final ParsableByteArray atomHeader;
private final Stack<ContainerAtom> containerAtoms;
@State
private int parserState;
@State private int parserState;
private int atomType;
private long atomSize;
private int atomHeaderBytesRead;
@ -344,7 +343,8 @@ public final class Mp4Extractor implements Extractor, SeekMap {
continue;
}
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i));
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable,
extractorOutput.track(i, track.type));
// Each sample has up to three bytes of overhead for the start code that replaces its length.
// Allow ten source samples per output sample, like the platform extractor.
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;

View file

@ -75,8 +75,7 @@ public final class Track {
* One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each
* sample.
*/
@Transformation
public final int sampleTransformation;
@Transformation public final int sampleTransformation;
/**
* Track encryption boxes for the different track sample descriptions. Entries may be null.
@ -94,7 +93,7 @@ public final class Track {
public final long[] editListMediaTimes;
/**
* For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. -1 for
* For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for
* other track types.
*/
public final int nalUnitLengthFieldLength;

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
@ -75,7 +76,7 @@ public class OggExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
TrackOutput trackOutput = output.track(0);
TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
output.endTracks();
// TODO: fix the case if sniff() isn't called
streamReader.init(output, trackOutput);

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