Merge pull request #6279 from google/dev-v2-r2.10.4

r2.10.4
This commit is contained in:
Oliver Woodman 2019-08-09 21:01:05 +01:00 committed by GitHub
commit 85c10b0256
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 2400 additions and 1111 deletions

View File

@ -1,5 +1,36 @@
# Release notes # # Release notes #
### 2.10.4 ###
* Offline: Add `Scheduler` implementation that uses `WorkManager`.
* Add ability to specify a description when creating notification channels via
ExoPlayer library classes.
* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language
tags instead of 3-letter ISO 639-2 language tags.
* Ensure the `SilenceMediaSource` position is in range
([#6229](https://github.com/google/ExoPlayer/issues/6229)).
* WAV: Calculate correct duration for clipped streams
([#6241](https://github.com/google/ExoPlayer/issues/6241)).
* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change
from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)).
* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata
([#5527](https://github.com/google/ExoPlayer/issues/5527)).
* Fix issue where initial seek positions get ignored when playing a preroll ad
([#6201](https://github.com/google/ExoPlayer/issues/6201)).
* Fix issue where invalid language tags were normalized to "und" instead of
keeping the original
([#6153](https://github.com/google/ExoPlayer/issues/6153)).
* Fix `DataSchemeDataSource` re-opening and range requests
([#6192](https://github.com/google/ExoPlayer/issues/6192)).
* Fix Flac and ALAC playback on some LG devices
([#5938](https://github.com/google/ExoPlayer/issues/5938)).
* Fix issue when calling `performClick` on `PlayerView` without
`PlayerControlView`
([#6260](https://github.com/google/ExoPlayer/issues/6260)).
* Fix issue where playback speeds are not used in adaptive track selections
after manual selection changes for other renderers
([#6256](https://github.com/google/ExoPlayer/issues/6256)).
### 2.10.3 ### ### 2.10.3 ###
* Display last frame when seeking to end of stream * Display last frame when seeking to end of stream

View File

@ -21,14 +21,6 @@ buildscript {
classpath 'com.novoda:bintray-release:0.9' classpath 'com.novoda:bintray-release:0.9'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0'
} }
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
configurations.all {
resolutionStrategy {
force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
}
}
} }
allprojects { allprojects {
repositories { repositories {

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.10.3' releaseVersion = '2.10.4'
releaseVersionCode = 2010003 releaseVersionCode = 2010004
minSdkVersion = 16 minSdkVersion = 16
targetSdkVersion = 28 targetSdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28

View File

@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback' include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher' include modulePrefix + 'extension-jobdispatcher'
include modulePrefix + 'extension-workmanager'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
@ -60,3 +61,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')

View File

@ -47,17 +47,6 @@ android {
// The demo app isn't indexed and doesn't have translations. // The demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation' disable 'GoogleAppIndexingWarning','MissingTranslation'
} }
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
} }
dependencies { dependencies {

View File

@ -25,7 +25,7 @@
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="${castOptionsProvider}" /> android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity" <activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

View File

@ -53,7 +53,7 @@ dependencies {
implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima') implementation project(modulePrefix + 'extension-ima')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View File

@ -62,7 +62,7 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.legacy:legacy-support-core-ui:1.0.0' implementation 'androidx.legacy:legacy-support-core-ui:1.0.0'
implementation 'androidx.fragment:fragment:1.0.0' implementation 'androidx.fragment:fragment:1.0.0'
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.0.0'

View File

@ -41,7 +41,8 @@ public class DemoDownloadService extends DownloadService {
FOREGROUND_NOTIFICATION_ID, FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID, CHANNEL_ID,
R.string.exo_download_notification_channel_name); R.string.exo_download_notification_channel_name,
/* channelDescriptionResourceId= */ 0);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
} }

View File

@ -31,8 +31,8 @@ android {
} }
dependencies { dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.2.0' api 'com.google.android.gms:play-services-cast-framework:17.0.0'
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View File

@ -83,8 +83,6 @@ public final class CastPlayer extends BasePlayer {
private final CastTimelineTracker timelineTracker; private final CastTimelineTracker timelineTracker;
private final Timeline.Period period; private final Timeline.Period period;
private RemoteMediaClient remoteMediaClient;
// Result callbacks. // Result callbacks.
private final StatusListener statusListener; private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback; private final SeekResultCallback seekResultCallback;
@ -93,9 +91,10 @@ public final class CastPlayer extends BasePlayer {
private final CopyOnWriteArrayList<ListenerHolder> listeners; private final CopyOnWriteArrayList<ListenerHolder> listeners;
private final ArrayList<ListenerNotificationTask> notificationsBatch; private final ArrayList<ListenerNotificationTask> notificationsBatch;
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks; private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
private SessionAvailabilityListener sessionAvailabilityListener; @Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state. // Internal state.
@Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline; private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups; private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection; private TrackSelectionArray currentTrackSelection;
@ -148,6 +147,7 @@ public final class CastPlayer extends BasePlayer {
* starts at position 0. * starts at position 0.
* @return The Cast {@code PendingResult}, or null if no session is available. * @return The Cast {@code PendingResult}, or null if no session is available.
*/ */
@Nullable
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) { public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
} }
@ -163,8 +163,9 @@ public final class CastPlayer extends BasePlayer {
* @param repeatMode The repeat mode for the created media queue. * @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available. * @return The Cast {@code PendingResult}, or null if no session is available.
*/ */
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex, @Nullable
long positionMs, @RepeatMode int repeatMode) { public PendingResult<MediaChannelResult> loadItems(
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) { if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true; waitingForInitialTimeline = true;
@ -180,6 +181,7 @@ public final class CastPlayer extends BasePlayer {
* @param items The items to append. * @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists. * @return The Cast {@code PendingResult}, or null if no media queue exists.
*/ */
@Nullable
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) { public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items); return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
} }
@ -194,6 +196,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist. * periodId} exist.
*/ */
@Nullable
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) { public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
@ -211,6 +214,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist. * periodId} exist.
*/ */
@Nullable
public PendingResult<MediaChannelResult> removeItem(int periodId) { public PendingResult<MediaChannelResult> removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null); return remoteMediaClient.queueRemoveItem(periodId, null);
@ -229,6 +233,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist. * periodId} exist.
*/ */
@Nullable
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) { public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
@ -246,6 +251,7 @@ public final class CastPlayer extends BasePlayer {
* @return The item that corresponds to the period with the given id, or null if no media queue or * @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist. * period with id {@code periodId} exist.
*/ */
@Nullable
public MediaQueueItem getItem(int periodId) { public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus(); MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
@ -264,9 +270,9 @@ public final class CastPlayer extends BasePlayer {
/** /**
* Sets a listener for updates on the cast session availability. * Sets a listener for updates on the cast session availability.
* *
* @param listener The {@link SessionAvailabilityListener}. * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
*/ */
public void setSessionAvailabilityListener(SessionAvailabilityListener listener) { public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
sessionAvailabilityListener = listener; sessionAvailabilityListener = listener;
} }
@ -322,6 +328,7 @@ public final class CastPlayer extends BasePlayer {
} }
@Override @Override
@Nullable
public ExoPlaybackException getPlaybackError() { public ExoPlaybackException getPlaybackError() {
return null; return null;
} }
@ -529,7 +536,7 @@ public final class CastPlayer extends BasePlayer {
// Internal methods. // Internal methods.
public void updateInternalState() { private void updateInternalState() {
if (remoteMediaClient == null) { if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now. // There is no session. We leave the state of the player as it is now.
return; return;
@ -675,7 +682,8 @@ public final class CastPlayer extends BasePlayer {
} }
} }
private @Nullable MediaStatus getMediaStatus() { @Nullable
private MediaStatus getMediaStatus() {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
} }

View File

@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions; import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider; import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider; import com.google.android.gms.cast.framework.SessionProvider;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@ -36,7 +37,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider {
@Override @Override
public List<SessionProvider> getAdditionalSessionProviders(Context context) { public List<SessionProvider> getAdditionalSessionProviders(Context context) {
return null; return Collections.emptyList();
} }
} }

View File

@ -31,9 +31,9 @@ android {
} }
dependencies { dependencies {
api 'org.chromium.net:cronet-embedded:73.3683.76' api 'org.chromium.net:cronet-embedded:75.3770.101'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -38,7 +38,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -172,10 +172,35 @@ import java.util.List;
private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) { private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
switch (mimeType) { switch (mimeType) {
case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_AAC:
case MimeTypes.AUDIO_ALAC:
case MimeTypes.AUDIO_OPUS: case MimeTypes.AUDIO_OPUS:
return initializationData.get(0); return initializationData.get(0);
case MimeTypes.AUDIO_ALAC:
return getAlacExtraData(initializationData);
case MimeTypes.AUDIO_VORBIS: case MimeTypes.AUDIO_VORBIS:
return getVorbisExtraData(initializationData);
default:
// Other codecs do not require extra data.
return null;
}
}
private static byte[] getAlacExtraData(List<byte[]> initializationData) {
// FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra
// data. initializationData[0] contains only the magic cookie, and so we need to package it into
// an ALAC atom. See:
// https://ffmpeg.org/doxygen/0.6/alac_8c.html
// https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt
byte[] magicCookie = initializationData.get(0);
int alacAtomLength = 12 + magicCookie.length;
ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength);
alacAtom.putInt(alacAtomLength);
alacAtom.putInt(0x616c6163); // type=alac
alacAtom.putInt(0); // version=0, flags=0
alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length);
return alacAtom.array();
}
private static byte[] getVorbisExtraData(List<byte[]> initializationData) {
byte[] header0 = initializationData.get(0); byte[] header0 = initializationData.get(0);
byte[] header1 = initializationData.get(1); byte[] header1 = initializationData.get(1);
byte[] extraData = new byte[header0.length + header1.length + 6]; byte[] extraData = new byte[header0.length + header1.length + 6];
@ -188,10 +213,6 @@ import java.util.List;
extraData[header0.length + 5] = (byte) (header1.length & 0xFF); extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length); System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
return extraData; return extraData;
default:
// Other codecs do not require extra data.
return null;
}
} }
private native long ffmpegInitialize( private native long ffmpegInitialize(

View File

@ -39,7 +39,8 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')

View File

@ -9,6 +9,9 @@
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*; *;
} }
-keep class com.google.android.exoplayer2.util.FlacStreamInfo { -keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
*;
}
-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
*; *;
} }

View File

@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest {
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
SeekMap seekMap = seeker.getSeekMap(); SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull(); assertThat(seekMap).isNotNull();
@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest {
decoderJni.setData(input); decoderJni.setData(input);
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000); seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue(); assertThat(seeker.isSeeking()).isTrue();

View File

@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
public class FlacExtractorTest { public class FlacExtractorTest {
@Before @Before
public void setUp() throws Exception { public void setUp() {
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
fail("Flac library not available."); fail("Flac library not available.");
} }

View File

@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
private final FlacDecoderJni decoderJni; private final FlacDecoderJni decoderJni;
public FlacBinarySearchSeeker( public FlacBinarySearchSeeker(
FlacStreamInfo streamInfo, FlacStreamMetadata streamMetadata,
long firstFramePosition, long firstFramePosition,
long inputLength, long inputLength,
FlacDecoderJni decoderJni) { FlacDecoderJni decoderJni) {
super( super(
new FlacSeekTimestampConverter(streamInfo), new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni), new FlacTimestampSeeker(decoderJni),
streamInfo.durationUs(), streamMetadata.durationUs(),
/* floorTimePosition= */ 0, /* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamInfo.totalSamples, /* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition, /* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength, /* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(), /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize)); /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni); this.decoderJni = Assertions.checkNotNull(decoderJni);
} }
@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
* the timestamp for a stream seek time position. * the timestamp for a stream seek time position.
*/ */
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
private final FlacStreamInfo streamInfo; private final FlacStreamMetadata streamMetadata;
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) { public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
this.streamInfo = streamInfo; this.streamMetadata = streamMetadata;
} }
@Override @Override
public long timeUsToTargetTime(long timeUs) { public long timeUsToTargetTime(long timeUs) {
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs); return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
} }
} }
} }

View File

@ -15,11 +15,13 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
@ -56,21 +58,20 @@ import java.util.List;
} }
decoderJni = new FlacDecoderJni(); decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
FlacStreamInfo streamInfo; FlacStreamMetadata streamMetadata;
try { try {
streamInfo = decoderJni.decodeMetadata(); streamMetadata = decoderJni.decodeStreamMetadata();
} catch (ParserException e) {
throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// Never happens. // Never happens.
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
if (streamInfo == null) {
throw new FlacDecoderException("Metadata decoding failed");
}
int initialInputBufferSize = int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize); setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamInfo.maxDecodedFrameSize(); maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
} }
@Override @Override
@ -94,6 +95,7 @@ import java.util.List;
} }
@Override @Override
@Nullable
protected FlacDecoderException decode( protected FlacDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) { if (reset) {

View File

@ -15,9 +15,12 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -37,14 +40,14 @@ import java.nio.ByteBuffer;
} }
} }
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.
private final long nativeDecoderContext; private final long nativeDecoderContext;
private ByteBuffer byteBufferData; @Nullable private ByteBuffer byteBufferData;
private ExtractorInput extractorInput; @Nullable private ExtractorInput extractorInput;
@Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput; private boolean endOfExtractorInput;
private byte[] tempBuffer;
public FlacDecoderJni() throws FlacDecoderException { public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
@ -57,67 +60,79 @@ import java.nio.ByteBuffer;
} }
/** /**
* Sets data to be parsed by libflac. * Sets the data to be parsed.
* @param byteBufferData Source {@link ByteBuffer} *
* @param byteBufferData Source {@link ByteBuffer}.
*/ */
public void setData(ByteBuffer byteBufferData) { public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData; this.byteBufferData = byteBufferData;
this.extractorInput = null; this.extractorInput = null;
this.tempBuffer = null;
} }
/** /**
* Sets data to be parsed by libflac. * Sets the data to be parsed.
* @param extractorInput Source {@link ExtractorInput} *
* @param extractorInput Source {@link ExtractorInput}.
*/ */
public void setData(ExtractorInput extractorInput) { public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null; this.byteBufferData = null;
this.extractorInput = extractorInput; this.extractorInput = extractorInput;
if (tempBuffer == null) {
this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
}
endOfExtractorInput = false; endOfExtractorInput = false;
if (tempBuffer == null) {
tempBuffer = new byte[TEMP_BUFFER_SIZE];
}
} }
/**
* Returns whether the end of the data to be parsed has been reached, or true if no data was set.
*/
public boolean isEndOfData() { public boolean isEndOfData() {
if (byteBufferData != null) { if (byteBufferData != null) {
return byteBufferData.remaining() == 0; return byteBufferData.remaining() == 0;
} else if (extractorInput != null) { } else if (extractorInput != null) {
return endOfExtractorInput; return endOfExtractorInput;
} } else {
return true; return true;
} }
}
/** Clears the data to be parsed. */
public void clearData() {
byteBufferData = null;
extractorInput = null;
}
/** /**
* Reads up to {@code length} bytes from the data source. * Reads up to {@code length} bytes from the data source.
* <p> *
* This method blocks until at least one byte of data can be read, the end of the input is * <p>This method blocks until at least one byte of data can be read, the end of the input is
* detected or an exception is thrown. * detected or an exception is thrown.
* <p>
* This method is called from the native code.
* *
* @param target A target {@link ByteBuffer} into which data should be written. * @param target A target {@link ByteBuffer} into which data should be written.
* @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been
* zero; it just means all the data read from the source. * read from the source, then 0 is returned.
*/ */
@SuppressWarnings("unused") // Called from native code.
public int read(ByteBuffer target) throws IOException, InterruptedException { public int read(ByteBuffer target) throws IOException, InterruptedException {
int byteCount = target.remaining(); int byteCount = target.remaining();
if (byteBufferData != null) { if (byteBufferData != null) {
byteCount = Math.min(byteCount, byteBufferData.remaining()); byteCount = Math.min(byteCount, byteBufferData.remaining());
int originalLimit = byteBufferData.limit(); int originalLimit = byteBufferData.limit();
byteBufferData.limit(byteBufferData.position() + byteCount); byteBufferData.limit(byteBufferData.position() + byteCount);
target.put(byteBufferData); target.put(byteBufferData);
byteBufferData.limit(originalLimit); byteBufferData.limit(originalLimit);
} else if (extractorInput != null) { } else if (extractorInput != null) {
ExtractorInput extractorInput = this.extractorInput;
byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE); byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
int read = readFromExtractorInput(0, byteCount); int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
if (read < 4) { if (read < 4) {
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in // Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
// the buffer of the input. Do another read to reduce the number of calls to this method // the buffer of the input. Do another read to reduce the number of calls to this method
// from the native code. // from the native code.
read += readFromExtractorInput(read, byteCount - read); read +=
readFromExtractorInput(
extractorInput, tempBuffer, read, /* length= */ byteCount - read);
} }
byteCount = read; byteCount = read;
target.put(tempBuffer, 0, byteCount); target.put(tempBuffer, 0, byteCount);
@ -127,9 +142,13 @@ import java.nio.ByteBuffer;
return byteCount; return byteCount;
} }
/** Decodes and consumes the StreamInfo section from the FLAC stream. */ /** Decodes and consumes the metadata from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext); FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
if (streamMetadata == null) {
throw new ParserException("Failed to decode stream metadata");
}
return streamMetadata;
} }
/** /**
@ -234,7 +253,8 @@ import java.nio.ByteBuffer;
flacRelease(nativeDecoderContext); flacRelease(nativeDecoderContext);
} }
private int readFromExtractorInput(int offset, int length) private int readFromExtractorInput(
ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
throws IOException, InterruptedException { throws IOException, InterruptedException {
int read = extractorInput.read(tempBuffer, offset, length); int read = extractorInput.read(tempBuffer, offset, length);
if (read == C.RESULT_END_OF_INPUT) { if (read == C.RESULT_END_OF_INPUT) {
@ -246,7 +266,7 @@ import java.nio.ByteBuffer;
private native long flacInit(); private native long flacInit();
private native FlacStreamInfo flacDecodeMetadata(long context) private native FlacStreamMetadata flacDecodeMetadata(long context)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)

View File

@ -21,7 +21,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -33,7 +33,8 @@ import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
@ -42,6 +43,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* Facilitates the extraction of data from the FLAC container format. * Facilitates the extraction of data from the FLAC container format.
@ -74,23 +78,20 @@ public final class FlacExtractor implements Extractor {
*/ */
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
private final ParsableByteArray outputBuffer;
private final Id3Peeker id3Peeker; private final Id3Peeker id3Peeker;
private final boolean isId3MetadataDisabled; private final boolean id3MetadataDisabled;
private FlacDecoderJni decoderJni; @Nullable private FlacDecoderJni decoderJni;
private @MonotonicNonNull ExtractorOutput extractorOutput;
private @MonotonicNonNull TrackOutput trackOutput;
private ExtractorOutput extractorOutput; private boolean streamMetadataDecoded;
private TrackOutput trackOutput; private @MonotonicNonNull FlacStreamMetadata streamMetadata;
private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
private ParsableByteArray outputBuffer; @Nullable private Metadata id3Metadata;
private ByteBuffer outputByteBuffer; @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
private FlacStreamInfo streamInfo;
private Metadata id3Metadata;
private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
private boolean readPastStreamInfo;
/** Constructs an instance with flags = 0. */ /** Constructs an instance with flags = 0. */
public FlacExtractor() { public FlacExtractor() {
@ -103,8 +104,9 @@ public final class FlacExtractor implements Extractor {
* @param flags Flags that control the extractor's behavior. * @param flags Flags that control the extractor's behavior.
*/ */
public FlacExtractor(int flags) { public FlacExtractor(int flags) {
outputBuffer = new ParsableByteArray();
id3Peeker = new Id3Peeker(); id3Peeker = new Id3Peeker();
isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
} }
@Override @Override
@ -130,17 +132,19 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public int read(final ExtractorInput input, PositionHolder seekPosition) public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) { if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input); id3Metadata = peekId3Data(input);
} }
decoderJni.setData(input); FlacDecoderJni decoderJni = initDecoderJni(input);
readPastStreamInfo(input); try {
decodeStreamMetadata(input);
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) { if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition); return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
} }
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
long lastDecodePosition = decoderJni.getDecodePosition(); long lastDecodePosition = decoderJni.getDecodePosition();
try { try {
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
@ -152,26 +156,29 @@ public final class FlacExtractor implements Extractor {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
} finally {
decoderJni.clearData();
}
} }
@Override @Override
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
if (position == 0) { if (position == 0) {
readPastStreamInfo = false; streamMetadataDecoded = false;
} }
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.reset(position); decoderJni.reset(position);
} }
if (flacBinarySearchSeeker != null) { if (binarySearchSeeker != null) {
flacBinarySearchSeeker.setSeekTargetUs(timeUs); binarySearchSeeker.setSeekTargetUs(timeUs);
} }
} }
@Override @Override
public void release() { public void release() {
flacBinarySearchSeeker = null; binarySearchSeeker = null;
if (decoderJni != null) { if (decoderJni != null) {
decoderJni.release(); decoderJni.release();
decoderJni = null; decoderJni = null;
@ -179,123 +186,141 @@ public final class FlacExtractor implements Extractor {
} }
/** /**
* Peeks ID3 tag data (if present) at the beginning of the input. * Peeks ID3 tag data at the beginning of the input.
* *
* @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
* present in the input.
*/ */
@Nullable @Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition(); input.resetPeekPosition();
Id3Decoder.FramePredicate id3FramePredicate = Id3Decoder.FramePredicate id3FramePredicate =
isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate); return id3Peeker.peekId3Data(input, id3FramePredicate);
} }
@EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
private FlacDecoderJni initDecoderJni(ExtractorInput input) {
FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);
decoderJni.setData(input);
return decoderJni;
}
@RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
@EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
if (streamMetadataDecoded) {
return;
}
FlacStreamMetadata streamMetadata;
try {
streamMetadata = decoderJni.decodeStreamMetadata();
} catch (IOException e) {
decoderJni.reset(/* newPosition= */ 0);
input.setRetryPosition(/* position= */ 0, e);
throw e;
}
streamMetadataDecoded = true;
if (this.streamMetadata == null) {
this.streamMetadata = streamMetadata;
binarySearchSeeker =
outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
if (streamMetadata.metadata != null) {
metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
}
outputFormat(streamMetadata, metadata, trackOutput);
outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
}
}
@RequiresNonNull("binarySearchSeeker")
private int handlePendingSeek(
ExtractorInput input,
PositionHolder seekPosition,
ParsableByteArray outputBuffer,
OutputFrameHolder outputFrameHolder,
TrackOutput trackOutput)
throws InterruptedException, IOException {
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
}
return seekResult;
}
/** /**
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
* *
* @return Whether the input begins with {@link #FLAC_SIGNATURE}. * @return Whether the input begins with {@link #FLAC_SIGNATURE}.
*/ */
private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException { private static boolean peekFlacSignature(ExtractorInput input)
throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length]; byte[] header = new byte[FLAC_SIGNATURE.length];
input.peekFully(header, 0, FLAC_SIGNATURE.length); input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE); return Arrays.equals(header, FLAC_SIGNATURE);
} }
private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException { /**
if (readPastStreamInfo) { * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
return; * handle seeks.
} */
@Nullable
FlacStreamInfo streamInfo = decodeStreamInfo(input); private static FlacBinarySearchSeeker outputSeekMap(
readPastStreamInfo = true; FlacDecoderJni decoderJni,
if (this.streamInfo == null) { FlacStreamMetadata streamMetadata,
updateFlacStreamInfo(input, streamInfo); long streamLength,
} ExtractorOutput output) {
} boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
FlacBinarySearchSeeker binarySearchSeeker = null;
private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) { SeekMap seekMap;
this.streamInfo = streamInfo; if (hasSeekTable) {
outputSeekMap(input, streamInfo); seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
outputFormat(streamInfo); } else if (streamLength != C.LENGTH_UNSET) {
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
}
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
throws InterruptedException, IOException {
try {
FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
if (streamInfo == null) {
throw new IOException("Metadata decoding failed");
}
return streamInfo;
} catch (IOException e) {
decoderJni.reset(0);
input.setRetryPosition(0, e);
throw e;
}
}
private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
SeekMap seekMap =
hasSeekTable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
: getSeekMapForNonSeekTableFlac(input, streamInfo);
extractorOutput.seekMap(seekMap);
}
private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
long inputLength = input.getLength();
if (inputLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition(); long firstFramePosition = decoderJni.getDecodePosition();
flacBinarySearchSeeker = binarySearchSeeker =
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
return flacBinarySearchSeeker.getSeekMap(); seekMap = binarySearchSeeker.getSeekMap();
} else { // can't seek at all, because there's no SeekTable and the input length is unknown. } else {
return new SeekMap.Unseekable(streamInfo.durationUs()); seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
} }
output.seekMap(seekMap);
return binarySearchSeeker;
} }
private void outputFormat(FlacStreamInfo streamInfo) { private static void outputFormat(
FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
Format mediaFormat = Format mediaFormat =
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
/* id= */ null, /* id= */ null,
MimeTypes.AUDIO_RAW, MimeTypes.AUDIO_RAW,
/* codecs= */ null, /* codecs= */ null,
streamInfo.bitRate(), streamMetadata.bitRate(),
streamInfo.maxDecodedFrameSize(), streamMetadata.maxDecodedFrameSize(),
streamInfo.channels, streamMetadata.channels,
streamInfo.sampleRate, streamMetadata.sampleRate,
getPcmEncoding(streamInfo.bitsPerSample), getPcmEncoding(streamMetadata.bitsPerSample),
/* encoderDelay= */ 0, /* encoderDelay= */ 0,
/* encoderPadding= */ 0, /* encoderPadding= */ 0,
/* initializationData= */ null, /* initializationData= */ null,
/* drmInitData= */ null, /* drmInitData= */ null,
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* language= */ null, /* language= */ null,
isId3MetadataDisabled ? null : id3Metadata); metadata);
trackOutput.format(mediaFormat); output.format(mediaFormat);
} }
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) private static void outputSample(
throws InterruptedException, IOException { ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
int seekResult = sampleData.setPosition(0);
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); output.sampleData(sampleData, size);
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; output.sampleMetadata(
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
}
return seekResult;
}
private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
outputBuffer.setPosition(0);
trackOutput.sampleData(outputBuffer, size);
trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
} }
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */ /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */

