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

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

View File

@ -1,5 +1,36 @@
# Release notes #
### 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

View File

@ -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 {

View File

@ -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

View File

@ -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')

View File

@ -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 {

View File

@ -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"

View File

@ -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'

View File

@ -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'

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}

View File

@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.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();
}
}

View File

@ -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')
}

View File

@ -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')
}

View File

@ -172,28 +172,49 @@ 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:
byte[] header0 = initializationData.get(0);
byte[] header1 = initializationData.get(1);
byte[] extraData = new byte[header0.length + header1.length + 6];
extraData[0] = (byte) (header0.length >> 8);
extraData[1] = (byte) (header0.length & 0xFF);
System.arraycopy(header0, 0, extraData, 2, header0.length);
extraData[header0.length + 2] = 0;
extraData[header0.length + 3] = 0;
extraData[header0.length + 4] = (byte) (header1.length >> 8);
extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
return extraData;
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];
extraData[0] = (byte) (header0.length >> 8);
extraData[1] = (byte) (header0.length & 0xFF);
System.arraycopy(header0, 0, extraData, 2, header0.length);
extraData[header0.length + 2] = 0;
extraData[header0.length + 3] = 0;
extraData[header0.length + 4] = (byte) (header1.length >> 8);
extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
return extraData;
}
private native long ffmpegInitialize(
String codecName,
@Nullable byte[] extraData,

View File

@ -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')

View File

@ -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 {
*;
}

View File

@ -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();

View File

@ -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.");
}

View File

@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.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);
}
}
}

View File

@ -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) {

View File

@ -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;
}
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)

View File

@ -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,48 +132,53 @@ 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);
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition);
}
long lastDecodePosition = decoderJni.getDecodePosition();
FlacDecoderJni decoderJni = initDecoderJni(input);
try {
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
}
int outputSize = outputByteBuffer.limit();
if (outputSize == 0) {
return RESULT_END_OF_INPUT;
}
decodeStreamMetadata(input);
writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
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);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
}
int outputSize = outputByteBuffer.limit();
if (outputSize == 0) {
return RESULT_END_OF_INPUT;
}
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. */

View File

@ -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) {

View File

@ -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,

View File

@ -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;

View File

@ -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
}

View File

@ -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')
}

View File

@ -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

View File

@ -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;

View File

@ -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'
}

View File

@ -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'
}

View File

@ -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

View File

@ -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) {

View File

@ -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;
}

View File

@ -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')
}

View File

@ -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

View File

@ -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;

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ android {
}
dependencies {
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

View File

@ -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();
}

View File

@ -1304,8 +1304,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
Pair<Object, Long> defaultPosition =
getPeriodPosition(
timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
newContentPositionUs = defaultPosition.second;
newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs);
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;
}
} 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.

View File

@ -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}

View File

@ -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;
/**

View File

@ -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,

View File

@ -48,7 +48,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
/**
* If {@link #periodId} refers to an ad, the position of the suspended content relative to the
* 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. */

View File

@ -364,8 +364,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return Collections.singletonList(passthroughDecoderInfo);
}
}
return mediaCodecSelector.getDecoderInfos(
format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false);
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);

View File

@ -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.

View File

@ -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

View File

@ -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);
}

View File

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

View File

@ -119,7 +119,7 @@ public interface TrackOutput {
* Called to write sample data to the output.
*
* @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);

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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];

View File

@ -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}.

View File

@ -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,145 +104,110 @@ 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) {
return position;
}
currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2));
long position = getNextSeekPosition(input);
if (position != C.POSITION_UNSET) {
return position;
}
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 (granuleDistance < 0) {
end = initialPosition;
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) {
end = start;
return start;
}
long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
long nextPosition = input.getPosition() - offset
+ (granuleDistance * (end - start) / (endGranule - startGranule));
nextPosition = Math.max(nextPosition, start);
nextPosition = Math.min(nextPosition, end - 1);
return nextPosition;
if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {
return C.POSITION_UNSET;
}
// position accepted (before target granule and within MATCH_RANGE)
input.skipFully(pageSize);
return -(pageHeader.granulePosition + 2);
if (granuleDistance < 0) {
end = currentPosition;
endGranule = pageHeader.granulePosition;
} else {
start = input.getPosition() + pageSize;
startGranule = pageHeader.granulePosition;
}
if (end - start < MATCH_BYTE_RANGE) {
end = start;
return start;
}
long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
long nextPosition =
input.getPosition()
- offset
+ (granuleDistance * (end - start) / (endGranule - startGranule));
return Util.constrainValue(nextPosition, start, end - 1);
}
private long getEstimatedPosition(long position, long granuleDistance, long offset) {
position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset;
if (position < startPosition) {
position = startPosition;
/**
* 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);
}
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);
}
}
}

View File

@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.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();
}
}

View File

@ -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.
*/

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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;

View File

@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.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")) {
Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id);
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() {

View File

@ -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);

View File

@ -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);
}

