mirror of
https://github.com/androidx/media.git
synced 2025-05-06 15:10:34 +08:00
commit
85c10b0256
@ -1,5 +1,36 @@
|
||||
# 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 ###
|
||||
|
||||
* Display last frame when seeking to end of stream
|
||||
|
@ -21,14 +21,6 @@ buildscript {
|
||||
classpath 'com.novoda:bintray-release:0.9'
|
||||
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 {
|
||||
repositories {
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.10.3'
|
||||
releaseVersionCode = 2010003
|
||||
releaseVersion = '2.10.4'
|
||||
releaseVersionCode = 2010004
|
||||
minSdkVersion = 16
|
||||
targetSdkVersion = 28
|
||||
compileSdkVersion = 28
|
||||
|
@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9'
|
||||
include modulePrefix + 'extension-rtmp'
|
||||
include modulePrefix + 'extension-leanback'
|
||||
include modulePrefix + 'extension-jobdispatcher'
|
||||
include modulePrefix + 'extension-workmanager'
|
||||
|
||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||
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-leanback').projectDir = new File(rootDir, 'extensions/leanback')
|
||||
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
|
||||
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')
|
||||
|
@ -47,17 +47,6 @@ android {
|
||||
// The demo app isn't indexed and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
|
||||
flavorDimensions "receiver"
|
||||
|
||||
productFlavors {
|
||||
defaultCast {
|
||||
dimension "receiver"
|
||||
manifestPlaceholders =
|
||||
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -25,7 +25,7 @@
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
|
||||
<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"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
|
@ -53,7 +53,7 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
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'
|
||||
|
@ -62,7 +62,7 @@ android {
|
||||
}
|
||||
|
||||
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.fragment:fragment:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
|
@ -41,7 +41,8 @@ public class DemoDownloadService extends DownloadService {
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||
CHANNEL_ID,
|
||||
R.string.exo_download_notification_channel_name);
|
||||
R.string.exo_download_notification_channel_name,
|
||||
/* channelDescriptionResourceId= */ 0);
|
||||
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'com.google.android.gms:play-services-cast-framework:16.2.0'
|
||||
implementation 'androidx.annotation:annotation:1.0.2'
|
||||
api 'com.google.android.gms:play-services-cast-framework:17.0.0'
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
|
@ -83,8 +83,6 @@ public final class CastPlayer extends BasePlayer {
|
||||
private final CastTimelineTracker timelineTracker;
|
||||
private final Timeline.Period period;
|
||||
|
||||
private RemoteMediaClient remoteMediaClient;
|
||||
|
||||
// Result callbacks.
|
||||
private final StatusListener statusListener;
|
||||
private final SeekResultCallback seekResultCallback;
|
||||
@ -93,9 +91,10 @@ public final class CastPlayer extends BasePlayer {
|
||||
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
||||
private final ArrayList<ListenerNotificationTask> notificationsBatch;
|
||||
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
|
||||
private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
|
||||
// Internal state.
|
||||
@Nullable private RemoteMediaClient remoteMediaClient;
|
||||
private CastTimeline currentTimeline;
|
||||
private TrackGroupArray currentTrackGroups;
|
||||
private TrackSelectionArray currentTrackSelection;
|
||||
@ -148,6 +147,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
* starts at position 0.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
|
||||
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.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
|
||||
long positionMs, @RepeatMode int repeatMode) {
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> loadItems(
|
||||
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient != null) {
|
||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
||||
waitingForInitialTimeline = true;
|
||||
@ -180,6 +181,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
* @param items The items to append.
|
||||
* @return The Cast {@code PendingResult}, or null if no media queue exists.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... 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
|
||||
* periodId} exist.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
|
||||
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|
||||
|| 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
|
||||
* periodId} exist.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
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
|
||||
* periodId} exist.
|
||||
*/
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
|
||||
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
|
||||
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
|
||||
* period with id {@code periodId} exist.
|
||||
*/
|
||||
@Nullable
|
||||
public MediaQueueItem getItem(int periodId) {
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
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.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
@ -322,6 +328,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ExoPlaybackException getPlaybackError() {
|
||||
return null;
|
||||
}
|
||||
@ -529,7 +536,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
// Internal methods.
|
||||
|
||||
public void updateInternalState() {
|
||||
private void updateInternalState() {
|
||||
if (remoteMediaClient == null) {
|
||||
// There is no session. We leave the state of the player as it is now.
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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.OptionsProvider;
|
||||
import com.google.android.gms.cast.framework.SessionProvider;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -36,7 +37,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider {
|
||||
|
||||
@Override
|
||||
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,9 +31,9 @@ android {
|
||||
}
|
||||
|
||||
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 'androidx.annotation:annotation:1.0.2'
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
testImplementation project(modulePrefix + 'library')
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
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
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
@ -172,10 +172,35 @@ import java.util.List;
|
||||
private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.AUDIO_AAC:
|
||||
case MimeTypes.AUDIO_ALAC:
|
||||
case MimeTypes.AUDIO_OPUS:
|
||||
return initializationData.get(0);
|
||||
case MimeTypes.AUDIO_ALAC:
|
||||
return getAlacExtraData(initializationData);
|
||||
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[] header1 = initializationData.get(1);
|
||||
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);
|
||||
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
|
||||
return extraData;
|
||||
default:
|
||||
// Other codecs do not require extra data.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private native long ffmpegInitialize(
|
||||
|
@ -39,7 +39,8 @@ android {
|
||||
|
||||
dependencies {
|
||||
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 'androidx.test:runner:' + androidXTestVersion
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
|
@ -9,6 +9,9 @@
|
||||
-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 {
|
||||
*;
|
||||
}
|
||||
|
@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest {
|
||||
|
||||
FlacBinarySearchSeeker seeker =
|
||||
new FlacBinarySearchSeeker(
|
||||
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
||||
decoderJni.decodeStreamMetadata(),
|
||||
/* firstFramePosition= */ 0,
|
||||
data.length,
|
||||
decoderJni);
|
||||
|
||||
SeekMap seekMap = seeker.getSeekMap();
|
||||
assertThat(seekMap).isNotNull();
|
||||
@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest {
|
||||
decoderJni.setData(input);
|
||||
FlacBinarySearchSeeker seeker =
|
||||
new FlacBinarySearchSeeker(
|
||||
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
|
||||
decoderJni.decodeStreamMetadata(),
|
||||
/* firstFramePosition= */ 0,
|
||||
data.length,
|
||||
decoderJni);
|
||||
|
||||
seeker.setSeekTargetUs(/* timeUs= */ 1000);
|
||||
assertThat(seeker.isSeeking()).isTrue();
|
||||
|
@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
|
||||
public class FlacExtractorTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
public void setUp() {
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
fail("Flac library not available.");
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
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.nio.ByteBuffer;
|
||||
|
||||
@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
|
||||
private final FlacDecoderJni decoderJni;
|
||||
|
||||
public FlacBinarySearchSeeker(
|
||||
FlacStreamInfo streamInfo,
|
||||
FlacStreamMetadata streamMetadata,
|
||||
long firstFramePosition,
|
||||
long inputLength,
|
||||
FlacDecoderJni decoderJni) {
|
||||
super(
|
||||
new FlacSeekTimestampConverter(streamInfo),
|
||||
new FlacSeekTimestampConverter(streamMetadata),
|
||||
new FlacTimestampSeeker(decoderJni),
|
||||
streamInfo.durationUs(),
|
||||
streamMetadata.durationUs(),
|
||||
/* floorTimePosition= */ 0,
|
||||
/* ceilingTimePosition= */ streamInfo.totalSamples,
|
||||
/* ceilingTimePosition= */ streamMetadata.totalSamples,
|
||||
/* floorBytePosition= */ firstFramePosition,
|
||||
/* ceilingBytePosition= */ inputLength,
|
||||
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
|
||||
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
|
||||
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
|
||||
/* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
|
||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||
}
|
||||
|
||||
@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
|
||||
* the timestamp for a stream seek time position.
|
||||
*/
|
||||
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
|
||||
private final FlacStreamInfo streamInfo;
|
||||
private final FlacStreamMetadata streamMetadata;
|
||||
|
||||
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
|
||||
this.streamMetadata = streamMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long timeUsToTargetTime(long timeUs) {
|
||||
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
|
||||
return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,13 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
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.SimpleDecoder;
|
||||
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.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
@ -56,21 +58,20 @@ import java.util.List;
|
||||
}
|
||||
decoderJni = new FlacDecoderJni();
|
||||
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
|
||||
FlacStreamInfo streamInfo;
|
||||
FlacStreamMetadata streamMetadata;
|
||||
try {
|
||||
streamInfo = decoderJni.decodeMetadata();
|
||||
streamMetadata = decoderJni.decodeStreamMetadata();
|
||||
} catch (ParserException e) {
|
||||
throw new FlacDecoderException("Failed to decode StreamInfo", e);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// Never happens.
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
if (streamInfo == null) {
|
||||
throw new FlacDecoderException("Metadata decoding failed");
|
||||
}
|
||||
|
||||
int initialInputBufferSize =
|
||||
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
|
||||
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
|
||||
setInitialInputBufferSize(initialInputBufferSize);
|
||||
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
|
||||
maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -94,6 +95,7 @@ import java.util.List;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected FlacDecoderException decode(
|
||||
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
if (reset) {
|
||||
|
@ -15,9 +15,12 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
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.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 ByteBuffer byteBufferData;
|
||||
private ExtractorInput extractorInput;
|
||||
@Nullable private ByteBuffer byteBufferData;
|
||||
@Nullable private ExtractorInput extractorInput;
|
||||
@Nullable private byte[] tempBuffer;
|
||||
private boolean endOfExtractorInput;
|
||||
private byte[] tempBuffer;
|
||||
|
||||
public FlacDecoderJni() throws FlacDecoderException {
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
@ -57,67 +60,79 @@ import java.nio.ByteBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets data to be parsed by libflac.
|
||||
* @param byteBufferData Source {@link ByteBuffer}
|
||||
* Sets the data to be parsed.
|
||||
*
|
||||
* @param byteBufferData Source {@link ByteBuffer}.
|
||||
*/
|
||||
public void setData(ByteBuffer byteBufferData) {
|
||||
this.byteBufferData = byteBufferData;
|
||||
this.extractorInput = null;
|
||||
this.tempBuffer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets data to be parsed by libflac.
|
||||
* @param extractorInput Source {@link ExtractorInput}
|
||||
* Sets the data to be parsed.
|
||||
*
|
||||
* @param extractorInput Source {@link ExtractorInput}.
|
||||
*/
|
||||
public void setData(ExtractorInput extractorInput) {
|
||||
this.byteBufferData = null;
|
||||
this.extractorInput = extractorInput;
|
||||
if (tempBuffer == null) {
|
||||
this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
|
||||
}
|
||||
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() {
|
||||
if (byteBufferData != null) {
|
||||
return byteBufferData.remaining() == 0;
|
||||
} else if (extractorInput != null) {
|
||||
return endOfExtractorInput;
|
||||
}
|
||||
} else {
|
||||
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.
|
||||
* <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.
|
||||
* <p>
|
||||
* This method is called from the native code.
|
||||
*
|
||||
* @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
|
||||
* zero; it just means all the data read from the source.
|
||||
* @return Returns the number of bytes read, or -1 on failure. If all of the data has already been
|
||||
* read from the source, then 0 is returned.
|
||||
*/
|
||||
@SuppressWarnings("unused") // Called from native code.
|
||||
public int read(ByteBuffer target) throws IOException, InterruptedException {
|
||||
int byteCount = target.remaining();
|
||||
if (byteBufferData != null) {
|
||||
byteCount = Math.min(byteCount, byteBufferData.remaining());
|
||||
int originalLimit = byteBufferData.limit();
|
||||
byteBufferData.limit(byteBufferData.position() + byteCount);
|
||||
|
||||
target.put(byteBufferData);
|
||||
|
||||
byteBufferData.limit(originalLimit);
|
||||
} else if (extractorInput != null) {
|
||||
ExtractorInput extractorInput = this.extractorInput;
|
||||
byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
|
||||
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
|
||||
int read = readFromExtractorInput(0, byteCount);
|
||||
int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
|
||||
if (read < 4) {
|
||||
// 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
|
||||
// from the native code.
|
||||
read += readFromExtractorInput(read, byteCount - read);
|
||||
read +=
|
||||
readFromExtractorInput(
|
||||
extractorInput, tempBuffer, read, /* length= */ byteCount - read);
|
||||
}
|
||||
byteCount = read;
|
||||
target.put(tempBuffer, 0, byteCount);
|
||||
@ -127,9 +142,13 @@ import java.nio.ByteBuffer;
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
/** Decodes and consumes the StreamInfo section from the FLAC stream. */
|
||||
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException {
|
||||
return flacDecodeMetadata(nativeDecoderContext);
|
||||
/** Decodes and consumes the metadata from the FLAC stream. */
|
||||
public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
|
||||
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);
|
||||
}
|
||||
|
||||
private int readFromExtractorInput(int offset, int length)
|
||||
private int readFromExtractorInput(
|
||||
ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
|
||||
throws IOException, InterruptedException {
|
||||
int read = extractorInput.read(tempBuffer, offset, length);
|
||||
if (read == C.RESULT_END_OF_INPUT) {
|
||||
@ -246,7 +266,7 @@ import java.nio.ByteBuffer;
|
||||
|
||||
private native long flacInit();
|
||||
|
||||
private native FlacStreamInfo flacDecodeMetadata(long context)
|
||||
private native FlacStreamMetadata flacDecodeMetadata(long context)
|
||||
throws IOException, InterruptedException;
|
||||
|
||||
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
|
||||
|
@ -21,7 +21,7 @@ import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.ExtractorInput;
|
||||
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.metadata.Metadata;
|
||||
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.ParsableByteArray;
|
||||
import java.io.IOException;
|
||||
@ -42,6 +43,9 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.nio.ByteBuffer;
|
||||
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.
|
||||
@ -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 final ParsableByteArray outputBuffer;
|
||||
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 TrackOutput trackOutput;
|
||||
private boolean streamMetadataDecoded;
|
||||
private @MonotonicNonNull FlacStreamMetadata streamMetadata;
|
||||
private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
|
||||
|
||||
private ParsableByteArray outputBuffer;
|
||||
private ByteBuffer outputByteBuffer;
|
||||
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
|
||||
private FlacStreamInfo streamInfo;
|
||||
|
||||
private Metadata id3Metadata;
|
||||
private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
|
||||
|
||||
private boolean readPastStreamInfo;
|
||||
@Nullable private Metadata id3Metadata;
|
||||
@Nullable private FlacBinarySearchSeeker binarySearchSeeker;
|
||||
|
||||
/** Constructs an instance with flags = 0. */
|
||||
public FlacExtractor() {
|
||||
@ -103,8 +104,9 @@ public final class FlacExtractor implements Extractor {
|
||||
* @param flags Flags that control the extractor's behavior.
|
||||
*/
|
||||
public FlacExtractor(int flags) {
|
||||
outputBuffer = new ParsableByteArray();
|
||||
id3Peeker = new Id3Peeker();
|
||||
isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
|
||||
id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -130,17 +132,19 @@ public final class FlacExtractor implements Extractor {
|
||||
@Override
|
||||
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
|
||||
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
|
||||
id3Metadata = peekId3Data(input);
|
||||
}
|
||||
|
||||
decoderJni.setData(input);
|
||||
readPastStreamInfo(input);
|
||||
FlacDecoderJni decoderJni = initDecoderJni(input);
|
||||
try {
|
||||
decodeStreamMetadata(input);
|
||||
|
||||
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
|
||||
return handlePendingSeek(input, seekPosition);
|
||||
if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
|
||||
return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
|
||||
}
|
||||
|
||||
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||
long lastDecodePosition = decoderJni.getDecodePosition();
|
||||
try {
|
||||
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
|
||||
@ -152,26 +156,29 @@ public final class FlacExtractor implements Extractor {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
|
||||
outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
|
||||
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
||||
} finally {
|
||||
decoderJni.clearData();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(long position, long timeUs) {
|
||||
if (position == 0) {
|
||||
readPastStreamInfo = false;
|
||||
streamMetadataDecoded = false;
|
||||
}
|
||||
if (decoderJni != null) {
|
||||
decoderJni.reset(position);
|
||||
}
|
||||
if (flacBinarySearchSeeker != null) {
|
||||
flacBinarySearchSeeker.setSeekTargetUs(timeUs);
|
||||
if (binarySearchSeeker != null) {
|
||||
binarySearchSeeker.setSeekTargetUs(timeUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
flacBinarySearchSeeker = null;
|
||||
binarySearchSeeker = null;
|
||||
if (decoderJni != null) {
|
||||
decoderJni.release();
|
||||
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
|
||||
* present in the input.
|
||||
* @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
|
||||
*/
|
||||
@Nullable
|
||||
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
|
||||
input.resetPeekPosition();
|
||||
Id3Decoder.FramePredicate id3FramePredicate =
|
||||
isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
|
||||
id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
|
||||
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.
|
||||
*
|
||||
* @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];
|
||||
input.peekFully(header, 0, FLAC_SIGNATURE.length);
|
||||
input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);
|
||||
return Arrays.equals(header, FLAC_SIGNATURE);
|
||||
}
|
||||
|
||||
private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
|
||||
if (readPastStreamInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlacStreamInfo streamInfo = decodeStreamInfo(input);
|
||||
readPastStreamInfo = true;
|
||||
if (this.streamInfo == null) {
|
||||
updateFlacStreamInfo(input, streamInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
|
||||
this.streamInfo = streamInfo;
|
||||
outputSeekMap(input, streamInfo);
|
||||
outputFormat(streamInfo);
|
||||
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) {
|
||||
/**
|
||||
* Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
|
||||
* handle seeks.
|
||||
*/
|
||||
@Nullable
|
||||
private static FlacBinarySearchSeeker outputSeekMap(
|
||||
FlacDecoderJni decoderJni,
|
||||
FlacStreamMetadata streamMetadata,
|
||||
long streamLength,
|
||||
ExtractorOutput output) {
|
||||
boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
|
||||
FlacBinarySearchSeeker binarySearchSeeker = null;
|
||||
SeekMap seekMap;
|
||||
if (hasSeekTable) {
|
||||
seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
|
||||
} else if (streamLength != C.LENGTH_UNSET) {
|
||||
long firstFramePosition = decoderJni.getDecodePosition();
|
||||
flacBinarySearchSeeker =
|
||||
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
|
||||
return flacBinarySearchSeeker.getSeekMap();
|
||||
} else { // can't seek at all, because there's no SeekTable and the input length is unknown.
|
||||
return new SeekMap.Unseekable(streamInfo.durationUs());
|
||||
binarySearchSeeker =
|
||||
new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
|
||||
seekMap = binarySearchSeeker.getSeekMap();
|
||||
} else {
|
||||
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.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
streamInfo.bitRate(),
|
||||
streamInfo.maxDecodedFrameSize(),
|
||||
streamInfo.channels,
|
||||
streamInfo.sampleRate,
|
||||
getPcmEncoding(streamInfo.bitsPerSample),
|
||||
streamMetadata.bitRate(),
|
||||
streamMetadata.maxDecodedFrameSize(),
|
||||
streamMetadata.channels,
|
||||
streamMetadata.sampleRate,
|
||||
getPcmEncoding(streamMetadata.bitsPerSample),
|
||||
/* encoderDelay= */ 0,
|
||||
/* encoderPadding= */ 0,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null,
|
||||
isId3MetadataDisabled ? null : id3Metadata);
|
||||
trackOutput.format(mediaFormat);
|
||||
metadata);
|
||||
output.format(mediaFormat);
|
||||
}
|
||||
|
||||
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws InterruptedException, IOException {
|
||||
int seekResult =
|
||||
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
|
||||
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
||||
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);
|
||||
private static void outputSample(
|
||||
ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
|
||||
sampleData.setPosition(0);
|
||||
output.sampleData(sampleData, size);
|
||||
output.sampleMetadata(
|
||||
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
|
||||
}
|
||||
|
||||
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
|
||||
|
@ -14,9 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "include/flac_parser.h"
|
||||
|
||||
#define LOG_TAG "flac_jni"
|
||||
@ -95,19 +98,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
|
||||
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 =
|
||||
context->parser->getStreamInfo();
|
||||
|
||||
jclass cls = env->FindClass(
|
||||
jclass flacStreamMetadataClass = env->FindClass(
|
||||
"com/google/android/exoplayer2/util/"
|
||||
"FlacStreamInfo");
|
||||
jmethodID constructor = env->GetMethodID(cls, "<init>", "(IIIIIIIJ)V");
|
||||
"FlacStreamMetadata");
|
||||
jmethodID flacStreamMetadataConstructor =
|
||||
env->GetMethodID(flacStreamMetadataClass, "<init>",
|
||||
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
|
||||
|
||||
return env->NewObject(cls, constructor, streamInfo.min_blocksize,
|
||||
streamInfo.max_blocksize, streamInfo.min_framesize,
|
||||
streamInfo.max_framesize, streamInfo.sample_rate,
|
||||
streamInfo.channels, streamInfo.bits_per_sample,
|
||||
streamInfo.total_samples);
|
||||
return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
|
||||
streamInfo.min_blocksize, streamInfo.max_blocksize,
|
||||
streamInfo.min_framesize, streamInfo.max_framesize,
|
||||
streamInfo.sample_rate, streamInfo.channels,
|
||||
streamInfo.bits_per_sample, streamInfo.total_samples,
|
||||
commentList, pictureFrames);
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
|
||||
|
@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
|
||||
case FLAC__METADATA_TYPE_SEEKTABLE:
|
||||
mSeekTable = &metadata->data.seek_table;
|
||||
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:
|
||||
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
|
||||
break;
|
||||
@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source)
|
||||
mCurrentPos(0LL),
|
||||
mEOF(false),
|
||||
mStreamInfoValid(false),
|
||||
mVorbisCommentsValid(false),
|
||||
mPicturesValid(false),
|
||||
mWriteRequested(false),
|
||||
mWriteCompleted(false),
|
||||
mWriteBuffer(NULL),
|
||||
@ -266,6 +305,10 @@ bool FLACParser::init() {
|
||||
FLAC__METADATA_TYPE_STREAMINFO);
|
||||
FLAC__stream_decoder_set_metadata_respond(mDecoder,
|
||||
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;
|
||||
initStatus = FLAC__stream_decoder_init_stream(
|
||||
mDecoder, read_callback, seek_callback, tell_callback, length_callback,
|
||||
|
@ -19,6 +19,10 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// libFLAC parser
|
||||
#include "FLAC/stream_decoder.h"
|
||||
|
||||
@ -26,6 +30,17 @@
|
||||
|
||||
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 {
|
||||
public:
|
||||
FLACParser(DataSource *source);
|
||||
@ -44,6 +59,14 @@ class FLACParser {
|
||||
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 {
|
||||
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
|
||||
}
|
||||
@ -71,6 +94,10 @@ class FLACParser {
|
||||
mEOF = false;
|
||||
if (newPosition == 0) {
|
||||
mStreamInfoValid = false;
|
||||
mVorbisCommentsValid = false;
|
||||
mPicturesValid = false;
|
||||
mVorbisComments.clear();
|
||||
mPictures.clear();
|
||||
FLAC__stream_decoder_reset(mDecoder);
|
||||
} else {
|
||||
FLAC__stream_decoder_flush(mDecoder);
|
||||
@ -116,6 +143,14 @@ class FLACParser {
|
||||
const FLAC__StreamMetadata_SeekTable *mSeekTable;
|
||||
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
|
||||
bool mWriteRequested;
|
||||
bool mWriteCompleted;
|
||||
|
@ -33,7 +33,7 @@ android {
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
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'
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ android {
|
||||
dependencies {
|
||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
|
||||
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'
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
# ExoPlayer Firebase JobDispatcher extension #
|
||||
|
||||
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
|
||||
|
||||
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
|
||||
|
||||
## Getting the extension ##
|
||||
@ -20,4 +24,3 @@ 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
|
||||
|
||||
|
@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util;
|
||||
*
|
||||
* @see <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 {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
@ -32,7 +32,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
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'
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
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
|
||||
api 'com.squareup.okhttp3:okhttp:3.12.1'
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
|
||||
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.opus;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
@ -150,6 +151,7 @@ import java.util.List;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected OpusDecoderException decode(
|
||||
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||
if (reset) {
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.opus;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||
|
||||
@ -49,9 +50,8 @@ public final class OpusLibrary {
|
||||
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() {
|
||||
return isAvailable() ? opusGetVersion() : null;
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ android {
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
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')
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:1.0.2'
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
||||
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
|
||||
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.vp9;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.Surface;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||
@ -120,8 +121,9 @@ import java.nio.ByteBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
|
||||
boolean reset) {
|
||||
@Nullable
|
||||
protected VpxDecoderException decode(
|
||||
VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) {
|
||||
ByteBuffer inputData = inputBuffer.data;
|
||||
int inputSize = inputData.limit();
|
||||
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.vp9;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||
|
||||
@ -49,9 +50,8 @@ public final class VpxLibrary {
|
||||
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() {
|
||||
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
|
||||
* null otherwise.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getBuildConfig() {
|
||||
return isAvailable() ? vpxGetBuildConfig() : null;
|
||||
}
|
||||
|
22
extensions/workmanager/README.md
Normal file
22
extensions/workmanager/README.md
Normal 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
|
49
extensions/workmanager/build.gradle
Normal file
49
extensions/workmanager/build.gradle
Normal 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'
|
18
extensions/workmanager/src/main/AndroidManifest.xml
Normal file
18
extensions/workmanager/src/main/AndroidManifest.xml
Normal 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"/>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ android {
|
||||
}
|
||||
|
||||
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-compat-qual:' + checkerframeworkVersion
|
||||
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
|
||||
|
@ -532,7 +532,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
public long getContentPosition() {
|
||||
if (isPlayingAd()) {
|
||||
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 {
|
||||
return getCurrentPosition();
|
||||
}
|
||||
|
@ -1304,8 +1304,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
Pair<Object, Long> defaultPosition =
|
||||
getPeriodPosition(
|
||||
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;
|
||||
newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs);
|
||||
}
|
||||
} 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
|
||||
// window we can restart from.
|
||||
|
@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
|
||||
|
||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.10.3";
|
||||
public static final String VERSION = "2.10.4";
|
||||
|
||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.3";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.4";
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
// 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}
|
||||
|
@ -29,7 +29,8 @@ import com.google.android.exoplayer2.util.Util;
|
||||
public final long startPositionUs;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
|
@ -144,7 +144,9 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
MediaPeriodInfo info) {
|
||||
long rendererPositionOffsetUs =
|
||||
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);
|
||||
MediaPeriodHolder newPeriodHolder =
|
||||
new MediaPeriodHolder(
|
||||
@ -560,6 +562,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
}
|
||||
|
||||
long startPositionUs;
|
||||
long contentPositionUs;
|
||||
int nextWindowIndex =
|
||||
timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;
|
||||
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
|
||||
// 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.
|
||||
contentPositionUs = C.TIME_UNSET;
|
||||
Pair<Object, Long> defaultPosition =
|
||||
timeline.getPeriodPosition(
|
||||
window,
|
||||
@ -587,12 +591,13 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
windowSequenceNumber = nextWindowSequenceNumber++;
|
||||
}
|
||||
} else {
|
||||
// We're starting to buffer a new period within the same window.
|
||||
startPositionUs = 0;
|
||||
contentPositionUs = 0;
|
||||
}
|
||||
MediaPeriodId periodId =
|
||||
resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber);
|
||||
return getMediaPeriodInfo(
|
||||
periodId, /* contentPositionUs= */ startPositionUs, startPositionUs);
|
||||
return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs);
|
||||
}
|
||||
|
||||
MediaPeriodId currentPeriodId = mediaPeriodInfo.id;
|
||||
@ -616,13 +621,11 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
mediaPeriodInfo.contentPositionUs,
|
||||
currentPeriodId.windowSequenceNumber);
|
||||
} else {
|
||||
// Play content from the ad group position. As a special case, if we're transitioning from a
|
||||
// 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).
|
||||
// Play content from the ad group position.
|
||||
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 =
|
||||
timeline.getPeriodPosition(
|
||||
window,
|
||||
|
@ -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
|
||||
* 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;
|
||||
/** The current playback state. One of the {@link Player}.STATE_ constants. */
|
||||
|
@ -364,8 +364,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
return Collections.singletonList(passthroughDecoderInfo);
|
||||
}
|
||||
}
|
||||
return mediaCodecSelector.getDecoderInfos(
|
||||
List<MediaCodecInfo> decoderInfos =
|
||||
mediaCodecSelector.getDecoderInfos(
|
||||
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);
|
||||
codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name);
|
||||
passthroughEnabled = codecInfo.passthrough;
|
||||
String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.mimeType;
|
||||
String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType;
|
||||
MediaFormat mediaFormat =
|
||||
getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate);
|
||||
codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0);
|
||||
|
@ -30,6 +30,7 @@ import java.util.Arrays;
|
||||
private static final int MINIMUM_PITCH = 65;
|
||||
private static final int MAXIMUM_PITCH = 400;
|
||||
private static final int AMDF_FREQUENCY = 4000;
|
||||
private static final int BYTES_PER_SAMPLE = 2;
|
||||
|
||||
private final int inputSampleRateHz;
|
||||
private final int channelCount;
|
||||
@ -157,9 +158,9 @@ import java.util.Arrays;
|
||||
maxDiff = 0;
|
||||
}
|
||||
|
||||
/** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */
|
||||
public int getFramesAvailable() {
|
||||
return outputFrameCount;
|
||||
/** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */
|
||||
public int getOutputSize() {
|
||||
return outputFrameCount * channelCount * BYTES_PER_SAMPLE;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
@ -210,7 +210,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
||||
sonic.queueInput(shortBuffer);
|
||||
inputBuffer.position(inputBuffer.position() + inputSize);
|
||||
}
|
||||
int outputSize = sonic.getFramesAvailable() * channelCount * 2;
|
||||
int outputSize = sonic.getOutputSize();
|
||||
if (outputSize > 0) {
|
||||
if (buffer.capacity() < outputSize) {
|
||||
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
|
||||
@ -243,7 +243,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
||||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0);
|
||||
return inputEnded && (sonic == null || sonic.getOutputSize() == 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -301,5 +301,6 @@ public abstract class SimpleDecoder<
|
||||
* @param reset Whether the decoder must be reset before decoding.
|
||||
* @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);
|
||||
}
|
||||
|
@ -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];
|
||||
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
|
||||
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
|
||||
|
@ -119,7 +119,7 @@ public interface TrackOutput {
|
||||
* Called to write sample data to the output.
|
||||
*
|
||||
* @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);
|
||||
|
||||
|
@ -15,8 +15,10 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor.flv;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
@ -44,7 +46,7 @@ import java.util.Map;
|
||||
private long durationUs;
|
||||
|
||||
public ScriptTagPayloadReader() {
|
||||
super(null);
|
||||
super(new DummyTrackOutput());
|
||||
durationUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
@ -138,7 +140,10 @@ import java.util.Map;
|
||||
ArrayList<Object> list = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int type = readAmfType(data);
|
||||
list.add(readAmfData(data, type));
|
||||
Object value = readAmfData(data, type);
|
||||
if (value != null) {
|
||||
list.add(value);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@ -157,7 +162,10 @@ import java.util.Map;
|
||||
if (type == AMF_TYPE_END_MARKER) {
|
||||
break;
|
||||
}
|
||||
array.put(key, readAmfData(data, type));
|
||||
Object value = readAmfData(data, type);
|
||||
if (value != null) {
|
||||
array.put(key, value);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
@ -174,7 +182,10 @@ import java.util.Map;
|
||||
for (int i = 0; i < count; i++) {
|
||||
String key = readAmfString(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;
|
||||
}
|
||||
@ -191,6 +202,7 @@ import java.util.Map;
|
||||
return date;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Object readAmfData(ParsableByteArray data, int type) {
|
||||
switch (type) {
|
||||
case AMF_TYPE_NUMBER:
|
||||
@ -208,8 +220,8 @@ import java.util.Map;
|
||||
case AMF_TYPE_DATE:
|
||||
return readAmfDate(data);
|
||||
default:
|
||||
// We don't log a warning because there are types that we knowingly don't support.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ public final class Mp3Extractor implements Extractor {
|
||||
private Seeker seeker;
|
||||
private long basisTimeUs;
|
||||
private long samplesRead;
|
||||
private long firstSamplePosition;
|
||||
private int sampleBytesRemaining;
|
||||
|
||||
public Mp3Extractor() {
|
||||
@ -214,6 +215,13 @@ public final class Mp3Extractor implements Extractor {
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null,
|
||||
(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);
|
||||
}
|
||||
|
@ -1140,10 +1140,6 @@ import java.util.List;
|
||||
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,
|
||||
language);
|
||||
} else if (childAtomType == Atom.TYPE_alac) {
|
||||
initializationData = new byte[childAtomSize];
|
||||
parent.setPosition(childPosition);
|
||||
parent.readBytes(initializationData, /* offset= */ 0, childAtomSize);
|
||||
} else if (childAtomType == Atom.TYPE_dOps) {
|
||||
// Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic
|
||||
// Signature and the body of the dOps atom.
|
||||
@ -1152,7 +1148,7 @@ import java.util.List;
|
||||
System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length);
|
||||
parent.setPosition(childPosition + Atom.HEADER_SIZE);
|
||||
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;
|
||||
initializationData = new byte[childAtomBodySize];
|
||||
parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);
|
||||
|
@ -123,6 +123,7 @@ public final class Track {
|
||||
* @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no
|
||||
* such entry exists.
|
||||
*/
|
||||
@Nullable
|
||||
public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) {
|
||||
return sampleDescriptionEncryptionBoxes == null ? null
|
||||
: sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
|
||||
|
@ -52,7 +52,7 @@ public final class TrackEncryptionBox {
|
||||
* If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the
|
||||
* track encryption box or sample group description box. Null otherwise.
|
||||
*/
|
||||
public final byte[] defaultInitializationVector;
|
||||
@Nullable public final byte[] defaultInitializationVector;
|
||||
|
||||
/**
|
||||
* @param isEncrypted See {@link #isEncrypted}.
|
||||
|
@ -16,29 +16,32 @@
|
||||
package com.google.android.exoplayer2.extractor.ogg;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
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.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Seeks in an Ogg stream. */
|
||||
/* package */ final class DefaultOggSeeker implements OggSeeker {
|
||||
|
||||
@VisibleForTesting public static final int MATCH_RANGE = 72000;
|
||||
@VisibleForTesting public static final int MATCH_BYTE_RANGE = 100000;
|
||||
private static final int MATCH_RANGE = 72000;
|
||||
private static final int MATCH_BYTE_RANGE = 100000;
|
||||
private static final int DEFAULT_OFFSET = 30000;
|
||||
|
||||
private static final int STATE_SEEK_TO_END = 0;
|
||||
private static final int STATE_READ_LAST_PAGE = 1;
|
||||
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 long startPosition;
|
||||
private final long endPosition;
|
||||
private final long payloadStartPosition;
|
||||
private final long payloadEndPosition;
|
||||
private final StreamReader streamReader;
|
||||
|
||||
private int state;
|
||||
@ -54,26 +57,27 @@ import java.io.IOException;
|
||||
/**
|
||||
* 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 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 firstPayloadPageGranulePosition The granule position of the first payload page.
|
||||
* @param firstPayloadPageIsLastPage Whether the first payload page is also the last page in the
|
||||
* ogg stream.
|
||||
* @param firstPayloadPageIsLastPage Whether the first payload page is also the last page.
|
||||
*/
|
||||
public DefaultOggSeeker(
|
||||
long startPosition,
|
||||
long endPosition,
|
||||
StreamReader streamReader,
|
||||
long payloadStartPosition,
|
||||
long payloadEndPosition,
|
||||
long firstPayloadPageSize,
|
||||
long firstPayloadPageGranulePosition,
|
||||
boolean firstPayloadPageIsLastPage) {
|
||||
Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition);
|
||||
Assertions.checkArgument(
|
||||
payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);
|
||||
this.streamReader = streamReader;
|
||||
this.startPosition = startPosition;
|
||||
this.endPosition = endPosition;
|
||||
if (firstPayloadPageSize == endPosition - startPosition || firstPayloadPageIsLastPage) {
|
||||
this.payloadStartPosition = payloadStartPosition;
|
||||
this.payloadEndPosition = payloadEndPosition;
|
||||
if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition
|
||||
|| firstPayloadPageIsLastPage) {
|
||||
totalGranules = firstPayloadPageGranulePosition;
|
||||
state = STATE_IDLE;
|
||||
} else {
|
||||
@ -90,7 +94,7 @@ import java.io.IOException;
|
||||
positionBeforeSeekToEnd = input.getPosition();
|
||||
state = STATE_READ_LAST_PAGE;
|
||||
// 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) {
|
||||
return lastPageSearchPosition;
|
||||
}
|
||||
@ -100,91 +104,77 @@ import java.io.IOException;
|
||||
state = STATE_IDLE;
|
||||
return positionBeforeSeekToEnd;
|
||||
case STATE_SEEK:
|
||||
long currentGranule;
|
||||
if (targetGranule == 0) {
|
||||
currentGranule = 0;
|
||||
} else {
|
||||
long position = getNextSeekPosition(targetGranule, input);
|
||||
if (position >= 0) {
|
||||
long position = getNextSeekPosition(input);
|
||||
if (position != C.POSITION_UNSET) {
|
||||
return position;
|
||||
}
|
||||
currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2));
|
||||
}
|
||||
state = STATE_SKIP;
|
||||
// Fall through.
|
||||
case STATE_SKIP:
|
||||
skipToPageOfTargetGranule(input);
|
||||
state = STATE_IDLE;
|
||||
return -(currentGranule + 2);
|
||||
return -(startGranule + 2);
|
||||
default:
|
||||
// Never happens.
|
||||
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
|
||||
public OggSeekMap createSeekMap() {
|
||||
return totalGranules != 0 ? new OggSeekMap() : null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void resetSeeking() {
|
||||
start = startPosition;
|
||||
end = endPosition;
|
||||
@Override
|
||||
public void startSeek(long targetGranule) {
|
||||
this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1);
|
||||
state = STATE_SEEK;
|
||||
start = payloadStartPosition;
|
||||
end = payloadEndPosition;
|
||||
startGranule = 0;
|
||||
endGranule = totalGranules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput}
|
||||
* has to seek and then be passed for another call until a negative number is returned. If a
|
||||
* negative number is returned the input is at a position which is before the target page and at
|
||||
* which it is sensible to just skip pages to the target granule and pre-roll instead of doing
|
||||
* another seek request.
|
||||
* Performs a single step of a seeking binary search, returning the byte position from which data
|
||||
* should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged.
|
||||
* If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be
|
||||
* called to skip to the target page.
|
||||
*
|
||||
* @param targetGranule the target granule position to seek to.
|
||||
* @param input the {@link ExtractorInput} to read from.
|
||||
* @return the position to seek the {@link ExtractorInput} to for a next call or -(currentGranule
|
||||
* + 2) if it's close enough to skip to the target page.
|
||||
* @throws IOException thrown if reading from the input fails.
|
||||
* @throws InterruptedException thrown if interrupted while reading from the input.
|
||||
* @param input The {@link ExtractorInput} to read from.
|
||||
* @return The byte position from which data should be provided for the next step, or {@link
|
||||
* C#POSITION_UNSET} if the search has converged.
|
||||
* @throws IOException If reading from the input fails.
|
||||
* @throws InterruptedException If interrupted while reading from the input.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public long getNextSeekPosition(long targetGranule, ExtractorInput input)
|
||||
throws IOException, InterruptedException {
|
||||
private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException {
|
||||
if (start == end) {
|
||||
return -(startGranule + 2);
|
||||
return C.POSITION_UNSET;
|
||||
}
|
||||
|
||||
long initialPosition = input.getPosition();
|
||||
long currentPosition = input.getPosition();
|
||||
if (!skipToNextPage(input, end)) {
|
||||
if (start == initialPosition) {
|
||||
if (start == currentPosition) {
|
||||
throw new IOException("No ogg page can be found.");
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
pageHeader.populate(input, false);
|
||||
pageHeader.populate(input, /* quiet= */ false);
|
||||
input.resetPeekPosition();
|
||||
|
||||
long granuleDistance = targetGranule - pageHeader.granulePosition;
|
||||
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) {
|
||||
end = initialPosition;
|
||||
end = currentPosition;
|
||||
endGranule = pageHeader.granulePosition;
|
||||
} else {
|
||||
start = input.getPosition() + pageSize;
|
||||
startGranule = pageHeader.granulePosition;
|
||||
if (end - start + pageSize < MATCH_BYTE_RANGE) {
|
||||
input.skipFully(pageSize);
|
||||
return -(startGranule + 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (end - start < MATCH_BYTE_RANGE) {
|
||||
@ -193,52 +183,31 @@ import java.io.IOException;
|
||||
}
|
||||
|
||||
long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
|
||||
long nextPosition = input.getPosition() - offset
|
||||
long nextPosition =
|
||||
input.getPosition()
|
||||
- offset
|
||||
+ (granuleDistance * (end - start) / (endGranule - startGranule));
|
||||
|
||||
nextPosition = Math.max(nextPosition, start);
|
||||
nextPosition = Math.min(nextPosition, end - 1);
|
||||
return nextPosition;
|
||||
return Util.constrainValue(nextPosition, start, end - 1);
|
||||
}
|
||||
|
||||
// position accepted (before target granule and within MATCH_RANGE)
|
||||
input.skipFully(pageSize);
|
||||
return -(pageHeader.granulePosition + 2);
|
||||
/**
|
||||
* Skips forward to the start of the page containing the {@code targetGranule}.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
input.resetPeekPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -251,7 +220,7 @@ import java.io.IOException;
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
|
||||
if (!skipToNextPage(input, endPosition)) {
|
||||
if (!skipToNextPage(input, payloadEndPosition)) {
|
||||
// Not found until eof.
|
||||
throw new EOFException();
|
||||
}
|
||||
@ -263,13 +232,12 @@ import java.io.IOException;
|
||||
* @param input The {@code ExtractorInput} to skip to the next page.
|
||||
* @param limit The limit up to which the search should take place.
|
||||
* @return Whether the next page was found.
|
||||
* @throws IOException thrown if peeking/reading from the input fails.
|
||||
* @throws InterruptedException thrown if interrupted while peeking/reading from the input.
|
||||
* @throws IOException If peeking/reading from the input fails.
|
||||
* @throws InterruptedException If interrupted while peeking/reading from the input.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean skipToNextPage(ExtractorInput input, long limit)
|
||||
private boolean skipToNextPage(ExtractorInput input, long limit)
|
||||
throws IOException, InterruptedException {
|
||||
limit = Math.min(limit + 3, endPosition);
|
||||
limit = Math.min(limit + 3, payloadEndPosition);
|
||||
byte[] buffer = new byte[2048];
|
||||
int peekLength = buffer.length;
|
||||
while (true) {
|
||||
@ -310,39 +278,35 @@ import java.io.IOException;
|
||||
long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException {
|
||||
skipToNextPage(input);
|
||||
pageHeader.reset();
|
||||
while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) {
|
||||
pageHeader.populate(input, false);
|
||||
while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) {
|
||||
pageHeader.populate(input, /* quiet= */ false);
|
||||
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
|
||||
}
|
||||
return pageHeader.granulePosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param input the {@link ExtractorInput} to read from.
|
||||
* @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;
|
||||
private final class OggSeekMap implements SeekMap {
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
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.ParsableByteArray;
|
||||
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 FlacStreamInfo streamInfo;
|
||||
private FlacStreamMetadata streamMetadata;
|
||||
private FlacOggSeeker flacOggSeeker;
|
||||
|
||||
public static boolean verifyBitstreamType(ParsableByteArray data) {
|
||||
@ -50,7 +50,7 @@ import java.util.List;
|
||||
protected void reset(boolean headerData) {
|
||||
super.reset(headerData);
|
||||
if (headerData) {
|
||||
streamInfo = null;
|
||||
streamMetadata = null;
|
||||
flacOggSeeker = null;
|
||||
}
|
||||
}
|
||||
@ -71,14 +71,24 @@ import java.util.List;
|
||||
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
|
||||
throws IOException, InterruptedException {
|
||||
byte[] data = packet.data;
|
||||
if (streamInfo == null) {
|
||||
streamInfo = new FlacStreamInfo(data, 17);
|
||||
if (streamMetadata == null) {
|
||||
streamMetadata = new FlacStreamMetadata(data, 17);
|
||||
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
|
||||
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
|
||||
List<byte[]> initializationData = Collections.singletonList(metadata);
|
||||
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null,
|
||||
Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate,
|
||||
initializationData, null, 0, null);
|
||||
setupData.format =
|
||||
Format.createAudioSampleFormat(
|
||||
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) {
|
||||
flacOggSeeker = new FlacOggSeeker();
|
||||
flacOggSeeker.parseSeekTable(packet);
|
||||
@ -175,11 +185,9 @@ import java.util.List;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long startSeek(long timeUs) {
|
||||
long granule = convertTimeToGranule(timeUs);
|
||||
int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
|
||||
public void startSeek(long targetGranule) {
|
||||
int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true);
|
||||
pendingSeekGranule = seekPointGranules[index];
|
||||
return granule;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -211,7 +219,7 @@ import java.util.List;
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
return streamInfo.durationUs();
|
||||
return streamMetadata.durationUs();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,7 +38,13 @@ import java.io.IOException;
|
||||
|
||||
public int revision;
|
||||
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 streamSerialNumber;
|
||||
public long pageSequenceNumber;
|
||||
public long pageChecksum;
|
||||
@ -72,10 +78,10 @@ import java.io.IOException;
|
||||
* Peeks an Ogg page header and updates this {@link OggPageHeader}.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} to read from.
|
||||
* @param quiet If {@code true}, no exceptions are thrown but {@code false} is returned if
|
||||
* something goes wrong.
|
||||
* @return {@code true} if the read was successful. The read fails if the end of the input is
|
||||
* encountered without reading data.
|
||||
* @param quiet Whether to return {@code false} rather than throwing an exception if the header
|
||||
* cannot be populated.
|
||||
* @return Whether the read was successful. The read fails if the end of the input is encountered
|
||||
* without reading data.
|
||||
* @throws IOException If reading data fails or the stream is invalid.
|
||||
* @throws InterruptedException If the thread is interrupted.
|
||||
*/
|
||||
|
@ -33,16 +33,14 @@ import java.io.IOException;
|
||||
SeekMap createSeekMap();
|
||||
|
||||
/**
|
||||
* Initializes a seek operation.
|
||||
* Starts a seek operation.
|
||||
*
|
||||
* @param timeUs The seek position in microseconds.
|
||||
* @return The granule position targeted by the seek.
|
||||
* @param targetGranule The target granule position.
|
||||
*/
|
||||
long startSeek(long timeUs);
|
||||
void startSeek(long targetGranule);
|
||||
|
||||
/**
|
||||
* Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a
|
||||
* progressive seek.
|
||||
* Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek.
|
||||
* <p/>
|
||||
* 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
|
||||
|
@ -91,7 +91,8 @@ import java.io.IOException;
|
||||
reset(!seekMapSet);
|
||||
} else {
|
||||
if (state != STATE_READ_HEADERS) {
|
||||
targetGranule = oggSeeker.startSeek(timeUs);
|
||||
targetGranule = convertTimeToGranule(timeUs);
|
||||
oggSeeker.startSeek(targetGranule);
|
||||
state = STATE_READ_PAYLOAD;
|
||||
}
|
||||
}
|
||||
@ -147,9 +148,9 @@ import java.io.IOException;
|
||||
boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.
|
||||
oggSeeker =
|
||||
new DefaultOggSeeker(
|
||||
this,
|
||||
payloadStartPosition,
|
||||
input.getLength(),
|
||||
this,
|
||||
firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
|
||||
firstPayloadPageHeader.granulePosition,
|
||||
isLastPage);
|
||||
@ -248,13 +249,13 @@ import java.io.IOException;
|
||||
private static final class UnseekableOggSeeker implements OggSeeker {
|
||||
|
||||
@Override
|
||||
public long read(ExtractorInput input) throws IOException, InterruptedException {
|
||||
public long read(ExtractorInput input) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long startSeek(long timeUs) {
|
||||
return 0;
|
||||
public void startSeek(long targetGranule) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -87,12 +87,14 @@ public final class WavExtractor implements Extractor {
|
||||
if (!wavHeader.hasDataBounds()) {
|
||||
WavHeaderReader.skipToData(input, wavHeader);
|
||||
extractorOutput.seekMap(wavHeader);
|
||||
} else if (input.getPosition() == 0) {
|
||||
input.skipFully(wavHeader.getDataStartPosition());
|
||||
}
|
||||
|
||||
long dataLimit = wavHeader.getDataLimit();
|
||||
Assertions.checkState(dataLimit != C.POSITION_UNSET);
|
||||
long dataEndPosition = wavHeader.getDataEndPosition();
|
||||
Assertions.checkState(dataEndPosition != C.POSITION_UNSET);
|
||||
|
||||
long bytesLeft = dataLimit - input.getPosition();
|
||||
long bytesLeft = dataEndPosition - input.getPosition();
|
||||
if (bytesLeft <= 0) {
|
||||
return Extractor.RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
@ -33,23 +33,29 @@ import com.google.android.exoplayer2.util.Util;
|
||||
private final int blockAlignment;
|
||||
/** Bits per sample for the audio data. */
|
||||
private final int bitsPerSample;
|
||||
/** The PCM encoding */
|
||||
@C.PcmEncoding
|
||||
private final int encoding;
|
||||
/** The PCM encoding. */
|
||||
@C.PcmEncoding private final int encoding;
|
||||
|
||||
/** Offset to the start of sample data. */
|
||||
private long dataStartPosition;
|
||||
/** Total size in bytes of the sample data. */
|
||||
private long dataSize;
|
||||
/** Position of the start of the sample data, in bytes. */
|
||||
private int dataStartPosition;
|
||||
/** Position of the end of the sample data (exclusive), in bytes. */
|
||||
private long dataEndPosition;
|
||||
|
||||
public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment,
|
||||
int bitsPerSample, @C.PcmEncoding int encoding) {
|
||||
public WavHeader(
|
||||
int numChannels,
|
||||
int sampleRateHz,
|
||||
int averageBytesPerSecond,
|
||||
int blockAlignment,
|
||||
int bitsPerSample,
|
||||
@C.PcmEncoding int encoding) {
|
||||
this.numChannels = numChannels;
|
||||
this.sampleRateHz = sampleRateHz;
|
||||
this.averageBytesPerSecond = averageBytesPerSecond;
|
||||
this.blockAlignment = blockAlignment;
|
||||
this.bitsPerSample = bitsPerSample;
|
||||
this.encoding = encoding;
|
||||
dataStartPosition = C.POSITION_UNSET;
|
||||
dataEndPosition = C.POSITION_UNSET;
|
||||
}
|
||||
|
||||
// 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.
|
||||
*
|
||||
* @param dataStartPosition The data start position in bytes.
|
||||
* @param dataSize The data size in bytes.
|
||||
* @param dataStartPosition The position of the start of the sample data, 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.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() {
|
||||
return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET;
|
||||
/**
|
||||
* Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if
|
||||
* 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. */
|
||||
public boolean hasDataBounds() {
|
||||
return dataStartPosition != 0 && dataSize != 0;
|
||||
return dataStartPosition != C.POSITION_UNSET;
|
||||
}
|
||||
|
||||
// SeekMap implementation.
|
||||
@ -84,12 +101,13 @@ import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
@Override
|
||||
public long getDurationUs() {
|
||||
long numFrames = dataSize / blockAlignment;
|
||||
long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment;
|
||||
return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekPoints getSeekPoints(long timeUs) {
|
||||
long dataSize = dataEndPosition - dataStartPosition;
|
||||
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
|
||||
// Constrain to nearest preceding frame offset.
|
||||
positionOffset = (positionOffset / blockAlignment) * blockAlignment;
|
||||
|
@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
||||
/** 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, ...
|
||||
input.advancePeekPosition((int) chunkHeader.size - 16);
|
||||
|
||||
return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment,
|
||||
bitsPerSample, encoding);
|
||||
return new WavHeader(
|
||||
numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,11 +121,13 @@ import java.io.IOException;
|
||||
ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
|
||||
// Skip all chunks until we hit the data header.
|
||||
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);
|
||||
}
|
||||
long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size;
|
||||
// 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;
|
||||
}
|
||||
if (bytesToSkip > Integer.MAX_VALUE) {
|
||||
@ -138,7 +139,14 @@ import java.io.IOException;
|
||||
// Skip past the "data" header.
|
||||
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() {
|
||||
|
@ -54,8 +54,15 @@ public final class MediaCodecInfo {
|
||||
public final @Nullable String mimeType;
|
||||
|
||||
/**
|
||||
* The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this
|
||||
* is a passthrough codec.
|
||||
* The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this
|
||||
* 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;
|
||||
|
||||
@ -98,6 +105,7 @@ public final class MediaCodecInfo {
|
||||
return new MediaCodecInfo(
|
||||
name,
|
||||
/* mimeType= */ null,
|
||||
/* codecMimeType= */ null,
|
||||
/* capabilities= */ null,
|
||||
/* passthrough= */ true,
|
||||
/* forceDisableAdaptive= */ false,
|
||||
@ -109,26 +117,10 @@ public final class MediaCodecInfo {
|
||||
*
|
||||
* @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.
|
||||
* @return The created instance.
|
||||
*/
|
||||
public static MediaCodecInfo newInstance(String name, String mimeType,
|
||||
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 codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}.
|
||||
* 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
|
||||
* {@code null} if not known.
|
||||
* @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}.
|
||||
* @param forceSecure Whether {@link #secure} should be forced to {@code true}.
|
||||
* @return The created instance.
|
||||
@ -136,22 +128,31 @@ public final class MediaCodecInfo {
|
||||
public static MediaCodecInfo newInstance(
|
||||
String name,
|
||||
String mimeType,
|
||||
CodecCapabilities capabilities,
|
||||
String codecMimeType,
|
||||
@Nullable CodecCapabilities capabilities,
|
||||
boolean forceDisableAdaptive,
|
||||
boolean forceSecure) {
|
||||
return new MediaCodecInfo(
|
||||
name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure);
|
||||
name,
|
||||
mimeType,
|
||||
codecMimeType,
|
||||
capabilities,
|
||||
/* passthrough= */ false,
|
||||
forceDisableAdaptive,
|
||||
forceSecure);
|
||||
}
|
||||
|
||||
private MediaCodecInfo(
|
||||
String name,
|
||||
@Nullable String mimeType,
|
||||
@Nullable String codecMimeType,
|
||||
@Nullable CodecCapabilities capabilities,
|
||||
boolean passthrough,
|
||||
boolean forceDisableAdaptive,
|
||||
boolean forceSecure) {
|
||||
this.name = Assertions.checkNotNull(name);
|
||||
this.mimeType = mimeType;
|
||||
this.codecMimeType = codecMimeType;
|
||||
this.capabilities = capabilities;
|
||||
this.passthrough = passthrough;
|
||||
adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);
|
||||
|
@ -1806,9 +1806,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
*/
|
||||
private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
|
||||
String name = codecInfo.name;
|
||||
return (Util.SDK_INT <= 17
|
||||
&& ("OMX.rk.video_decoder.avc".equals(name)
|
||||
|| "OMX.allwinner.video.decoder.avc".equals(name)))
|
||||
return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name))
|
||||
|| (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name))
|
||||
|| ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
|
||||
}
|
||||
|
||||
|
@ -161,24 +161,17 @@ public final class MediaCodecUtil {
|
||||
Util.SDK_INT >= 21
|
||||
? new MediaCodecListCompatV21(secure, tunneling)
|
||||
: 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) {
|
||||
// Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the
|
||||
// legacy path. We also try this path on API levels 22 and 23 as a defensive measure.
|
||||
mediaCodecList = new MediaCodecListCompatV16();
|
||||
decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType);
|
||||
decoderInfos = getDecoderInfosInternal(key, mediaCodecList);
|
||||
if (!decoderInfos.isEmpty()) {
|
||||
Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
|
||||
+ ". 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);
|
||||
List<MediaCodecInfo> unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos);
|
||||
decoderInfosCache.put(key, unmodifiableDecoderInfos);
|
||||
@ -249,13 +242,11 @@ public final class MediaCodecUtil {
|
||||
*
|
||||
* @param key The codec key.
|
||||
* @param mediaCodecList The codec list.
|
||||
* @param requestedMimeType The originally requested MIME type, which may differ from the codec
|
||||
* key MIME type if the codec key is being considered as a fallback.
|
||||
* @return The codec information for usable codecs matching the specified key.
|
||||
* @throws DecoderQueryException If there was an error querying the available decoders.
|
||||
*/
|
||||
private static ArrayList<MediaCodecInfo> getDecoderInfosInternal(CodecKey key,
|
||||
MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException {
|
||||
MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
|
||||
try {
|
||||
ArrayList<MediaCodecInfo> decoderInfos = new ArrayList<>();
|
||||
String mimeType = key.mimeType;
|
||||
@ -265,28 +256,27 @@ public final class MediaCodecUtil {
|
||||
for (int i = 0; i < numberOfCodecs; i++) {
|
||||
android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);
|
||||
String name = codecInfo.getName();
|
||||
String supportedType =
|
||||
getCodecSupportedType(codecInfo, name, secureDecodersExplicit, requestedMimeType);
|
||||
if (supportedType == null) {
|
||||
String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType);
|
||||
if (codecMimeType == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType);
|
||||
CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType);
|
||||
boolean tunnelingSupported =
|
||||
mediaCodecList.isFeatureSupported(
|
||||
CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities);
|
||||
CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities);
|
||||
boolean tunnelingRequired =
|
||||
mediaCodecList.isFeatureRequired(
|
||||
CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities);
|
||||
CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities);
|
||||
if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) {
|
||||
continue;
|
||||
}
|
||||
boolean secureSupported =
|
||||
mediaCodecList.isFeatureSupported(
|
||||
CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities);
|
||||
CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities);
|
||||
boolean secureRequired =
|
||||
mediaCodecList.isFeatureRequired(
|
||||
CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities);
|
||||
CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities);
|
||||
if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) {
|
||||
continue;
|
||||
}
|
||||
@ -295,12 +285,18 @@ public final class MediaCodecUtil {
|
||||
|| (!secureDecodersExplicit && !key.secure)) {
|
||||
decoderInfos.add(
|
||||
MediaCodecInfo.newInstance(
|
||||
name, mimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ false));
|
||||
name,
|
||||
mimeType,
|
||||
codecMimeType,
|
||||
capabilities,
|
||||
forceDisableAdaptive,
|
||||
/* forceSecure= */ false));
|
||||
} else if (!secureDecodersExplicit && secureSupported) {
|
||||
decoderInfos.add(
|
||||
MediaCodecInfo.newInstance(
|
||||
name + ".secure",
|
||||
mimeType,
|
||||
codecMimeType,
|
||||
capabilities,
|
||||
forceDisableAdaptive,
|
||||
/* forceSecure= */ true));
|
||||
@ -314,7 +310,7 @@ public final class MediaCodecUtil {
|
||||
} else {
|
||||
// Rethrow error querying primary codec capabilities, or secondary codec
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@ -328,25 +324,35 @@ public final class MediaCodecUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the codec's supported type for decoding {@code requestedMimeType} on the current
|
||||
* device, or {@code null} if the codec can't be used.
|
||||
* Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if
|
||||
* the codec can't be used.
|
||||
*
|
||||
* @param info The codec information.
|
||||
* @param name The name of the codec
|
||||
* @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.
|
||||
* @param requestedMimeType The originally requested MIME type, which may differ from the codec
|
||||
* key MIME type if the codec key is being considered as a fallback.
|
||||
* @return The codec's supported type for decoding {@code requestedMimeType}, or {@code null} if
|
||||
* the codec can't be used.
|
||||
* @param mimeType The MIME type.
|
||||
* @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if
|
||||
* the codec can't be used. If non-null, the returned type will be equal to {@code mimeType}
|
||||
* except in cases where the codec is known to use a non-standard MIME type alias.
|
||||
*/
|
||||
@Nullable
|
||||
private static String getCodecSupportedType(
|
||||
private static String getCodecMimeType(
|
||||
android.media.MediaCodecInfo info,
|
||||
String name,
|
||||
boolean secureDecodersExplicit,
|
||||
String requestedMimeType) {
|
||||
if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) {
|
||||
if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) {
|
||||
String mimeType) {
|
||||
if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) {
|
||||
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
|
||||
// video/dolby-vision.
|
||||
if ("OMX.MS.HEVCDV.Decoder".equals(name)) {
|
||||
@ -355,15 +361,12 @@ public final class MediaCodecUtil {
|
||||
|| "OMX.realtek.video.decoder.tunneled".equals(name)) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -373,12 +376,14 @@ public final class MediaCodecUtil {
|
||||
* @param info The codec information.
|
||||
* @param name The name of the codec
|
||||
* @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.
|
||||
* @param requestedMimeType The originally requested MIME type, which may differ from the codec
|
||||
* key MIME type if the codec key is being considered as a fallback.
|
||||
* @param mimeType The MIME type.
|
||||
* @return Whether the specified codec is usable for decoding on the current device.
|
||||
*/
|
||||
private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name,
|
||||
boolean secureDecodersExplicit, String requestedMimeType) {
|
||||
private static boolean isCodecUsableDecoder(
|
||||
android.media.MediaCodecInfo info,
|
||||
String name,
|
||||
boolean secureDecodersExplicit,
|
||||
String mimeType) {
|
||||
if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) {
|
||||
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].
|
||||
if (MimeTypes.AUDIO_E_AC3_JOC.equals(requestedMimeType)
|
||||
if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)
|
||||
&& "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
@ -174,6 +174,7 @@ public abstract class DownloadService extends Service {
|
||||
@Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater;
|
||||
@Nullable private final String channelId;
|
||||
@StringRes private final int channelNameResourceId;
|
||||
@StringRes private final int channelDescriptionResourceId;
|
||||
|
||||
private DownloadManager downloadManager;
|
||||
private int lastStartId;
|
||||
@ -214,7 +215,23 @@ public abstract class DownloadService extends Service {
|
||||
foregroundNotificationId,
|
||||
foregroundNotificationUpdateInterval,
|
||||
/* 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
|
||||
* foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}.
|
||||
* @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
|
||||
* characters. The value may be truncated if it is too long. Ignored if {@code
|
||||
* notification channel. The recommended maximum length is 40 characters. The value may be
|
||||
* truncated if it's too long. Ignored if {@code channelId} is null or if {@code
|
||||
* 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(
|
||||
int foregroundNotificationId,
|
||||
long foregroundNotificationUpdateInterval,
|
||||
@Nullable String channelId,
|
||||
@StringRes int channelNameResourceId) {
|
||||
@StringRes int channelNameResourceId,
|
||||
@StringRes int channelDescriptionResourceId) {
|
||||
if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) {
|
||||
this.foregroundNotificationUpdater = null;
|
||||
this.channelId = null;
|
||||
this.channelNameResourceId = 0;
|
||||
this.channelDescriptionResourceId = 0;
|
||||
} else {
|
||||
this.foregroundNotificationUpdater =
|
||||
new ForegroundNotificationUpdater(
|
||||
foregroundNotificationId, foregroundNotificationUpdateInterval);
|
||||
this.channelId = channelId;
|
||||
this.channelNameResourceId = channelNameResourceId;
|
||||
this.channelDescriptionResourceId = channelDescriptionResourceId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -543,7 +568,11 @@ public abstract class DownloadService extends Service {
|
||||
public void onCreate() {
|
||||
if (channelId != null) {
|
||||
NotificationUtil.createNotificationChannel(
|
||||
this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW);
|
||||
this,
|
||||
channelId,
|
||||
channelNameResourceId,
|
||||
channelDescriptionResourceId,
|
||||
NotificationUtil.IMPORTANCE_LOW);
|
||||
}
|
||||
Class<? extends DownloadService> clazz = getClass();
|
||||
DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz);
|
||||
|
@ -106,13 +106,16 @@ public interface MediaPeriod extends SequenceableLoader {
|
||||
* Performs a track selection.
|
||||
*
|
||||
* <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
|
||||
* 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
|
||||
* corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
|
||||
* 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.
|
||||
*
|
||||
* @param selections The renderer track selections.
|
||||
|
@ -118,6 +118,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
|
||||
@NullableType SampleStream[] streams,
|
||||
boolean[] streamResetFlags,
|
||||
long positionUs) {
|
||||
positionUs = constrainSeekPosition(positionUs);
|
||||
for (int i = 0; i < selections.length; i++) {
|
||||
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
|
||||
sampleStreams.remove(streams[i]);
|
||||
@ -144,6 +145,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
|
||||
|
||||
@Override
|
||||
public long seekToUs(long positionUs) {
|
||||
positionUs = constrainSeekPosition(positionUs);
|
||||
for (int i = 0; i < sampleStreams.size(); i++) {
|
||||
((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs);
|
||||
}
|
||||
@ -152,7 +154,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
|
||||
|
||||
@Override
|
||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||
return positionUs;
|
||||
return constrainSeekPosition(positionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -172,6 +174,10 @@ public final class SilenceMediaSource extends BaseMediaSource {
|
||||
|
||||
@Override
|
||||
public void reevaluateBuffer(long positionUs) {}
|
||||
|
||||
private long constrainSeekPosition(long positionUs) {
|
||||
return Util.constrainValue(positionUs, 0, durationUs);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SilenceSampleStream implements SampleStream {
|
||||
@ -187,7 +193,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
|
||||
}
|
||||
|
||||
public void seekTo(long positionUs) {
|
||||
positionBytes = getAudioByteCount(positionUs);
|
||||
positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -28,9 +28,10 @@ import java.lang.annotation.RetentionPolicy;
|
||||
*/
|
||||
public class Cue {
|
||||
|
||||
/**
|
||||
* An unset position or width.
|
||||
*/
|
||||
/** The empty cue. */
|
||||
public static final Cue EMPTY = new Cue("");
|
||||
|
||||
/** An unset position or width. */
|
||||
public static final float DIMEN_UNSET = Float.MIN_VALUE;
|
||||
|
||||
/**
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.text;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -69,6 +70,7 @@ public abstract class SimpleSubtitleDecoder extends
|
||||
|
||||
@SuppressWarnings("ByteBufferBackingArray")
|
||||
@Override
|
||||
@Nullable
|
||||
protected final SubtitleDecoderException decode(
|
||||
SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) {
|
||||
try {
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.decoder.OutputBuffer;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -45,22 +46,22 @@ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti
|
||||
|
||||
@Override
|
||||
public int getEventTimeCount() {
|
||||
return subtitle.getEventTimeCount();
|
||||
return Assertions.checkNotNull(subtitle).getEventTimeCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEventTime(int index) {
|
||||
return subtitle.getEventTime(index) + subsampleOffsetUs;
|
||||
return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextEventTimeIndex(long timeUs) {
|
||||
return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs);
|
||||
return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cue> getCues(long timeUs) {
|
||||
return subtitle.getCues(timeUs - subsampleOffsetUs);
|
||||
return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.text.pgs;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
|
||||
import com.google.android.exoplayer2.text.Subtitle;
|
||||
@ -41,7 +42,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
|
||||
private final ParsableByteArray inflatedBuffer;
|
||||
private final CueBuilder cueBuilder;
|
||||
|
||||
private Inflater inflater;
|
||||
@Nullable private Inflater inflater;
|
||||
|
||||
public PgsDecoder() {
|
||||
super("PgsDecoder");
|
||||
@ -76,6 +77,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) {
|
||||
int limit = buffer.limit();
|
||||
int sectionType = buffer.readUnsignedByte();
|
||||
@ -197,6 +199,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
|
||||
bitmapY = buffer.readUnsignedShort();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Cue build() {
|
||||
if (planeWidth == 0
|
||||
|| planeHeight == 0
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.text.ssa;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
@ -49,7 +50,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
||||
private int formatTextIndex;
|
||||
|
||||
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
|
||||
* samples.
|
||||
*/
|
||||
public SsaDecoder(List<byte[]> initializationData) {
|
||||
public SsaDecoder(@Nullable List<byte[]> initializationData) {
|
||||
super("SsaDecoder");
|
||||
if (initializationData != null && !initializationData.isEmpty()) {
|
||||
haveInitializationData = true;
|
||||
@ -201,7 +202,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
||||
cues.add(new Cue(text));
|
||||
cueTimesUs.add(startTimeUs);
|
||||
if (endTimeUs != C.TIME_UNSET) {
|
||||
cues.add(null);
|
||||
cues.add(Cue.EMPTY);
|
||||
cueTimesUs.add(endTimeUs);
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import java.util.List;
|
||||
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.
|
||||
*/
|
||||
public SsaSubtitle(Cue[] cues, long[] cueTimesUs) {
|
||||
@ -61,7 +61,7 @@ import java.util.List;
|
||||
@Override
|
||||
public List<Cue> getCues(long timeUs) {
|
||||
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.
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
|
@ -111,11 +111,13 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
|
||||
// Read and parse the text and tags.
|
||||
textBuilder.setLength(0);
|
||||
tags.clear();
|
||||
while (!TextUtils.isEmpty(currentLine = subripData.readLine())) {
|
||||
currentLine = subripData.readLine();
|
||||
while (!TextUtils.isEmpty(currentLine)) {
|
||||
if (textBuilder.length() > 0) {
|
||||
textBuilder.append("<br>");
|
||||
}
|
||||
textBuilder.append(processLine(currentLine, tags));
|
||||
currentLine = subripData.readLine();
|
||||
}
|
||||
|
||||
Spanned text = Html.fromHtml(textBuilder.toString());
|
||||
@ -132,7 +134,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
|
||||
cues.add(buildCue(text, alignmentTag));
|
||||
|
||||
if (haveEndTimecode) {
|
||||
cues.add(null);
|
||||
cues.add(Cue.EMPTY);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ import java.util.List;
|
||||
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.
|
||||
*/
|
||||
public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
|
||||
@ -61,7 +61,7 @@ import java.util.List;
|
||||
@Override
|
||||
public List<Cue> getCues(long timeUs) {
|
||||
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.
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
|
@ -65,6 +65,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||
private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f;
|
||||
|
||||
private final ParsableByteArray parsableByteArray;
|
||||
|
||||
private boolean customVerticalPlacement;
|
||||
private int defaultFontFace;
|
||||
private int defaultColorRgba;
|
||||
@ -80,10 +81,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||
public Tx3gDecoder(List<byte[]> initializationData) {
|
||||
super("Tx3gDecoder");
|
||||
parsableByteArray = new ParsableByteArray();
|
||||
decodeInitializationData(initializationData);
|
||||
}
|
||||
|
||||
private void decodeInitializationData(List<byte[]> initializationData) {
|
||||
if (initializationData != null && initializationData.size() == 1
|
||||
&& (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) {
|
||||
byte[] initializationBytes = initializationData.get(0);
|
||||
@ -151,8 +149,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||
}
|
||||
parsableByteArray.setPosition(position + atomSize);
|
||||
}
|
||||
return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION,
|
||||
Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET));
|
||||
return new Tx3gSubtitle(
|
||||
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)
|
||||
|
@ -2318,14 +2318,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
|
||||
if (TextUtils.equals(format.language, language)) {
|
||||
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)) {
|
||||
return 2;
|
||||
}
|
||||
// Partial match where only the main language tag is the same (e.g. "fra-fr" and "fra-ca")
|
||||
if (format.language.length() >= 3
|
||||
&& language.length() >= 3
|
||||
&& format.language.substring(0, 3).equals(language.substring(0, 3))) {
|
||||
// Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca")
|
||||
String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0];
|
||||
String queryMainLanguage = Util.splitAtFirst(language, "-")[0];
|
||||
if (formatMainLanguage.equals(queryMainLanguage)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Base64;
|
||||
@ -29,9 +31,10 @@ public final class DataSchemeDataSource extends BaseDataSource {
|
||||
|
||||
public static final String SCHEME_DATA = "data";
|
||||
|
||||
private @Nullable DataSpec dataSpec;
|
||||
private int bytesRead;
|
||||
private @Nullable byte[] data;
|
||||
@Nullable private DataSpec dataSpec;
|
||||
@Nullable private byte[] data;
|
||||
private int endPosition;
|
||||
private int readPosition;
|
||||
|
||||
public DataSchemeDataSource() {
|
||||
super(/* isNetwork= */ false);
|
||||
@ -41,6 +44,7 @@ public final class DataSchemeDataSource extends BaseDataSource {
|
||||
public long open(DataSpec dataSpec) throws IOException {
|
||||
transferInitializing(dataSpec);
|
||||
this.dataSpec = dataSpec;
|
||||
readPosition = (int) dataSpec.position;
|
||||
Uri uri = dataSpec.uri;
|
||||
String scheme = uri.getScheme();
|
||||
if (!SCHEME_DATA.equals(scheme)) {
|
||||
@ -61,8 +65,14 @@ public final class DataSchemeDataSource extends BaseDataSource {
|
||||
// TODO: Add support for other charsets.
|
||||
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);
|
||||
return data.length;
|
||||
return (long) endPosition - readPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -70,29 +80,29 @@ public final class DataSchemeDataSource extends BaseDataSource {
|
||||
if (readLength == 0) {
|
||||
return 0;
|
||||
}
|
||||
int remainingBytes = data.length - bytesRead;
|
||||
int remainingBytes = endPosition - readPosition;
|
||||
if (remainingBytes == 0) {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
readLength = Math.min(readLength, remainingBytes);
|
||||
System.arraycopy(data, bytesRead, buffer, offset, readLength);
|
||||
bytesRead += readLength;
|
||||
System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength);
|
||||
readPosition += readLength;
|
||||
bytesTransferred(readLength);
|
||||
return readLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getUri() {
|
||||
@Nullable
|
||||
public Uri getUri() {
|
||||
return dataSpec != null ? dataSpec.uri : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
public void close() {
|
||||
if (data != null) {
|
||||
data = null;
|
||||
transferEnded();
|
||||
}
|
||||
dataSpec = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,12 +15,18 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
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 stream info.
|
||||
*/
|
||||
public final class FlacStreamInfo {
|
||||
/** Holder for FLAC metadata. */
|
||||
public final class FlacStreamMetadata {
|
||||
|
||||
private static final String TAG = "FlacStreamMetadata";
|
||||
|
||||
public final int minBlockSize;
|
||||
public final int maxBlockSize;
|
||||
@ -30,16 +36,19 @@ public final class FlacStreamInfo {
|
||||
public final int channels;
|
||||
public final int bitsPerSample;
|
||||
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 offset Offset of the structure in the array
|
||||
* @param data An array containing binary FLAC stream info metadata.
|
||||
* @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
|
||||
* METADATA_BLOCK_STREAMINFO</a>
|
||||
*/
|
||||
public FlacStreamInfo(byte[] data, int offset) {
|
||||
public FlacStreamMetadata(byte[] data, int offset) {
|
||||
ParsableBitArray scratch = new ParsableBitArray(data);
|
||||
scratch.setPosition(offset * 8);
|
||||
this.minBlockSize = scratch.readBits(16);
|
||||
@ -49,14 +58,11 @@ public final class FlacStreamInfo {
|
||||
this.sampleRate = scratch.readBits(20);
|
||||
this.channels = scratch.readBits(3) + 1;
|
||||
this.bitsPerSample = scratch.readBits(5) + 1;
|
||||
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32)
|
||||
| (scratch.readBits(32) & 0xFFFFFFFFL);
|
||||
// Remaining 16 bytes is md5 value
|
||||
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);
|
||||
this.metadata = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a FlacStreamInfo given the parameters.
|
||||
*
|
||||
* @param minBlockSize Minimum block size of the FLAC stream.
|
||||
* @param maxBlockSize Maximum block 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 bitsPerSample Number of bits per sample 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
|
||||
* 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 maxBlockSize,
|
||||
int minFrameSize,
|
||||
@ -76,7 +88,9 @@ public final class FlacStreamInfo {
|
||||
int sampleRate,
|
||||
int channels,
|
||||
int bitsPerSample,
|
||||
long totalSamples) {
|
||||
long totalSamples,
|
||||
List<String> vorbisComments,
|
||||
List<PictureFrame> pictureFrames) {
|
||||
this.minBlockSize = minBlockSize;
|
||||
this.maxBlockSize = maxBlockSize;
|
||||
this.minFrameSize = minFrameSize;
|
||||
@ -85,6 +99,7 @@ public final class FlacStreamInfo {
|
||||
this.channels = channels;
|
||||
this.bitsPerSample = bitsPerSample;
|
||||
this.totalSamples = totalSamples;
|
||||
this.metadata = buildMetadata(vorbisComments, pictureFrames);
|
||||
}
|
||||
|
||||
/** Returns the maximum size for a decoded frame from the FLAC stream. */
|
||||
@ -126,4 +141,27 @@ public final class FlacStreamInfo {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
@ -61,6 +61,14 @@ public final class NotificationUtil {
|
||||
/** @see 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
|
||||
* 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
|
||||
* too long.
|
||||
* @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
|
||||
* Intent#ACTION_LOCALE_CHANGED} broadcast. The recommended maximum length is 40 characters.
|
||||
* The value may be truncated if it is too long.
|
||||
* The recommended maximum length is 40 characters. The string may be truncated if it's too
|
||||
* long. You can rename the channel when the system locale changes by listening for the {@link
|
||||
* 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
|
||||
* posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link
|
||||
* #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link
|
||||
* #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}.
|
||||
*/
|
||||
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) {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel =
|
||||
new NotificationChannel(id, context.getString(nameResourceId), importance);
|
||||
if (descriptionResourceId != 0) {
|
||||
channel.setDescription(context.getString(descriptionResourceId));
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Formatter;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
@ -135,6 +136,10 @@ public final class Util {
|
||||
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
|
||||
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() {}
|
||||
|
||||
/**
|
||||
@ -450,18 +455,31 @@ public final class Util {
|
||||
if (language == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Locale locale = getLocaleForLanguageTag(language);
|
||||
int localeLanguageLength = locale.getLanguage().length();
|
||||
String normLanguage = locale.getISO3Language();
|
||||
if (normLanguage.isEmpty()) {
|
||||
return toLowerInvariant(language);
|
||||
// Locale data (especially for API < 21) may produce tags with '_' instead of the
|
||||
// standard-conformant '-'.
|
||||
String normalizedTag = language.replace('_', '-');
|
||||
if (Util.SDK_INT >= 21) {
|
||||
// Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags.
|
||||
normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag);
|
||||
}
|
||||
String normTag = getLocaleLanguageTag(locale);
|
||||
return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength));
|
||||
} catch (MissingResourceException e) {
|
||||
return toLowerInvariant(language);
|
||||
if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) {
|
||||
// Tag isn't valid, keep using the original.
|
||||
normalizedTag = 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() {
|
||||
Configuration config = Resources.getSystem().getConfiguration();
|
||||
return SDK_INT >= 24
|
||||
? getSystemLocalesV24()
|
||||
: new String[] {getLocaleLanguageTag(Resources.getSystem().getConfiguration().locale)};
|
||||
? getSystemLocalesV24(config)
|
||||
: SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()};
|
||||
}
|
||||
|
||||
@TargetApi(24)
|
||||
private static String[] getSystemLocalesV24() {
|
||||
return Util.split(Resources.getSystem().getConfiguration().getLocales().toLanguageTags(), ",");
|
||||
}
|
||||
|
||||
private static Locale getLocaleForLanguageTag(String languageTag) {
|
||||
return Util.SDK_INT >= 21 ? getLocaleForLanguageTagV21(languageTag) : new Locale(languageTag);
|
||||
private static String[] getSystemLocalesV24(Configuration config) {
|
||||
return Util.split(config.getLocales().toLanguageTags(), ",");
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private static Locale getLocaleForLanguageTagV21(String languageTag) {
|
||||
return Locale.forLanguageTag(languageTag);
|
||||
}
|
||||
|
||||
private static String getLocaleLanguageTag(Locale locale) {
|
||||
return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString();
|
||||
private static String[] getSystemLocaleV21(Configuration config) {
|
||||
return new String[] {config.locale.toLanguageTag()};
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private static String getLocaleLanguageTagV21(Locale locale) {
|
||||
return locale.toLanguageTag();
|
||||
private static String normalizeLanguageCodeSyntaxV21(String languageTag) {
|
||||
return Locale.forLanguageTag(languageTag).toLanguageTag();
|
||||
}
|
||||
|
||||
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
|
||||
* "most significant bit first".
|
||||
|
@ -551,10 +551,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
Format format,
|
||||
MediaCrypto crypto,
|
||||
float codecOperatingRate) {
|
||||
String codecMimeType = codecInfo.codecMimeType;
|
||||
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
|
||||
MediaFormat mediaFormat =
|
||||
getMediaFormat(
|
||||
format,
|
||||
codecMimeType,
|
||||
codecMaxValues,
|
||||
codecOperatingRate,
|
||||
deviceNeedsNoPostProcessWorkaround,
|
||||
@ -1111,6 +1113,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
* Returns the framework {@link MediaFormat} that should be used to configure the decoder.
|
||||
*
|
||||
* @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 codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
|
||||
* no codec operating rate should be set.
|
||||
@ -1123,13 +1126,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
@SuppressLint("InlinedApi")
|
||||
protected MediaFormat getMediaFormat(
|
||||
Format format,
|
||||
String codecMimeType,
|
||||
CodecMaxValues codecMaxValues,
|
||||
float codecOperatingRate,
|
||||
boolean deviceNeedsNoPostProcessWorkaround,
|
||||
int tunnelingAudioSessionId) {
|
||||
MediaFormat mediaFormat = new MediaFormat();
|
||||
// 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_HEIGHT, format.height);
|
||||
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
|
||||
@ -1429,6 +1433,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
||||
case "1713":
|
||||
case "1714":
|
||||
case "A10-70F":
|
||||
case "A10-70L":
|
||||
case "A1601":
|
||||
case "A2016a40":
|
||||
case "A7000-a":
|
||||
|
@ -1,6 +1,6 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 26122
|
||||
duration = 26125
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
|
@ -1,6 +1,6 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 26122
|
||||
duration = 26125
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
|
@ -1,6 +1,6 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 26122
|
||||
duration = 26125
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
|
@ -1,6 +1,6 @@
|
||||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 26122
|
||||
duration = 26125
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
|
@ -20,6 +20,7 @@ import static org.junit.Assert.fail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.Surface;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
@ -2608,6 +2609,56 @@ public final class ExoPlayerTest {
|
||||
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.
|
||||
|
||||
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
|
||||
|
@ -16,13 +16,16 @@
|
||||
package com.google.android.exoplayer2.extractor.ogg;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
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.OggTestData;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
import org.junit.Test;
|
||||
@ -32,13 +35,15 @@ import org.junit.runner.RunWith;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class DefaultOggSeekerTest {
|
||||
|
||||
private final Random random = new Random(0);
|
||||
|
||||
@Test
|
||||
public void testSetupWithUnsetEndPositionFails() {
|
||||
try {
|
||||
new DefaultOggSeeker(
|
||||
/* startPosition= */ 0,
|
||||
/* endPosition= */ C.LENGTH_UNSET,
|
||||
/* streamReader= */ new TestStreamReader(),
|
||||
/* payloadStartPosition= */ 0,
|
||||
/* payloadEndPosition= */ C.LENGTH_UNSET,
|
||||
/* firstPayloadPageSize= */ 1,
|
||||
/* firstPayloadPageGranulePosition= */ 1,
|
||||
/* 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 {
|
||||
OggTestFile testFile = OggTestFile.generate(random, 1000);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build();
|
||||
TestStreamReader streamReader = new TestStreamReader();
|
||||
DefaultOggSeeker oggSeeker =
|
||||
new DefaultOggSeeker(
|
||||
/* startPosition= */ 0,
|
||||
/* endPosition= */ testFile.data.length,
|
||||
/* streamReader= */ streamReader,
|
||||
/* payloadStartPosition= */ 0,
|
||||
/* payloadEndPosition= */ testFile.data.length,
|
||||
/* firstPayloadPageSize= */ testFile.firstPayloadPageSize,
|
||||
/* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition,
|
||||
/* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount,
|
||||
/* firstPayloadPageIsLastPage= */ false);
|
||||
OggPageHeader pageHeader = new OggPageHeader();
|
||||
|
||||
@ -78,89 +172,96 @@ public final class DefaultOggSeekerTest {
|
||||
input.setPosition((int) nextSeekPosition);
|
||||
}
|
||||
|
||||
// Test granule 0 from file start
|
||||
assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0);
|
||||
// Test granule 0 from file start.
|
||||
long granule = seekTo(input, oggSeeker, 0, 0);
|
||||
assertThat(granule).isEqualTo(0);
|
||||
assertThat(input.getPosition()).isEqualTo(0);
|
||||
|
||||
// Test granule 0 from file end
|
||||
assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0);
|
||||
// Test granule 0 from file end.
|
||||
granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1);
|
||||
assertThat(granule).isEqualTo(0);
|
||||
assertThat(input.getPosition()).isEqualTo(0);
|
||||
|
||||
{ // Test last granule
|
||||
long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0);
|
||||
long position = testFile.data.length;
|
||||
assertThat(
|
||||
(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();
|
||||
}
|
||||
// Test last granule.
|
||||
granule = seekTo(input, oggSeeker, testFile.granuleCount - 1, 0);
|
||||
assertThat(granule).isEqualTo(testFile.granuleCount - testFile.lastPayloadPageGranuleCount);
|
||||
assertThat(input.getPosition()).isEqualTo(testFile.data.length - testFile.lastPayloadPageSize);
|
||||
|
||||
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);
|
||||
|
||||
long currentGranule = seekTo(input, oggSeeker, targetGranule, initialPosition);
|
||||
granule = seekTo(input, oggSeeker, targetGranule, initialPosition);
|
||||
long currentPosition = input.getPosition();
|
||||
|
||||
assertWithMessage("getNextSeekPosition() didn't leave input on a page start.")
|
||||
.that(pageHeader.populate(input, true))
|
||||
.isTrue();
|
||||
|
||||
if (currentGranule == 0) {
|
||||
if (granule == 0) {
|
||||
assertThat(currentPosition).isEqualTo(0);
|
||||
} else {
|
||||
int previousPageStart = testFile.findPreviousPageStart(currentPosition);
|
||||
input.setPosition(previousPageStart);
|
||||
assertThat(pageHeader.populate(input, true)).isTrue();
|
||||
assertThat(currentGranule).isEqualTo(pageHeader.granulePosition);
|
||||
pageHeader.populate(input, false);
|
||||
assertThat(granule).isEqualTo(pageHeader.granulePosition);
|
||||
}
|
||||
|
||||
input.setPosition((int) currentPosition);
|
||||
oggSeeker.skipToPageOfGranule(input, targetGranule, -1);
|
||||
long positionDiff = Math.abs(input.getPosition() - currentPosition);
|
||||
pageHeader.populate(input, false);
|
||||
// The target granule should be within the current page.
|
||||
assertThat(granule).isAtMost(targetGranule);
|
||||
assertThat(targetGranule).isLessThan(pageHeader.granulePosition);
|
||||
}
|
||||
}
|
||||
|
||||
long granuleDiff = currentGranule - targetGranule;
|
||||
if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0)
|
||||
&& positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) {
|
||||
fail(
|
||||
"granuleDiff ("
|
||||
+ granuleDiff
|
||||
+ ") or positionDiff ("
|
||||
+ positionDiff
|
||||
+ ") is more than allowed.");
|
||||
private static void skipToNextPage(ExtractorInput extractorInput)
|
||||
throws IOException, InterruptedException {
|
||||
DefaultOggSeeker oggSeeker =
|
||||
new DefaultOggSeeker(
|
||||
/* streamReader= */ new FlacReader(),
|
||||
/* payloadStartPosition= */ 0,
|
||||
/* payloadEndPosition= */ extractorInput.getLength(),
|
||||
/* firstPayloadPageSize= */ 1,
|
||||
/* 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)
|
||||
throws IOException, InterruptedException {
|
||||
long nextSeekPosition = initialPosition;
|
||||
oggSeeker.startSeek(targetGranule);
|
||||
int count = 0;
|
||||
oggSeeker.resetSeeking();
|
||||
|
||||
do {
|
||||
input.setPosition((int) nextSeekPosition);
|
||||
nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input);
|
||||
|
||||
while (nextSeekPosition >= 0) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -171,8 +272,7 @@ public final class DefaultOggSeekerTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
|
||||
throws IOException, InterruptedException {
|
||||
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -30,35 +30,39 @@ import java.util.Random;
|
||||
private static final int MAX_GRANULES_IN_PAGE = 100000;
|
||||
|
||||
public final byte[] data;
|
||||
public final long lastGranule;
|
||||
public final int packetCount;
|
||||
public final int granuleCount;
|
||||
public final int pageCount;
|
||||
public final int firstPayloadPageSize;
|
||||
public final long firstPayloadPageGranulePosition;
|
||||
public final int firstPayloadPageGranuleCount;
|
||||
public final int lastPayloadPageSize;
|
||||
public final int lastPayloadPageGranuleCount;
|
||||
|
||||
private OggTestFile(
|
||||
byte[] data,
|
||||
long lastGranule,
|
||||
int packetCount,
|
||||
int granuleCount,
|
||||
int pageCount,
|
||||
int firstPayloadPageSize,
|
||||
long firstPayloadPageGranulePosition) {
|
||||
int firstPayloadPageGranuleCount,
|
||||
int lastPayloadPageSize,
|
||||
int lastPayloadPageGranuleCount) {
|
||||
this.data = data;
|
||||
this.lastGranule = lastGranule;
|
||||
this.packetCount = packetCount;
|
||||
this.granuleCount = granuleCount;
|
||||
this.pageCount = pageCount;
|
||||
this.firstPayloadPageSize = firstPayloadPageSize;
|
||||
this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition;
|
||||
this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount;
|
||||
this.lastPayloadPageSize = lastPayloadPageSize;
|
||||
this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount;
|
||||
}
|
||||
|
||||
public static OggTestFile generate(Random random, int pageCount) {
|
||||
ArrayList<byte[]> fileData = new ArrayList<>();
|
||||
int fileSize = 0;
|
||||
long granule = 0;
|
||||
int packetLength = -1;
|
||||
int packetCount = 0;
|
||||
int granuleCount = 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++) {
|
||||
int headerType = 0x00;
|
||||
@ -71,17 +75,17 @@ import java.util.Random;
|
||||
if (i == pageCount - 1) {
|
||||
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);
|
||||
byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount);
|
||||
granuleCount += pageGranuleCount;
|
||||
byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount);
|
||||
fileData.add(header);
|
||||
fileSize += header.length;
|
||||
int pageSize = header.length;
|
||||
|
||||
byte[] laces = new byte[pageSegmentCount];
|
||||
int bodySize = 0;
|
||||
for (int j = 0; j < pageSegmentCount; j++) {
|
||||
if (packetLength < 0) {
|
||||
packetCount++;
|
||||
if (i < pageCount - 1) {
|
||||
packetLength = random.nextInt(MAX_PACKET_LENGTH);
|
||||
} else {
|
||||
@ -96,14 +100,19 @@ import java.util.Random;
|
||||
packetLength -= 255;
|
||||
}
|
||||
fileData.add(laces);
|
||||
fileSize += laces.length;
|
||||
pageSize += laces.length;
|
||||
|
||||
byte[] payload = TestUtil.buildTestData(bodySize, random);
|
||||
fileData.add(payload);
|
||||
fileSize += payload.length;
|
||||
pageSize += payload.length;
|
||||
|
||||
fileSize += pageSize;
|
||||
if (i == 0) {
|
||||
firstPayloadPageSize = header.length + bodySize;
|
||||
firstPayloadPageGranulePosition = granule;
|
||||
firstPayloadPageSize = pageSize;
|
||||
firstPayloadPageGranuleCount = pageGranuleCount;
|
||||
} else if (i == pageCount - 1) {
|
||||
lastPageloadPageSize = pageSize;
|
||||
lastPayloadPageGranuleCount = pageGranuleCount;
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,11 +124,12 @@ import java.util.Random;
|
||||
}
|
||||
return new OggTestFile(
|
||||
file,
|
||||
granule,
|
||||
packetCount,
|
||||
granuleCount,
|
||||
pageCount,
|
||||
firstPayloadPageSize,
|
||||
firstPayloadPageGranulePosition);
|
||||
firstPayloadPageGranuleCount,
|
||||
lastPageloadPageSize,
|
||||
lastPayloadPageGranuleCount);
|
||||
}
|
||||
|
||||
public int findPreviousPageStart(long position) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user