View File

@ -14,9 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
#include <jni.h>
#include <android/log.h> #include <android/log.h>
#include <jni.h>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include "include/flac_parser.h" #include "include/flac_parser.h"
#define LOG_TAG "flac_jni" #define LOG_TAG "flac_jni"
@ -95,19 +98,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
return NULL; return NULL;
} }
jclass arrayListClass = env->FindClass("java/util/ArrayList");
jmethodID arrayListConstructor =
env->GetMethodID(arrayListClass, "<init>", "()V");
jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
jmethodID arrayListAddMethod =
env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
if (context->parser->areVorbisCommentsValid()) {
std::vector<std::string> vorbisComments =
context->parser->getVorbisComments();
for (std::vector<std::string>::const_iterator vorbisComment =
vorbisComments.begin();
vorbisComment != vorbisComments.end(); ++vorbisComment) {
jstring commentString = env->NewStringUTF((*vorbisComment).c_str());
env->CallBooleanMethod(commentList, arrayListAddMethod, commentString);
env->DeleteLocalRef(commentString);
}
}
jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor);
bool picturesValid = context->parser->arePicturesValid();
if (picturesValid) {
std::vector<FlacPicture> pictures = context->parser->getPictures();
jclass pictureFrameClass = env->FindClass(
"com/google/android/exoplayer2/metadata/flac/PictureFrame");
jmethodID pictureFrameConstructor =
env->GetMethodID(pictureFrameClass, "<init>",
"(ILjava/lang/String;Ljava/lang/String;IIII[B)V");
for (std::vector<FlacPicture>::const_iterator picture = pictures.begin();
picture != pictures.end(); ++picture) {
jstring mimeType = env->NewStringUTF(picture->mimeType.c_str());
jstring description = env->NewStringUTF(picture->description.c_str());
jbyteArray pictureData = env->NewByteArray(picture->data.size());
env->SetByteArrayRegion(pictureData, 0, picture->data.size(),
(signed char *)&picture->data[0]);
jobject pictureFrame = env->NewObject(
pictureFrameClass, pictureFrameConstructor, picture->type, mimeType,
description, picture->width, picture->height, picture->depth,
picture->colors, pictureData);
env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame);
env->DeleteLocalRef(mimeType);
env->DeleteLocalRef(description);
env->DeleteLocalRef(pictureData);
}
}
const FLAC__StreamMetadata_StreamInfo &streamInfo = const FLAC__StreamMetadata_StreamInfo &streamInfo =
context->parser->getStreamInfo(); context->parser->getStreamInfo();
jclass cls = env->FindClass( jclass flacStreamMetadataClass = env->FindClass(
"com/google/android/exoplayer2/util/" "com/google/android/exoplayer2/util/"
"FlacStreamInfo"); "FlacStreamMetadata");
jmethodID constructor = env->GetMethodID(cls, "<init>", "(IIIIIIIJ)V"); jmethodID flacStreamMetadataConstructor =
env->GetMethodID(flacStreamMetadataClass, "<init>",
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
return env->NewObject(cls, constructor, streamInfo.min_blocksize, return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.max_blocksize, streamInfo.min_framesize, streamInfo.min_blocksize, streamInfo.max_blocksize,
streamInfo.max_framesize, streamInfo.sample_rate, streamInfo.min_framesize, streamInfo.max_framesize,
streamInfo.channels, streamInfo.bits_per_sample, streamInfo.sample_rate, streamInfo.channels,
streamInfo.total_samples); streamInfo.bits_per_sample, streamInfo.total_samples,
commentList, pictureFrames);
} }
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {

View File

@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE: case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table; mSeekTable = &metadata->data.seek_table;
break; break;
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
if (!mVorbisCommentsValid) {
FLAC__StreamMetadata_VorbisComment vorbisComment =
metadata->data.vorbis_comment;
for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {
FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =
vorbisComment.comments[i];
if (vorbisCommentEntry.entry != NULL) {
std::string comment(
reinterpret_cast<char *>(vorbisCommentEntry.entry),
vorbisCommentEntry.length);
mVorbisComments.push_back(comment);
}
}
mVorbisCommentsValid = true;
} else {
ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
}
break;
case FLAC__METADATA_TYPE_PICTURE: {
const FLAC__StreamMetadata_Picture *parsedPicture =
&metadata->data.picture;
FlacPicture picture;
picture.mimeType.assign(std::string(parsedPicture->mime_type));
picture.description.assign(
std::string((char *)parsedPicture->description));
picture.data.assign(parsedPicture->data,
parsedPicture->data + parsedPicture->data_length);
picture.width = parsedPicture->width;
picture.height = parsedPicture->height;
picture.depth = parsedPicture->depth;
picture.colors = parsedPicture->colors;
picture.type = parsedPicture->type;
mPictures.push_back(picture);
mPicturesValid = true;
break;
}
default: default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break; break;
@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source)
mCurrentPos(0LL), mCurrentPos(0LL),
mEOF(false), mEOF(false),
mStreamInfoValid(false), mStreamInfoValid(false),
mVorbisCommentsValid(false),
mPicturesValid(false),
mWriteRequested(false), mWriteRequested(false),
mWriteCompleted(false), mWriteCompleted(false),
mWriteBuffer(NULL), mWriteBuffer(NULL),
@ -266,6 +305,10 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO); FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE); FLAC__METADATA_TYPE_SEEKTABLE);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_PICTURE);
FLAC__StreamDecoderInitStatus initStatus; FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream( initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback, mDecoder, read_callback, seek_callback, tell_callback, length_callback,

View File

@ -19,6 +19,10 @@
#include <stdint.h> #include <stdint.h>
#include <cstdlib>
#include <string>
#include <vector>
// libFLAC parser // libFLAC parser
#include "FLAC/stream_decoder.h" #include "FLAC/stream_decoder.h"
@ -26,6 +30,17 @@
typedef int status_t; typedef int status_t;
struct FlacPicture {
int type;
std::string mimeType;
std::string description;
FLAC__uint32 width;
FLAC__uint32 height;
FLAC__uint32 depth;
FLAC__uint32 colors;
std::vector<char> data;
};
class FLACParser { class FLACParser {
public: public:
FLACParser(DataSource *source); FLACParser(DataSource *source);
@ -44,6 +59,14 @@ class FLACParser {
return mStreamInfo; return mStreamInfo;
} }
bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
std::vector<std::string> getVorbisComments() { return mVorbisComments; }
bool arePicturesValid() const { return mPicturesValid; }
const std::vector<FlacPicture> &getPictures() const { return mPictures; }
int64_t getLastFrameTimestamp() const { int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
} }
@ -71,6 +94,10 @@ class FLACParser {
mEOF = false; mEOF = false;
if (newPosition == 0) { if (newPosition == 0) {
mStreamInfoValid = false; mStreamInfoValid = false;
mVorbisCommentsValid = false;
mPicturesValid = false;
mVorbisComments.clear();
mPictures.clear();
FLAC__stream_decoder_reset(mDecoder); FLAC__stream_decoder_reset(mDecoder);
} else { } else {
FLAC__stream_decoder_flush(mDecoder); FLAC__stream_decoder_flush(mDecoder);
@ -116,6 +143,14 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable; const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset; uint64_t firstFrameOffset;
// cached when the VORBIS_COMMENT metadata is parsed by libFLAC
std::vector<std::string> mVorbisComments;
bool mVorbisCommentsValid;
// cached when the PICTURE metadata is parsed by libFLAC
std::vector<FlacPicture> mPictures;
bool mPicturesValid;
// cached when a decoded PCM block is "written" by libFLAC parser // cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested; bool mWriteRequested;
bool mWriteCompleted; bool mWriteCompleted;

View File

@ -33,7 +33,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
api 'com.google.vr:sdk-base:1.190.0' api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }

View File

@ -34,7 +34,7 @@ android {
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -1,7 +1,11 @@
# ExoPlayer Firebase JobDispatcher extension # # ExoPlayer Firebase JobDispatcher extension #
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ## ## Getting the extension ##
@ -20,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][]. [top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md

View File

@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util;
* *
* @see <a * @see <a
* href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a> * href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a>
* @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link
* com.google.android.exoplayer2.scheduler.PlatformScheduler}.
*/ */
@Deprecated
public final class JobDispatcherScheduler implements Scheduler { public final class JobDispatcherScheduler implements Scheduler {
private static final boolean DEBUG = false; private static final boolean DEBUG = false;

View File

@ -32,7 +32,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.leanback:leanback:1.0.0' implementation 'androidx.leanback:leanback:1.0.0'
} }

View File

@ -33,7 +33,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
api 'com.squareup.okhttp3:okhttp:3.12.1' api 'com.squareup.okhttp3:okhttp:3.12.1'
} }

View File

@ -39,6 +39,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.1.0'
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.opus; package com.google.android.exoplayer2.ext.opus;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@ -150,6 +151,7 @@ import java.util.List;
} }
@Override @Override
@Nullable
protected OpusDecoderException decode( protected OpusDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) { if (reset) {

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.opus; package com.google.android.exoplayer2.ext.opus;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.LibraryLoader;
@ -49,9 +50,8 @@ public final class OpusLibrary {
return LOADER.isAvailable(); return LOADER.isAvailable();
} }
/** /** Returns the version of the underlying library if available, or null otherwise. */
* Returns the version of the underlying library if available, or null otherwise. @Nullable
*/
public static String getVersion() { public static String getVersion() {
return isAvailable() ? opusGetVersion() : null; return isAvailable() ? opusGetVersion() : null;
} }

View File

@ -33,7 +33,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'net.butterflytv.utils:rtmp-client:3.0.1' implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
} }