View File

@ -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,42 +324,49 @@ 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)) {
// Handle decoders that declare support for DV via MIME types that aren't
// video/dolby-vision.
if ("OMX.MS.HEVCDV.Decoder".equals(name)) {
return "video/hevcdv";
} else if ("OMX.RTK.video.decoder".equals(name)
|| "OMX.realtek.video.decoder.tunneled".equals(name)) {
return "video/dv_hevc";
}
}
String mimeType) {
if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) {
return null;
}
String[] supportedTypes = info.getSupportedTypes();
for (String supportedType : supportedTypes) {
if (supportedType.equalsIgnoreCase(requestedMimeType)) {
return supportedType;
}
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)) {
return "video/hevcdv";
} else if ("OMX.RTK.video.decoder".equals(name)
|| "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";
}
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;
}
@ -386,11 +391,11 @@ public final class MediaCodecUtil {
// Work around broken audio decoders.
if (Util.SDK_INT < 21
&& ("CIPAACDecoder".equals(name)
|| "CIPMP3Decoder".equals(name)
|| "CIPVorbisDecoder".equals(name)
|| "CIPAMRNBDecoder".equals(name)
|| "AACDecoder".equals(name)
|| "MP3Decoder".equals(name))) {
|| "CIPMP3Decoder".equals(name)
|| "CIPVorbisDecoder".equals(name)
|| "CIPAMRNBDecoder".equals(name)
|| "AACDecoder".equals(name)
|| "MP3Decoder".equals(name))) {
return false;
}
@ -399,7 +404,7 @@ public final class MediaCodecUtil {
if (Util.SDK_INT < 18
&& "OMX.MTK.AUDIO.DECODER.AAC".equals(name)
&& ("a70".equals(Util.DEVICE)
|| ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) {
|| ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) {
return false;
}
@ -408,17 +413,17 @@ public final class MediaCodecUtil {
if (Util.SDK_INT == 16
&& "OMX.qcom.audio.decoder.mp3".equals(name)
&& ("dlxu".equals(Util.DEVICE) // HTC Butterfly
|| "protou".equals(Util.DEVICE) // HTC Desire X
|| "ville".equals(Util.DEVICE) // HTC One S
|| "villeplus".equals(Util.DEVICE)
|| "villec2".equals(Util.DEVICE)
|| Util.DEVICE.startsWith("gee") // LGE Optimus G
|| "C6602".equals(Util.DEVICE) // Sony Xperia Z
|| "C6603".equals(Util.DEVICE)
|| "C6606".equals(Util.DEVICE)
|| "C6616".equals(Util.DEVICE)
|| "L36h".equals(Util.DEVICE)
|| "SO-02E".equals(Util.DEVICE))) {
|| "protou".equals(Util.DEVICE) // HTC Desire X
|| "ville".equals(Util.DEVICE) // HTC One S
|| "villeplus".equals(Util.DEVICE)
|| "villec2".equals(Util.DEVICE)
|| Util.DEVICE.startsWith("gee") // LGE Optimus G
|| "C6602".equals(Util.DEVICE) // Sony Xperia Z
|| "C6603".equals(Util.DEVICE)
|| "C6606".equals(Util.DEVICE)
|| "C6616".equals(Util.DEVICE)
|| "L36h".equals(Util.DEVICE)
|| "SO-02E".equals(Util.DEVICE))) {
return false;
}
@ -426,9 +431,9 @@ public final class MediaCodecUtil {
if (Util.SDK_INT == 16
&& "OMX.qcom.audio.decoder.aac".equals(name)
&& ("C1504".equals(Util.DEVICE) // Sony Xperia E
|| "C1505".equals(Util.DEVICE)
|| "C1604".equals(Util.DEVICE) // Sony Xperia E dual
|| "C1605".equals(Util.DEVICE))) {
|| "C1505".equals(Util.DEVICE)
|| "C1604".equals(Util.DEVICE) // Sony Xperia E dual
|| "C1605".equals(Util.DEVICE))) {
return false;
}
@ -437,13 +442,13 @@ public final class MediaCodecUtil {
&& ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name))
&& "samsung".equals(Util.MANUFACTURER)
&& (Util.DEVICE.startsWith("zeroflte") // Galaxy S6
|| Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge
|| Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+
|| "SC-05G".equals(Util.DEVICE) // Galaxy S6
|| "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active
|| "404SC".equals(Util.DEVICE) // Galaxy S6 Edge
|| "SC-04G".equals(Util.DEVICE)
|| "SCV31".equals(Util.DEVICE))) {
|| Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge
|| Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+
|| "SC-05G".equals(Util.DEVICE) // Galaxy S6
|| "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active
|| "404SC".equals(Util.DEVICE) // Galaxy S6 Edge
|| "SC-04G".equals(Util.DEVICE)
|| "SCV31".equals(Util.DEVICE))) {
return false;
}
@ -453,10 +458,10 @@ public final class MediaCodecUtil {
&& "OMX.SEC.vp8.dec".equals(name)
&& "samsung".equals(Util.MANUFACTURER)
&& (Util.DEVICE.startsWith("d2")
|| Util.DEVICE.startsWith("serrano")
|| Util.DEVICE.startsWith("jflte")
|| Util.DEVICE.startsWith("santos")
|| Util.DEVICE.startsWith("t0"))) {
|| Util.DEVICE.startsWith("serrano")
|| Util.DEVICE.startsWith("jflte")
|| Util.DEVICE.startsWith("santos")
|| Util.DEVICE.startsWith("t0"))) {
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;
}

