Merge branch 'dev-v2' of https://github.com/google/ExoPlayer into dev-v2
@ -1,9 +1,69 @@
|
||||
# Release notes #
|
||||
|
||||
### r2.1.1 ###
|
||||
### r2.2.0 ###
|
||||
|
||||
Bugfix release only. Users of r2.1.0 and r2.0.x should proactively update to
|
||||
this version.
|
||||
* Demo app: Automatic recovery from BehindLiveWindowException, plus improved
|
||||
handling of pausing and resuming live streams
|
||||
([#2344](https://github.com/google/ExoPlayer/issues/2344)).
|
||||
* AndroidTV: Added Support for tunneled video playback
|
||||
([#1688](https://github.com/google/ExoPlayer/issues/1688)).
|
||||
* DRM: Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and
|
||||
added support for using offline licenses
|
||||
([#876](https://github.com/google/ExoPlayer/issues/876)).
|
||||
* DRM: Introduce OfflineLicenseHelper to help with offline license acquisition,
|
||||
renewal and release.
|
||||
* UI: Updated player control assets. Added vector drawables for use on API level
|
||||
21 and above.
|
||||
* UI: Made player control seek bar work correctly with key events if focusable
|
||||
([#2278](https://github.com/google/ExoPlayer/issues/2278)).
|
||||
* HLS: Improved support for streams that use EXT-X-DISCONTINUITY without
|
||||
EXT-X-DISCONTINUITY-SEQUENCE
|
||||
([#1789](https://github.com/google/ExoPlayer/issues/1789)).
|
||||
* HLS: Support for EXT-X-START tag
|
||||
([#1544](https://github.com/google/ExoPlayer/issues/1544)).
|
||||
* HLS: Check #EXTM3U header is present when parsing the playlist. Fail
|
||||
gracefully if not ([#2301](https://github.com/google/ExoPlayer/issues/2301)).
|
||||
* HLS: Fix memory leak
|
||||
([#2319](https://github.com/google/ExoPlayer/issues/2319)).
|
||||
* HLS: Fix non-seamless first adaptation where master playlist omits resolution
|
||||
tags ([#2096](https://github.com/google/ExoPlayer/issues/2096)).
|
||||
* HLS: Fix handling of WebVTT subtitle renditions with non-standard segment file
|
||||
extensions ([#2025](https://github.com/google/ExoPlayer/issues/2025) and
|
||||
[#2355](https://github.com/google/ExoPlayer/issues/2355)).
|
||||
* HLS: Better handle inconsistent HLS playlist update
|
||||
([#2249](https://github.com/google/ExoPlayer/issues/2249)).
|
||||
* DASH: Don't overflow when dealing with large segment numbers
|
||||
([#2311](https://github.com/google/ExoPlayer/issues/2311)).
|
||||
* DASH: Fix propagation of language from the manifest
|
||||
([#2335](https://github.com/google/ExoPlayer/issues/2335)).
|
||||
* SmoothStreaming: Work around "Offset to sample data was negative" failures
|
||||
([#2292](https://github.com/google/ExoPlayer/issues/2292),
|
||||
[#2101](https://github.com/google/ExoPlayer/issues/2101) and
|
||||
[#1152](https://github.com/google/ExoPlayer/issues/1152)).
|
||||
* MP3/ID3: Added support for parsing Chapter and URL link frames
|
||||
([#2316](https://github.com/google/ExoPlayer/issues/2316)).
|
||||
* MP3/ID3: Handle ID3 frames that end with empty text field
|
||||
([#2309](https://github.com/google/ExoPlayer/issues/2309)).
|
||||
* Added ClippingMediaSource for playing clipped portions of media
|
||||
([#1988](https://github.com/google/ExoPlayer/issues/1988)).
|
||||
* Added convenience methods to query whether the current window is dynamic and
|
||||
seekable ([#2320](https://github.com/google/ExoPlayer/issues/2320)).
|
||||
* Support setting of default headers on HttpDataSource.Factory implementations
|
||||
([#2166](https://github.com/google/ExoPlayer/issues/2166)).
|
||||
* Fixed cache failures when using an encrypted cache content index.
|
||||
* Fix visual artifacts when switching output surface
|
||||
([#2093](https://github.com/google/ExoPlayer/issues/2093)).
|
||||
* Fix gradle + proguard configurations.
|
||||
* Fix player position when replacing the MediaSource
|
||||
([#2369](https://github.com/google/ExoPlayer/issues/2369)).
|
||||
* Misc bug fixes, including
|
||||
[#2330](https://github.com/google/ExoPlayer/issues/2330),
|
||||
[#2269](https://github.com/google/ExoPlayer/issues/2269),
|
||||
[#2252](https://github.com/google/ExoPlayer/issues/2252),
|
||||
[#2264](https://github.com/google/ExoPlayer/issues/2264) and
|
||||
[#2290](https://github.com/google/ExoPlayer/issues/2290).
|
||||
|
||||
### r2.1.1 ###
|
||||
|
||||
* Fix some subtitle types (e.g. WebVTT) being displayed out of sync
|
||||
([#2208](https://github.com/google/ExoPlayer/issues/2208)).
|
||||
@ -52,9 +112,9 @@ this version.
|
||||
* Improved flexibility of SimpleExoPlayer
|
||||
([#2102](https://github.com/google/ExoPlayer/issues/2102)).
|
||||
* Fix issue where only the audio of a video would play due to capability
|
||||
detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007))
|
||||
([#2034](https://github.com/google/ExoPlayer/issues/2034))
|
||||
([#2157](https://github.com/google/ExoPlayer/issues/2157)).
|
||||
detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007),
|
||||
[#2034](https://github.com/google/ExoPlayer/issues/2034) and
|
||||
[#2157](https://github.com/google/ExoPlayer/issues/2157)).
|
||||
* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck
|
||||
buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)).
|
||||
* Correctly set SimpleExoPlayerView surface aspect ratio when an active player
|
||||
@ -186,6 +246,14 @@ 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.14 ###
|
||||
|
||||
* Fixed cache failures when using an encrypted cache content index.
|
||||
* SmoothStreaming: Work around "Offset to sample data was negative" failures
|
||||
([#2292](https://github.com/google/ExoPlayer/issues/2292),
|
||||
[#2101](https://github.com/google/ExoPlayer/issues/2101) and
|
||||
[#1152](https://github.com/google/ExoPlayer/issues/1152)).
|
||||
|
||||
### r1.5.13 ###
|
||||
|
||||
* Improvements to the upstream cache package.
|
||||
|
@ -29,13 +29,13 @@ allprojects {
|
||||
jcenter()
|
||||
}
|
||||
project.ext {
|
||||
compileSdkVersion=24
|
||||
targetSdkVersion=24
|
||||
buildToolsVersion='23.0.3'
|
||||
compileSdkVersion=25
|
||||
targetSdkVersion=25
|
||||
buildToolsVersion='25'
|
||||
releaseRepoName = 'exoplayer'
|
||||
releaseUserOrg = 'google'
|
||||
releaseGroupId = 'com.google.android.exoplayer'
|
||||
releaseVersion = 'r2.1.1'
|
||||
releaseVersion = 'r2.2.0'
|
||||
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,8 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2101"
|
||||
android:versionName="2.1.1">
|
||||
android:versionCode="2200"
|
||||
android:versionName="2.2.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<application
|
||||
android:label="@string/application_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@drawable/ic_banner"
|
||||
android:largeHeap="true"
|
||||
android:allowBackup="false"
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
@ -239,19 +237,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
|
||||
@ -317,8 +305,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
}
|
||||
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
|
||||
: new ConcatenatingMediaSource(mediaSources);
|
||||
player.seekTo(resumeWindow, resumePosition);
|
||||
player.prepare(mediaSource, false, false);
|
||||
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
|
||||
if (haveResumePosition) {
|
||||
player.seekTo(resumeWindow, resumePosition);
|
||||
}
|
||||
player.prepare(mediaSource, !haveResumePosition, false);
|
||||
playerNeedsSource = false;
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
@ -346,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);
|
||||
}
|
||||
@ -377,7 +374,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
}
|
||||
|
||||
private void clearResumePosition() {
|
||||
resumeWindow = 0;
|
||||
resumeWindow = C.INDEX_UNSET;
|
||||
resumePosition = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
@ -422,7 +419,12 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
// Do nothing.
|
||||
if (playerNeedsSource) {
|
||||
// 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.
|
||||
updateResumePosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -461,11 +463,12 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
playerNeedsSource = true;
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearResumePosition();
|
||||
initializePlayer();
|
||||
} else {
|
||||
updateResumePosition();
|
||||
updateButtonVisibilities();
|
||||
showControls();
|
||||
}
|
||||
updateButtonVisibilities();
|
||||
showControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 9.6 KiB |
BIN
demo/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
demo/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
demo/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -16,8 +16,7 @@
|
||||
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<!-- The user visible name of the application. [CHAR LIMIT=20] -->
|
||||
<string name="application_name">ExoPlayer2 Demo</string>
|
||||
<string name="application_name">ExoPlayer</string>
|
||||
|
||||
<string name="video">Video</string>
|
||||
|
||||
|
@ -63,6 +63,7 @@ git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \
|
||||
--enable-decoder=vorbis \
|
||||
--enable-decoder=opus \
|
||||
--enable-decoder=flac \
|
||||
--enable-decoder=alac \
|
||||
&& \
|
||||
make -j4 && \
|
||||
make install-libs
|
||||
|
@ -19,8 +19,8 @@ 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.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.BufferProcessor;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
@ -43,21 +43,12 @@ 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 bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers
|
||||
* before they are output.
|
||||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioCapabilities audioCapabilities) {
|
||||
super(eventHandler, eventListener, audioCapabilities);
|
||||
BufferProcessor... bufferProcessors) {
|
||||
super(eventHandler, eventListener, bufferProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
@ -88,6 +89,13 @@ import java.util.List;
|
||||
if (!hasOutputFormat) {
|
||||
channelCount = ffmpegGetChannelCount(nativeContext);
|
||||
sampleRate = ffmpegGetSampleRate(nativeContext);
|
||||
if (sampleRate == 0 && "alac".equals(codecName)) {
|
||||
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
|
||||
// See https://trac.ffmpeg.org/ticket/6096
|
||||
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
|
||||
parsableExtraData.setPosition(extraData.length - 4);
|
||||
sampleRate = parsableExtraData.readUnsignedIntToInt();
|
||||
}
|
||||
hasOutputFormat = true;
|
||||
}
|
||||
outputBuffer.data.position(0);
|
||||
@ -123,6 +131,7 @@ import java.util.List;
|
||||
private static byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.AUDIO_AAC:
|
||||
case MimeTypes.AUDIO_ALAC:
|
||||
case MimeTypes.AUDIO_OPUS:
|
||||
return initializationData.get(0);
|
||||
case MimeTypes.AUDIO_VORBIS:
|
||||
|
@ -92,6 +92,8 @@ public final class FfmpegLibrary {
|
||||
return "amrwb";
|
||||
case MimeTypes.AUDIO_FLAC:
|
||||
return "flac";
|
||||
case MimeTypes.AUDIO_ALAC:
|
||||
return "alac";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -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 {
|
||||
*;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -17,8 +17,8 @@ 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.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.BufferProcessor;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
@ -38,21 +38,12 @@ 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 bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers
|
||||
* before they are output.
|
||||
*/
|
||||
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioCapabilities audioCapabilities) {
|
||||
super(eventHandler, eventListener, audioCapabilities);
|
||||
BufferProcessor... bufferProcessors) {
|
||||
super(eventHandler, eventListener, bufferProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ 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.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.BufferProcessor;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
@ -40,35 +40,26 @@ 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 bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers
|
||||
* before they are output.
|
||||
*/
|
||||
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
|
||||
super(eventHandler, eventListener);
|
||||
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
BufferProcessor... bufferProcessors) {
|
||||
super(eventHandler, eventListener, bufferProcessors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio
|
||||
* buffers before they are 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,
|
||||
BufferProcessor... bufferProcessors) {
|
||||
super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,
|
||||
bufferProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
BIN
library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4
Normal 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
|
@ -6,7 +6,7 @@ numberOfTracks = 1
|
||||
track 0:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
id = 0
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/ac3
|
||||
maxInputSize = -1
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
@ -213,11 +214,15 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
private static AdaptationSet newAdaptationSets(Representation... representations) {
|
||||
return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations));
|
||||
return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -16,9 +16,9 @@
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -17,11 +17,11 @@ 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;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
@ -30,6 +30,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Random;
|
||||
|
||||
@ -92,7 +93,7 @@ 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);
|
||||
}
|
||||
|
||||
@ -178,8 +179,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));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.metadata.scte35;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Test for {@link SpliceInfoDecoder}.
|
||||
*/
|
||||
public final class SpliceInfoDecoderTest extends TestCase {
|
||||
|
||||
private SpliceInfoDecoder decoder;
|
||||
private MetadataInputBuffer inputBuffer;
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
decoder = new SpliceInfoDecoder();
|
||||
inputBuffer = new MetadataInputBuffer();
|
||||
}
|
||||
|
||||
public void testWrappedAroundTimeSignalCommand() throws MetadataDecoderException {
|
||||
byte[] rawTimeSignalSection = new byte[] {
|
||||
0, // table_id.
|
||||
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
|
||||
0x14, // section_length(8).
|
||||
0x00, // protocol_version.
|
||||
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
|
||||
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
|
||||
0x00, // cw_index.
|
||||
0x00, // tier(8).
|
||||
0x00, // tier(4), splice_command_length(4).
|
||||
0x05, // splice_command_length(8).
|
||||
0x06, // splice_command_type = time_signal.
|
||||
// Start of splice_time().
|
||||
(byte) 0x80, // time_specified_flag, reserved, pts_time(1).
|
||||
0x52, 0x03, 0x02, (byte) 0x8f, // pts_time(32). PTS for a second after playback position.
|
||||
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
|
||||
|
||||
// The playback position is 57:15:58.43 approximately.
|
||||
// With this offset, the playback position pts before wrapping is 0x451ebf851.
|
||||
Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L);
|
||||
assertEquals(1, metadata.length());
|
||||
assertEquals(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs),
|
||||
((TimeSignalCommand) metadata.get(0)).playbackPositionUs);
|
||||
}
|
||||
|
||||
public void test2SpliceInsertCommands() throws MetadataDecoderException {
|
||||
byte[] rawSpliceInsertCommand1 = new byte[] {
|
||||
0, // table_id.
|
||||
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
|
||||
0x19, // section_length(8).
|
||||
0x00, // protocol_version.
|
||||
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
|
||||
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
|
||||
0x00, // cw_index.
|
||||
0x00, // tier(8).
|
||||
0x00, // tier(4), splice_command_length(4).
|
||||
0x0e, // splice_command_length(8).
|
||||
0x05, // splice_command_type = splice_insert.
|
||||
// Start of splice_insert().
|
||||
0x00, 0x00, 0x00, 0x42, // splice_event_id.
|
||||
0x00, // splice_event_cancel_indicator, reserved.
|
||||
0x40, // out_of_network_indicator, program_splice_flag, duration_flag,
|
||||
// splice_immediate_flag, reserved.
|
||||
// start of splice_time().
|
||||
(byte) 0x80, // time_specified_flag, reserved, pts_time(1).
|
||||
0x00, 0x00, 0x00, 0x00, // PTS for playback position 3s.
|
||||
0x00, 0x10, // unique_program_id.
|
||||
0x01, // avail_num.
|
||||
0x02, // avails_expected.
|
||||
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
|
||||
|
||||
Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000);
|
||||
assertEquals(1, metadata.length());
|
||||
SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0);
|
||||
assertEquals(66, command.spliceEventId);
|
||||
assertFalse(command.spliceEventCancelIndicator);
|
||||
assertFalse(command.outOfNetworkIndicator);
|
||||
assertTrue(command.programSpliceFlag);
|
||||
assertFalse(command.spliceImmediateFlag);
|
||||
assertEquals(3000000, command.programSplicePlaybackPositionUs);
|
||||
assertEquals(C.TIME_UNSET, command.breakDuration);
|
||||
assertEquals(16, command.uniqueProgramId);
|
||||
assertEquals(1, command.availNum);
|
||||
assertEquals(2, command.availsExpected);
|
||||
|
||||
byte[] rawSpliceInsertCommand2 = new byte[] {
|
||||
0, // table_id.
|
||||
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
|
||||
0x22, // section_length(8).
|
||||
0x00, // protocol_version.
|
||||
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
|
||||
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
|
||||
0x00, // cw_index.
|
||||
0x00, // tier(8).
|
||||
0x00, // tier(4), splice_command_length(4).
|
||||
0x13, // splice_command_length(8).
|
||||
0x05, // splice_command_type = splice_insert.
|
||||
// Start of splice_insert().
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // splice_event_id.
|
||||
0x00, // splice_event_cancel_indicator, reserved.
|
||||
0x00, // out_of_network_indicator, program_splice_flag, duration_flag,
|
||||
// splice_immediate_flag, reserved.
|
||||
0x02, // component_count.
|
||||
0x10, // component_tag.
|
||||
// start of splice_time().
|
||||
(byte) 0x81, // time_specified_flag, reserved, pts_time(1).
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // PTS for playback position 10s.
|
||||
// start of splice_time().
|
||||
0x11, // component_tag.
|
||||
0x00, // time_specified_flag, reserved.
|
||||
0x00, 0x20, // unique_program_id.
|
||||
0x01, // avail_num.
|
||||
0x02, // avails_expected.
|
||||
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
|
||||
|
||||
// By changing the subsample offset we force adjuster reconstruction.
|
||||
long subsampleOffset = 1000011;
|
||||
metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset);
|
||||
assertEquals(1, metadata.length());
|
||||
command = (SpliceInsertCommand) metadata.get(0);
|
||||
assertEquals(0xffffffffL, command.spliceEventId);
|
||||
assertFalse(command.spliceEventCancelIndicator);
|
||||
assertFalse(command.outOfNetworkIndicator);
|
||||
assertFalse(command.programSpliceFlag);
|
||||
assertFalse(command.spliceImmediateFlag);
|
||||
assertEquals(C.TIME_UNSET, command.programSplicePlaybackPositionUs);
|
||||
assertEquals(C.TIME_UNSET, command.breakDuration);
|
||||
List<SpliceInsertCommand.ComponentSplice> componentSplices = command.componentSpliceList;
|
||||
assertEquals(2, componentSplices.size());
|
||||
assertEquals(16, componentSplices.get(0).componentTag);
|
||||
assertEquals(1000000, componentSplices.get(0).componentSplicePlaybackPositionUs);
|
||||
assertEquals(17, componentSplices.get(1).componentTag);
|
||||
assertEquals(C.TIME_UNSET, componentSplices.get(1).componentSplicePts);
|
||||
assertEquals(32, command.uniqueProgramId);
|
||||
assertEquals(1, command.availNum);
|
||||
assertEquals(2, command.availsExpected);
|
||||
}
|
||||
|
||||
private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset)
|
||||
throws MetadataDecoderException{
|
||||
inputBuffer.clear();
|
||||
inputBuffer.data = ByteBuffer.allocate(data.length).put(data);
|
||||
inputBuffer.timeUs = timeUs;
|
||||
inputBuffer.subsampleOffsetUs = subsampleOffset;
|
||||
return decoder.decode(inputBuffer);
|
||||
}
|
||||
|
||||
private static long removePtsConversionPrecisionError(long timeUs, long offsetUs) {
|
||||
return TimestampAdjuster.ptsToUs(TimestampAdjuster.usToPts(timeUs - offsetUs)) + offsetUs;
|
||||
}
|
||||
|
||||
}
|
@ -20,6 +20,8 @@ import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DashManifestParser}.
|
||||
@ -70,34 +72,57 @@ public class DashManifestParserTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
public void testParseCea608AccessibilityChannel() {
|
||||
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel("CC1=eng"));
|
||||
assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel("CC2=eng"));
|
||||
assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel("CC3=eng"));
|
||||
assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel("CC4=eng"));
|
||||
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC1=eng")));
|
||||
assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC2=eng")));
|
||||
assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC3=eng")));
|
||||
assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC4=eng")));
|
||||
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(null));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(""));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC0=eng"));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC5=eng"));
|
||||
assertEquals(Format.NO_VALUE,
|
||||
DashManifestParser.parseCea608AccessibilityChannel("Wrong format"));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors(null)));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC0=eng")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC5=eng")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("Wrong format")));
|
||||
}
|
||||
|
||||
public void testParseCea708AccessibilityChannel() {
|
||||
assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel("1=lang:eng"));
|
||||
assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel("2=lang:eng"));
|
||||
assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel("3=lang:eng"));
|
||||
assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel("62=lang:eng"));
|
||||
assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel("63=lang:eng"));
|
||||
assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("1=lang:eng")));
|
||||
assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("2=lang:eng")));
|
||||
assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("3=lang:eng")));
|
||||
assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("62=lang:eng")));
|
||||
assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("63=lang:eng")));
|
||||
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(null));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(""));
|
||||
assertEquals(Format.NO_VALUE,
|
||||
DashManifestParser.parseCea708AccessibilityChannel("0=lang:eng"));
|
||||
assertEquals(Format.NO_VALUE,
|
||||
DashManifestParser.parseCea708AccessibilityChannel("64=lang:eng"));
|
||||
assertEquals(Format.NO_VALUE,
|
||||
DashManifestParser.parseCea708AccessibilityChannel("Wrong format"));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors(null)));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("0=lang:eng")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("64=lang:eng")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("Wrong format")));
|
||||
}
|
||||
|
||||
private static List<SchemeValuePair> buildCea608AccessibilityDescriptors(String value) {
|
||||
return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-608:2015", value));
|
||||
}
|
||||
|
||||
private static List<SchemeValuePair> buildCea708AccessibilityDescriptors(String value) {
|
||||
return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-708:2015", value));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString = "#EXTM3U\n"
|
||||
+ "#EXT-X-VERSION:3\n"
|
||||
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
||||
+ "#EXT-X-TARGETDURATION:8\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
|
||||
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
|
||||
@ -71,6 +72,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
||||
assertEquals(HlsPlaylist.TYPE_MEDIA, playlist.type);
|
||||
|
||||
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
|
||||
assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType);
|
||||
|
||||
assertEquals(2679, mediaPlaylist.mediaSequence);
|
||||
assertEquals(3, mediaPlaylist.version);
|
||||
|
@ -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 java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
@ -119,9 +119,22 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
C.LENGTH_UNSET, KEY_2)));
|
||||
}
|
||||
|
||||
public void testIgnoreCacheForUnsetLengthRequests() throws Exception {
|
||||
CacheDataSource cacheDataSource = createCacheDataSource(false, true,
|
||||
CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
|
||||
assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET);
|
||||
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);
|
||||
|
||||
@ -171,15 +184,27 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
|
||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||
boolean simulateUnknownLength) {
|
||||
Builder builder = new Builder();
|
||||
return createCacheDataSource(setReadException, simulateUnknownLength,
|
||||
CacheDataSource.FLAG_BLOCK_ON_CACHE);
|
||||
}
|
||||
|
||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
|
||||
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, CacheDataSource.FLAG_BLOCK_ON_CACHE,
|
||||
MAX_CACHE_FILE_SIZE);
|
||||
FakeDataSource upstream =
|
||||
builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build();
|
||||
return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
|
||||
flags, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -515,7 +515,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 +535,11 @@ 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;
|
||||
|
||||
/**
|
||||
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
|
||||
|
@ -1215,7 +1215,7 @@ import java.io.IOException;
|
||||
|
||||
long newLoadingPeriodStartPositionUs;
|
||||
if (loadingPeriodHolder == null) {
|
||||
newLoadingPeriodStartPositionUs = playbackInfo.startPositionUs;
|
||||
newLoadingPeriodStartPositionUs = playbackInfo.positionUs;
|
||||
} else {
|
||||
int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
|
||||
if (newLoadingPeriodIndex
|
||||
|
@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
|
||||
/**
|
||||
* The version of the library, expressed as a string.
|
||||
*/
|
||||
String VERSION = "2.1.1";
|
||||
String VERSION = "2.2.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 = 2001001;
|
||||
int VERSION_INT = 2002000;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||
|
@ -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;
|
||||
@ -447,16 +447,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,
|
||||
@ -681,9 +684,6 @@ public final class Format implements Parcelable {
|
||||
dest.writeParcelable(metadata, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Creator} implementation.
|
||||
*/
|
||||
public static final Creator<Format> CREATOR = new Creator<Format>() {
|
||||
|
||||
@Override
|
||||
|
@ -29,6 +29,7 @@ import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.BufferProcessor;
|
||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
@ -479,8 +480,8 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) {
|
||||
player.prepare(mediaSource, resetPosition, resetTimeline);
|
||||
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
||||
player.prepare(mediaSource, resetPosition, resetState);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -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, buildBufferProcessors(), 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 bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio
|
||||
* buffers before they are 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) {
|
||||
BufferProcessor[] bufferProcessors, ArrayList<Renderer> out) {
|
||||
out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
|
||||
mainHandler, eventListener, AudioCapabilities.getCapabilities(context)));
|
||||
mainHandler, eventListener, AudioCapabilities.getCapabilities(context), bufferProcessors));
|
||||
|
||||
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, BufferProcessor[].class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
|
||||
bufferProcessors);
|
||||
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, BufferProcessor[].class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
|
||||
bufferProcessors);
|
||||
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, BufferProcessor[].class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
|
||||
bufferProcessors);
|
||||
out.add(extensionRendererIndex++, renderer);
|
||||
Log.i(TAG, "Loaded FfmpegAudioRenderer.");
|
||||
} catch (ClassNotFoundException e) {
|
||||
@ -787,6 +793,14 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array of {@link BufferProcessor}s which will process PCM audio buffers before they
|
||||
* are output.
|
||||
*/
|
||||
protected BufferProcessor[] buildBufferProcessors() {
|
||||
return new BufferProcessor[0];
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void removeSurfaceCallbacks() {
|
||||
|
@ -25,7 +25,6 @@ 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;
|
||||
@ -54,7 +53,9 @@ 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 #release()} when the instance is no longer required.
|
||||
* 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
|
||||
* {@link #release()} when the instance is no longer required.
|
||||
*/
|
||||
public final class AudioTrack {
|
||||
|
||||
@ -89,6 +90,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}.
|
||||
*/
|
||||
@ -120,13 +136,15 @@ public final class AudioTrack {
|
||||
public static final class WriteException extends Exception {
|
||||
|
||||
/**
|
||||
* An error value returned from {@link android.media.AudioTrack#write(byte[], int, int)}.
|
||||
* The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or
|
||||
* {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
|
||||
*/
|
||||
public final int errorCode;
|
||||
|
||||
/**
|
||||
* @param errorCode An error value returned from
|
||||
* {@link android.media.AudioTrack#write(byte[], int, int)}.
|
||||
* @param errorCode The error value returned from
|
||||
* {@link android.media.AudioTrack#write(byte[], int, int)} or
|
||||
* {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
|
||||
*/
|
||||
public WriteException(int errorCode) {
|
||||
super("AudioTrack write failed: " + errorCode);
|
||||
@ -212,15 +230,15 @@ public final class AudioTrack {
|
||||
/**
|
||||
* AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
|
||||
* than this amount.
|
||||
*
|
||||
* <p>This is a fail safe that should not be required on correctly functioning devices.
|
||||
* <p>
|
||||
* This is a fail safe that should not be required on correctly functioning devices.
|
||||
*/
|
||||
private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND;
|
||||
|
||||
/**
|
||||
* AudioTrack latencies are deemed impossibly large if they are greater than this amount.
|
||||
*
|
||||
* <p>This is a fail safe that should not be required on correctly functioning devices.
|
||||
* <p>
|
||||
* This is a fail safe that should not be required on correctly functioning devices.
|
||||
*/
|
||||
private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;
|
||||
|
||||
@ -251,6 +269,7 @@ public final class AudioTrack {
|
||||
public static boolean failOnSpuriousAudioTimestamp = false;
|
||||
|
||||
private final AudioCapabilities audioCapabilities;
|
||||
private final BufferProcessor[] bufferProcessors;
|
||||
private final Listener listener;
|
||||
private final ConditionVariable releasingConditionVariable;
|
||||
private final long[] playheadOffsets;
|
||||
@ -264,12 +283,12 @@ 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;
|
||||
@ -295,12 +314,10 @@ 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 ByteBuffer inputBuffer;
|
||||
private ByteBuffer outputBuffer;
|
||||
private byte[] preV21OutputBuffer;
|
||||
private int preV21OutputBufferOffset;
|
||||
|
||||
private boolean playing;
|
||||
private int audioSessionId;
|
||||
@ -309,11 +326,18 @@ 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 bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio
|
||||
* buffers before they are output. May be empty.
|
||||
* @param listener Listener for audio track events.
|
||||
*/
|
||||
public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) {
|
||||
public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors,
|
||||
Listener listener) {
|
||||
this.audioCapabilities = audioCapabilities;
|
||||
this.bufferProcessors = new BufferProcessor[bufferProcessors.length + 1];
|
||||
this.bufferProcessors[0] = new ResamplingBufferProcessor();
|
||||
System.arraycopy(bufferProcessors, 0, this.bufferProcessors, 1, bufferProcessors.length);
|
||||
this.listener = listener;
|
||||
releasingConditionVariable = new ConditionVariable(true);
|
||||
if (Util.SDK_INT >= 18) {
|
||||
@ -386,7 +410,7 @@ public final class AudioTrack {
|
||||
// The AudioTrack has started, but we don't have any samples to compute a smoothed position.
|
||||
currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs;
|
||||
} else {
|
||||
// getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the
|
||||
// getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the
|
||||
// system clock (and a smoothed offset between it and the playhead position) so as to
|
||||
// prevent jitter in the reported positions.
|
||||
currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs;
|
||||
@ -410,9 +434,23 @@ 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 {
|
||||
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
|
||||
@C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding;
|
||||
if (!passthrough) {
|
||||
for (BufferProcessor bufferProcessor : bufferProcessors) {
|
||||
try {
|
||||
bufferProcessor.configure(sampleRate, channelCount, encoding);
|
||||
} catch (BufferProcessor.UnhandledFormatException e) {
|
||||
throw new ConfigurationException(e);
|
||||
}
|
||||
encoding = bufferProcessor.getOutputEncoding();
|
||||
}
|
||||
}
|
||||
|
||||
int channelConfig;
|
||||
switch (channelCount) {
|
||||
case 1:
|
||||
@ -440,7 +478,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.
|
||||
@ -458,25 +496,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 (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;
|
||||
@ -484,28 +510,28 @@ 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;
|
||||
|
||||
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;
|
||||
@ -527,15 +553,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();
|
||||
|
||||
@ -607,8 +633,10 @@ public final class AudioTrack {
|
||||
* @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) {
|
||||
@ -616,26 +644,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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -648,27 +662,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;
|
||||
@ -690,21 +702,35 @@ 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];
|
||||
|
||||
inputBuffer = buffer;
|
||||
if (!passthrough) {
|
||||
for (BufferProcessor bufferProcessor : bufferProcessors) {
|
||||
buffer = bufferProcessor.handleBuffer(buffer);
|
||||
}
|
||||
int originalPosition = buffer.position();
|
||||
buffer.get(temporaryBuffer, 0, bytesRemaining);
|
||||
buffer.position(originalPosition);
|
||||
temporaryBufferOffset = 0;
|
||||
}
|
||||
outputBuffer = buffer;
|
||||
if (Util.SDK_INT < 21) {
|
||||
int bytesRemaining = outputBuffer.remaining();
|
||||
if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) {
|
||||
preV21OutputBuffer = new byte[bytesRemaining];
|
||||
}
|
||||
int originalPosition = outputBuffer.position();
|
||||
outputBuffer.get(preV21OutputBuffer, 0, bytesRemaining);
|
||||
outputBuffer.position(originalPosition);
|
||||
preV21OutputBufferOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
buffer = useResampledBuffer ? resampledBuffer : buffer;
|
||||
int bytesRemaining = buffer.remaining();
|
||||
if (writeOutputBuffer(presentationTimeUs)) {
|
||||
inputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean writeOutputBuffer(long presentationTimeUs) throws WriteException {
|
||||
int bytesRemaining = outputBuffer.remaining();
|
||||
int bytesWritten = 0;
|
||||
if (Util.SDK_INT < 21) { // passthrough == false
|
||||
// Work out how many bytes we can write without the risk of blocking.
|
||||
@ -713,18 +739,21 @@ public final class AudioTrack {
|
||||
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;
|
||||
outputBuffer.position(outputBuffer.position() + bytesWritten);
|
||||
}
|
||||
buffer.position(buffer.position() + bytesWritten);
|
||||
}
|
||||
} else if (tunneling) {
|
||||
bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, outputBuffer, bytesRemaining,
|
||||
presentationTimeUs);
|
||||
} else {
|
||||
bytesWritten = tunneling
|
||||
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
|
||||
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
|
||||
bytesWritten = writeNonBlockingV21(audioTrack, outputBuffer, bytesRemaining);
|
||||
}
|
||||
|
||||
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
|
||||
if (bytesWritten < 0) {
|
||||
throw new WriteException(bytesWritten);
|
||||
}
|
||||
@ -736,7 +765,6 @@ public final class AudioTrack {
|
||||
if (passthrough) {
|
||||
submittedEncodedFrames += framesPerEncodedSample;
|
||||
}
|
||||
currentSourceBuffer = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -812,7 +840,7 @@ public final class AudioTrack {
|
||||
* audio session id has changed. Enabling tunneling requires platform API version 21 onwards.
|
||||
*
|
||||
* @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 < 21.
|
||||
*/
|
||||
public void enableTunnelingV21(int tunnelingAudioSessionId) {
|
||||
Assertions.checkState(Util.SDK_INT >= 21);
|
||||
@ -880,8 +908,11 @@ public final class AudioTrack {
|
||||
submittedPcmBytes = 0;
|
||||
submittedEncodedFrames = 0;
|
||||
framesPerEncodedSample = 0;
|
||||
currentSourceBuffer = null;
|
||||
inputBuffer = null;
|
||||
avSyncHeader = null;
|
||||
for (BufferProcessor bufferProcessor : bufferProcessors) {
|
||||
bufferProcessor.flush();
|
||||
}
|
||||
bytesUntilNextAvSync = 0;
|
||||
startMediaTimeState = START_NOT_SET;
|
||||
latencyUs = 0;
|
||||
@ -915,6 +946,9 @@ public final class AudioTrack {
|
||||
public void release() {
|
||||
reset();
|
||||
releaseKeepSessionIdAudioTrack();
|
||||
for (BufferProcessor bufferProcessor : bufferProcessors) {
|
||||
bufferProcessor.release();
|
||||
}
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
playing = false;
|
||||
}
|
||||
@ -1089,7 +1123,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1124,82 +1158,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) {
|
||||
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Interface for processors of audio buffers.
|
||||
*/
|
||||
public interface BufferProcessor {
|
||||
|
||||
/**
|
||||
* Exception thrown when a processor can't be configured for a given input 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures this processor to take input buffers with the specified 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.
|
||||
* @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
|
||||
*/
|
||||
void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
|
||||
throws UnhandledFormatException;
|
||||
|
||||
/**
|
||||
* Returns the encoding used in buffers output by this processor.
|
||||
*/
|
||||
@C.Encoding
|
||||
int getOutputEncoding();
|
||||
|
||||
/**
|
||||
* Processes the data in the specified input buffer in its entirety.
|
||||
*
|
||||
* @param input A buffer containing the input data to process.
|
||||
* @return A buffer containing the processed output. This may be the same as the input buffer if
|
||||
* no processing was required.
|
||||
*/
|
||||
ByteBuffer handleBuffer(ByteBuffer input);
|
||||
|
||||
/**
|
||||
* Clears any state in preparation for receiving a new stream of buffers.
|
||||
*/
|
||||
void flush();
|
||||
|
||||
/**
|
||||
* Releases any resources associated with this instance.
|
||||
*/
|
||||
void release();
|
||||
|
||||
}
|
@ -121,13 +121,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 bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers
|
||||
* before they are output.
|
||||
*/
|
||||
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
|
||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, Handler eventHandler,
|
||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
|
||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
|
||||
BufferProcessor... bufferProcessors) {
|
||||
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
|
||||
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
|
||||
audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener());
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
}
|
||||
|
||||
@ -183,7 +186,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) {
|
||||
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
|
||||
MediaCrypto crypto) {
|
||||
if (passthroughEnabled) {
|
||||
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
|
||||
passthroughMediaFormat = format.getFrameworkMediaFormatV16();
|
||||
@ -218,14 +222,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
}
|
||||
|
||||
@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);
|
||||
try {
|
||||
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0);
|
||||
} catch (AudioTrack.ConfigurationException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A {@link BufferProcessor} that outputs buffers in {@link C#ENCODING_PCM_16BIT}.
|
||||
*/
|
||||
/* package */ final class ResamplingBufferProcessor implements BufferProcessor {
|
||||
|
||||
@C.PcmEncoding
|
||||
private int encoding;
|
||||
private ByteBuffer outputBuffer;
|
||||
|
||||
public ResamplingBufferProcessor() {
|
||||
encoding = C.ENCODING_INVALID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void 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 (encoding == C.ENCODING_PCM_16BIT) {
|
||||
outputBuffer = null;
|
||||
}
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputEncoding() {
|
||||
return C.ENCODING_PCM_16BIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer handleBuffer(ByteBuffer buffer) {
|
||||
int position = buffer.position();
|
||||
int limit = buffer.limit();
|
||||
int size = limit - position;
|
||||
|
||||
int resampledSize;
|
||||
switch (encoding) {
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
// No processing required.
|
||||
return buffer;
|
||||
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_INVALID:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
// Never happens.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
if (outputBuffer == null || outputBuffer.capacity() < resampledSize) {
|
||||
outputBuffer = ByteBuffer.allocateDirect(resampledSize).order(buffer.order());
|
||||
} else {
|
||||
outputBuffer.clear();
|
||||
}
|
||||
|
||||
// Samples are little endian.
|
||||
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++) {
|
||||
outputBuffer.put((byte) 0);
|
||||
outputBuffer.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 = position; i < limit; i += 3) {
|
||||
outputBuffer.put(buffer.get(i + 1));
|
||||
outputBuffer.put(buffer.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) {
|
||||
outputBuffer.put(buffer.get(i + 2));
|
||||
outputBuffer.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();
|
||||
}
|
||||
|
||||
outputBuffer.flip();
|
||||
return outputBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
outputBuffer = null;
|
||||
}
|
||||
|
||||
}
|
@ -102,10 +102,12 @@ 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 bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers
|
||||
* before they are output.
|
||||
*/
|
||||
public SimpleDecoderAudioRenderer(Handler eventHandler,
|
||||
AudioRendererEventListener eventListener) {
|
||||
this(eventHandler, eventListener, null);
|
||||
AudioRendererEventListener eventListener, BufferProcessor... bufferProcessors) {
|
||||
this(eventHandler, eventListener, null, null, false, bufferProcessors);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,13 +135,16 @@ 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 bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio
|
||||
* buffers before they are output.
|
||||
*/
|
||||
public SimpleDecoderAudioRenderer(Handler eventHandler,
|
||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
|
||||
BufferProcessor... bufferProcessors) {
|
||||
super(C.TRACK_TYPE_AUDIO);
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
|
||||
audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener());
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
formatHolder = new FormatHolder();
|
||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||
@ -193,8 +198,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 +260,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) {
|
||||
|
@ -280,6 +280,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 +289,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.
|
||||
@ -530,9 +532,8 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
|
||||
}
|
||||
|
||||
private void postKeyRequest(byte[] scope, int keyType) {
|
||||
KeyRequest keyRequest;
|
||||
try {
|
||||
keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
|
||||
KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
|
||||
optionalKeyRequestParameters);
|
||||
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
|
||||
} catch (Exception e) {
|
||||
@ -564,7 +565,8 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
|
||||
}
|
||||
} else {
|
||||
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
|
||||
if (keySetId != null && keySetId.length != 0) {
|
||||
if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null))
|
||||
&& keySetId != null && keySetId.length != 0) {
|
||||
offlineLicenseKeySetId = keySetId;
|
||||
}
|
||||
state = STATE_OPENED_WITH_KEYS;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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()) {
|
||||
|
@ -93,7 +93,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -210,11 +210,14 @@ 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);
|
||||
ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format,
|
||||
adaptationSet.type);
|
||||
InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation,
|
||||
extractorWrapper);
|
||||
if (initializationChunk == null) {
|
||||
return null;
|
||||
}
|
||||
Format sampleFormat = initializationChunk.getSampleFormat();
|
||||
Format sampleFormat = extractorWrapper.getSampleFormat();
|
||||
if (sampleFormat != null) {
|
||||
drmInitData = sampleFormat.drmInitData;
|
||||
}
|
||||
@ -288,8 +291,9 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
|
||||
return session;
|
||||
}
|
||||
|
||||
private static InitializationChunk loadInitializationChunk(final DataSource dataSource,
|
||||
final Representation representation) throws IOException, InterruptedException {
|
||||
private static InitializationChunk loadInitializationChunk(DataSource dataSource,
|
||||
Representation representation, ChunkExtractorWrapper extractorWrapper)
|
||||
throws IOException, InterruptedException {
|
||||
RangedUri rangedUri = representation.getInitializationUri();
|
||||
if (rangedUri == null) {
|
||||
return null;
|
||||
@ -298,18 +302,17 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
|
||||
rangedUri.length, representation.getCacheKey());
|
||||
InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec,
|
||||
representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */,
|
||||
newWrappedExtractor(representation.format));
|
||||
extractorWrapper);
|
||||
initializationChunk.load();
|
||||
return initializationChunk;
|
||||
}
|
||||
|
||||
private static ChunkExtractorWrapper newWrappedExtractor(final Format format) {
|
||||
private static ChunkExtractorWrapper newWrappedExtractor(Format format, int trackType) {
|
||||
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 */);
|
||||
return new ChunkExtractorWrapper(extractor, format, trackType);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
@ -445,23 +447,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 +521,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;
|
||||
|
@ -102,4 +102,5 @@ public interface Extractor {
|
||||
* Releases all kept resources.
|
||||
*/
|
||||
void release();
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -546,11 +546,9 @@ public final class MatroskaExtractor implements Extractor {
|
||||
}
|
||||
break;
|
||||
case ID_TRACK_ENTRY:
|
||||
if (tracks.get(currentTrack.number) == null && isCodecSupported(currentTrack.codecId)) {
|
||||
if (isCodecSupported(currentTrack.codecId)) {
|
||||
currentTrack.initializeOutput(extractorOutput, currentTrack.number);
|
||||
tracks.put(currentTrack.number, currentTrack);
|
||||
} else {
|
||||
// We've seen this track entry before, or the codec is unsupported. Do nothing.
|
||||
}
|
||||
currentTrack = null;
|
||||
break;
|
||||
@ -692,6 +690,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;
|
||||
}
|
||||
@ -1525,6 +1526,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;
|
||||
@ -1532,10 +1534,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;
|
||||
@ -1548,10 +1552,12 @@ 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 if (MimeTypes.TEXT_SSA.equals(mimeType)) {
|
||||
@ -1561,7 +1567,7 @@ public final class MatroskaExtractor implements Extractor {
|
||||
throw new ParserException("Unexpected MIME type.");
|
||||
}
|
||||
|
||||
this.output = output.track(number);
|
||||
this.output = output.track(number, type);
|
||||
this.output.format(format);
|
||||
}
|
||||
|
||||
|
@ -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,18 @@ 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})
|
||||
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;
|
||||
|
||||
/**
|
||||
* The maximum number of bytes to search when synchronizing, before giving up.
|
||||
*/
|
||||
@ -72,6 +87,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 +109,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 +145,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();
|
||||
}
|
||||
|
||||
@ -350,7 +377,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();
|
||||
|
@ -135,6 +135,7 @@ import java.util.List;
|
||||
public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09");
|
||||
public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC");
|
||||
public static final int TYPE_camm = Util.getIntegerCodeForString("camm");
|
||||
public static final int TYPE_alac = Util.getIntegerCodeForString("alac");
|
||||
|
||||
public final int type;
|
||||
|
||||
|
@ -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);
|
||||
@ -604,7 +608,7 @@ import java.util.List;
|
||||
|| childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl
|
||||
|| childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb
|
||||
|| childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt
|
||||
|| childAtomType == Atom.TYPE__mp3) {
|
||||
|| childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) {
|
||||
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
|
||||
language, isQuickTime, drmInitData, out, i);
|
||||
} else if (childAtomType == Atom.TYPE_TTML) {
|
||||
@ -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;
|
||||
}
|
||||
@ -839,6 +846,8 @@ import java.util.List;
|
||||
mimeType = MimeTypes.AUDIO_RAW;
|
||||
} else if (atomType == Atom.TYPE__mp3) {
|
||||
mimeType = MimeTypes.AUDIO_MPEG;
|
||||
} else if (atomType == Atom.TYPE_alac) {
|
||||
mimeType = MimeTypes.AUDIO_ALAC;
|
||||
}
|
||||
|
||||
byte[] initializationData = null;
|
||||
@ -876,6 +885,10 @@ import java.util.List;
|
||||
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,
|
||||
language);
|
||||
} else if (childAtomType == Atom.TYPE_alac) {
|
||||
initializationData = new byte[childAtomSize];
|
||||
parent.setPosition(childPosition);
|
||||
parent.readBytes(initializationData, 0, childAtomSize);
|
||||
}
|
||||
childPosition += childAtomSize;
|
||||
}
|
||||
|
@ -31,14 +31,15 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
|
||||
import com.google.android.exoplayer2.text.cea.CeaUtil;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -67,15 +68,13 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
};
|
||||
|
||||
private static final String TAG = "FragmentedMp4Extractor";
|
||||
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
|
||||
|
||||
/**
|
||||
* Flags controlling the behavior of the extractor.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
|
||||
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED})
|
||||
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK,
|
||||
FLAG_SIDELOADED})
|
||||
public @interface Flags {}
|
||||
/**
|
||||
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
|
||||
@ -94,12 +93,19 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
* messages in the stream will be delivered as samples to this track.
|
||||
*/
|
||||
public static final int FLAG_ENABLE_EMSG_TRACK = 4;
|
||||
/**
|
||||
* Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages
|
||||
* contained within SEI NAL units in the stream will be delivered as samples to this track.
|
||||
*/
|
||||
public static final int FLAG_ENABLE_CEA608_TRACK = 8;
|
||||
/**
|
||||
* Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
|
||||
* container.
|
||||
*/
|
||||
private static final int FLAG_SIDELOADED = 8;
|
||||
private static final int FLAG_SIDELOADED = 16;
|
||||
|
||||
private static final String TAG = "FragmentedMp4Extractor";
|
||||
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
|
||||
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};
|
||||
|
||||
@ -120,7 +126,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
// Temporary arrays.
|
||||
private final ParsableByteArray nalStartCode;
|
||||
private final ParsableByteArray nalLength;
|
||||
private final ParsableByteArray nalPrefix;
|
||||
private final ParsableByteArray nalBuffer;
|
||||
private final ParsableByteArray encryptionSignalByte;
|
||||
|
||||
// Adjusts sample timestamps.
|
||||
@ -146,16 +153,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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,23 +179,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);
|
||||
nalPrefix = new ParsableByteArray(5);
|
||||
nalBuffer = new ParsableByteArray();
|
||||
encryptionSignalByte = new ParsableByteArray(1);
|
||||
extendedTypeScratch = new byte[16];
|
||||
containerAtoms = new Stack<>();
|
||||
@ -199,10 +216,10 @@ 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);
|
||||
maybeInitEventMessageTrack();
|
||||
maybeInitExtraTracks();
|
||||
extractorOutput.endTracks();
|
||||
}
|
||||
}
|
||||
@ -410,19 +427,19 @@ 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);
|
||||
trackBundles.put(track.id, 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);
|
||||
}
|
||||
maybeInitEventMessageTrack();
|
||||
maybeInitExtraTracks();
|
||||
extractorOutput.endTracks();
|
||||
} else {
|
||||
Assertions.checkState(trackBundles.size() == trackCount);
|
||||
}
|
||||
|
||||
// Initialization of tracks and default sample values.
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
Track track = tracks.valueAt(i);
|
||||
trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id));
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
Track track = tracks.valueAt(i);
|
||||
trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -437,13 +454,17 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeInitEventMessageTrack() {
|
||||
if ((flags & FLAG_ENABLE_EMSG_TRACK) == 0) {
|
||||
return;
|
||||
private void maybeInitExtraTracks() {
|
||||
if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) {
|
||||
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, C.TRACK_TYPE_TEXT);
|
||||
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
|
||||
null, Format.NO_VALUE, 0, null, null));
|
||||
}
|
||||
eventMessageTrackOutput = extractorOutput.track(trackBundles.size());
|
||||
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
|
||||
Format.OFFSET_SAMPLE_RELATIVE));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1045,29 +1066,50 @@ 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);
|
||||
// TODO: Don't try and process the SEI NAL unit if the payload is encrypted.
|
||||
processSeiNalUnitPayload = cea608TrackOutput != null
|
||||
&& NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
|
||||
sampleBytesWritten += 5;
|
||||
sampleSize += nalUnitLengthFieldLengthDiff;
|
||||
} 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,
|
||||
cea608TrackOutput);
|
||||
} else {
|
||||
// Write the payload of the NAL unit.
|
||||
writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
|
||||
}
|
||||
sampleBytesWritten += writtenBytes;
|
||||
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||
}
|
||||
|
@ -344,7 +344,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;
|
||||
|
@ -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);
|
||||
|
@ -65,7 +65,7 @@ public final class RawCcExtractor implements Extractor {
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
||||
trackOutput = output.track(0);
|
||||
trackOutput = output.track(0, C.TRACK_TYPE_TEXT);
|
||||
output.endTracks();
|
||||
trackOutput.format(format);
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
private final ParsableByteArray headerScratchBytes;
|
||||
private final String language;
|
||||
|
||||
private String trackFormatId;
|
||||
private TrackOutput output;
|
||||
|
||||
private int state;
|
||||
@ -84,7 +85,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
|
||||
output = extractorOutput.track(generator.getNextId());
|
||||
generator.generateNewId();
|
||||
trackFormatId = generator.getFormatId();
|
||||
output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -180,8 +183,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
headerScratchBits.skipBits(40);
|
||||
isEac3 = headerScratchBits.readBits(5) == 16;
|
||||
headerScratchBits.setPosition(headerScratchBits.getPosition() - 45);
|
||||
format = isEac3 ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, null, language , null)
|
||||
: Ac3Util.parseAc3SyncframeFormat(headerScratchBits, null, language, null);
|
||||
format = isEac3
|
||||
? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, trackFormatId, language , null)
|
||||
: Ac3Util.parseAc3SyncframeFormat(headerScratchBits, trackFormatId, language, null);
|
||||
output.format(format);
|
||||
}
|
||||
sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data)
|
||||
|
@ -61,6 +61,7 @@ import java.util.Collections;
|
||||
private final ParsableByteArray id3HeaderBuffer;
|
||||
private final String language;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
private TrackOutput id3Output;
|
||||
|
||||
@ -108,11 +109,14 @@ import java.util.Collections;
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
idGenerator.generateNewId();
|
||||
formatId = idGenerator.getFormatId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
|
||||
if (exposeId3) {
|
||||
id3Output = extractorOutput.track(idGenerator.getNextId());
|
||||
id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null,
|
||||
Format.NO_VALUE, null));
|
||||
idGenerator.generateNewId();
|
||||
id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
|
||||
id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(),
|
||||
MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null));
|
||||
} else {
|
||||
id3Output = new DummyTrackOutput();
|
||||
}
|
||||
@ -300,7 +304,7 @@ import java.util.Collections;
|
||||
Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
|
||||
audioSpecificConfig);
|
||||
|
||||
Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null,
|
||||
Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,
|
||||
Collections.singletonList(audioSpecificConfig), null, 0, language);
|
||||
// In this class a sample is an access unit, but the MediaFormat sample rate specifies the
|
||||
|
@ -74,10 +74,11 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
||||
case TsExtractor.TS_STREAM_TYPE_H262:
|
||||
return new PesReader(new H262Reader());
|
||||
case TsExtractor.TS_STREAM_TYPE_H264:
|
||||
return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader(
|
||||
new H264Reader(isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS)));
|
||||
return isSet(FLAG_IGNORE_H264_STREAM) ? null
|
||||
: new PesReader(new H264Reader(new SeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES),
|
||||
isSet(FLAG_DETECT_ACCESS_UNITS)));
|
||||
case TsExtractor.TS_STREAM_TYPE_H265:
|
||||
return new PesReader(new H265Reader());
|
||||
return new PesReader(new H265Reader(new SeiReader()));
|
||||
case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO:
|
||||
return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM)
|
||||
? null : new SectionReader(new SpliceInfoSectionReader());
|
||||
|
@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
private final ParsableByteArray headerScratchBytes;
|
||||
private final String language;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
|
||||
private int state;
|
||||
@ -79,7 +80,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
idGenerator.generateNewId();
|
||||
formatId = idGenerator.getFormatId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -165,7 +168,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
private void parseHeader() {
|
||||
byte[] frameData = headerScratchBytes.data;
|
||||
if (format == null) {
|
||||
format = DtsUtil.parseDtsFormat(frameData, null, language, null);
|
||||
format = DtsUtil.parseDtsFormat(frameData, formatId, language, null);
|
||||
output.format(format);
|
||||
}
|
||||
sampleSize = DtsUtil.getDtsFrameSize(frameData);
|
||||
|
@ -37,6 +37,7 @@ import java.util.Collections;
|
||||
private static final int START_EXTENSION = 0xB5;
|
||||
private static final int START_GROUP = 0xB8;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
|
||||
// Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.
|
||||
@ -78,7 +79,9 @@ import java.util.Collections;
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
idGenerator.generateNewId();
|
||||
formatId = idGenerator.getFormatId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -126,7 +129,7 @@ import java.util.Collections;
|
||||
int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0;
|
||||
if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) {
|
||||
// The csd data is complete, so we can decode and output the media format.
|
||||
Pair<Format, Long> result = parseCsdBuffer(csdBuffer);
|
||||
Pair<Format, Long> result = parseCsdBuffer(csdBuffer, formatId);
|
||||
output.format(result.first);
|
||||
frameDurationUs = result.second;
|
||||
hasOutputFormat = true;
|
||||
@ -166,10 +169,11 @@ import java.util.Collections;
|
||||
* Parses the {@link Format} and frame duration from a csd buffer.
|
||||
*
|
||||
* @param csdBuffer The csd buffer.
|
||||
* @param formatId The id for the generated format. May be null.
|
||||
* @return A pair consisting of the {@link Format} and the frame duration in microseconds, or
|
||||
* 0 if the duration could not be determined.
|
||||
*/
|
||||
private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer) {
|
||||
private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) {
|
||||
byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);
|
||||
|
||||
int firstByte = csdData[4] & 0xFF;
|
||||
@ -195,7 +199,7 @@ import java.util.Collections;
|
||||
break;
|
||||
}
|
||||
|
||||
Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_MPEG2, null,
|
||||
Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE,
|
||||
Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null);
|
||||
|
||||
|
@ -39,6 +39,7 @@ import java.util.List;
|
||||
private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
|
||||
private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
|
||||
|
||||
private final SeiReader seiReader;
|
||||
private final boolean allowNonIdrKeyframes;
|
||||
private final boolean detectAccessUnits;
|
||||
private final NalUnitTargetBuffer sps;
|
||||
@ -47,8 +48,8 @@ import java.util.List;
|
||||
private long totalBytesWritten;
|
||||
private final boolean[] prefixFlags;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
private SeiReader seiReader;
|
||||
private SampleReader sampleReader;
|
||||
|
||||
// State that should not be reset on seek.
|
||||
@ -61,15 +62,17 @@ import java.util.List;
|
||||
private final ParsableByteArray seiWrapper;
|
||||
|
||||
/**
|
||||
* @param seiReader An SEI reader for consuming closed caption channels.
|
||||
* @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as
|
||||
* synchronization samples (key-frames).
|
||||
* @param detectAccessUnits Whether to split the input stream into access units (samples) based on
|
||||
* slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs).
|
||||
*/
|
||||
public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
|
||||
prefixFlags = new boolean[3];
|
||||
public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
|
||||
this.seiReader = seiReader;
|
||||
this.allowNonIdrKeyframes = allowNonIdrKeyframes;
|
||||
this.detectAccessUnits = detectAccessUnits;
|
||||
prefixFlags = new boolean[3];
|
||||
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
|
||||
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
|
||||
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
|
||||
@ -88,9 +91,11 @@ import java.util.List;
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
idGenerator.generateNewId();
|
||||
formatId = idGenerator.getFormatId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
|
||||
sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);
|
||||
seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId()));
|
||||
seiReader.createTracks(extractorOutput, idGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -175,7 +180,7 @@ import java.util.List;
|
||||
initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength));
|
||||
NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength);
|
||||
NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength);
|
||||
output.format(Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null,
|
||||
output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE,
|
||||
initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null));
|
||||
hasOutputFormat = true;
|
||||
|
@ -44,9 +44,11 @@ import java.util.Collections;
|
||||
private static final int PREFIX_SEI_NUT = 39;
|
||||
private static final int SUFFIX_SEI_NUT = 40;
|
||||
|
||||
private final SeiReader seiReader;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
private SampleReader sampleReader;
|
||||
private SeiReader seiReader;
|
||||
|
||||
// State that should not be reset on seek.
|
||||
private boolean hasOutputFormat;
|
||||
@ -66,7 +68,11 @@ import java.util.Collections;
|
||||
// Scratch variables to avoid allocations.
|
||||
private final ParsableByteArray seiWrapper;
|
||||
|
||||
public H265Reader() {
|
||||
/**
|
||||
* @param seiReader An SEI reader for consuming closed caption channels.
|
||||
*/
|
||||
public H265Reader(SeiReader seiReader) {
|
||||
this.seiReader = seiReader;
|
||||
prefixFlags = new boolean[3];
|
||||
vps = new NalUnitTargetBuffer(VPS_NUT, 128);
|
||||
sps = new NalUnitTargetBuffer(SPS_NUT, 128);
|
||||
@ -90,9 +96,11 @@ import java.util.Collections;
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
idGenerator.generateNewId();
|
||||
formatId = idGenerator.getFormatId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO);
|
||||
sampleReader = new SampleReader(output);
|
||||
seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId()));
|
||||
seiReader.createTracks(extractorOutput, idGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -183,7 +191,7 @@ import java.util.Collections;
|
||||
sps.endNalUnit(discardPadding);
|
||||
pps.endNalUnit(discardPadding);
|
||||
if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) {
|
||||
output.format(parseMediaFormat(vps, sps, pps));
|
||||
output.format(parseMediaFormat(formatId, vps, sps, pps));
|
||||
hasOutputFormat = true;
|
||||
}
|
||||
}
|
||||
@ -205,8 +213,8 @@ import java.util.Collections;
|
||||
}
|
||||
}
|
||||
|
||||
private static Format parseMediaFormat(NalUnitTargetBuffer vps, NalUnitTargetBuffer sps,
|
||||
NalUnitTargetBuffer pps) {
|
||||
private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps,
|
||||
NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) {
|
||||
// Build codec-specific data.
|
||||
byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength];
|
||||
System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength);
|
||||
@ -311,7 +319,7 @@ import java.util.Collections;
|
||||
}
|
||||
}
|
||||
|
||||
return Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H265, null, Format.NO_VALUE,
|
||||
return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE,
|
||||
Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE,
|
||||
Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null);
|
||||
}
|
||||
|
@ -56,9 +56,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE,
|
||||
null));
|
||||
idGenerator.generateNewId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
|
||||
output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3,
|
||||
null, Format.NO_VALUE, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -38,6 +38,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
private final MpegAudioHeader header;
|
||||
private final String language;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
|
||||
private int state;
|
||||
@ -76,7 +77,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
idGenerator.generateNewId();
|
||||
formatId = idGenerator.getFormatId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -176,9 +179,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
frameSize = header.frameSize;
|
||||
if (!hasOutputFormat) {
|
||||
frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate;
|
||||
Format format = Format.createAudioSampleFormat(null, header.mimeType, null, Format.NO_VALUE,
|
||||
MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, null, null, 0,
|
||||
language);
|
||||
Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null,
|
||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate,
|
||||
null, null, 0, language);
|
||||
output.format(format);
|
||||
hasOutputFormat = true;
|
||||
}
|
||||
|
@ -16,12 +16,11 @@
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
|
||||
/**
|
||||
* Parses PES packet data and extracts samples.
|
||||
|
@ -23,10 +23,10 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
@ -16,10 +16,10 @@
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
|
||||
/**
|
||||
* Reads section data.
|
||||
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
|
@ -17,8 +17,10 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.text.cea.Cea608Decoder;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.text.cea.CeaUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
||||
@ -27,49 +29,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
*/
|
||||
/* package */ final class SeiReader {
|
||||
|
||||
private final TrackOutput output;
|
||||
private TrackOutput output;
|
||||
|
||||
public SeiReader(TrackOutput output) {
|
||||
this.output = output;
|
||||
output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null,
|
||||
Format.NO_VALUE, 0, null, null));
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
idGenerator.generateNewId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
|
||||
output.format(Format.createTextSampleFormat(idGenerator.getFormatId(),
|
||||
MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null));
|
||||
}
|
||||
|
||||
public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
|
||||
int b;
|
||||
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
|
||||
// Parse payload type.
|
||||
int payloadType = 0;
|
||||
do {
|
||||
b = seiBuffer.readUnsignedByte();
|
||||
payloadType += b;
|
||||
} while (b == 0xFF);
|
||||
// Parse payload size.
|
||||
int payloadSize = 0;
|
||||
do {
|
||||
b = seiBuffer.readUnsignedByte();
|
||||
payloadSize += b;
|
||||
} while (b == 0xFF);
|
||||
// Process the payload.
|
||||
if (Cea608Decoder.isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) {
|
||||
// Ignore country_code (1) + provider_code (2) + user_identifier (4)
|
||||
// + user_data_type_code (1).
|
||||
seiBuffer.skipBytes(8);
|
||||
// Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1).
|
||||
int ccCount = seiBuffer.readUnsignedByte() & 0x1F;
|
||||
// Ignore em_data (1)
|
||||
seiBuffer.skipBytes(1);
|
||||
// Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
|
||||
// + cc_data_1 (8) + cc_data_2 (8).
|
||||
int sampleLength = ccCount * 3;
|
||||
output.sampleData(seiBuffer, sampleLength);
|
||||
output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null);
|
||||
// Ignore trailing information in SEI, if any.
|
||||
seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3));
|
||||
} else {
|
||||
seiBuffer.skipBytes(payloadSize);
|
||||
}
|
||||
}
|
||||
CeaUtil.consume(pesTimeUs, seiBuffer, output);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,10 +18,10 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
|
||||
/**
|
||||
* Parses splice info sections as defined by SCTE35.
|
||||
@ -36,9 +36,10 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
|
||||
public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
|
||||
TsPayloadReader.TrackIdGenerator idGenerator) {
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null,
|
||||
Format.NO_VALUE, null));
|
||||
idGenerator.generateNewId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
|
||||
output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35,
|
||||
null, Format.NO_VALUE, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -25,16 +25,19 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Facilitates the extraction of data from the MPEG-2 TS container format.
|
||||
@ -79,7 +82,7 @@ public final class TsExtractor implements Extractor {
|
||||
private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
|
||||
|
||||
private final boolean hlsMode;
|
||||
private final TimestampAdjuster timestampAdjuster;
|
||||
private final List<TimestampAdjuster> timestampAdjusters;
|
||||
private final ParsableByteArray tsPacketBuffer;
|
||||
private final ParsableBitArray tsScratch;
|
||||
private final SparseIntArray continuityCounters;
|
||||
@ -89,18 +92,12 @@ public final class TsExtractor implements Extractor {
|
||||
|
||||
// Accessed only by the loading thread.
|
||||
private ExtractorOutput output;
|
||||
private int remainingPmts;
|
||||
private boolean tracksEnded;
|
||||
private TsPayloadReader id3Reader;
|
||||
|
||||
public TsExtractor() {
|
||||
this(new TimestampAdjuster(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
|
||||
*/
|
||||
public TsExtractor(TimestampAdjuster timestampAdjuster) {
|
||||
this(timestampAdjuster, new DefaultTsPayloadReaderFactory(), false);
|
||||
this(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,7 +108,12 @@ public final class TsExtractor implements Extractor {
|
||||
*/
|
||||
public TsExtractor(TimestampAdjuster timestampAdjuster,
|
||||
TsPayloadReader.Factory payloadReaderFactory, boolean hlsMode) {
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
if (hlsMode) {
|
||||
timestampAdjusters = Collections.singletonList(timestampAdjuster);
|
||||
} else {
|
||||
timestampAdjusters = new ArrayList<>();
|
||||
timestampAdjusters.add(timestampAdjuster);
|
||||
}
|
||||
this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);
|
||||
this.hlsMode = hlsMode;
|
||||
tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
|
||||
@ -150,7 +152,10 @@ public final class TsExtractor implements Extractor {
|
||||
|
||||
@Override
|
||||
public void seek(long position, long timeUs) {
|
||||
timestampAdjuster.reset();
|
||||
int timestampAdjustersCount = timestampAdjusters.size();
|
||||
for (int i = 0; i < timestampAdjustersCount; i++) {
|
||||
timestampAdjusters.get(i).reset();
|
||||
}
|
||||
tsPacketBuffer.reset();
|
||||
continuityCounters.clear();
|
||||
// Elementary stream readers' state should be cleared to get consistent behaviours when seeking.
|
||||
@ -307,8 +312,12 @@ public final class TsExtractor implements Extractor {
|
||||
} else {
|
||||
int pid = patScratch.readBits(13);
|
||||
tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid)));
|
||||
remainingPmts++;
|
||||
}
|
||||
}
|
||||
if (!hlsMode) {
|
||||
tsPayloadReaders.remove(TS_PAT_PID);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -345,10 +354,21 @@ public final class TsExtractor implements Extractor {
|
||||
// See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment.
|
||||
return;
|
||||
}
|
||||
// section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), program_number (16),
|
||||
// reserved (2), version_number (5), current_next_indicator (1), // section_number (8),
|
||||
// TimestampAdjuster assignment.
|
||||
TimestampAdjuster timestampAdjuster;
|
||||
if (hlsMode || remainingPmts == 1) {
|
||||
timestampAdjuster = timestampAdjusters.get(0);
|
||||
} else {
|
||||
timestampAdjuster = new TimestampAdjuster(timestampAdjusters.get(0).firstSampleTimestampUs);
|
||||
timestampAdjusters.add(timestampAdjuster);
|
||||
}
|
||||
|
||||
// section_syntax_indicator(1), '0'(1), reserved(2), section_length(12)
|
||||
sectionData.skipBytes(2);
|
||||
int programNumber = sectionData.readUnsignedShort();
|
||||
// reserved (2), version_number (5), current_next_indicator (1), section_number (8),
|
||||
// last_section_number (8), reserved (3), PCR_PID (13)
|
||||
sectionData.skipBytes(9);
|
||||
sectionData.skipBytes(5);
|
||||
|
||||
// Read program_info_length.
|
||||
sectionData.readBytes(pmtScratch, 2);
|
||||
@ -364,7 +384,7 @@ public final class TsExtractor implements Extractor {
|
||||
EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]);
|
||||
id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo);
|
||||
id3Reader.init(timestampAdjuster, output,
|
||||
new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
|
||||
new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
|
||||
}
|
||||
|
||||
int remainingEntriesLength = sectionData.bytesLeft();
|
||||
@ -393,7 +413,8 @@ public final class TsExtractor implements Extractor {
|
||||
} else {
|
||||
reader = payloadReaderFactory.createPayloadReader(streamType, esInfo);
|
||||
if (reader != null) {
|
||||
reader.init(timestampAdjuster, output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE));
|
||||
reader.init(timestampAdjuster, output,
|
||||
new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE));
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,13 +425,17 @@ public final class TsExtractor implements Extractor {
|
||||
if (hlsMode) {
|
||||
if (!tracksEnded) {
|
||||
output.endTracks();
|
||||
remainingPmts = 0;
|
||||
tracksEnded = true;
|
||||
}
|
||||
} else {
|
||||
tsPayloadReaders.remove(TS_PAT_PID);
|
||||
tsPayloadReaders.remove(pid);
|
||||
output.endTracks();
|
||||
remainingPmts--;
|
||||
if (remainingPmts == 0) {
|
||||
output.endTracks();
|
||||
tracksEnded = true;
|
||||
}
|
||||
}
|
||||
tracksEnded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,9 +17,9 @@ package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
|
||||
/**
|
||||
* Parses TS packet payload data.
|
||||
@ -81,17 +81,63 @@ public interface TsPayloadReader {
|
||||
*/
|
||||
final class TrackIdGenerator {
|
||||
|
||||
private final int firstId;
|
||||
private final int idIncrement;
|
||||
private int generatedIdCount;
|
||||
private static final int ID_UNSET = Integer.MIN_VALUE;
|
||||
|
||||
public TrackIdGenerator(int firstId, int idIncrement) {
|
||||
this.firstId = firstId;
|
||||
this.idIncrement = idIncrement;
|
||||
private final String formatIdPrefix;
|
||||
private final int firstTrackId;
|
||||
private final int trackIdIncrement;
|
||||
private int trackId;
|
||||
private String formatId;
|
||||
|
||||
public TrackIdGenerator(int firstTrackId, int trackIdIncrement) {
|
||||
this(ID_UNSET, firstTrackId, trackIdIncrement);
|
||||
}
|
||||
|
||||
public int getNextId() {
|
||||
return firstId + idIncrement * generatedIdCount++;
|
||||
public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) {
|
||||
this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : "";
|
||||
this.firstTrackId = firstTrackId;
|
||||
this.trackIdIncrement = trackIdIncrement;
|
||||
trackId = ID_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new set of track and track format ids. Must be called before {@code get*}
|
||||
* methods.
|
||||
*/
|
||||
public void generateNewId() {
|
||||
trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement;
|
||||
formatId = formatIdPrefix + trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last generated track id. Must be called after the first {@link #generateNewId()}
|
||||
* call.
|
||||
*
|
||||
* @return The last generated track id.
|
||||
*/
|
||||
public int getTrackId() {
|
||||
maybeThrowUninitializedError();
|
||||
return trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no
|
||||
* {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be
|
||||
* called after the first {@link #generateNewId()} call.
|
||||
*
|
||||
* @return The last generated format id, with the format {@code "programNumber/trackId"}. If no
|
||||
* {@code programNumber} was provided, the {@code trackId} alone is used as
|
||||
* format id.
|
||||
*/
|
||||
public String getFormatId() {
|
||||
maybeThrowUninitializedError();
|
||||
return formatId;
|
||||
}
|
||||
|
||||
private void maybeThrowUninitializedError() {
|
||||
if (trackId == ID_UNSET) {
|
||||
throw new IllegalStateException("generateNewId() must be called before retrieving ids.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ public final class WavExtractor implements Extractor, SeekMap {
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
extractorOutput = output;
|
||||
trackOutput = output.track(0);
|
||||
trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
|
||||
wavHeader = null;
|
||||
output.endTracks();
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.mediacodec;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Point;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo.AudioCapabilities;
|
||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||
@ -23,6 +24,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||
import android.media.MediaCodecInfo.VideoCapabilities;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
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;
|
||||
@ -141,39 +143,6 @@ public final class MediaCodecInfo {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the decoder supports video with a specified width and height.
|
||||
* <p>
|
||||
* Must not be called if the device SDK version is less than 21.
|
||||
*
|
||||
* @param width Width in pixels.
|
||||
* @param height Height in pixels.
|
||||
* @return Whether the decoder supports video with the given width and height.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public boolean isVideoSizeSupportedV21(int width, int height) {
|
||||
if (capabilities == null) {
|
||||
logNoSupport("size.caps");
|
||||
return false;
|
||||
}
|
||||
VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
|
||||
if (videoCapabilities == null) {
|
||||
logNoSupport("size.vCaps");
|
||||
return false;
|
||||
}
|
||||
if (!videoCapabilities.isSizeSupported(width, height)) {
|
||||
// Capabilities are known to be inaccurately reported for vertical resolutions on some devices
|
||||
// (b/31387661). If the video is vertical and the capabilities indicate support if the width
|
||||
// and height are swapped, we assume that the vertical resolution is also supported.
|
||||
if (width >= height || !videoCapabilities.isSizeSupported(height, width)) {
|
||||
logNoSupport("size.support, " + width + "x" + height);
|
||||
return false;
|
||||
}
|
||||
logAssumedSupport("size.rotated, " + width + "x" + height);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the decoder supports video with a given width, height and frame rate.
|
||||
* <p>
|
||||
@ -181,7 +150,8 @@ public final class MediaCodecInfo {
|
||||
*
|
||||
* @param width Width in pixels.
|
||||
* @param height Height in pixels.
|
||||
* @param frameRate Frame rate in frames per second.
|
||||
* @param frameRate Optional frame rate in frames per second. Ignored if set to
|
||||
* {@link Format#NO_VALUE} or any value less than or equal to 0.
|
||||
* @return Whether the decoder supports video with the given width, height and frame rate.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
@ -195,11 +165,12 @@ public final class MediaCodecInfo {
|
||||
logNoSupport("sizeAndRate.vCaps");
|
||||
return false;
|
||||
}
|
||||
if (!videoCapabilities.areSizeAndRateSupported(width, height, frameRate)) {
|
||||
if (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) {
|
||||
// Capabilities are known to be inaccurately reported for vertical resolutions on some devices
|
||||
// (b/31387661). If the video is vertical and the capabilities indicate support if the width
|
||||
// and height are swapped, we assume that the vertical resolution is also supported.
|
||||
if (width >= height || !videoCapabilities.areSizeAndRateSupported(height, width, frameRate)) {
|
||||
if (width >= height
|
||||
|| !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) {
|
||||
logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate);
|
||||
return false;
|
||||
}
|
||||
@ -208,6 +179,35 @@ public final class MediaCodecInfo {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the smallest video size greater than or equal to a specified size that also satisfies
|
||||
* the {@link MediaCodec}'s width and height alignment requirements.
|
||||
* <p>
|
||||
* Must not be called if the device SDK version is less than 21.
|
||||
*
|
||||
* @param width Width in pixels.
|
||||
* @param height Height in pixels.
|
||||
* @return The smallest video size greater than or equal to the specified size that also satisfies
|
||||
* the {@link MediaCodec}'s width and height alignment requirements, or null if not a video
|
||||
* codec.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public Point alignVideoSizeV21(int width, int height) {
|
||||
if (capabilities == null) {
|
||||
logNoSupport("align.caps");
|
||||
return null;
|
||||
}
|
||||
VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
|
||||
if (videoCapabilities == null) {
|
||||
logNoSupport("align.vCaps");
|
||||
return null;
|
||||
}
|
||||
int widthAlignment = videoCapabilities.getWidthAlignment();
|
||||
int heightAlignment = videoCapabilities.getHeightAlignment();
|
||||
return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment,
|
||||
Util.ceilDivide(height, heightAlignment) * heightAlignment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the decoder supports audio with a given sample rate.
|
||||
* <p>
|
||||
@ -279,6 +279,14 @@ public final class MediaCodecInfo {
|
||||
return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width,
|
||||
int height, double frameRate) {
|
||||
return frameRate == Format.NO_VALUE || frameRate <= 0
|
||||
? capabilities.isSizeSupported(width, height)
|
||||
: capabilities.areSizeAndRateSupported(width, height, frameRate);
|
||||
}
|
||||
|
||||
private static boolean isTunneling(CodecCapabilities capabilities) {
|
||||
return Util.SDK_INT >= 21 && isTunnelingV21(capabilities);
|
||||
}
|
||||
|
@ -183,6 +183,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
private boolean codecNeedsAdaptationWorkaround;
|
||||
private boolean codecNeedsEosPropagationWorkaround;
|
||||
private boolean codecNeedsEosFlushWorkaround;
|
||||
private boolean codecNeedsEosOutputExceptionWorkaround;
|
||||
private boolean codecNeedsMonoChannelCountWorkaround;
|
||||
private boolean codecNeedsAdaptationWorkaroundBuffer;
|
||||
private boolean shouldSkipAdaptationWorkaroundOutputBuffer;
|
||||
@ -201,6 +202,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
private boolean inputStreamEnded;
|
||||
private boolean outputStreamEnded;
|
||||
private boolean waitingForKeys;
|
||||
private boolean waitingForFirstSyncFrame;
|
||||
|
||||
protected DecoderCounters decoderCounters;
|
||||
|
||||
@ -276,11 +278,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
/**
|
||||
* Configures a newly created {@link MediaCodec}.
|
||||
*
|
||||
* @param codecInfo Information about the {@link MediaCodec} being configured.
|
||||
* @param codec The {@link MediaCodec} to configure.
|
||||
* @param format The format for which the codec is being configured.
|
||||
* @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
|
||||
* @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
|
||||
*/
|
||||
protected abstract void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto);
|
||||
protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
|
||||
MediaCrypto crypto) throws DecoderQueryException;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
protected final void maybeInitCodec() throws ExoPlaybackException {
|
||||
@ -338,6 +343,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName);
|
||||
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
|
||||
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
|
||||
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
|
||||
codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
|
||||
try {
|
||||
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||
@ -345,7 +351,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
codec = MediaCodec.createByCodecName(codecName);
|
||||
TraceUtil.endSection();
|
||||
TraceUtil.beginSection("configureCodec");
|
||||
configureCodec(codec, format, mediaCrypto);
|
||||
configureCodec(decoderInfo, codec, format, mediaCrypto);
|
||||
TraceUtil.endSection();
|
||||
TraceUtil.beginSection("startCodec");
|
||||
codec.start();
|
||||
@ -363,6 +369,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET;
|
||||
inputIndex = C.INDEX_UNSET;
|
||||
outputIndex = C.INDEX_UNSET;
|
||||
waitingForFirstSyncFrame = true;
|
||||
decoderCounters.decoderInitCount++;
|
||||
}
|
||||
|
||||
@ -501,13 +508,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
codecHotswapDeadlineMs = C.TIME_UNSET;
|
||||
inputIndex = C.INDEX_UNSET;
|
||||
outputIndex = C.INDEX_UNSET;
|
||||
waitingForFirstSyncFrame = true;
|
||||
waitingForKeys = false;
|
||||
shouldSkipOutputBuffer = false;
|
||||
decodeOnlyPresentationTimestamps.clear();
|
||||
codecNeedsAdaptationWorkaroundBuffer = false;
|
||||
shouldSkipAdaptationWorkaroundOutputBuffer = false;
|
||||
if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) {
|
||||
// Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053].
|
||||
releaseCodec();
|
||||
maybeInitCodec();
|
||||
} else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) {
|
||||
@ -630,6 +637,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (waitingForFirstSyncFrame && !buffer.isKeyFrame()) {
|
||||
buffer.clear();
|
||||
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
|
||||
// The buffer we just cleared contained reconfiguration data. We need to re-write this
|
||||
// data into a subsequent buffer (if there is one).
|
||||
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
waitingForFirstSyncFrame = false;
|
||||
boolean bufferEncrypted = buffer.isEncrypted();
|
||||
waitingForKeys = shouldWaitForKeys(bufferEncrypted);
|
||||
if (waitingForKeys) {
|
||||
@ -763,8 +780,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
*
|
||||
* @param codec The {@link MediaCodec} instance.
|
||||
* @param outputFormat The new output format.
|
||||
* @throws ExoPlaybackException Thrown if an error occurs handling the new output format.
|
||||
*/
|
||||
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
|
||||
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
|
||||
throws ExoPlaybackException {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@ -849,7 +868,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
|
||||
throws ExoPlaybackException {
|
||||
if (outputIndex < 0) {
|
||||
outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
|
||||
if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
|
||||
try {
|
||||
outputIndex = codec.dequeueOutputBuffer(outputBufferInfo,
|
||||
getDequeueOutputBufferTimeoutUs());
|
||||
} catch (IllegalStateException e) {
|
||||
processEndOfStream();
|
||||
if (outputStreamEnded) {
|
||||
// Release the codec, as it's in an error state.
|
||||
releaseCodec();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
outputIndex = codec.dequeueOutputBuffer(outputBufferInfo,
|
||||
getDequeueOutputBufferTimeoutUs());
|
||||
}
|
||||
if (outputIndex >= 0) {
|
||||
// We've dequeued a buffer.
|
||||
if (shouldSkipAdaptationWorkaroundOutputBuffer) {
|
||||
@ -888,9 +922,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex],
|
||||
outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs,
|
||||
shouldSkipOutputBuffer)) {
|
||||
boolean processedOutputBuffer;
|
||||
if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
|
||||
try {
|
||||
processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec,
|
||||
outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags,
|
||||
outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer);
|
||||
} catch (IllegalStateException e) {
|
||||
processEndOfStream();
|
||||
if (outputStreamEnded) {
|
||||
// Release the codec, as it's in an error state.
|
||||
releaseCodec();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec,
|
||||
outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags,
|
||||
outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer);
|
||||
}
|
||||
|
||||
if (processedOutputBuffer) {
|
||||
onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);
|
||||
outputIndex = C.INDEX_UNSET;
|
||||
return true;
|
||||
@ -902,7 +954,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
/**
|
||||
* Processes a new output format.
|
||||
*/
|
||||
private void processOutputFormat() {
|
||||
private void processOutputFormat() throws ExoPlaybackException {
|
||||
MediaFormat format = codec.getOutputFormat();
|
||||
if (codecNeedsAdaptationWorkaround
|
||||
&& format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT
|
||||
@ -992,6 +1044,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
* <p>
|
||||
* If true is returned, the renderer will work around the issue by releasing the decoder and
|
||||
* instantiating a new one rather than flushing the current instance.
|
||||
* <p>
|
||||
* See [Internal: b/8347958, b/8543366].
|
||||
*
|
||||
* @param name The name of the decoder.
|
||||
* @return True if the decoder is known to fail when flushed.
|
||||
@ -1061,6 +1115,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
* <p>
|
||||
* If true is returned, the renderer will work around the issue by instantiating a new decoder
|
||||
* when this case occurs.
|
||||
* <p>
|
||||
* See [Internal: b/8578467, b/23361053].
|
||||
*
|
||||
* @param name The name of the decoder.
|
||||
* @return True if the decoder is known to behave incorrectly if flushed after receiving an input
|
||||
@ -1073,6 +1129,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the decoder may throw an {@link IllegalStateException} from
|
||||
* {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or
|
||||
* {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input
|
||||
* buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set.
|
||||
* <p>
|
||||
* See [Internal: b/17933838].
|
||||
*
|
||||
* @param name The name of the decoder.
|
||||
* @return True if the decoder may throw an exception after receiving an end-of-stream buffer.
|
||||
*/
|
||||
private static boolean codecNeedsEosOutputExceptionWorkaround(String name) {
|
||||
return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the decoder is known to set the number of audio channels in the output format
|
||||
* to 2 for the given input format, whilst only actually outputting a single channel.
|
||||
|
@ -26,7 +26,6 @@ public final class PrivateCommand extends SpliceCommand {
|
||||
|
||||
public final long ptsAdjustment;
|
||||
public final long identifier;
|
||||
|
||||
public final byte[] commandBytes;
|
||||
|
||||
private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) {
|
||||
|
@ -21,6 +21,7 @@ import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
@ -37,6 +38,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
|
||||
private final ParsableByteArray sectionData;
|
||||
private final ParsableBitArray sectionHeader;
|
||||
|
||||
private TimestampAdjuster timestampAdjuster;
|
||||
|
||||
public SpliceInfoDecoder() {
|
||||
sectionData = new ParsableByteArray();
|
||||
sectionHeader = new ParsableBitArray();
|
||||
@ -44,6 +47,13 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
|
||||
|
||||
@Override
|
||||
public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException {
|
||||
// Internal timestamps adjustment.
|
||||
if (timestampAdjuster == null
|
||||
|| inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) {
|
||||
timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs);
|
||||
timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs);
|
||||
}
|
||||
|
||||
ByteBuffer buffer = inputBuffer.data;
|
||||
byte[] data = buffer.array();
|
||||
int size = buffer.limit();
|
||||
@ -69,10 +79,11 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
|
||||
command = SpliceScheduleCommand.parseFromSection(sectionData);
|
||||
break;
|
||||
case TYPE_SPLICE_INSERT:
|
||||
command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment);
|
||||
command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment,
|
||||
timestampAdjuster);
|
||||
break;
|
||||
case TYPE_TIME_SIGNAL:
|
||||
command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment);
|
||||
command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment, timestampAdjuster);
|
||||
break;
|
||||
case TYPE_PRIVATE_COMMAND:
|
||||
command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment);
|
||||
|
@ -19,6 +19,7 @@ import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -34,6 +35,7 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
public final boolean programSpliceFlag;
|
||||
public final boolean spliceImmediateFlag;
|
||||
public final long programSplicePts;
|
||||
public final long programSplicePlaybackPositionUs;
|
||||
public final List<ComponentSplice> componentSpliceList;
|
||||
public final boolean autoReturn;
|
||||
public final long breakDuration;
|
||||
@ -43,14 +45,16 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
|
||||
private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator,
|
||||
boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag,
|
||||
long programSplicePts, List<ComponentSplice> componentSpliceList, boolean autoReturn,
|
||||
long breakDuration, int uniqueProgramId, int availNum, int availsExpected) {
|
||||
long programSplicePts, long programSplicePlaybackPositionUs,
|
||||
List<ComponentSplice> componentSpliceList, boolean autoReturn, long breakDuration,
|
||||
int uniqueProgramId, int availNum, int availsExpected) {
|
||||
this.spliceEventId = spliceEventId;
|
||||
this.spliceEventCancelIndicator = spliceEventCancelIndicator;
|
||||
this.outOfNetworkIndicator = outOfNetworkIndicator;
|
||||
this.programSpliceFlag = programSpliceFlag;
|
||||
this.spliceImmediateFlag = spliceImmediateFlag;
|
||||
this.programSplicePts = programSplicePts;
|
||||
this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs;
|
||||
this.componentSpliceList = Collections.unmodifiableList(componentSpliceList);
|
||||
this.autoReturn = autoReturn;
|
||||
this.breakDuration = breakDuration;
|
||||
@ -66,6 +70,7 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
programSpliceFlag = in.readByte() == 1;
|
||||
spliceImmediateFlag = in.readByte() == 1;
|
||||
programSplicePts = in.readLong();
|
||||
programSplicePlaybackPositionUs = in.readLong();
|
||||
int componentSpliceListSize = in.readInt();
|
||||
List<ComponentSplice> componentSpliceList = new ArrayList<>(componentSpliceListSize);
|
||||
for (int i = 0; i < componentSpliceListSize; i++) {
|
||||
@ -80,7 +85,7 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
}
|
||||
|
||||
/* package */ static SpliceInsertCommand parseFromSection(ParsableByteArray sectionData,
|
||||
long ptsAdjustment) {
|
||||
long ptsAdjustment, TimestampAdjuster timestampAdjuster) {
|
||||
long spliceEventId = sectionData.readUnsignedInt();
|
||||
// splice_event_cancel_indicator(1), reserved(7).
|
||||
boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0;
|
||||
@ -88,7 +93,7 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
boolean programSpliceFlag = false;
|
||||
boolean spliceImmediateFlag = false;
|
||||
long programSplicePts = C.TIME_UNSET;
|
||||
ArrayList<ComponentSplice> componentSplices = new ArrayList<>();
|
||||
List<ComponentSplice> componentSplices = Collections.emptyList();
|
||||
int uniqueProgramId = 0;
|
||||
int availNum = 0;
|
||||
int availsExpected = 0;
|
||||
@ -112,7 +117,8 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
if (!spliceImmediateFlag) {
|
||||
componentSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment);
|
||||
}
|
||||
componentSplices.add(new ComponentSplice(componentTag, componentSplicePts));
|
||||
componentSplices.add(new ComponentSplice(componentTag, componentSplicePts,
|
||||
timestampAdjuster.adjustTsTimestamp(componentSplicePts)));
|
||||
}
|
||||
}
|
||||
if (durationFlag) {
|
||||
@ -125,7 +131,8 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
availsExpected = sectionData.readUnsignedByte();
|
||||
}
|
||||
return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,
|
||||
programSpliceFlag, spliceImmediateFlag, programSplicePts, componentSplices, autoReturn,
|
||||
programSpliceFlag, spliceImmediateFlag, programSplicePts,
|
||||
timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn,
|
||||
duration, uniqueProgramId, availNum, availsExpected);
|
||||
}
|
||||
|
||||
@ -136,19 +143,23 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
|
||||
public final int componentTag;
|
||||
public final long componentSplicePts;
|
||||
public final long componentSplicePlaybackPositionUs;
|
||||
|
||||
private ComponentSplice(int componentTag, long componentSplicePts) {
|
||||
private ComponentSplice(int componentTag, long componentSplicePts,
|
||||
long componentSplicePlaybackPositionUs) {
|
||||
this.componentTag = componentTag;
|
||||
this.componentSplicePts = componentSplicePts;
|
||||
this.componentSplicePlaybackPositionUs = componentSplicePlaybackPositionUs;
|
||||
}
|
||||
|
||||
public void writeToParcel(Parcel dest) {
|
||||
dest.writeInt(componentTag);
|
||||
dest.writeLong(componentSplicePts);
|
||||
dest.writeLong(componentSplicePlaybackPositionUs);
|
||||
}
|
||||
|
||||
public static ComponentSplice createFromParcel(Parcel in) {
|
||||
return new ComponentSplice(in.readInt(), in.readLong());
|
||||
return new ComponentSplice(in.readInt(), in.readLong(), in.readLong());
|
||||
}
|
||||
|
||||
}
|
||||
@ -163,6 +174,7 @@ public final class SpliceInsertCommand extends SpliceCommand {
|
||||
dest.writeByte((byte) (programSpliceFlag ? 1 : 0));
|
||||
dest.writeByte((byte) (spliceImmediateFlag ? 1 : 0));
|
||||
dest.writeLong(programSplicePts);
|
||||
dest.writeLong(programSplicePlaybackPositionUs);
|
||||
int componentSpliceListSize = componentSpliceList.size();
|
||||
dest.writeInt(componentSpliceListSize);
|
||||
for (int i = 0; i < componentSpliceListSize; i++) {
|
||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.metadata.scte35;
|
||||
import android.os.Parcel;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
|
||||
/**
|
||||
* Represents a time signal command as defined in SCTE35, Section 9.3.4.
|
||||
@ -25,14 +26,18 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
public final class TimeSignalCommand extends SpliceCommand {
|
||||
|
||||
public final long ptsTime;
|
||||
public final long playbackPositionUs;
|
||||
|
||||
private TimeSignalCommand(long ptsTime) {
|
||||
private TimeSignalCommand(long ptsTime, long playbackPositionUs) {
|
||||
this.ptsTime = ptsTime;
|
||||
this.playbackPositionUs = playbackPositionUs;
|
||||
}
|
||||
|
||||
/* package */ static TimeSignalCommand parseFromSection(ParsableByteArray sectionData,
|
||||
long ptsAdjustment) {
|
||||
return new TimeSignalCommand(parseSpliceTime(sectionData, ptsAdjustment));
|
||||
long ptsAdjustment, TimestampAdjuster timestampAdjuster) {
|
||||
long ptsTime = parseSpliceTime(sectionData, ptsAdjustment);
|
||||
long playbackPositionUs = timestampAdjuster.adjustTsTimestamp(ptsTime);
|
||||
return new TimeSignalCommand(ptsTime, playbackPositionUs);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,6 +66,7 @@ public final class TimeSignalCommand extends SpliceCommand {
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(ptsTime);
|
||||
dest.writeLong(playbackPositionUs);
|
||||
}
|
||||
|
||||
public static final Creator<TimeSignalCommand> CREATOR =
|
||||
@ -68,7 +74,7 @@ public final class TimeSignalCommand extends SpliceCommand {
|
||||
|
||||
@Override
|
||||
public TimeSignalCommand createFromParcel(Parcel in) {
|
||||
return new TimeSignalCommand(in.readLong());
|
||||
return new TimeSignalCommand(in.readLong(), in.readLong());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -381,7 +381,7 @@ import java.io.IOException;
|
||||
// ExtractorOutput implementation. Called by the loading thread.
|
||||
|
||||
@Override
|
||||
public TrackOutput track(int id) {
|
||||
public TrackOutput track(int id, int type) {
|
||||
DefaultTrackOutput trackOutput = sampleQueues.get(id);
|
||||
if (trackOutput == null) {
|
||||
trackOutput = new DefaultTrackOutput(allocator);
|
||||
@ -519,7 +519,7 @@ import java.io.IOException;
|
||||
}
|
||||
|
||||
private boolean isLoadableExceptionFatal(IOException e) {
|
||||
return e instanceof ExtractorMediaSource.UnrecognizedInputFormatException;
|
||||
return e instanceof UnrecognizedInputFormatException;
|
||||
}
|
||||
|
||||
private void notifyLoadError(final IOException error) {
|
||||
@ -625,7 +625,7 @@ import java.io.IOException;
|
||||
length += position;
|
||||
}
|
||||
input = new DefaultExtractorInput(dataSource, position, length);
|
||||
Extractor extractor = extractorHolder.selectExtractor(input);
|
||||
Extractor extractor = extractorHolder.selectExtractor(input, dataSource.getUri());
|
||||
if (pendingExtractorSeek) {
|
||||
extractor.seek(position, seekTimeUs);
|
||||
pendingExtractorSeek = false;
|
||||
@ -677,13 +677,13 @@ import java.io.IOException;
|
||||
* later calls.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which data should be read.
|
||||
* @param uri The {@link Uri} of the data.
|
||||
* @return An initialized extractor for reading {@code input}.
|
||||
* @throws ExtractorMediaSource.UnrecognizedInputFormatException Thrown if the input format
|
||||
* could not be detected.
|
||||
* @throws UnrecognizedInputFormatException Thrown if the input format could not be detected.
|
||||
* @throws IOException Thrown if the input could not be read.
|
||||
* @throws InterruptedException Thrown if the thread was interrupted.
|
||||
*/
|
||||
public Extractor selectExtractor(ExtractorInput input)
|
||||
public Extractor selectExtractor(ExtractorInput input, Uri uri)
|
||||
throws IOException, InterruptedException {
|
||||
if (extractor != null) {
|
||||
return extractor;
|
||||
@ -701,7 +701,8 @@ import java.io.IOException;
|
||||
}
|
||||
}
|
||||
if (extractor == null) {
|
||||
throw new ExtractorMediaSource.UnrecognizedInputFormatException(extractors);
|
||||
throw new UnrecognizedInputFormatException("None of the available extractors ("
|
||||
+ Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri);
|
||||
}
|
||||
extractor.init(extractorOutput);
|
||||
return extractor;
|
||||
|
@ -19,7 +19,6 @@ import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
@ -27,7 +26,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
@ -57,18 +55,6 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown if the input format could not recognized.
|
||||
*/
|
||||
public static final class UnrecognizedInputFormatException extends ParserException {
|
||||
|
||||
public UnrecognizedInputFormatException(Extractor[] extractors) {
|
||||
super("None of the available extractors ("
|
||||
+ Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The default minimum number of times to retry loading prior to failing for on-demand streams.
|
||||
*/
|
||||
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
|
||||
/**
|
||||
* Thrown if the input format was not recognized.
|
||||
*/
|
||||
public class UnrecognizedInputFormatException extends ParserException {
|
||||
|
||||
/**
|
||||
* The {@link Uri} from which the unrecognized data was read.
|
||||
*/
|
||||
public final Uri uri;
|
||||
|
||||
/**
|
||||
* @param message The detail message for the exception.
|
||||
* @param uri The {@link Uri} from which the unrecognized data was read.
|
||||
*/
|
||||
public UnrecognizedInputFormatException(String message, Uri uri) {
|
||||
super(message);
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
}
|
@ -17,7 +17,7 @@ package com.google.android.exoplayer2.source.chunk;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
@ -30,33 +30,19 @@ import java.io.IOException;
|
||||
/**
|
||||
* An {@link Extractor} wrapper for loading chunks containing a single track.
|
||||
* <p>
|
||||
* The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive
|
||||
* parsed data.
|
||||
* The wrapper allows switching of the {@link TrackOutput} that receives parsed data.
|
||||
*/
|
||||
public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput {
|
||||
|
||||
/**
|
||||
* Receives {@link SeekMap}s extracted by the wrapped {@link Extractor}.
|
||||
*/
|
||||
public interface SeekMapOutput {
|
||||
|
||||
/**
|
||||
* @see ExtractorOutput#seekMap(SeekMap)
|
||||
*/
|
||||
void seekMap(SeekMap seekMap);
|
||||
|
||||
}
|
||||
|
||||
public final Extractor extractor;
|
||||
|
||||
private final Format manifestFormat;
|
||||
private final boolean preferManifestDrmInitData;
|
||||
private final boolean resendFormatOnInit;
|
||||
private final int primaryTrackType;
|
||||
|
||||
private boolean extractorInitialized;
|
||||
private SeekMapOutput seekMapOutput;
|
||||
private TrackOutput trackOutput;
|
||||
private Format sentFormat;
|
||||
private SeekMap seekMap;
|
||||
private Format sampleFormat;
|
||||
|
||||
// Accessed only on the loader thread.
|
||||
private boolean seenTrack;
|
||||
@ -66,36 +52,44 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
|
||||
* @param extractor The extractor to wrap.
|
||||
* @param manifestFormat A manifest defined {@link Format} whose data should be merged into any
|
||||
* sample {@link Format} output from the {@link Extractor}.
|
||||
* @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat}
|
||||
* should be preferred when the sample and manifest {@link Format}s are merged.
|
||||
* @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when
|
||||
* it is initialized via {@link #init(SeekMapOutput, TrackOutput)}.
|
||||
* @param primaryTrackType The type of the primary track. Typically one of the {@link C}
|
||||
* {@code TRACK_TYPE_*} constants.
|
||||
*/
|
||||
public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat,
|
||||
boolean preferManifestDrmInitData, boolean resendFormatOnInit) {
|
||||
public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType) {
|
||||
this.extractor = extractor;
|
||||
this.manifestFormat = manifestFormat;
|
||||
this.preferManifestDrmInitData = preferManifestDrmInitData;
|
||||
this.resendFormatOnInit = resendFormatOnInit;
|
||||
this.primaryTrackType = primaryTrackType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the extractor to output to the provided {@link SeekMapOutput} and
|
||||
* {@link TrackOutput} instances, and configures it to receive data from a new chunk.
|
||||
* Returns the {@link SeekMap} most recently output by the extractor, or null.
|
||||
*/
|
||||
public SeekMap getSeekMap() {
|
||||
return seekMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample {@link Format} most recently output by the extractor, or null.
|
||||
*/
|
||||
public Format getSampleFormat() {
|
||||
return sampleFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to
|
||||
* receive data from a new chunk.
|
||||
*
|
||||
* @param seekMapOutput The {@link SeekMapOutput} that will receive extracted {@link SeekMap}s.
|
||||
* @param trackOutput The {@link TrackOutput} that will receive sample data.
|
||||
*/
|
||||
public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) {
|
||||
this.seekMapOutput = seekMapOutput;
|
||||
public void init(TrackOutput trackOutput) {
|
||||
this.trackOutput = trackOutput;
|
||||
if (!extractorInitialized) {
|
||||
extractor.init(this);
|
||||
extractorInitialized = true;
|
||||
} else {
|
||||
extractor.seek(0, 0);
|
||||
if (resendFormatOnInit && sentFormat != null) {
|
||||
trackOutput.format(sentFormat);
|
||||
if (sampleFormat != null && trackOutput != null) {
|
||||
trackOutput.format(sampleFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,7 +97,10 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
|
||||
// ExtractorOutput implementation.
|
||||
|
||||
@Override
|
||||
public TrackOutput track(int id) {
|
||||
public TrackOutput track(int id, int type) {
|
||||
if (primaryTrackType != C.TRACK_TYPE_UNKNOWN && primaryTrackType != type) {
|
||||
return new DummyTrackOutput();
|
||||
}
|
||||
Assertions.checkState(!seenTrack || seenTrackId == id);
|
||||
seenTrack = true;
|
||||
seenTrackId = id;
|
||||
@ -117,15 +114,17 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
|
||||
|
||||
@Override
|
||||
public void seekMap(SeekMap seekMap) {
|
||||
seekMapOutput.seekMap(seekMap);
|
||||
this.seekMap = seekMap;
|
||||
}
|
||||
|
||||
// TrackOutput implementation.
|
||||
|
||||
@Override
|
||||
public void format(Format format) {
|
||||
sentFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData);
|
||||
trackOutput.format(sentFormat);
|
||||
sampleFormat = format.copyWithManifestFormatInfo(manifestFormat);
|
||||
if (trackOutput != null) {
|
||||
trackOutput.format(sampleFormat);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,8 +20,6 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
@ -31,12 +29,11 @@ import java.io.IOException;
|
||||
/**
|
||||
* A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data.
|
||||
*/
|
||||
public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput {
|
||||
public class ContainerMediaChunk extends BaseMediaChunk {
|
||||
|
||||
private final int chunkCount;
|
||||
private final long sampleOffsetUs;
|
||||
private final ChunkExtractorWrapper extractorWrapper;
|
||||
private final Format sampleFormat;
|
||||
|
||||
private volatile int bytesLoaded;
|
||||
private volatile boolean loadCanceled;
|
||||
@ -56,19 +53,15 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput
|
||||
* underlying media are being merged into a single load.
|
||||
* @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor.
|
||||
* @param extractorWrapper A wrapped extractor to use for parsing the data.
|
||||
* @param sampleFormat The {@link Format} of the samples in the chunk, if known. May be null if
|
||||
* the data is known to define its own sample format.
|
||||
*/
|
||||
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
|
||||
int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
|
||||
int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper,
|
||||
Format sampleFormat) {
|
||||
int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) {
|
||||
super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
|
||||
endTimeUs, chunkIndex);
|
||||
this.chunkCount = chunkCount;
|
||||
this.sampleOffsetUs = sampleOffsetUs;
|
||||
this.extractorWrapper = extractorWrapper;
|
||||
this.sampleFormat = sampleFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -86,13 +79,6 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput
|
||||
return bytesLoaded;
|
||||
}
|
||||
|
||||
// SeekMapOutput implementation.
|
||||
|
||||
@Override
|
||||
public final void seekMap(SeekMap seekMap) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// Loadable implementation.
|
||||
|
||||
@Override
|
||||
@ -116,8 +102,8 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput
|
||||
if (bytesLoaded == 0) {
|
||||
// Set the target to ourselves.
|
||||
DefaultTrackOutput trackOutput = getTrackOutput();
|
||||
trackOutput.formatWithOffset(sampleFormat, sampleOffsetUs);
|
||||
extractorWrapper.init(this, trackOutput);
|
||||
trackOutput.setSampleOffsetUs(sampleOffsetUs);
|
||||
extractorWrapper.init(trackOutput);
|
||||
}
|
||||
// Load and decode the sample data.
|
||||
try {
|
||||
|