View File

@ -39,7 +39,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils-robolectric')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.vp9; package com.google.android.exoplayer2.ext.vp9;
import androidx.annotation.Nullable;
import android.view.Surface; import android.view.Surface;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
@ -120,8 +121,9 @@ import java.nio.ByteBuffer;
} }
@Override @Override
protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, @Nullable
boolean reset) { protected VpxDecoderException decode(
VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) {
ByteBuffer inputData = inputBuffer.data; ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit(); int inputSize = inputData.limit();
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.vp9; package com.google.android.exoplayer2.ext.vp9;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.LibraryLoader;
@ -49,9 +50,8 @@ public final class VpxLibrary {
return LOADER.isAvailable(); return LOADER.isAvailable();
} }
/** /** Returns the version of the underlying library if available, or null otherwise. */
* Returns the version of the underlying library if available, or null otherwise. @Nullable
*/
public static String getVersion() { public static String getVersion() {
return isAvailable() ? vpxGetVersion() : null; return isAvailable() ? vpxGetVersion() : null;
} }
@ -60,6 +60,7 @@ public final class VpxLibrary {
* Returns the configuration string with which the underlying library was built if available, or * Returns the configuration string with which the underlying library was built if available, or
* null otherwise. * null otherwise.
*/ */
@Nullable
public static String getBuildConfig() { public static String getBuildConfig() {
return isAvailable() ? vpxGetBuildConfig() : null; return isAvailable() ? vpxGetBuildConfig() : null;
} }

View File

@ -0,0 +1,22 @@
# ExoPlayer WorkManager extension
This extension provides a Scheduler implementation which uses [WorkManager][].
[WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html
## Getting the extension
The easiest way to use the extension is to add it as a gradle dependency:
```gradle
implementation 'com.google.android.exoplayer:extension-workmanager:2.X.X'
```
where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module
locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.work:work-runtime:2.1.0'
}
ext {
javadocTitle = 'WorkManager extension'
}
apply from: '../../javadoc_library.gradle'
ext {
releaseArtifact = 'extension-workmanager'
releaseDescription = 'WorkManager extension for ExoPlayer.'
}
apply from: '../../publish.gradle'

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<manifest package="com.google.android.exoplayer2.ext.workmanager"/>

View File

@ -0,0 +1,161 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.workmanager;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
/** A {@link Scheduler} that uses {@link WorkManager}. */
public final class WorkManagerScheduler implements Scheduler {
private static final boolean DEBUG = false;
private static final String TAG = "WorkManagerScheduler";
private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package";
private static final String KEY_REQUIREMENTS = "requirements";
private final String workName;
/**
* @param workName A name for work scheduled by this instance. If the same name was used by a
* previous instance, anything scheduled by the previous instance will be canceled by this
* instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} are
* called.
*/
public WorkManagerScheduler(String workName) {
this.workName = workName;
}
@Override
public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
Constraints constraints = buildConstraints(requirements);
Data inputData = buildInputData(requirements, servicePackage, serviceAction);
OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData);
logd("Scheduling work: " + workName);
WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest);
return true;
}
@Override
public boolean cancel() {
logd("Canceling work: " + workName);
WorkManager.getInstance().cancelUniqueWork(workName);
return true;
}
private static Constraints buildConstraints(Requirements requirements) {
Constraints.Builder builder = new Constraints.Builder();
if (requirements.isUnmeteredNetworkRequired()) {
builder.setRequiredNetworkType(NetworkType.UNMETERED);
} else if (requirements.isNetworkRequired()) {
builder.setRequiredNetworkType(NetworkType.CONNECTED);
} else {
builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
}
if (requirements.isChargingRequired()) {
builder.setRequiresCharging(true);
}
if (requirements.isIdleRequired() && Util.SDK_INT >= 23) {
setRequiresDeviceIdle(builder);
}
return builder.build();
}
@TargetApi(23)
private static void setRequiresDeviceIdle(Constraints.Builder builder) {
builder.setRequiresDeviceIdle(true);
}
private static Data buildInputData(
Requirements requirements, String servicePackage, String serviceAction) {
Data.Builder builder = new Data.Builder();
builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.putString(KEY_SERVICE_PACKAGE, servicePackage);
builder.putString(KEY_SERVICE_ACTION, serviceAction);
return builder.build();
}
private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) {
OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class);
builder.setConstraints(constraints);
builder.setInputData(inputData);
return builder.build();
}
private static void logd(String message) {
if (DEBUG) {
Log.d(TAG, message);
}
}
/** A {@link Worker} that starts the target service if the requirements are met. */
// This class needs to be public so that WorkManager can instantiate it.
public static final class SchedulerWorker extends Worker {
private final WorkerParameters workerParams;
private final Context context;
public SchedulerWorker(Context context, WorkerParameters workerParams) {
super(context, workerParams);
this.workerParams = workerParams;
this.context = context;
}
@Override
public Result doWork() {
logd("SchedulerWorker is started");
Data inputData = workerParams.getInputData();
Assertions.checkNotNull(inputData, "Work started without input data.");
Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0));
if (requirements.checkRequirements(context)) {
logd("Requirements are met");
String serviceAction = inputData.getString(KEY_SERVICE_ACTION);
String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE);
Assertions.checkNotNull(serviceAction, "Service action missing.");
Assertions.checkNotNull(servicePackage, "Service package missing.");
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
Util.startForegroundService(context, intent);
return Result.success();
} else {
logd("Requirements are not met");
return Result.retry();
}
}
}
}

View File

@ -58,7 +58,7 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion

View File

@ -532,7 +532,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
public long getContentPosition() { public long getContentPosition() {
if (isPlayingAd()) { if (isPlayingAd()) {
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); return playbackInfo.contentPositionUs == C.TIME_UNSET
? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs()
: period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
} else { } else {
return getCurrentPosition(); return getCurrentPosition();
} }

View File

@ -1304,8 +1304,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
Pair<Object, Long> defaultPosition = Pair<Object, Long> defaultPosition =
getPeriodPosition( getPeriodPosition(
timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second);
if (!newPeriodId.isAd()) {
// Keep unset start position if we need to play an ad first.
newContentPositionUs = defaultPosition.second; newContentPositionUs = defaultPosition.second;
newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); }
} else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) { } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) {
// The current period isn't in the new timeline. Attempt to resolve a subsequent period whose // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose
// window we can restart from. // window we can restart from.

View File

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.10.3"; public static final String VERSION = "2.10.4";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.3"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.4";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2010003; public static final int VERSION_INT = 2010004;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -29,7 +29,8 @@ import com.google.android.exoplayer2.util.Util;
public final long startPositionUs; public final long startPositionUs;
/** /**
* If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET}
* otherwise. * if this is not an ad or the next content media period should be played from its default
* position.
*/ */
public final long contentPositionUs; public final long contentPositionUs;
/** /**

View File

@ -144,7 +144,9 @@ import com.google.android.exoplayer2.util.Assertions;
MediaPeriodInfo info) { MediaPeriodInfo info) {
long rendererPositionOffsetUs = long rendererPositionOffsetUs =
loading == null loading == null
? (info.id.isAd() ? info.contentPositionUs : 0) ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET
? info.contentPositionUs
: 0)
: (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);
MediaPeriodHolder newPeriodHolder = MediaPeriodHolder newPeriodHolder =
new MediaPeriodHolder( new MediaPeriodHolder(
@ -560,6 +562,7 @@ import com.google.android.exoplayer2.util.Assertions;
} }
long startPositionUs; long startPositionUs;
long contentPositionUs;
int nextWindowIndex = int nextWindowIndex =
timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;
Object nextPeriodUid = period.uid; Object nextPeriodUid = period.uid;
@ -568,6 +571,7 @@ import com.google.android.exoplayer2.util.Assertions;
// We're starting to buffer a new window. When playback transitions to this window we'll // We're starting to buffer a new window. When playback transitions to this window we'll
// want it to be from its default start position, so project the default start position // want it to be from its default start position, so project the default start position
// forward by the duration of the buffer, and start buffering from this point. // forward by the duration of the buffer, and start buffering from this point.
contentPositionUs = C.TIME_UNSET;
Pair<Object, Long> defaultPosition = Pair<Object, Long> defaultPosition =
timeline.getPeriodPosition( timeline.getPeriodPosition(
window, window,
@ -587,12 +591,13 @@ import com.google.android.exoplayer2.util.Assertions;
windowSequenceNumber = nextWindowSequenceNumber++; windowSequenceNumber = nextWindowSequenceNumber++;
} }
} else { } else {
// We're starting to buffer a new period within the same window.
startPositionUs = 0; startPositionUs = 0;
contentPositionUs = 0;
} }
MediaPeriodId periodId = MediaPeriodId periodId =
resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber);
return getMediaPeriodInfo( return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs);
periodId, /* contentPositionUs= */ startPositionUs, startPositionUs);
} }
MediaPeriodId currentPeriodId = mediaPeriodInfo.id; MediaPeriodId currentPeriodId = mediaPeriodInfo.id;
@ -616,13 +621,11 @@ import com.google.android.exoplayer2.util.Assertions;
mediaPeriodInfo.contentPositionUs, mediaPeriodInfo.contentPositionUs,
currentPeriodId.windowSequenceNumber); currentPeriodId.windowSequenceNumber);
} else { } else {
// Play content from the ad group position. As a special case, if we're transitioning from a // Play content from the ad group position.
// preroll ad group to content and there are no other ad groups, project the start position
// forward as if this were a transition to a new window. No attempt is made to handle
// midrolls in live streams, as it's unclear what content position should play after an ad
// (server-side dynamic ad insertion is more appropriate for this use case).
long startPositionUs = mediaPeriodInfo.contentPositionUs; long startPositionUs = mediaPeriodInfo.contentPositionUs;
if (period.getAdGroupCount() == 1 && period.getAdGroupTimeUs(0) == 0) { if (startPositionUs == C.TIME_UNSET) {
// If we're transitioning from an ad group to content starting from its default position,
// project the start position forward as if this were a transition to a new window.
Pair<Object, Long> defaultPosition = Pair<Object, Long> defaultPosition =
timeline.getPeriodPosition( timeline.getPeriodPosition(
window, window,

View File

@ -48,7 +48,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
/** /**
* If {@link #periodId} refers to an ad, the position of the suspended content relative to the * If {@link #periodId} refers to an ad, the position of the suspended content relative to the
* start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET} * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET}
* if {@link #periodId} does not refer to an ad. * if {@link #periodId} does not refer to an ad or if the suspended content should be played from
* its default position.
*/ */
public final long contentPositionUs; public final long contentPositionUs;
/** The current playback state. One of the {@link Player}.STATE_ constants. */ /** The current playback state. One of the {@link Player}.STATE_ constants. */

View File

@ -364,8 +364,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return Collections.singletonList(passthroughDecoderInfo); return Collections.singletonList(passthroughDecoderInfo);
} }
} }
return mediaCodecSelector.getDecoderInfos( List<MediaCodecInfo> decoderInfos =
mediaCodecSelector.getDecoderInfos(
format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false);
if (MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType)) {
// E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D.
List<MediaCodecInfo> eac3DecoderInfos =
mediaCodecSelector.getDecoderInfos(
MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false);
decoderInfos.addAll(eac3DecoderInfos);
}
return Collections.unmodifiableList(decoderInfos);
} }
/** /**
@ -393,7 +402,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name); codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name);
passthroughEnabled = codecInfo.passthrough; passthroughEnabled = codecInfo.passthrough;
String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.mimeType; String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType;
MediaFormat mediaFormat = MediaFormat mediaFormat =
getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate); getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate);
codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0);

View File

@ -30,6 +30,7 @@ import java.util.Arrays;
private static final int MINIMUM_PITCH = 65; private static final int MINIMUM_PITCH = 65;
private static final int MAXIMUM_PITCH = 400; private static final int MAXIMUM_PITCH = 400;
private static final int AMDF_FREQUENCY = 4000; private static final int AMDF_FREQUENCY = 4000;
private static final int BYTES_PER_SAMPLE = 2;
private final int inputSampleRateHz; private final int inputSampleRateHz;
private final int channelCount; private final int channelCount;
@ -157,9 +158,9 @@ import java.util.Arrays;
maxDiff = 0; maxDiff = 0;
} }
/** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */ /** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */
public int getFramesAvailable() { public int getOutputSize() {
return outputFrameCount; return outputFrameCount * channelCount * BYTES_PER_SAMPLE;
} }
// Internal methods. // Internal methods.

View File

@ -210,7 +210,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
sonic.queueInput(shortBuffer); sonic.queueInput(shortBuffer);
inputBuffer.position(inputBuffer.position() + inputSize); inputBuffer.position(inputBuffer.position() + inputSize);
} }
int outputSize = sonic.getFramesAvailable() * channelCount * 2; int outputSize = sonic.getOutputSize();
if (outputSize > 0) { if (outputSize > 0) {
if (buffer.capacity() < outputSize) { if (buffer.capacity() < outputSize) {
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
@ -243,7 +243,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0); return inputEnded && (sonic == null || sonic.getOutputSize() == 0);
} }
@Override @Override

View File

@ -301,5 +301,6 @@ public abstract class SimpleDecoder<
* @param reset Whether the decoder must be reset before decoding. * @param reset Whether the decoder must be reset before decoding.
* @return A decoder exception if an error occurred, or null if decoding was successful. * @return A decoder exception if an error occurred, or null if decoding was successful.
*/ */
protected abstract @Nullable E decode(I inputBuffer, O outputBuffer, boolean reset); @Nullable
protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset);
} }

View File

@ -186,10 +186,6 @@ public final class MpegAudioHeader {
} }
} }
// Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that
// seeking to a given timestamp and playing from the start up to that timestamp give the same
// results for CBR streams. See also [internal: b/120390268].
bitrate = 8 * frameSize * sampleRate / samplesPerFrame;
String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);

View File

@ -119,7 +119,7 @@ public interface TrackOutput {
* Called to write sample data to the output. * Called to write sample data to the output.
* *
* @param data A {@link ParsableByteArray} from which to read the sample data. * @param data A {@link ParsableByteArray} from which to read the sample data.
* @param length The number of bytes to read. * @param length The number of bytes to read, starting from {@code data.getPosition()}.
*/ */
void sampleData(ParsableByteArray data, int length); void sampleData(ParsableByteArray data, int length);

View File

@ -15,8 +15,10 @@
*/ */
package com.google.android.exoplayer2.extractor.flv; package com.google.android.exoplayer2.extractor.flv;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@ -44,7 +46,7 @@ import java.util.Map;
private long durationUs; private long durationUs;
public ScriptTagPayloadReader() { public ScriptTagPayloadReader() {
super(null); super(new DummyTrackOutput());
durationUs = C.TIME_UNSET; durationUs = C.TIME_UNSET;
} }
@ -138,7 +140,10 @@ import java.util.Map;
ArrayList<Object> list = new ArrayList<>(count); ArrayList<Object> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
int type = readAmfType(data); int type = readAmfType(data);
list.add(readAmfData(data, type)); Object value = readAmfData(data, type);
if (value != null) {
list.add(value);
}
} }
return list; return list;
} }
@ -157,7 +162,10 @@ import java.util.Map;
if (type == AMF_TYPE_END_MARKER) { if (type == AMF_TYPE_END_MARKER) {
break; break;
} }
array.put(key, readAmfData(data, type)); Object value = readAmfData(data, type);
if (value != null) {
array.put(key, value);
}
} }
return array; return array;
} }
@ -174,7 +182,10 @@ import java.util.Map;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
String key = readAmfString(data); String key = readAmfString(data);
int type = readAmfType(data); int type = readAmfType(data);
array.put(key, readAmfData(data, type)); Object value = readAmfData(data, type);
if (value != null) {
array.put(key, value);
}
} }
return array; return array;
} }
@ -191,6 +202,7 @@ import java.util.Map;
return date; return date;
} }
@Nullable
private static Object readAmfData(ParsableByteArray data, int type) { private static Object readAmfData(ParsableByteArray data, int type) {
switch (type) { switch (type) {
case AMF_TYPE_NUMBER: case AMF_TYPE_NUMBER:
@ -208,8 +220,8 @@ import java.util.Map;
case AMF_TYPE_DATE: case AMF_TYPE_DATE:
return readAmfDate(data); return readAmfDate(data);
default: default:
// We don't log a warning because there are types that we knowingly don't support.
return null; return null;
} }
} }
} }

View File

@ -117,6 +117,7 @@ public final class Mp3Extractor implements Extractor {
private Seeker seeker; private Seeker seeker;
private long basisTimeUs; private long basisTimeUs;
private long samplesRead; private long samplesRead;
private long firstSamplePosition;
private int sampleBytesRemaining; private int sampleBytesRemaining;
public Mp3Extractor() { public Mp3Extractor() {
@ -214,6 +215,13 @@ public final class Mp3Extractor implements Extractor {
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* language= */ null, /* language= */ null,
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
firstSamplePosition = input.getPosition();
} else if (firstSamplePosition != 0) {
long inputPosition = input.getPosition();
if (inputPosition < firstSamplePosition) {
// Skip past the seek frame.
input.skipFully((int) (firstSamplePosition - inputPosition));
}
} }
return readSample(input); return readSample(input);
} }

View File

@ -1140,10 +1140,6 @@ import java.util.List;
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,
language); language);
} else if (childAtomType == Atom.TYPE_alac) {
initializationData = new byte[childAtomSize];
parent.setPosition(childPosition);
parent.readBytes(initializationData, /* offset= */ 0, childAtomSize);
} else if (childAtomType == Atom.TYPE_dOps) { } else if (childAtomType == Atom.TYPE_dOps) {
// Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic
// Signature and the body of the dOps atom. // Signature and the body of the dOps atom.
@ -1152,7 +1148,7 @@ import java.util.List;
System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length);
parent.setPosition(childPosition + Atom.HEADER_SIZE); parent.setPosition(childPosition + Atom.HEADER_SIZE);
parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); parent.readBytes(initializationData, opusMagic.length, childAtomBodySize);
} else if (childAtomSize == Atom.TYPE_dfLa) { } else if (childAtomSize == Atom.TYPE_dfLa || childAtomType == Atom.TYPE_alac) {
int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;
initializationData = new byte[childAtomBodySize]; initializationData = new byte[childAtomBodySize];
parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);