View File

@ -0,0 +1,144 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import java.util.Arrays;
/** A picture parsed from a FLAC file. */
public final class PictureFrame implements Metadata.Entry {
/** The type of the picture. */
public final int pictureType;
/** The mime type of the picture. */
public final String mimeType;
/** A description of the picture. */
public final String description;
/** The width of the picture in pixels. */
public final int width;
/** The height of the picture in pixels. */
public final int height;
/** The color depth of the picture in bits-per-pixel. */
public final int depth;
/** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */
public final int colors;
/** The encoded picture data. */
public final byte[] pictureData;
public PictureFrame(
int pictureType,
String mimeType,
String description,
int width,
int height,
int depth,
int colors,
byte[] pictureData) {
this.pictureType = pictureType;
this.mimeType = mimeType;
this.description = description;
this.width = width;
this.height = height;
this.depth = depth;
this.colors = colors;
this.pictureData = pictureData;
}
/* package */ PictureFrame(Parcel in) {
this.pictureType = in.readInt();
this.mimeType = castNonNull(in.readString());
this.description = castNonNull(in.readString());
this.width = in.readInt();
this.height = in.readInt();
this.depth = in.readInt();
this.colors = in.readInt();
this.pictureData = castNonNull(in.createByteArray());
}
@Override
public String toString() {
return "Picture: mimeType=" + mimeType + ", description=" + description;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PictureFrame other = (PictureFrame) obj;
return (pictureType == other.pictureType)
&& mimeType.equals(other.mimeType)
&& description.equals(other.description)
&& (width == other.width)
&& (height == other.height)
&& (depth == other.depth)
&& (colors == other.colors)
&& Arrays.equals(pictureData, other.pictureData);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + pictureType;
result = 31 * result + mimeType.hashCode();
result = 31 * result + description.hashCode();
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + depth;
result = 31 * result + colors;
result = 31 * result + Arrays.hashCode(pictureData);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(pictureType);
dest.writeString(mimeType);
dest.writeString(description);
dest.writeInt(width);
dest.writeInt(height);
dest.writeInt(depth);
dest.writeInt(colors);
dest.writeByteArray(pictureData);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<PictureFrame> CREATOR =
new Parcelable.Creator<PictureFrame>() {
@Override
public PictureFrame createFromParcel(Parcel in) {
return new PictureFrame(in);
}
@Override
public PictureFrame[] newArray(int size) {
return new PictureFrame[size];
}
};
}

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
/** A vorbis comment. */
public final class VorbisComment implements Metadata.Entry {
/** The key. */
public final String key;
/** The value. */
public final String value;
/**
* @param key The key.
* @param value The value.
*/
public VorbisComment(String key, String value) {
this.key = key;
this.value = value;
}
/* package */ VorbisComment(Parcel in) {
this.key = castNonNull(in.readString());
this.value = castNonNull(in.readString());
}
@Override
public String toString() {
return "VC: " + key + "=" + value;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
VorbisComment other = (VorbisComment) obj;
return key.equals(other.key) && value.equals(other.value);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + key.hashCode();
result = 31 * result + value.hashCode();
return result;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(key);
dest.writeString(value);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<VorbisComment> CREATOR =
new Parcelable.Creator<VorbisComment>() {
@Override
public VorbisComment createFromParcel(Parcel in) {
return new VorbisComment(in);
}
@Override
public VorbisComment[] newArray(int size) {
return new VorbisComment[size];
}
};
}

View File

@ -174,6 +174,7 @@ public abstract class DownloadService extends Service {
@Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater;
@Nullable private final 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);

View File

@ -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.

View File

@ -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

View File

@ -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;
/**

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
String normTag = getLocaleLanguageTag(locale);
return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength));
} catch (MissingResourceException e) {
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);
}
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".

View File

@ -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":

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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");
}
} while (nextSeekPosition >= 0);
input.setPosition((int) nextSeekPosition);
nextSeekPosition = oggSeeker.read(input);
}
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;
}
}

View File

@ -1,243 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.ogg;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.OggTestData;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.EOFException;
import java.io.IOException;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link DefaultOggSeeker} utility methods. */
@RunWith(AndroidJUnit4.class)
public final class DefaultOggSeekerUtilMethodsTest {
private final Random random = new Random(0);
@Test
public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(4000);
}
@Test
public void testSkipToNextPageOverlap() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(2046);
}
@Test
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
new byte[] {'x', 'O', 'g', 'g', 'S'}
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(1);
}
@Test
public void testSkipToNextPageNoMatch() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);
try {
skipToNextPage(extractorInput);
fail();
} catch (EOFException e) {
// expected
}
}
private static void skipToNextPage(ExtractorInput extractorInput)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ extractorInput.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
oggSeeker.skipToNextPage(extractorInput);
break;
} catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ }
}
}
@Test
public void testSkipToPageOfGranule() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
// expect to be granule of the previous page returned as elapsedSamples
skipToPageOfGranule(input, 54000, 40000);
// expect to be at the start of the third page
assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254)));
}
@Test
public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
skipToPageOfGranule(input, 40000, 20000);
// expect to be at the start of the second page
assertThat(input.getPosition()).isEqualTo(30 + (3 * 254));
}
@Test
public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
skipToPageOfGranule(input, 10000, -1);
assertThat(input.getPosition()).isEqualTo(0);
}
private void skipToPageOfGranule(ExtractorInput input, long granule,
long elapsedSamplesExpected) throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ input.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1))
.isEqualTo(elapsedSamplesExpected);
return;
} catch (FakeExtractorInput.SimulatedIOException e) {
input.resetPeekPosition();
}
}
}
@Test
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays(
TestUtil.buildTestData(100, random),
OggTestData.buildOggHeader(0x00, 20000, 66, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
OggTestData.buildOggHeader(0x00, 40000, 67, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
OggTestData.buildOggHeader(0x05, 60000, 68, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random)
), false);
assertReadGranuleOfLastPage(input, 60000);
}
@Test
public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (EOFException e) {
// ignored
}
}
@Test
public void testReadGranuleOfLastPageWithUnboundedLength()
throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(new byte[0], true);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (IllegalArgumentException e) {
// ignored
}
}
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ input.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected);
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
}
}
}
}

View File

@ -30,35 +30,39 @@ import java.util.Random;
private static final int MAX_GRANULES_IN_PAGE = 100000;
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