View File

@ -123,6 +123,7 @@ public final class Track {
* @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no
* such entry exists. * such entry exists.
*/ */
@Nullable
public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) {
return sampleDescriptionEncryptionBoxes == null ? null return sampleDescriptionEncryptionBoxes == null ? null
: sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];

View File

@ -52,7 +52,7 @@ public final class TrackEncryptionBox {
* If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the
* track encryption box or sample group description box. Null otherwise. * track encryption box or sample group description box. Null otherwise.
*/ */
public final byte[] defaultInitializationVector; @Nullable public final byte[] defaultInitializationVector;
/** /**
* @param isEncrypted See {@link #isEncrypted}. * @param isEncrypted See {@link #isEncrypted}.

View File

@ -16,29 +16,32 @@
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor.ogg;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
/** Seeks in an Ogg stream. */ /** Seeks in an Ogg stream. */
/* package */ final class DefaultOggSeeker implements OggSeeker { /* package */ final class DefaultOggSeeker implements OggSeeker {
@VisibleForTesting public static final int MATCH_RANGE = 72000; private static final int MATCH_RANGE = 72000;
@VisibleForTesting public static final int MATCH_BYTE_RANGE = 100000; private static final int MATCH_BYTE_RANGE = 100000;
private static final int DEFAULT_OFFSET = 30000; private static final int DEFAULT_OFFSET = 30000;
private static final int STATE_SEEK_TO_END = 0; private static final int STATE_SEEK_TO_END = 0;
private static final int STATE_READ_LAST_PAGE = 1; private static final int STATE_READ_LAST_PAGE = 1;
private static final int STATE_SEEK = 2; private static final int STATE_SEEK = 2;
private static final int STATE_IDLE = 3; private static final int STATE_SKIP = 3;
private static final int STATE_IDLE = 4;
private final OggPageHeader pageHeader = new OggPageHeader(); private final OggPageHeader pageHeader = new OggPageHeader();
private final long startPosition; private final long payloadStartPosition;
private final long endPosition; private final long payloadEndPosition;
private final StreamReader streamReader; private final StreamReader streamReader;
private int state; private int state;
@ -54,26 +57,27 @@ import java.io.IOException;
/** /**
* Constructs an OggSeeker. * Constructs an OggSeeker.
* *
* @param startPosition Start position of the payload (inclusive).
* @param endPosition End position of the payload (exclusive).
* @param streamReader The {@link StreamReader} that owns this seeker. * @param streamReader The {@link StreamReader} that owns this seeker.
* @param payloadStartPosition Start position of the payload (inclusive).
* @param payloadEndPosition End position of the payload (exclusive).
* @param firstPayloadPageSize The total size of the first payload page, in bytes. * @param firstPayloadPageSize The total size of the first payload page, in bytes.
* @param firstPayloadPageGranulePosition The granule position of the first payload page. * @param firstPayloadPageGranulePosition The granule position of the first payload page.
* @param firstPayloadPageIsLastPage Whether the first payload page is also the last page in the * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page.
* ogg stream.
*/ */
public DefaultOggSeeker( public DefaultOggSeeker(
long startPosition,
long endPosition,
StreamReader streamReader, StreamReader streamReader,
long payloadStartPosition,
long payloadEndPosition,
long firstPayloadPageSize, long firstPayloadPageSize,
long firstPayloadPageGranulePosition, long firstPayloadPageGranulePosition,
boolean firstPayloadPageIsLastPage) { boolean firstPayloadPageIsLastPage) {
Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); Assertions.checkArgument(
payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);
this.streamReader = streamReader; this.streamReader = streamReader;
this.startPosition = startPosition; this.payloadStartPosition = payloadStartPosition;
this.endPosition = endPosition; this.payloadEndPosition = payloadEndPosition;
if (firstPayloadPageSize == endPosition - startPosition || firstPayloadPageIsLastPage) { if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition
|| firstPayloadPageIsLastPage) {
totalGranules = firstPayloadPageGranulePosition; totalGranules = firstPayloadPageGranulePosition;
state = STATE_IDLE; state = STATE_IDLE;
} else { } else {
@ -90,7 +94,7 @@ import java.io.IOException;
positionBeforeSeekToEnd = input.getPosition(); positionBeforeSeekToEnd = input.getPosition();
state = STATE_READ_LAST_PAGE; state = STATE_READ_LAST_PAGE;
// Seek to the end just before the last page of stream to get the duration. // Seek to the end just before the last page of stream to get the duration.
long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE; long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE;
if (lastPageSearchPosition > positionBeforeSeekToEnd) { if (lastPageSearchPosition > positionBeforeSeekToEnd) {
return lastPageSearchPosition; return lastPageSearchPosition;
} }
@ -100,91 +104,77 @@ import java.io.IOException;
state = STATE_IDLE; state = STATE_IDLE;
return positionBeforeSeekToEnd; return positionBeforeSeekToEnd;
case STATE_SEEK: case STATE_SEEK:
long currentGranule; long position = getNextSeekPosition(input);
if (targetGranule == 0) { if (position != C.POSITION_UNSET) {
currentGranule = 0;
} else {
long position = getNextSeekPosition(targetGranule, input);
if (position >= 0) {
return position; return position;
} }
currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2)); state = STATE_SKIP;
} // Fall through.
case STATE_SKIP:
skipToPageOfTargetGranule(input);
state = STATE_IDLE; state = STATE_IDLE;
return -(currentGranule + 2); return -(startGranule + 2);
default: default:
// Never happens. // Never happens.
throw new IllegalStateException(); throw new IllegalStateException();
} }
} }
@Override
public long startSeek(long timeUs) {
Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK);
targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs);
state = STATE_SEEK;
resetSeeking();
return targetGranule;
}
@Override @Override
public OggSeekMap createSeekMap() { public OggSeekMap createSeekMap() {
return totalGranules != 0 ? new OggSeekMap() : null; return totalGranules != 0 ? new OggSeekMap() : null;
} }
@VisibleForTesting @Override
public void resetSeeking() { public void startSeek(long targetGranule) {
start = startPosition; this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1);
end = endPosition; state = STATE_SEEK;
start = payloadStartPosition;
end = payloadEndPosition;
startGranule = 0; startGranule = 0;
endGranule = totalGranules; endGranule = totalGranules;
} }
/** /**
* Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} * Performs a single step of a seeking binary search, returning the byte position from which data
* has to seek and then be passed for another call until a negative number is returned. If a * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged.
* negative number is returned the input is at a position which is before the target page and at * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be
* which it is sensible to just skip pages to the target granule and pre-roll instead of doing * called to skip to the target page.
* another seek request.
* *
* @param targetGranule the target granule position to seek to. * @param input The {@link ExtractorInput} to read from.
* @param input the {@link ExtractorInput} to read from. * @return The byte position from which data should be provided for the next step, or {@link
* @return the position to seek the {@link ExtractorInput} to for a next call or -(currentGranule * C#POSITION_UNSET} if the search has converged.
* + 2) if it's close enough to skip to the target page. * @throws IOException If reading from the input fails.
* @throws IOException thrown if reading from the input fails. * @throws InterruptedException If interrupted while reading from the input.
* @throws InterruptedException thrown if interrupted while reading from the input.
*/ */
@VisibleForTesting private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException {
public long getNextSeekPosition(long targetGranule, ExtractorInput input)
throws IOException, InterruptedException {
if (start == end) { if (start == end) {
return -(startGranule + 2); return C.POSITION_UNSET;
} }
long initialPosition = input.getPosition(); long currentPosition = input.getPosition();
if (!skipToNextPage(input, end)) { if (!skipToNextPage(input, end)) {
if (start == initialPosition) { if (start == currentPosition) {
throw new IOException("No ogg page can be found."); throw new IOException("No ogg page can be found.");
} }
return start; return start;
} }
pageHeader.populate(input, false); pageHeader.populate(input, /* quiet= */ false);
input.resetPeekPosition(); input.resetPeekPosition();
long granuleDistance = targetGranule - pageHeader.granulePosition; long granuleDistance = targetGranule - pageHeader.granulePosition;
int pageSize = pageHeader.headerSize + pageHeader.bodySize; int pageSize = pageHeader.headerSize + pageHeader.bodySize;
if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) { if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {
return C.POSITION_UNSET;
}
if (granuleDistance < 0) { if (granuleDistance < 0) {
end = initialPosition; end = currentPosition;
endGranule = pageHeader.granulePosition; endGranule = pageHeader.granulePosition;
} else { } else {
start = input.getPosition() + pageSize; start = input.getPosition() + pageSize;
startGranule = pageHeader.granulePosition; startGranule = pageHeader.granulePosition;
if (end - start + pageSize < MATCH_BYTE_RANGE) {
input.skipFully(pageSize);
return -(startGranule + 2);
}
} }
if (end - start < MATCH_BYTE_RANGE) { if (end - start < MATCH_BYTE_RANGE) {
@ -193,52 +183,31 @@ import java.io.IOException;
} }
long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
long nextPosition = input.getPosition() - offset long nextPosition =
input.getPosition()
- offset
+ (granuleDistance * (end - start) / (endGranule - startGranule)); + (granuleDistance * (end - start) / (endGranule - startGranule));
return Util.constrainValue(nextPosition, start, end - 1);
nextPosition = Math.max(nextPosition, start);
nextPosition = Math.min(nextPosition, end - 1);
return nextPosition;
} }
// position accepted (before target granule and within MATCH_RANGE) /**
input.skipFully(pageSize); * Skips forward to the start of the page containing the {@code targetGranule}.
return -(pageHeader.granulePosition + 2); *
* @param input The {@link ExtractorInput} to read from.
* @throws ParserException If populating the page header fails.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input.
*/
private void skipToPageOfTargetGranule(ExtractorInput input)
throws IOException, InterruptedException {
pageHeader.populate(input, /* quiet= */ false);
while (pageHeader.granulePosition <= targetGranule) {
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
start = input.getPosition();
startGranule = pageHeader.granulePosition;
pageHeader.populate(input, /* quiet= */ false);
} }
input.resetPeekPosition();
private long getEstimatedPosition(long position, long granuleDistance, long offset) {
position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset;
if (position < startPosition) {
position = startPosition;
}
if (position >= endPosition) {
position = endPosition - 1;
}
return position;
}
private class OggSeekMap implements SeekMap {
@Override
public boolean isSeekable() {
return true;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
if (timeUs == 0) {
return new SeekPoints(new SeekPoint(0, startPosition));
}
long granule = streamReader.convertTimeToGranule(timeUs);
long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET);
return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
}
@Override
public long getDurationUs() {
return streamReader.convertGranuleToTime(totalGranules);
}
} }
/** /**
@ -251,7 +220,7 @@ import java.io.IOException;
*/ */
@VisibleForTesting @VisibleForTesting
void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException { void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
if (!skipToNextPage(input, endPosition)) { if (!skipToNextPage(input, payloadEndPosition)) {
// Not found until eof. // Not found until eof.
throw new EOFException(); throw new EOFException();
} }
@ -263,13 +232,12 @@ import java.io.IOException;
* @param input The {@code ExtractorInput} to skip to the next page. * @param input The {@code ExtractorInput} to skip to the next page.
* @param limit The limit up to which the search should take place. * @param limit The limit up to which the search should take place.
* @return Whether the next page was found. * @return Whether the next page was found.
* @throws IOException thrown if peeking/reading from the input fails. * @throws IOException If peeking/reading from the input fails.
* @throws InterruptedException thrown if interrupted while peeking/reading from the input. * @throws InterruptedException If interrupted while peeking/reading from the input.
*/ */
@VisibleForTesting private boolean skipToNextPage(ExtractorInput input, long limit)
boolean skipToNextPage(ExtractorInput input, long limit)
throws IOException, InterruptedException { throws IOException, InterruptedException {
limit = Math.min(limit + 3, endPosition); limit = Math.min(limit + 3, payloadEndPosition);
byte[] buffer = new byte[2048]; byte[] buffer = new byte[2048];
int peekLength = buffer.length; int peekLength = buffer.length;
while (true) { while (true) {
@ -310,39 +278,35 @@ import java.io.IOException;
long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException { long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException {
skipToNextPage(input); skipToNextPage(input);
pageHeader.reset(); pageHeader.reset();
while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) { while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) {
pageHeader.populate(input, false); pageHeader.populate(input, /* quiet= */ false);
input.skipFully(pageHeader.headerSize + pageHeader.bodySize); input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
} }
return pageHeader.granulePosition; return pageHeader.granulePosition;
} }
/** private final class OggSeekMap implements SeekMap {
* Skips to the position of the start of the page containing the {@code targetGranule} and returns
* the granule of the page previous to the target page. @Override
* public boolean isSeekable() {
* @param input the {@link ExtractorInput} to read from. return true;
* @param targetGranule the target granule.
* @param currentGranule the current granule or -1 if it's unknown.
* @return the granule of the prior page or the {@code currentGranule} if there isn't a prior
* page.
* @throws ParserException thrown if populating the page header fails.
* @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from the input.
*/
@VisibleForTesting
long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule)
throws IOException, InterruptedException {
pageHeader.populate(input, false);
while (pageHeader.granulePosition < targetGranule) {
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
// Store in a member field to be able to resume after IOExceptions.
currentGranule = pageHeader.granulePosition;
// Peek next header.
pageHeader.populate(input, false);
}
input.resetPeekPosition();
return currentGranule;
} }
@Override
public SeekPoints getSeekPoints(long timeUs) {
long targetGranule = streamReader.convertTimeToGranule(timeUs);
long estimatedPosition =
payloadStartPosition
+ (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules)
- DEFAULT_OFFSET;
estimatedPosition =
Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1);
return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
}
@Override
public long getDurationUs() {
return streamReader.convertGranuleToTime(totalGranules);
}
}
} }

View File

@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -38,7 +38,7 @@ import java.util.List;
private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
private FlacStreamInfo streamInfo; private FlacStreamMetadata streamMetadata;
private FlacOggSeeker flacOggSeeker; private FlacOggSeeker flacOggSeeker;
public static boolean verifyBitstreamType(ParsableByteArray data) { public static boolean verifyBitstreamType(ParsableByteArray data) {
@ -50,7 +50,7 @@ import java.util.List;
protected void reset(boolean headerData) { protected void reset(boolean headerData) {
super.reset(headerData); super.reset(headerData);
if (headerData) { if (headerData) {
streamInfo = null; streamMetadata = null;
flacOggSeeker = null; flacOggSeeker = null;
} }
} }
@ -71,14 +71,24 @@ import java.util.List;
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws IOException, InterruptedException { throws IOException, InterruptedException {
byte[] data = packet.data; byte[] data = packet.data;
if (streamInfo == null) { if (streamMetadata == null) {
streamInfo = new FlacStreamInfo(data, 17); streamMetadata = new FlacStreamMetadata(data, 17);
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
List<byte[]> initializationData = Collections.singletonList(metadata); List<byte[]> initializationData = Collections.singletonList(metadata);
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null, setupData.format =
Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, Format.createAudioSampleFormat(
initializationData, null, 0, null); null,
MimeTypes.AUDIO_FLAC,
null,
Format.NO_VALUE,
streamMetadata.bitRate(),
streamMetadata.channels,
streamMetadata.sampleRate,
initializationData,
null,
0,
null);
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
flacOggSeeker = new FlacOggSeeker(); flacOggSeeker = new FlacOggSeeker();
flacOggSeeker.parseSeekTable(packet); flacOggSeeker.parseSeekTable(packet);
@ -175,11 +185,9 @@ import java.util.List;
} }
@Override @Override
public long startSeek(long timeUs) { public void startSeek(long targetGranule) {
long granule = convertTimeToGranule(timeUs); int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true);
int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
pendingSeekGranule = seekPointGranules[index]; pendingSeekGranule = seekPointGranules[index];
return granule;
} }
@Override @Override
@ -211,7 +219,7 @@ import java.util.List;
@Override @Override
public long getDurationUs() { public long getDurationUs() {
return streamInfo.durationUs(); return streamMetadata.durationUs();
} }
} }

View File

@ -38,7 +38,13 @@ import java.io.IOException;
public int revision; public int revision;
public int type; public int type;
/**
* The absolute granule position of the page. This is the total number of samples from the start
* of the file up to the <em>end</em> of the page. Samples partially in the page that continue on
* the next page do not count.
*/
public long granulePosition; public long granulePosition;
public long streamSerialNumber; public long streamSerialNumber;
public long pageSequenceNumber; public long pageSequenceNumber;
public long pageChecksum; public long pageChecksum;
@ -72,10 +78,10 @@ import java.io.IOException;
* Peeks an Ogg page header and updates this {@link OggPageHeader}. * Peeks an Ogg page header and updates this {@link OggPageHeader}.
* *
* @param input The {@link ExtractorInput} to read from. * @param input The {@link ExtractorInput} to read from.
* @param quiet If {@code true}, no exceptions are thrown but {@code false} is returned if * @param quiet Whether to return {@code false} rather than throwing an exception if the header
* something goes wrong. * cannot be populated.
* @return {@code true} if the read was successful. The read fails if the end of the input is * @return Whether the read was successful. The read fails if the end of the input is encountered
* encountered without reading data. * without reading data.
* @throws IOException If reading data fails or the stream is invalid. * @throws IOException If reading data fails or the stream is invalid.
* @throws InterruptedException If the thread is interrupted. * @throws InterruptedException If the thread is interrupted.
*/ */

View File

@ -33,16 +33,14 @@ import java.io.IOException;
SeekMap createSeekMap(); SeekMap createSeekMap();
/** /**
* Initializes a seek operation. * Starts a seek operation.
* *
* @param timeUs The seek position in microseconds. * @param targetGranule The target granule position.
* @return The granule position targeted by the seek.
*/ */
long startSeek(long timeUs); void startSeek(long targetGranule);
/** /**
* Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek.
* progressive seek.
* <p/> * <p/>
* If more data is required or if the position of the input needs to be modified then a position * If more data is required or if the position of the input needs to be modified then a position
* from which data should be provided is returned. Else a negative value is returned. If a seek * from which data should be provided is returned. Else a negative value is returned. If a seek

View File

@ -91,7 +91,8 @@ import java.io.IOException;
reset(!seekMapSet); reset(!seekMapSet);
} else { } else {
if (state != STATE_READ_HEADERS) { if (state != STATE_READ_HEADERS) {
targetGranule = oggSeeker.startSeek(timeUs); targetGranule = convertTimeToGranule(timeUs);
oggSeeker.startSeek(targetGranule);
state = STATE_READ_PAYLOAD; state = STATE_READ_PAYLOAD;
} }
} }
@ -147,9 +148,9 @@ import java.io.IOException;
boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.
oggSeeker = oggSeeker =
new DefaultOggSeeker( new DefaultOggSeeker(
this,
payloadStartPosition, payloadStartPosition,
input.getLength(), input.getLength(),
this,
firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
firstPayloadPageHeader.granulePosition, firstPayloadPageHeader.granulePosition,
isLastPage); isLastPage);
@ -248,13 +249,13 @@ import java.io.IOException;
private static final class UnseekableOggSeeker implements OggSeeker { private static final class UnseekableOggSeeker implements OggSeeker {
@Override @Override
public long read(ExtractorInput input) throws IOException, InterruptedException { public long read(ExtractorInput input) {
return -1; return -1;
} }
@Override @Override
public long startSeek(long timeUs) { public void startSeek(long targetGranule) {
return 0; // Do nothing.
} }
@Override @Override

View File

@ -87,12 +87,14 @@ public final class WavExtractor implements Extractor {
if (!wavHeader.hasDataBounds()) { if (!wavHeader.hasDataBounds()) {
WavHeaderReader.skipToData(input, wavHeader); WavHeaderReader.skipToData(input, wavHeader);
extractorOutput.seekMap(wavHeader); extractorOutput.seekMap(wavHeader);
} else if (input.getPosition() == 0) {
input.skipFully(wavHeader.getDataStartPosition());
} }
long dataLimit = wavHeader.getDataLimit(); long dataEndPosition = wavHeader.getDataEndPosition();
Assertions.checkState(dataLimit != C.POSITION_UNSET); Assertions.checkState(dataEndPosition != C.POSITION_UNSET);
long bytesLeft = dataLimit - input.getPosition(); long bytesLeft = dataEndPosition - input.getPosition();
if (bytesLeft <= 0) { if (bytesLeft <= 0) {
return Extractor.RESULT_END_OF_INPUT; return Extractor.RESULT_END_OF_INPUT;
} }

View File

@ -33,23 +33,29 @@ import com.google.android.exoplayer2.util.Util;
private final int blockAlignment; private final int blockAlignment;
/** Bits per sample for the audio data. */ /** Bits per sample for the audio data. */
private final int bitsPerSample; private final int bitsPerSample;
/** The PCM encoding */ /** The PCM encoding. */
@C.PcmEncoding @C.PcmEncoding private final int encoding;
private final int encoding;
/** Offset to the start of sample data. */ /** Position of the start of the sample data, in bytes. */
private long dataStartPosition; private int dataStartPosition;
/** Total size in bytes of the sample data. */ /** Position of the end of the sample data (exclusive), in bytes. */
private long dataSize; private long dataEndPosition;
public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, public WavHeader(
int bitsPerSample, @C.PcmEncoding int encoding) { int numChannels,
int sampleRateHz,
int averageBytesPerSecond,
int blockAlignment,
int bitsPerSample,
@C.PcmEncoding int encoding) {
this.numChannels = numChannels; this.numChannels = numChannels;
this.sampleRateHz = sampleRateHz; this.sampleRateHz = sampleRateHz;
this.averageBytesPerSecond = averageBytesPerSecond; this.averageBytesPerSecond = averageBytesPerSecond;
this.blockAlignment = blockAlignment; this.blockAlignment = blockAlignment;
this.bitsPerSample = bitsPerSample; this.bitsPerSample = bitsPerSample;
this.encoding = encoding; this.encoding = encoding;
dataStartPosition = C.POSITION_UNSET;
dataEndPosition = C.POSITION_UNSET;
} }
// Data bounds. // Data bounds.
@ -57,22 +63,33 @@ import com.google.android.exoplayer2.util.Util;
/** /**
* Sets the data start position and size in bytes of sample data in this WAV. * Sets the data start position and size in bytes of sample data in this WAV.
* *
* @param dataStartPosition The data start position in bytes. * @param dataStartPosition The position of the start of the sample data, in bytes.
* @param dataSize The data size in bytes. * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes.
*/ */
public void setDataBounds(long dataStartPosition, long dataSize) { public void setDataBounds(int dataStartPosition, long dataEndPosition) {
this.dataStartPosition = dataStartPosition; this.dataStartPosition = dataStartPosition;
this.dataSize = dataSize; this.dataEndPosition = dataEndPosition;
} }
/** Returns the data limit, or {@link C#POSITION_UNSET} if the data bounds have not been set. */ /**
public long getDataLimit() { * Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if
return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; * the data bounds have not been set.
*/
public int getDataStartPosition() {
return dataStartPosition;
}
/**
* Returns the position of the end of the sample data (exclusive), in bytes, or {@link
* C#POSITION_UNSET} if the data bounds have not been set.
*/
public long getDataEndPosition() {
return dataEndPosition;
} }
/** Returns whether the data start position and size have been set. */ /** Returns whether the data start position and size have been set. */
public boolean hasDataBounds() { public boolean hasDataBounds() {
return dataStartPosition != 0 && dataSize != 0; return dataStartPosition != C.POSITION_UNSET;
} }
// SeekMap implementation. // SeekMap implementation.
@ -84,12 +101,13 @@ import com.google.android.exoplayer2.util.Util;
@Override @Override
public long getDurationUs() { public long getDurationUs() {
long numFrames = dataSize / blockAlignment; long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment;
return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz;
} }
@Override @Override
public SeekPoints getSeekPoints(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
long dataSize = dataEndPosition - dataStartPosition;
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
// Constrain to nearest preceding frame offset. // Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / blockAlignment) * blockAlignment; positionOffset = (positionOffset / blockAlignment) * blockAlignment;

View File

@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
@ -92,8 +91,8 @@ import java.io.IOException;
// If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ...
input.advancePeekPosition((int) chunkHeader.size - 16); input.advancePeekPosition((int) chunkHeader.size - 16);
return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, return new WavHeader(
bitsPerSample, encoding); numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding);
} }
/** /**
@ -122,11 +121,13 @@ import java.io.IOException;
ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
// Skip all chunks until we hit the data header. // Skip all chunks until we hit the data header.
ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
while (chunkHeader.id != Util.getIntegerCodeForString("data")) { while (chunkHeader.id != WavUtil.DATA_FOURCC) {
if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) {
Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id);
}
long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size;
// Override size of RIFF chunk, since it describes its size as the entire file. // Override size of RIFF chunk, since it describes its size as the entire file.
if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { if (chunkHeader.id == WavUtil.RIFF_FOURCC) {
bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4;
} }
if (bytesToSkip > Integer.MAX_VALUE) { if (bytesToSkip > Integer.MAX_VALUE) {
@ -138,7 +139,14 @@ import java.io.IOException;
// Skip past the "data" header. // Skip past the "data" header.
input.skipFully(ChunkHeader.SIZE_IN_BYTES); input.skipFully(ChunkHeader.SIZE_IN_BYTES);
wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); int dataStartPosition = (int) input.getPosition();
long dataEndPosition = dataStartPosition + chunkHeader.size;
long inputLength = input.getLength();
if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) {
Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength);
dataEndPosition = inputLength;
}
wavHeader.setDataBounds(dataStartPosition, dataEndPosition);
} }
private WavHeaderReader() { private WavHeaderReader() {

View File

@ -54,8 +54,15 @@ public final class MediaCodecInfo {
public final @Nullable String mimeType; public final @Nullable String mimeType;
/** /**
* The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this
* is a passthrough codec. * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a
* non-standard MIME type alias.
*/
@Nullable public final String codecMimeType;
/**
* The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not
* known.
*/ */
public final @Nullable CodecCapabilities capabilities; public final @Nullable CodecCapabilities capabilities;
@ -98,6 +105,7 @@ public final class MediaCodecInfo {
return new MediaCodecInfo( return new MediaCodecInfo(
name, name,
/* mimeType= */ null, /* mimeType= */ null,
/* codecMimeType= */ null,
/* capabilities= */ null, /* capabilities= */ null,
/* passthrough= */ true, /* passthrough= */ true,
/* forceDisableAdaptive= */ false, /* forceDisableAdaptive= */ false,
@ -109,26 +117,10 @@ public final class MediaCodecInfo {
* *
* @param name The name of the {@link MediaCodec}. * @param name The name of the {@link MediaCodec}.
* @param mimeType A mime type supported by the {@link MediaCodec}. * @param mimeType A mime type supported by the {@link MediaCodec}.
* @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. * @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}.
* @return The created instance. * Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias.
*/ * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or
public static MediaCodecInfo newInstance(String name, String mimeType, * {@code null} if not known.
CodecCapabilities capabilities) {
return new MediaCodecInfo(
name,
mimeType,
capabilities,
/* passthrough= */ false,
/* forceDisableAdaptive= */ false,
/* forceSecure= */ false);
}
/**
* Creates an instance.
*
* @param name The name of the {@link MediaCodec}.
* @param mimeType A mime type supported by the {@link MediaCodec}.
* @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
* @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}.
* @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @param forceSecure Whether {@link #secure} should be forced to {@code true}.
* @return The created instance. * @return The created instance.
@ -136,22 +128,31 @@ public final class MediaCodecInfo {
public static MediaCodecInfo newInstance( public static MediaCodecInfo newInstance(
String name, String name,
String mimeType, String mimeType,
CodecCapabilities capabilities, String codecMimeType,
@Nullable CodecCapabilities capabilities,
boolean forceDisableAdaptive, boolean forceDisableAdaptive,
boolean forceSecure) { boolean forceSecure) {
return new MediaCodecInfo( return new MediaCodecInfo(
name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure); name,
mimeType,
codecMimeType,
capabilities,
/* passthrough= */ false,
forceDisableAdaptive,
forceSecure);
} }
private MediaCodecInfo( private MediaCodecInfo(
String name, String name,
@Nullable String mimeType, @Nullable String mimeType,
@Nullable String codecMimeType,
@Nullable CodecCapabilities capabilities, @Nullable CodecCapabilities capabilities,
boolean passthrough, boolean passthrough,
boolean forceDisableAdaptive, boolean forceDisableAdaptive,
boolean forceSecure) { boolean forceSecure) {
this.name = Assertions.checkNotNull(name); this.name = Assertions.checkNotNull(name);
this.mimeType = mimeType; this.mimeType = mimeType;
this.codecMimeType = codecMimeType;
this.capabilities = capabilities; this.capabilities = capabilities;
this.passthrough = passthrough; this.passthrough = passthrough;
adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);

View File

@ -1806,9 +1806,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/ */
private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) { private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
String name = codecInfo.name; String name = codecInfo.name;
return (Util.SDK_INT <= 17 return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name))
&& ("OMX.rk.video_decoder.avc".equals(name) || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name))
|| "OMX.allwinner.video.decoder.avc".equals(name)))
|| ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
} }

View File

@ -161,24 +161,17 @@ public final class MediaCodecUtil {
Util.SDK_INT >= 21 Util.SDK_INT >= 21
? new MediaCodecListCompatV21(secure, tunneling) ? new MediaCodecListCompatV21(secure, tunneling)
: new MediaCodecListCompatV16(); : new MediaCodecListCompatV16();
ArrayList<MediaCodecInfo> decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); ArrayList<MediaCodecInfo> decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) {
// Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the
// legacy path. We also try this path on API levels 22 and 23 as a defensive measure. // legacy path. We also try this path on API levels 22 and 23 as a defensive measure.
mediaCodecList = new MediaCodecListCompatV16(); mediaCodecList = new MediaCodecListCompatV16();
decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
if (!decoderInfos.isEmpty()) { if (!decoderInfos.isEmpty()) {
Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
+ ". Assuming: " + decoderInfos.get(0).name); + ". Assuming: " + decoderInfos.get(0).name);
} }
} }
if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) {
// E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D.
CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure, key.tunneling);
ArrayList<MediaCodecInfo> eac3DecoderInfos =
getDecoderInfosInternal(eac3Key, mediaCodecList, MimeTypes.AUDIO_E_AC3);
decoderInfos.addAll(eac3DecoderInfos);
}
applyWorkarounds(mimeType, decoderInfos); applyWorkarounds(mimeType, decoderInfos);
List<MediaCodecInfo> unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); List<MediaCodecInfo> unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos);
decoderInfosCache.put(key, unmodifiableDecoderInfos); decoderInfosCache.put(key, unmodifiableDecoderInfos);
@ -249,13 +242,11 @@ public final class MediaCodecUtil {
* *
* @param key The codec key. * @param key The codec key.
* @param mediaCodecList The codec list. * @param mediaCodecList The codec list.
* @param requestedMimeType The originally requested MIME type, which may differ from the codec
* key MIME type if the codec key is being considered as a fallback.
* @return The codec information for usable codecs matching the specified key. * @return The codec information for usable codecs matching the specified key.
* @throws DecoderQueryException If there was an error querying the available decoders. * @throws DecoderQueryException If there was an error querying the available decoders.
*/ */
private static ArrayList<MediaCodecInfo> getDecoderInfosInternal(CodecKey key, private static ArrayList<MediaCodecInfo> getDecoderInfosInternal(CodecKey key,
MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
try { try {
ArrayList<MediaCodecInfo> decoderInfos = new ArrayList<>(); ArrayList<MediaCodecInfo> decoderInfos = new ArrayList<>();
String mimeType = key.mimeType; String mimeType = key.mimeType;
@ -265,28 +256,27 @@ public final class MediaCodecUtil {
for (int i = 0; i < numberOfCodecs; i++) { for (int i = 0; i < numberOfCodecs; i++) {
android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);
String name = codecInfo.getName(); String name = codecInfo.getName();
String supportedType = String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType);
getCodecSupportedType(codecInfo, name, secureDecodersExplicit, requestedMimeType); if (codecMimeType == null) {
if (supportedType == null) {
continue; continue;
} }
try { try {
CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType);
boolean tunnelingSupported = boolean tunnelingSupported =
mediaCodecList.isFeatureSupported( mediaCodecList.isFeatureSupported(
CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities);
boolean tunnelingRequired = boolean tunnelingRequired =
mediaCodecList.isFeatureRequired( mediaCodecList.isFeatureRequired(
CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities);
if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) { if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) {
continue; continue;
} }
boolean secureSupported = boolean secureSupported =
mediaCodecList.isFeatureSupported( mediaCodecList.isFeatureSupported(
CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities);
boolean secureRequired = boolean secureRequired =
mediaCodecList.isFeatureRequired( mediaCodecList.isFeatureRequired(
CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities);
if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) {
continue; continue;
} }
@ -295,12 +285,18 @@ public final class MediaCodecUtil {
|| (!secureDecodersExplicit && !key.secure)) { || (!secureDecodersExplicit && !key.secure)) {
decoderInfos.add( decoderInfos.add(
MediaCodecInfo.newInstance( MediaCodecInfo.newInstance(
name, mimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ false)); name,
mimeType,
codecMimeType,
capabilities,
forceDisableAdaptive,
/* forceSecure= */ false));
} else if (!secureDecodersExplicit && secureSupported) { } else if (!secureDecodersExplicit && secureSupported) {
decoderInfos.add( decoderInfos.add(
MediaCodecInfo.newInstance( MediaCodecInfo.newInstance(
name + ".secure", name + ".secure",
mimeType, mimeType,
codecMimeType,
capabilities, capabilities,
forceDisableAdaptive, forceDisableAdaptive,
/* forceSecure= */ true)); /* forceSecure= */ true));
@ -314,7 +310,7 @@ public final class MediaCodecUtil {
} else { } else {
// Rethrow error querying primary codec capabilities, or secondary codec // Rethrow error querying primary codec capabilities, or secondary codec
// capabilities if API level is greater than 23. // capabilities if API level is greater than 23.
Log.e(TAG, "Failed to query codec " + name + " (" + supportedType + ")"); Log.e(TAG, "Failed to query codec " + name + " (" + codecMimeType + ")");
throw e; throw e;
} }
} }
@ -328,25 +324,35 @@ public final class MediaCodecUtil {
} }
/** /**
* Returns the codec's supported type for decoding {@code requestedMimeType} on the current * Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if
* device, or {@code null} if the codec can't be used. * the codec can't be used.
* *
* @param info The codec information. * @param info The codec information.
* @param name The name of the codec * @param name The name of the codec
* @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.
* @param requestedMimeType The originally requested MIME type, which may differ from the codec * @param mimeType The MIME type.
* key MIME type if the codec key is being considered as a fallback. * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if
* @return The codec's supported type for decoding {@code requestedMimeType}, or {@code null} if * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType}
* the codec can't be used. * except in cases where the codec is known to use a non-standard MIME type alias.
*/ */
@Nullable @Nullable
private static String getCodecSupportedType( private static String getCodecMimeType(
android.media.MediaCodecInfo info, android.media.MediaCodecInfo info,
String name, String name,
boolean secureDecodersExplicit, boolean secureDecodersExplicit,
String requestedMimeType) { String mimeType) {
if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) { if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) {
if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { return null;
}
String[] supportedTypes = info.getSupportedTypes();
for (String supportedType : supportedTypes) {
if (supportedType.equalsIgnoreCase(mimeType)) {
return supportedType;
}
}
if (mimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) {
// Handle decoders that declare support for DV via MIME types that aren't // Handle decoders that declare support for DV via MIME types that aren't
// video/dolby-vision. // video/dolby-vision.
if ("OMX.MS.HEVCDV.Decoder".equals(name)) { if ("OMX.MS.HEVCDV.Decoder".equals(name)) {
@ -355,15 +361,12 @@ public final class MediaCodecUtil {
|| "OMX.realtek.video.decoder.tunneled".equals(name)) { || "OMX.realtek.video.decoder.tunneled".equals(name)) {
return "video/dv_hevc"; return "video/dv_hevc";
} }
} else if (mimeType.equals(MimeTypes.AUDIO_ALAC) && "OMX.lge.alac.decoder".equals(name)) {
return "audio/x-lg-alac";
} else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && "OMX.lge.flac.decoder".equals(name)) {
return "audio/x-lg-flac";
} }
String[] supportedTypes = info.getSupportedTypes();
for (String supportedType : supportedTypes) {
if (supportedType.equalsIgnoreCase(requestedMimeType)) {
return supportedType;
}
}
}
return null; return null;
} }
@ -373,12 +376,14 @@ public final class MediaCodecUtil {
* @param info The codec information. * @param info The codec information.
* @param name The name of the codec * @param name The name of the codec
* @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.
* @param requestedMimeType The originally requested MIME type, which may differ from the codec * @param mimeType The MIME type.
* key MIME type if the codec key is being considered as a fallback.
* @return Whether the specified codec is usable for decoding on the current device. * @return Whether the specified codec is usable for decoding on the current device.
*/ */
private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, private static boolean isCodecUsableDecoder(
boolean secureDecodersExplicit, String requestedMimeType) { android.media.MediaCodecInfo info,
String name,
boolean secureDecodersExplicit,
String mimeType) {
if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) {
return false; return false;
} }
@ -467,7 +472,7 @@ public final class MediaCodecUtil {
} }
// MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041]. // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041].
if (MimeTypes.AUDIO_E_AC3_JOC.equals(requestedMimeType) if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)
&& "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
return false; return false;
} }

View File

@ -0,0 +1,144 @@
/*
* Copyright (C) 2019 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.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import java.util.Arrays;
/** A picture parsed from a FLAC file. */
public final class PictureFrame implements Metadata.Entry {
/** The type of the picture. */
public final int pictureType;
/** The mime type of the picture. */
public final String mimeType;
/** A description of the picture. */
public final String description;
/** The width of the picture in pixels. */
public final int width;
/** The height of the picture in pixels. */
public final int height;
/** The color depth of the picture in bits-per-pixel. */
public final int depth;
/** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */
public final int colors;
/** The encoded picture data. */
public final byte[] pictureData;
public PictureFrame(
int pictureType,
String mimeType,
String description,
int width,
int height,
int depth,
int colors,
byte[] pictureData) {
this.pictureType = pictureType;
this.mimeType = mimeType;
this.description = description;
this.width = width;
this.height = height;
this.depth = depth;
this.colors = colors;
this.pictureData = pictureData;
}
/* package */ PictureFrame(Parcel in) {
this.pictureType = in.readInt();
this.mimeType = castNonNull(in.readString());
this.description = castNonNull(in.readString());
this.width = in.readInt();
this.height = in.readInt();
this.depth = in.readInt();
this.colors = in.readInt();
this.pictureData = castNonNull(in.createByteArray());
}
@Override
public String toString() {
return "Picture: mimeType=" + mimeType + ", description=" + description;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PictureFrame other = (PictureFrame) obj;
return (pictureType == other.pictureType)
&& mimeType.equals(other.mimeType)
&& description.equals(other.description)
&& (width == other.width)
&& (height == other.height)
&& (depth == other.depth)
&& (colors == other.colors)
&& Arrays.equals(pictureData, other.pictureData);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + pictureType;
result = 31 * result + mimeType.hashCode();
result = 31 * result + description.hashCode();
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + depth;
result = 31 * result + colors;
result = 31 * result + Arrays.hashCode(pictureData);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(pictureType);
dest.writeString(mimeType);
dest.writeString(description);
dest.writeInt(width);
dest.writeInt(height);
dest.writeInt(depth);
dest.writeInt(colors);
dest.writeByteArray(pictureData);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<PictureFrame> CREATOR =
new Parcelable.Creator<PictureFrame>() {
@Override
public PictureFrame createFromParcel(Parcel in) {
return new PictureFrame(in);
}
@Override
public PictureFrame[] newArray(int size) {
return new PictureFrame[size];
}
};
}

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2019 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.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
/** A vorbis comment. */
public final class VorbisComment implements Metadata.Entry {
/** The key. */
public final String key;
/** The value. */
public final String value;
/**
* @param key The key.
* @param value The value.
*/
public VorbisComment(String key, String value) {
this.key = key;
this.value = value;
}
/* package */ VorbisComment(Parcel in) {
this.key = castNonNull(in.readString());
this.value = castNonNull(in.readString());
}
@Override
public String toString() {
return "VC: " + key + "=" + value;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
VorbisComment other = (VorbisComment) obj;
return key.equals(other.key) && value.equals(other.value);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + key.hashCode();
result = 31 * result + value.hashCode();
return result;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(key);
dest.writeString(value);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<VorbisComment> CREATOR =
new Parcelable.Creator<VorbisComment>() {
@Override
public VorbisComment createFromParcel(Parcel in) {
return new VorbisComment(in);
}
@Override
public VorbisComment[] newArray(int size) {
return new VorbisComment[size];
}
};
}

View File

@ -174,6 +174,7 @@ public abstract class DownloadService extends Service {
@Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater;
@Nullable private final String channelId; @Nullable private final String channelId;
@StringRes private final int channelNameResourceId; @StringRes private final int channelNameResourceId;
@StringRes private final int channelDescriptionResourceId;
private DownloadManager downloadManager; private DownloadManager downloadManager;
private int lastStartId; private int lastStartId;
@ -214,7 +215,23 @@ public abstract class DownloadService extends Service {
foregroundNotificationId, foregroundNotificationId,
foregroundNotificationUpdateInterval, foregroundNotificationUpdateInterval,
/* channelId= */ null, /* channelId= */ null,
/* channelNameResourceId= */ 0); /* channelNameResourceId= */ 0,
/* channelDescriptionResourceId= */ 0);
}
/** @deprecated Use {@link #DownloadService(int, long, String, int, int)}. */
@Deprecated
protected DownloadService(
int foregroundNotificationId,
long foregroundNotificationUpdateInterval,
@Nullable String channelId,
@StringRes int channelNameResourceId) {
this(
foregroundNotificationId,
foregroundNotificationUpdateInterval,
channelId,
channelNameResourceId,
/* channelDescriptionResourceId= */ 0);
} }
/** /**
@ -230,25 +247,33 @@ public abstract class DownloadService extends Service {
* unique per package. The value may be truncated if it's too long. Ignored if {@code * unique per package. The value may be truncated if it's too long. Ignored if {@code
* foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}.
* @param channelNameResourceId A string resource identifier for the user visible name of the * @param channelNameResourceId A string resource identifier for the user visible name of the
* channel, if {@code channelId} is specified. The recommended maximum length is 40 * notification channel. The recommended maximum length is 40 characters. The value may be
* characters. The value may be truncated if it is too long. Ignored if {@code * truncated if it's too long. Ignored if {@code channelId} is null or if {@code
* foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}.
* @param channelDescriptionResourceId A string resource identifier for the user visible
* description of the notification channel, or 0 if no description is provided. The
* recommended maximum length is 300 characters. The value may be truncated if it is too long.
* Ignored if {@code channelId} is null or if {@code foregroundNotificationId} is {@link
* #FOREGROUND_NOTIFICATION_ID_NONE}.
*/ */
protected DownloadService( protected DownloadService(
int foregroundNotificationId, int foregroundNotificationId,
long foregroundNotificationUpdateInterval, long foregroundNotificationUpdateInterval,
@Nullable String channelId, @Nullable String channelId,
@StringRes int channelNameResourceId) { @StringRes int channelNameResourceId,
@StringRes int channelDescriptionResourceId) {
if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) { if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) {
this.foregroundNotificationUpdater = null; this.foregroundNotificationUpdater = null;
this.channelId = null; this.channelId = null;
this.channelNameResourceId = 0; this.channelNameResourceId = 0;
this.channelDescriptionResourceId = 0;
} else { } else {
this.foregroundNotificationUpdater = this.foregroundNotificationUpdater =
new ForegroundNotificationUpdater( new ForegroundNotificationUpdater(
foregroundNotificationId, foregroundNotificationUpdateInterval); foregroundNotificationId, foregroundNotificationUpdateInterval);
this.channelId = channelId; this.channelId = channelId;
this.channelNameResourceId = channelNameResourceId; this.channelNameResourceId = channelNameResourceId;
this.channelDescriptionResourceId = channelDescriptionResourceId;
} }
} }
@ -543,7 +568,11 @@ public abstract class DownloadService extends Service {
public void onCreate() { public void onCreate() {
if (channelId != null) { if (channelId != null) {
NotificationUtil.createNotificationChannel( NotificationUtil.createNotificationChannel(
this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW); this,
channelId,
channelNameResourceId,
channelDescriptionResourceId,
NotificationUtil.IMPORTANCE_LOW);
} }
Class<? extends DownloadService> clazz = getClass(); Class<? extends DownloadService> clazz = getClass();
DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz);

View File

@ -106,13 +106,16 @@ public interface MediaPeriod extends SequenceableLoader {
* Performs a track selection. * Performs a track selection.
* *
* <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} * <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
* indicating whether the existing {@code SampleStream} can be retained for each selection, and * indicating whether the existing {@link SampleStream} can be retained for each selection, and
* the existing {@code stream}s themselves. The call will update {@code streams} to reflect the * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the
* provided selections, clearing, setting and replacing entries as required. If an existing sample * provided selections, clearing, setting and replacing entries as required. If an existing sample
* stream is retained but with the requirement that the consuming renderer be reset, then the * stream is retained but with the requirement that the consuming renderer be reset, then the
* corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
* if a new sample stream is created. * if a new sample stream is created.
* *
* <p>Note that previously received {@link TrackSelection TrackSelections} are no longer valid and
* references need to be replaced even if the corresponding {@link SampleStream} is kept.
*
* <p>This method is only called after the period has been prepared. * <p>This method is only called after the period has been prepared.
* *
* @param selections The renderer track selections. * @param selections The renderer track selections.

View File

@ -118,6 +118,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
@NullableType SampleStream[] streams, @NullableType SampleStream[] streams,
boolean[] streamResetFlags, boolean[] streamResetFlags,
long positionUs) { long positionUs) {
positionUs = constrainSeekPosition(positionUs);
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
sampleStreams.remove(streams[i]); sampleStreams.remove(streams[i]);
@ -144,6 +145,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
@Override @Override
public long seekToUs(long positionUs) { public long seekToUs(long positionUs) {
positionUs = constrainSeekPosition(positionUs);
for (int i = 0; i < sampleStreams.size(); i++) { for (int i = 0; i < sampleStreams.size(); i++) {
((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs); ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs);
} }
@ -152,7 +154,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
@Override @Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
return positionUs; return constrainSeekPosition(positionUs);
} }
@Override @Override
@ -172,6 +174,10 @@ public final class SilenceMediaSource extends BaseMediaSource {
@Override @Override
public void reevaluateBuffer(long positionUs) {} public void reevaluateBuffer(long positionUs) {}
private long constrainSeekPosition(long positionUs) {
return Util.constrainValue(positionUs, 0, durationUs);
}
} }
private static final class SilenceSampleStream implements SampleStream { private static final class SilenceSampleStream implements SampleStream {
@ -187,7 +193,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
} }
public void seekTo(long positionUs) { public void seekTo(long positionUs) {
positionBytes = getAudioByteCount(positionUs); positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes);
} }
@Override @Override

View File

@ -28,9 +28,10 @@ import java.lang.annotation.RetentionPolicy;
*/ */
public class Cue { public class Cue {
/** /** The empty cue. */
* An unset position or width. public static final Cue EMPTY = new Cue("");
*/
/** An unset position or width. */
public static final float DIMEN_UNSET = Float.MIN_VALUE; public static final float DIMEN_UNSET = Float.MIN_VALUE;
/** /**

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.text; package com.google.android.exoplayer2.text;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -69,6 +70,7 @@ public abstract class SimpleSubtitleDecoder extends
@SuppressWarnings("ByteBufferBackingArray") @SuppressWarnings("ByteBufferBackingArray")
@Override @Override
@Nullable
protected final SubtitleDecoderException decode( protected final SubtitleDecoderException decode(
SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) {
try { try {

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.OutputBuffer; import com.google.android.exoplayer2.decoder.OutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import java.util.List; import java.util.List;
/** /**
@ -45,22 +46,22 @@ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti
@Override @Override
public int getEventTimeCount() { public int getEventTimeCount() {
return subtitle.getEventTimeCount(); return Assertions.checkNotNull(subtitle).getEventTimeCount();
} }
@Override @Override
public long getEventTime(int index) { public long getEventTime(int index) {
return subtitle.getEventTime(index) + subsampleOffsetUs; return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs;
} }
@Override @Override
public int getNextEventTimeIndex(long timeUs) { public int getNextEventTimeIndex(long timeUs) {
return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs); return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs);
} }
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
return subtitle.getCues(timeUs - subsampleOffsetUs); return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs);
} }
@Override @Override

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.text.pgs; package com.google.android.exoplayer2.text.pgs;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
@ -41,7 +42,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
private final ParsableByteArray inflatedBuffer; private final ParsableByteArray inflatedBuffer;
private final CueBuilder cueBuilder; private final CueBuilder cueBuilder;
private Inflater inflater; @Nullable private Inflater inflater;
public PgsDecoder() { public PgsDecoder() {
super("PgsDecoder"); super("PgsDecoder");
@ -76,6 +77,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
} }
} }
@Nullable
private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) {
int limit = buffer.limit(); int limit = buffer.limit();
int sectionType = buffer.readUnsignedByte(); int sectionType = buffer.readUnsignedByte();
@ -197,6 +199,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
bitmapY = buffer.readUnsignedShort(); bitmapY = buffer.readUnsignedShort();
} }
@Nullable
public Cue build() { public Cue build() {
if (planeWidth == 0 if (planeWidth == 0
|| planeHeight == 0 || planeHeight == 0

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.text.ssa; package com.google.android.exoplayer2.text.ssa;
import androidx.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
@ -49,7 +50,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
private int formatTextIndex; private int formatTextIndex;
public SsaDecoder() { public SsaDecoder() {
this(null); this(/* initializationData= */ null);
} }
/** /**
@ -58,7 +59,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
* format line. The second must contain an SSA header that will be assumed common to all * format line. The second must contain an SSA header that will be assumed common to all
* samples. * samples.
*/ */
public SsaDecoder(List<byte[]> initializationData) { public SsaDecoder(@Nullable List<byte[]> initializationData) {
super("SsaDecoder"); super("SsaDecoder");
if (initializationData != null && !initializationData.isEmpty()) { if (initializationData != null && !initializationData.isEmpty()) {
haveInitializationData = true; haveInitializationData = true;
@ -201,7 +202,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
cues.add(new Cue(text)); cues.add(new Cue(text));
cueTimesUs.add(startTimeUs); cueTimesUs.add(startTimeUs);
if (endTimeUs != C.TIME_UNSET) { if (endTimeUs != C.TIME_UNSET) {
cues.add(null); cues.add(Cue.EMPTY);
cueTimesUs.add(endTimeUs); cueTimesUs.add(endTimeUs);
} }
} }

View File

@ -32,7 +32,7 @@ import java.util.List;
private final long[] cueTimesUs; private final long[] cueTimesUs;
/** /**
* @param cues The cues in the subtitle. Null entries may be used to represent empty cues. * @param cues The cues in the subtitle.
* @param cueTimesUs The cue times, in microseconds. * @param cueTimesUs The cue times, in microseconds.
*/ */
public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { public SsaSubtitle(Cue[] cues, long[] cueTimesUs) {
@ -61,7 +61,7 @@ import java.util.List;
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
if (index == -1 || cues[index] == null) { if (index == -1 || cues[index] == Cue.EMPTY) {
// timeUs is earlier than the start of the first cue, or we have an empty cue. // timeUs is earlier than the start of the first cue, or we have an empty cue.
return Collections.emptyList(); return Collections.emptyList();
} else { } else {

View File

@ -111,11 +111,13 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
// Read and parse the text and tags. // Read and parse the text and tags.
textBuilder.setLength(0); textBuilder.setLength(0);
tags.clear(); tags.clear();
while (!TextUtils.isEmpty(currentLine = subripData.readLine())) { currentLine = subripData.readLine();
while (!TextUtils.isEmpty(currentLine)) {
if (textBuilder.length() > 0) { if (textBuilder.length() > 0) {
textBuilder.append("<br>"); textBuilder.append("<br>");
} }
textBuilder.append(processLine(currentLine, tags)); textBuilder.append(processLine(currentLine, tags));
currentLine = subripData.readLine();
} }
Spanned text = Html.fromHtml(textBuilder.toString()); Spanned text = Html.fromHtml(textBuilder.toString());
@ -132,7 +134,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
cues.add(buildCue(text, alignmentTag)); cues.add(buildCue(text, alignmentTag));
if (haveEndTimecode) { if (haveEndTimecode) {
cues.add(null); cues.add(Cue.EMPTY);
} }
} }

View File

@ -32,7 +32,7 @@ import java.util.List;
private final long[] cueTimesUs; private final long[] cueTimesUs;
/** /**
* @param cues The cues in the subtitle. Null entries may be used to represent empty cues. * @param cues The cues in the subtitle.
* @param cueTimesUs The cue times, in microseconds. * @param cueTimesUs The cue times, in microseconds.
*/ */
public SubripSubtitle(Cue[] cues, long[] cueTimesUs) { public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
@ -61,7 +61,7 @@ import java.util.List;
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
if (index == -1 || cues[index] == null) { if (index == -1 || cues[index] == Cue.EMPTY) {
// timeUs is earlier than the start of the first cue, or we have an empty cue. // timeUs is earlier than the start of the first cue, or we have an empty cue.
return Collections.emptyList(); return Collections.emptyList();
} else { } else {

View File

@ -65,6 +65,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f; private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f;
private final ParsableByteArray parsableByteArray; private final ParsableByteArray parsableByteArray;
private boolean customVerticalPlacement; private boolean customVerticalPlacement;
private int defaultFontFace; private int defaultFontFace;
private int defaultColorRgba; private int defaultColorRgba;
@ -80,10 +81,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
public Tx3gDecoder(List<byte[]> initializationData) { public Tx3gDecoder(List<byte[]> initializationData) {
super("Tx3gDecoder"); super("Tx3gDecoder");
parsableByteArray = new ParsableByteArray(); parsableByteArray = new ParsableByteArray();
decodeInitializationData(initializationData);
}
private void decodeInitializationData(List<byte[]> initializationData) {
if (initializationData != null && initializationData.size() == 1 if (initializationData != null && initializationData.size() == 1
&& (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) { && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) {
byte[] initializationBytes = initializationData.get(0); byte[] initializationBytes = initializationData.get(0);
@ -151,8 +149,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
} }
parsableByteArray.setPosition(position + atomSize); parsableByteArray.setPosition(position + atomSize);
} }
return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, return new Tx3gSubtitle(
Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); new Cue(
cueText,
/* textAlignment= */ null,
verticalPlacement,
Cue.LINE_TYPE_FRACTION,
Cue.ANCHOR_TYPE_START,
Cue.DIMEN_UNSET,
Cue.TYPE_UNSET,
Cue.DIMEN_UNSET));
} }
private static String readSubtitleText(ParsableByteArray parsableByteArray) private static String readSubtitleText(ParsableByteArray parsableByteArray)

View File

@ -2318,14 +2318,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
if (TextUtils.equals(format.language, language)) { if (TextUtils.equals(format.language, language)) {
return 3; return 3;
} }
// Partial match where one language is a subset of the other (e.g. "zho-hans" and "zho-hans-hk") // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk")
if (format.language.startsWith(language) || language.startsWith(format.language)) { if (format.language.startsWith(language) || language.startsWith(format.language)) {
return 2; return 2;
} }
// Partial match where only the main language tag is the same (e.g. "fra-fr" and "fra-ca") // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca")
if (format.language.length() >= 3 String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0];
&& language.length() >= 3 String queryMainLanguage = Util.splitAtFirst(language, "-")[0];
&& format.language.substring(0, 3).equals(language.substring(0, 3))) { if (formatMainLanguage.equals(queryMainLanguage)) {
return 1; return 1;
} }
return 0; return 0;

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.upstream; package com.google.android.exoplayer2.upstream;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.util.Base64; import android.util.Base64;
@ -29,9 +31,10 @@ public final class DataSchemeDataSource extends BaseDataSource {
public static final String SCHEME_DATA = "data"; public static final String SCHEME_DATA = "data";
private @Nullable DataSpec dataSpec; @Nullable private DataSpec dataSpec;
private int bytesRead; @Nullable private byte[] data;
private @Nullable byte[] data; private int endPosition;
private int readPosition;
public DataSchemeDataSource() { public DataSchemeDataSource() {
super(/* isNetwork= */ false); super(/* isNetwork= */ false);
@ -41,6 +44,7 @@ public final class DataSchemeDataSource extends BaseDataSource {
public long open(DataSpec dataSpec) throws IOException { public long open(DataSpec dataSpec) throws IOException {
transferInitializing(dataSpec); transferInitializing(dataSpec);
this.dataSpec = dataSpec; this.dataSpec = dataSpec;
readPosition = (int) dataSpec.position;
Uri uri = dataSpec.uri; Uri uri = dataSpec.uri;
String scheme = uri.getScheme(); String scheme = uri.getScheme();
if (!SCHEME_DATA.equals(scheme)) { if (!SCHEME_DATA.equals(scheme)) {
@ -61,8 +65,14 @@ public final class DataSchemeDataSource extends BaseDataSource {
// TODO: Add support for other charsets. // TODO: Add support for other charsets.
data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME));
} }
endPosition =
dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length;
if (endPosition > data.length || readPosition > endPosition) {
data = null;
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
transferStarted(dataSpec); transferStarted(dataSpec);
return data.length; return (long) endPosition - readPosition;
} }
@Override @Override
@ -70,29 +80,29 @@ public final class DataSchemeDataSource extends BaseDataSource {
if (readLength == 0) { if (readLength == 0) {
return 0; return 0;
} }
int remainingBytes = data.length - bytesRead; int remainingBytes = endPosition - readPosition;
if (remainingBytes == 0) { if (remainingBytes == 0) {
return C.RESULT_END_OF_INPUT; return C.RESULT_END_OF_INPUT;
} }
readLength = Math.min(readLength, remainingBytes); readLength = Math.min(readLength, remainingBytes);
System.arraycopy(data, bytesRead, buffer, offset, readLength); System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength);
bytesRead += readLength; readPosition += readLength;
bytesTransferred(readLength); bytesTransferred(readLength);
return readLength; return readLength;
} }
@Override @Override
public @Nullable Uri getUri() { @Nullable
public Uri getUri() {
return dataSpec != null ? dataSpec.uri : null; return dataSpec != null ? dataSpec.uri : null;
} }
@Override @Override
public void close() throws IOException { public void close() {
if (data != null) { if (data != null) {
data = null; data = null;
transferEnded(); transferEnded();
} }
dataSpec = null; dataSpec = null;
} }
} }

View File

@ -15,12 +15,18 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import java.util.ArrayList;
import java.util.List;
/** /** Holder for FLAC metadata. */
* Holder for FLAC stream info. public final class FlacStreamMetadata {
*/
public final class FlacStreamInfo { private static final String TAG = "FlacStreamMetadata";
public final int minBlockSize; public final int minBlockSize;
public final int maxBlockSize; public final int maxBlockSize;
@ -30,16 +36,19 @@ public final class FlacStreamInfo {
public final int channels; public final int channels;
public final int bitsPerSample; public final int bitsPerSample;
public final long totalSamples; public final long totalSamples;
@Nullable public final Metadata metadata;
private static final String SEPARATOR = "=";
/** /**
* Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. * Parses binary FLAC stream info metadata.
* *
* @param data An array holding FLAC stream info metadata structure * @param data An array containing binary FLAC stream info metadata.
* @param offset Offset of the structure in the array * @param offset The offset of the stream info metadata in {@code data}.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format * @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a> * METADATA_BLOCK_STREAMINFO</a>
*/ */
public FlacStreamInfo(byte[] data, int offset) { public FlacStreamMetadata(byte[] data, int offset) {
ParsableBitArray scratch = new ParsableBitArray(data); ParsableBitArray scratch = new ParsableBitArray(data);
scratch.setPosition(offset * 8); scratch.setPosition(offset * 8);
this.minBlockSize = scratch.readBits(16); this.minBlockSize = scratch.readBits(16);
@ -49,14 +58,11 @@ public final class FlacStreamInfo {
this.sampleRate = scratch.readBits(20); this.sampleRate = scratch.readBits(20);
this.channels = scratch.readBits(3) + 1; this.channels = scratch.readBits(3) + 1;
this.bitsPerSample = scratch.readBits(5) + 1; this.bitsPerSample = scratch.readBits(5) + 1;
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);
| (scratch.readBits(32) & 0xFFFFFFFFL); this.metadata = null;
// Remaining 16 bytes is md5 value
} }
/** /**
* Constructs a FlacStreamInfo given the parameters.
*
* @param minBlockSize Minimum block size of the FLAC stream. * @param minBlockSize Minimum block size of the FLAC stream.
* @param maxBlockSize Maximum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream.
* @param minFrameSize Minimum frame size of the FLAC stream. * @param minFrameSize Minimum frame size of the FLAC stream.
@ -65,10 +71,16 @@ public final class FlacStreamInfo {
* @param channels Number of channels of the FLAC stream. * @param channels Number of channels of the FLAC stream.
* @param bitsPerSample Number of bits per sample of the FLAC stream. * @param bitsPerSample Number of bits per sample of the FLAC stream.
* @param totalSamples Total samples of the FLAC stream. * @param totalSamples Total samples of the FLAC stream.
* @param vorbisComments Vorbis comments. Each entry must be in key=value form.
* @param pictureFrames Picture frames.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format * @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a> * METADATA_BLOCK_STREAMINFO</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_vorbis_comment">FLAC format
* METADATA_BLOCK_VORBIS_COMMENT</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC format
* METADATA_BLOCK_PICTURE</a>
*/ */
public FlacStreamInfo( public FlacStreamMetadata(
int minBlockSize, int minBlockSize,
int maxBlockSize, int maxBlockSize,
int minFrameSize, int minFrameSize,
@ -76,7 +88,9 @@ public final class FlacStreamInfo {
int sampleRate, int sampleRate,
int channels, int channels,
int bitsPerSample, int bitsPerSample,
long totalSamples) { long totalSamples,
List<String> vorbisComments,
List<PictureFrame> pictureFrames) {
this.minBlockSize = minBlockSize; this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize; this.maxBlockSize = maxBlockSize;
this.minFrameSize = minFrameSize; this.minFrameSize = minFrameSize;
@ -85,6 +99,7 @@ public final class FlacStreamInfo {
this.channels = channels; this.channels = channels;
this.bitsPerSample = bitsPerSample; this.bitsPerSample = bitsPerSample;
this.totalSamples = totalSamples; this.totalSamples = totalSamples;
this.metadata = buildMetadata(vorbisComments, pictureFrames);
} }
/** Returns the maximum size for a decoded frame from the FLAC stream. */ /** Returns the maximum size for a decoded frame from the FLAC stream. */
@ -126,4 +141,27 @@ public final class FlacStreamInfo {
} }
return approxBytesPerFrame; return approxBytesPerFrame;
} }
@Nullable
private static Metadata buildMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
return null;
}
ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();
for (int i = 0; i < vorbisComments.size(); i++) {
String vorbisComment = vorbisComments.get(i);
String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);
if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment);
} else {
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry);
}
}
metadataEntries.addAll(pictureFrames);
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
}
} }

View File

@ -61,6 +61,14 @@ public final class NotificationUtil {
/** @see NotificationManager#IMPORTANCE_HIGH */ /** @see NotificationManager#IMPORTANCE_HIGH */
public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH; public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH;
/** @deprecated Use {@link #createNotificationChannel(Context, String, int, int, int)}. */
@Deprecated
public static void createNotificationChannel(
Context context, String id, @StringRes int nameResourceId, @Importance int importance) {
createNotificationChannel(
context, id, nameResourceId, /* descriptionResourceId= */ 0, importance);
}
/** /**
* Creates a notification channel that notifications can be posted to. See {@link * Creates a notification channel that notifications can be posted to. See {@link
* NotificationChannel} and {@link * NotificationChannel} and {@link
@ -70,21 +78,33 @@ public final class NotificationUtil {
* @param id The id of the channel. Must be unique per package. The value may be truncated if it's * @param id The id of the channel. Must be unique per package. The value may be truncated if it's
* too long. * too long.
* @param nameResourceId A string resource identifier for the user visible name of the channel. * @param nameResourceId A string resource identifier for the user visible name of the channel.
* You can rename this channel when the system locale changes by listening for the {@link * The recommended maximum length is 40 characters. The string may be truncated if it's too
* Intent#ACTION_LOCALE_CHANGED} broadcast. The recommended maximum length is 40 characters. * long. You can rename the channel when the system locale changes by listening for the {@link
* The value may be truncated if it is too long. * Intent#ACTION_LOCALE_CHANGED} broadcast.
* @param descriptionResourceId A string resource identifier for the user visible description of
* the channel, or 0 if no description is provided. The recommended maximum length is 300
* characters. The value may be truncated if it is too long. You can change the description of
* the channel when the system locale changes by listening for the {@link
* Intent#ACTION_LOCALE_CHANGED} broadcast.
* @param importance The importance of the channel. This controls how interruptive notifications * @param importance The importance of the channel. This controls how interruptive notifications
* posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link * posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link
* #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link
* #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}. * #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}.
*/ */
public static void createNotificationChannel( public static void createNotificationChannel(
Context context, String id, @StringRes int nameResourceId, @Importance int importance) { Context context,
String id,
@StringRes int nameResourceId,
@StringRes int descriptionResourceId,
@Importance int importance) {
if (Util.SDK_INT >= 26) { if (Util.SDK_INT >= 26) {
NotificationManager notificationManager = NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = NotificationChannel channel =
new NotificationChannel(id, context.getString(nameResourceId), importance); new NotificationChannel(id, context.getString(nameResourceId), importance);
if (descriptionResourceId != 0) {
channel.setDescription(context.getString(descriptionResourceId));
}
notificationManager.createNotificationChannel(channel); notificationManager.createNotificationChannel(channel);
} }
} }

View File

@ -71,6 +71,7 @@ import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Formatter; import java.util.Formatter;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.MissingResourceException; import java.util.MissingResourceException;
@ -135,6 +136,10 @@ public final class Util {
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
// Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter
// ISO 639-2 code back to the corresponding 2-letter code.
@Nullable private static HashMap<String, String> languageTagIso3ToIso2;
private Util() {} private Util() {}
/** /**
@ -450,18 +455,31 @@ public final class Util {
if (language == null) { if (language == null) {
return null; return null;
} }
try { // Locale data (especially for API < 21) may produce tags with '_' instead of the
Locale locale = getLocaleForLanguageTag(language); // standard-conformant '-'.
int localeLanguageLength = locale.getLanguage().length(); String normalizedTag = language.replace('_', '-');
String normLanguage = locale.getISO3Language(); if (Util.SDK_INT >= 21) {
if (normLanguage.isEmpty()) { // Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags.
return toLowerInvariant(language); normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag);
} }
String normTag = getLocaleLanguageTag(locale); if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) {
return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength)); // Tag isn't valid, keep using the original.
} catch (MissingResourceException e) { normalizedTag = language;
return toLowerInvariant(language);
} }
normalizedTag = Util.toLowerInvariant(normalizedTag);
String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0];
if (mainLanguage.length() == 3) {
// 3-letter ISO 639-2/B or ISO 639-2/T language codes will not be converted to 2-letter ISO
// 639-1 codes automatically.
if (languageTagIso3ToIso2 == null) {
languageTagIso3ToIso2 = createIso3ToIso2Map();
}
String iso2Language = languageTagIso3ToIso2.get(mainLanguage);
if (iso2Language != null) {
normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3);
}
}
return normalizedTag;
} }
/** /**
@ -1955,32 +1973,25 @@ public final class Util {
} }
private static String[] getSystemLocales() { private static String[] getSystemLocales() {
Configuration config = Resources.getSystem().getConfiguration();
return SDK_INT >= 24 return SDK_INT >= 24
? getSystemLocalesV24() ? getSystemLocalesV24(config)
: new String[] {getLocaleLanguageTag(Resources.getSystem().getConfiguration().locale)}; : SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()};
} }
@TargetApi(24) @TargetApi(24)
private static String[] getSystemLocalesV24() { private static String[] getSystemLocalesV24(Configuration config) {
return Util.split(Resources.getSystem().getConfiguration().getLocales().toLanguageTags(), ","); return Util.split(config.getLocales().toLanguageTags(), ",");
}
private static Locale getLocaleForLanguageTag(String languageTag) {
return Util.SDK_INT >= 21 ? getLocaleForLanguageTagV21(languageTag) : new Locale(languageTag);
} }
@TargetApi(21) @TargetApi(21)
private static Locale getLocaleForLanguageTagV21(String languageTag) { private static String[] getSystemLocaleV21(Configuration config) {
return Locale.forLanguageTag(languageTag); return new String[] {config.locale.toLanguageTag()};
}
private static String getLocaleLanguageTag(Locale locale) {
return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString();
} }
@TargetApi(21) @TargetApi(21)
private static String getLocaleLanguageTagV21(Locale locale) { private static String normalizeLanguageCodeSyntaxV21(String languageTag) {
return locale.toLanguageTag(); return Locale.forLanguageTag(languageTag).toLanguageTag();
} }
private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) {
@ -2013,6 +2024,54 @@ public final class Util {
} }
} }
private static HashMap<String, String> createIso3ToIso2Map() {
String[] iso2Languages = Locale.getISOLanguages();
HashMap<String, String> iso3ToIso2 =
new HashMap<>(
/* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length);
for (String iso2 : iso2Languages) {
try {
// This returns the ISO 639-2/T code for the language.
String iso3 = new Locale(iso2).getISO3Language();
if (!TextUtils.isEmpty(iso3)) {
iso3ToIso2.put(iso3, iso2);
}
} catch (MissingResourceException e) {
// Shouldn't happen for list of known languages, but we don't want to throw either.
}
}
// Add additional ISO 639-2/B codes to mapping.
for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) {
iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]);
}
return iso3ToIso2;
}
// See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes.
private static final String[] iso3BibliographicalToIso2 =
new String[] {
"alb", "sq",
"arm", "hy",
"baq", "eu",
"bur", "my",
"tib", "bo",
"chi", "zh",
"cze", "cs",
"dut", "nl",
"ger", "de",
"gre", "el",
"fre", "fr",
"geo", "ka",
"ice", "is",
"mac", "mk",
"mao", "mi",
"may", "ms",
"per", "fa",
"rum", "ro",
"slo", "sk",
"wel", "cy"
};
/** /**
* Allows the CRC calculation to be done byte by byte instead of bit per bit being the order * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order
* "most significant bit first". * "most significant bit first".

View File

@ -551,10 +551,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Format format, Format format,
MediaCrypto crypto, MediaCrypto crypto,
float codecOperatingRate) { float codecOperatingRate) {
String codecMimeType = codecInfo.codecMimeType;
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
MediaFormat mediaFormat = MediaFormat mediaFormat =
getMediaFormat( getMediaFormat(
format, format,
codecMimeType,
codecMaxValues, codecMaxValues,
codecOperatingRate, codecOperatingRate,
deviceNeedsNoPostProcessWorkaround, deviceNeedsNoPostProcessWorkaround,
@ -1111,6 +1113,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* Returns the framework {@link MediaFormat} that should be used to configure the decoder. * Returns the framework {@link MediaFormat} that should be used to configure the decoder.
* *
* @param format The format of media. * @param format The format of media.
* @param codecMimeType The MIME type handled by the codec.
* @param codecMaxValues Codec max values that should be used when configuring the decoder. * @param codecMaxValues Codec max values that should be used when configuring the decoder.
* @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
* no codec operating rate should be set. * no codec operating rate should be set.
@ -1123,13 +1126,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
protected MediaFormat getMediaFormat( protected MediaFormat getMediaFormat(
Format format, Format format,
String codecMimeType,
CodecMaxValues codecMaxValues, CodecMaxValues codecMaxValues,
float codecOperatingRate, float codecOperatingRate,
boolean deviceNeedsNoPostProcessWorkaround, boolean deviceNeedsNoPostProcessWorkaround,
int tunnelingAudioSessionId) { int tunnelingAudioSessionId) {
MediaFormat mediaFormat = new MediaFormat(); MediaFormat mediaFormat = new MediaFormat();
// Set format parameters that should always be set. // Set format parameters that should always be set.
mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType);
mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width); mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width);
mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height); mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height);
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
@ -1429,6 +1433,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "1713": case "1713":
case "1714": case "1714":
case "A10-70F": case "A10-70F":
case "A10-70L":
case "A1601": case "A1601":
case "A2016a40": case "A2016a40":
case "A7000-a": case "A7000-a":

View File

@ -1,6 +1,6 @@
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26122 duration = 26125
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:

View File

@ -1,6 +1,6 @@
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26122 duration = 26125
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:

View File

@ -1,6 +1,6 @@
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26122 duration = 26125
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:

View File

@ -1,6 +1,6 @@
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26122 duration = 26125
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:

View File

@ -20,6 +20,7 @@ import static org.junit.Assert.fail;
import android.content.Context; import android.content.Context;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.view.Surface; import android.view.Surface;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -2608,6 +2609,56 @@ public final class ExoPlayerTest {
assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs)); assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs));
} }
@Test
public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception {
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse("https://ad1"))
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse("https://ad2"))
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse("https://ad3"));
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10_000_000,
adPlaybackState));
final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, null);
AtomicReference<Player> playerReference = new AtomicReference<>();
AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET);
EventListener eventListener =
new EventListener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {
contentStartPositionMs.set(playerReference.get().getContentPosition());
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder("contentWithInitialSeekAfterPrerollAd")
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
playerReference.set(player);
player.addListener(eventListener);
}
})
.seek(5_000)
.build();
new ExoPlayerTestRunner.Builder()
.setMediaSource(fakeMediaSource)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(contentStartPositionMs.get()).isAtLeast(5_000L);
}
// Internal methods. // Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {

View File

@ -16,13 +16,16 @@
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor.ogg;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.OggTestData;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.Random; import java.util.Random;
import org.junit.Test; import org.junit.Test;
@ -32,13 +35,15 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class DefaultOggSeekerTest { public final class DefaultOggSeekerTest {
private final Random random = new Random(0);
@Test @Test
public void testSetupWithUnsetEndPositionFails() { public void testSetupWithUnsetEndPositionFails() {
try { try {
new DefaultOggSeeker( new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ C.LENGTH_UNSET,
/* streamReader= */ new TestStreamReader(), /* streamReader= */ new TestStreamReader(),
/* payloadStartPosition= */ 0,
/* payloadEndPosition= */ C.LENGTH_UNSET,
/* firstPayloadPageSize= */ 1, /* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 1, /* firstPayloadPageGranulePosition= */ 1,
/* firstPayloadPageIsLastPage= */ false); /* firstPayloadPageIsLastPage= */ false);
@ -56,17 +61,106 @@ public final class DefaultOggSeekerTest {
} }
} }
@Test
public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput =
OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)),
false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(4000);
}
@Test
public void testSkipToNextPageOverlap() throws Exception {
FakeExtractorInput extractorInput =
OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)),
false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(2046);
}
@Test
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput =
OggTestData.createInput(
TestUtil.joinByteArrays(new byte[] {'x', 'O', 'g', 'g', 'S'}), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(1);
}
@Test
public void testSkipToNextPageNoMatch() throws Exception {
FakeExtractorInput extractorInput =
OggTestData.createInput(new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);
try {
skipToNextPage(extractorInput);
fail();
} catch (EOFException e) {
// expected
}
}
@Test
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
FakeExtractorInput input =
OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(100, random),
OggTestData.buildOggHeader(0x00, 20000, 66, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
OggTestData.buildOggHeader(0x00, 40000, 67, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
OggTestData.buildOggHeader(0x05, 60000, 68, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random)),
false);
assertReadGranuleOfLastPage(input, 60000);
}
@Test
public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (EOFException e) {
// Ignored.
}
}
@Test
public void testReadGranuleOfLastPageWithUnboundedLength()
throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(new byte[0], true);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (IllegalArgumentException e) {
// Ignored.
}
}
private void testSeeking(Random random) throws IOException, InterruptedException { private void testSeeking(Random random) throws IOException, InterruptedException {
OggTestFile testFile = OggTestFile.generate(random, 1000); OggTestFile testFile = OggTestFile.generate(random, 1000);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build();
TestStreamReader streamReader = new TestStreamReader(); TestStreamReader streamReader = new TestStreamReader();
DefaultOggSeeker oggSeeker = DefaultOggSeeker oggSeeker =
new DefaultOggSeeker( new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ testFile.data.length,
/* streamReader= */ streamReader, /* streamReader= */ streamReader,
/* payloadStartPosition= */ 0,
/* payloadEndPosition= */ testFile.data.length,
/* firstPayloadPageSize= */ testFile.firstPayloadPageSize, /* firstPayloadPageSize= */ testFile.firstPayloadPageSize,
/* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition, /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount,
/* firstPayloadPageIsLastPage= */ false); /* firstPayloadPageIsLastPage= */ false);
OggPageHeader pageHeader = new OggPageHeader(); OggPageHeader pageHeader = new OggPageHeader();
@ -78,89 +172,96 @@ public final class DefaultOggSeekerTest {
input.setPosition((int) nextSeekPosition); input.setPosition((int) nextSeekPosition);
} }
// Test granule 0 from file start // Test granule 0 from file start.
assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0); long granule = seekTo(input, oggSeeker, 0, 0);
assertThat(granule).isEqualTo(0);
assertThat(input.getPosition()).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0);
// Test granule 0 from file end // Test granule 0 from file end.
assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0); granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1);
assertThat(granule).isEqualTo(0);
assertThat(input.getPosition()).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0);
{ // Test last granule // Test last granule.
long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0); granule = seekTo(input, oggSeeker, testFile.granuleCount - 1, 0);
long position = testFile.data.length; assertThat(granule).isEqualTo(testFile.granuleCount - testFile.lastPayloadPageGranuleCount);
assertThat( assertThat(input.getPosition()).isEqualTo(testFile.data.length - testFile.lastPayloadPageSize);
(testFile.lastGranule > currentGranule && position > input.getPosition())
|| (testFile.lastGranule == currentGranule && position == input.getPosition()))
.isTrue();
}
{ // Test exact granule
input.setPosition(testFile.data.length / 2);
oggSeeker.skipToNextPage(input);
assertThat(pageHeader.populate(input, true)).isTrue();
long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize;
long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0);
assertThat(
(pageHeader.granulePosition > currentGranule && position > input.getPosition())
|| (pageHeader.granulePosition == currentGranule
&& position == input.getPosition()))
.isTrue();
}
for (int i = 0; i < 100; i += 1) { for (int i = 0; i < 100; i += 1) {
long targetGranule = (long) (random.nextDouble() * testFile.lastGranule); long targetGranule = random.nextInt(testFile.granuleCount);
int initialPosition = random.nextInt(testFile.data.length); int initialPosition = random.nextInt(testFile.data.length);
granule = seekTo(input, oggSeeker, targetGranule, initialPosition);
long currentGranule = seekTo(input, oggSeeker, targetGranule, initialPosition);
long currentPosition = input.getPosition(); long currentPosition = input.getPosition();
if (granule == 0) {
assertWithMessage("getNextSeekPosition() didn't leave input on a page start.")
.that(pageHeader.populate(input, true))
.isTrue();
if (currentGranule == 0) {
assertThat(currentPosition).isEqualTo(0); assertThat(currentPosition).isEqualTo(0);
} else { } else {
int previousPageStart = testFile.findPreviousPageStart(currentPosition); int previousPageStart = testFile.findPreviousPageStart(currentPosition);
input.setPosition(previousPageStart); input.setPosition(previousPageStart);
assertThat(pageHeader.populate(input, true)).isTrue(); pageHeader.populate(input, false);
assertThat(currentGranule).isEqualTo(pageHeader.granulePosition); assertThat(granule).isEqualTo(pageHeader.granulePosition);
} }
input.setPosition((int) currentPosition); input.setPosition((int) currentPosition);
oggSeeker.skipToPageOfGranule(input, targetGranule, -1); pageHeader.populate(input, false);
long positionDiff = Math.abs(input.getPosition() - currentPosition); // The target granule should be within the current page.
assertThat(granule).isAtMost(targetGranule);
assertThat(targetGranule).isLessThan(pageHeader.granulePosition);
}
}
long granuleDiff = currentGranule - targetGranule; private static void skipToNextPage(ExtractorInput extractorInput)
if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0) throws IOException, InterruptedException {
&& positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) { DefaultOggSeeker oggSeeker =
fail( new DefaultOggSeeker(
"granuleDiff (" /* streamReader= */ new FlacReader(),
+ granuleDiff /* payloadStartPosition= */ 0,
+ ") or positionDiff (" /* payloadEndPosition= */ extractorInput.getLength(),
+ positionDiff /* firstPayloadPageSize= */ 1,
+ ") is more than allowed."); /* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
oggSeeker.skipToNextPage(extractorInput);
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
/* ignored */
} }
} }
} }
private long seekTo( private static void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* streamReader= */ new FlacReader(),
/* payloadStartPosition= */ 0,
/* payloadEndPosition= */ input.getLength(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected);
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// Ignored.
}
}
}
private static long seekTo(
FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
long nextSeekPosition = initialPosition; long nextSeekPosition = initialPosition;
oggSeeker.startSeek(targetGranule);
int count = 0; int count = 0;
oggSeeker.resetSeeking(); while (nextSeekPosition >= 0) {
do {
input.setPosition((int) nextSeekPosition);
nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input);
if (count++ > 100) { if (count++ > 100) {
fail("infinite loop?"); fail("Seek failed to converge in 100 iterations");
}
input.setPosition((int) nextSeekPosition);
nextSeekPosition = oggSeeker.read(input);
} }
} while (nextSeekPosition >= 0);
return -(nextSeekPosition + 2); return -(nextSeekPosition + 2);
} }
@ -171,8 +272,7 @@ public final class DefaultOggSeekerTest {
} }
@Override @Override
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
throws IOException, InterruptedException {
return false; return false;
} }
} }

View File

@ -1,243 +0,0 @@
/*
* 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.extractor.ogg;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.OggTestData;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.EOFException;
import java.io.IOException;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link DefaultOggSeeker} utility methods. */
@RunWith(AndroidJUnit4.class)
public final class DefaultOggSeekerUtilMethodsTest {
private final Random random = new Random(0);
@Test
public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(4000);
}
@Test
public void testSkipToNextPageOverlap() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(2046);
}
@Test
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
new byte[] {'x', 'O', 'g', 'g', 'S'}
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(1);
}
@Test
public void testSkipToNextPageNoMatch() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);
try {
skipToNextPage(extractorInput);
fail();
} catch (EOFException e) {
// expected
}
}
private static void skipToNextPage(ExtractorInput extractorInput)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ extractorInput.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
oggSeeker.skipToNextPage(extractorInput);
break;
} catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ }
}
}
@Test
public void testSkipToPageOfGranule() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
// expect to be granule of the previous page returned as elapsedSamples
skipToPageOfGranule(input, 54000, 40000);
// expect to be at the start of the third page
assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254)));
}
@Test
public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
skipToPageOfGranule(input, 40000, 20000);
// expect to be at the start of the second page
assertThat(input.getPosition()).isEqualTo(30 + (3 * 254));
}
@Test
public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
skipToPageOfGranule(input, 10000, -1);
assertThat(input.getPosition()).isEqualTo(0);
}
private void skipToPageOfGranule(ExtractorInput input, long granule,
long elapsedSamplesExpected) throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ input.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1))
.isEqualTo(elapsedSamplesExpected);
return;
} catch (FakeExtractorInput.SimulatedIOException e) {
input.resetPeekPosition();
}
}
}
@Test
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays(
TestUtil.buildTestData(100, random),
OggTestData.buildOggHeader(0x00, 20000, 66, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
OggTestData.buildOggHeader(0x00, 40000, 67, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
OggTestData.buildOggHeader(0x05, 60000, 68, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random)
), false);
assertReadGranuleOfLastPage(input, 60000);
}
@Test
public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (EOFException e) {
// ignored
}
}
@Test
public void testReadGranuleOfLastPageWithUnboundedLength()
throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(new byte[0], true);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (IllegalArgumentException e) {
// ignored
}
}
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ input.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected);
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
}
}
}
}

View File

@ -30,35 +30,39 @@ import java.util.Random;
private static final int MAX_GRANULES_IN_PAGE = 100000; private static final int MAX_GRANULES_IN_PAGE = 100000;
public final byte[] data; public final byte[] data;
public final long lastGranule; public final int granuleCount;
public final int packetCount;
public final int pageCount; public final int pageCount;
public final int firstPayloadPageSize; public final int firstPayloadPageSize;
public final long firstPayloadPageGranulePosition; public final int firstPayloadPageGranuleCount;
public final int lastPayloadPageSize;
public final int lastPayloadPageGranuleCount;
private OggTestFile( private OggTestFile(
byte[] data, byte[] data,
long lastGranule, int granuleCount,
int packetCount,
int pageCount, int pageCount,
int firstPayloadPageSize, int firstPayloadPageSize,
long firstPayloadPageGranulePosition) { int firstPayloadPageGranuleCount,
int lastPayloadPageSize,
int lastPayloadPageGranuleCount) {
this.data = data; this.data = data;
this.lastGranule = lastGranule; this.granuleCount = granuleCount;
this.packetCount = packetCount;
this.pageCount = pageCount; this.pageCount = pageCount;
this.firstPayloadPageSize = firstPayloadPageSize; this.firstPayloadPageSize = firstPayloadPageSize;
this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition; this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount;
this.lastPayloadPageSize = lastPayloadPageSize;
this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount;
} }
public static OggTestFile generate(Random random, int pageCount) { public static OggTestFile generate(Random random, int pageCount) {
ArrayList<byte[]> fileData = new ArrayList<>(); ArrayList<byte[]> fileData = new ArrayList<>();
int fileSize = 0; int fileSize = 0;
long granule = 0; int granuleCount = 0;
int packetLength = -1;
int packetCount = 0;
int firstPayloadPageSize = 0; int firstPayloadPageSize = 0;
long firstPayloadPageGranulePosition = 0; int firstPayloadPageGranuleCount = 0;
int lastPageloadPageSize = 0;
int lastPayloadPageGranuleCount = 0;
int packetLength = -1;
for (int i = 0; i < pageCount; i++) { for (int i = 0; i < pageCount; i++) {
int headerType = 0x00; int headerType = 0x00;
@ -71,17 +75,17 @@ import java.util.Random;
if (i == pageCount - 1) { if (i == pageCount - 1) {
headerType |= 4; headerType |= 4;
} }
granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; int pageGranuleCount = random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1;
int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT); int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT);
byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); granuleCount += pageGranuleCount;
byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount);
fileData.add(header); fileData.add(header);
fileSize += header.length; int pageSize = header.length;
byte[] laces = new byte[pageSegmentCount]; byte[] laces = new byte[pageSegmentCount];
int bodySize = 0; int bodySize = 0;
for (int j = 0; j < pageSegmentCount; j++) { for (int j = 0; j < pageSegmentCount; j++) {
if (packetLength < 0) { if (packetLength < 0) {
packetCount++;
if (i < pageCount - 1) { if (i < pageCount - 1) {
packetLength = random.nextInt(MAX_PACKET_LENGTH); packetLength = random.nextInt(MAX_PACKET_LENGTH);
} else { } else {
@ -96,14 +100,19 @@ import java.util.Random;
packetLength -= 255; packetLength -= 255;
} }
fileData.add(laces); fileData.add(laces);
fileSize += laces.length; pageSize += laces.length;
byte[] payload = TestUtil.buildTestData(bodySize, random); byte[] payload = TestUtil.buildTestData(bodySize, random);
fileData.add(payload); fileData.add(payload);
fileSize += payload.length; pageSize += payload.length;
fileSize += pageSize;
if (i == 0) { if (i == 0) {
firstPayloadPageSize = header.length + bodySize; firstPayloadPageSize = pageSize;
firstPayloadPageGranulePosition = granule; firstPayloadPageGranuleCount = pageGranuleCount;
} else if (i == pageCount - 1) {
lastPageloadPageSize = pageSize;
lastPayloadPageGranuleCount = pageGranuleCount;
} }
} }
@ -115,11 +124,12 @@ import java.util.Random;
} }
return new OggTestFile( return new OggTestFile(
file, file,
granule, granuleCount,
packetCount,
pageCount, pageCount,
firstPayloadPageSize, firstPayloadPageSize,
firstPayloadPageGranulePosition); firstPayloadPageGranuleCount,
lastPageloadPageSize,
lastPayloadPageGranuleCount);
} }
public int findPreviousPageStart(long position) { public int findPreviousPageStart(long position) {

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