Commit 85c10b02 by Oliver Woodman Committed by GitHub

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

r2.10.4
parents 51acd815 d1ac2727
Showing with 1411 additions and 620 deletions
# Release notes #
### 2.10.4 ###
* Offline: Add `Scheduler` implementation that uses `WorkManager`.
* Add ability to specify a description when creating notification channels via
ExoPlayer library classes.
* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language
tags instead of 3-letter ISO 639-2 language tags.
* Ensure the `SilenceMediaSource` position is in range
([#6229](https://github.com/google/ExoPlayer/issues/6229)).
* WAV: Calculate correct duration for clipped streams
([#6241](https://github.com/google/ExoPlayer/issues/6241)).
* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change
from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)).
* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata
([#5527](https://github.com/google/ExoPlayer/issues/5527)).
* Fix issue where initial seek positions get ignored when playing a preroll ad
([#6201](https://github.com/google/ExoPlayer/issues/6201)).
* Fix issue where invalid language tags were normalized to "und" instead of
keeping the original
([#6153](https://github.com/google/ExoPlayer/issues/6153)).
* Fix `DataSchemeDataSource` re-opening and range requests
([#6192](https://github.com/google/ExoPlayer/issues/6192)).
* Fix Flac and ALAC playback on some LG devices
([#5938](https://github.com/google/ExoPlayer/issues/5938)).
* Fix issue when calling `performClick` on `PlayerView` without
`PlayerControlView`
([#6260](https://github.com/google/ExoPlayer/issues/6260)).
* Fix issue where playback speeds are not used in adaptive track selections
after manual selection changes for other renderers
([#6256](https://github.com/google/ExoPlayer/issues/6256)).
### 2.10.3 ###
* Display last frame when seeking to end of stream
......
......@@ -21,14 +21,6 @@ buildscript {
classpath 'com.novoda:bintray-release:0.9'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0'
}
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
configurations.all {
resolutionStrategy {
force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
}
}
}
allprojects {
repositories {
......
......@@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.10.3'
releaseVersionCode = 2010003
releaseVersion = '2.10.4'
releaseVersionCode = 2010004
minSdkVersion = 16
targetSdkVersion = 28
compileSdkVersion = 28
......
......@@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher'
include modulePrefix + 'extension-workmanager'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
......@@ -60,3 +61,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')
......@@ -47,17 +47,6 @@ android {
// The demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
}
dependencies {
......
......@@ -25,7 +25,7 @@
android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="${castOptionsProvider}" />
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
......
......@@ -53,7 +53,7 @@ dependencies {
implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
......@@ -62,7 +62,7 @@ android {
}
dependencies {
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.legacy:legacy-support-core-ui:1.0.0'
implementation 'androidx.fragment:fragment:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
......
......@@ -41,7 +41,8 @@ public class DemoDownloadService extends DownloadService {
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID,
R.string.exo_download_notification_channel_name);
R.string.exo_download_notification_channel_name,
/* channelDescriptionResourceId= */ 0);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
}
......
......@@ -31,8 +31,8 @@ android {
}
dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.2.0'
implementation 'androidx.annotation:annotation:1.0.2'
api 'com.google.android.gms:play-services-cast-framework:17.0.0'
implementation 'androidx.annotation:annotation:1.1.0'
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
......
......@@ -83,8 +83,6 @@ public final class CastPlayer extends BasePlayer {
private final CastTimelineTracker timelineTracker;
private final Timeline.Period period;
private RemoteMediaClient remoteMediaClient;
// Result callbacks.
private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback;
......@@ -93,9 +91,10 @@ public final class CastPlayer extends BasePlayer {
private final CopyOnWriteArrayList<ListenerHolder> listeners;
private final ArrayList<ListenerNotificationTask> notificationsBatch;
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
private SessionAvailabilityListener sessionAvailabilityListener;
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
@Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
......@@ -148,6 +147,7 @@ public final class CastPlayer extends BasePlayer {
* starts at position 0.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
@Nullable
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
}
......@@ -163,8 +163,9 @@ public final class CastPlayer extends BasePlayer {
* @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
long positionMs, @RepeatMode int repeatMode) {
@Nullable
public PendingResult<MediaChannelResult> loadItems(
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true;
......@@ -180,6 +181,7 @@ public final class CastPlayer extends BasePlayer {
* @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists.
*/
@Nullable
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
}
......@@ -194,6 +196,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
@Nullable
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
......@@ -211,6 +214,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
@Nullable
public PendingResult<MediaChannelResult> removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null);
......@@ -229,6 +233,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
@Nullable
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
......@@ -246,6 +251,7 @@ public final class CastPlayer extends BasePlayer {
* @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*/
@Nullable
public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
......@@ -264,9 +270,9 @@ public final class CastPlayer extends BasePlayer {
/**
* Sets a listener for updates on the cast session availability.
*
* @param listener The {@link SessionAvailabilityListener}.
* @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
*/
public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
sessionAvailabilityListener = listener;
}
......@@ -322,6 +328,7 @@ public final class CastPlayer extends BasePlayer {
}
@Override
@Nullable
public ExoPlaybackException getPlaybackError() {
return null;
}
......@@ -529,7 +536,7 @@ public final class CastPlayer extends BasePlayer {
// Internal methods.
public void updateInternalState() {
private void updateInternalState() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return;
......@@ -675,7 +682,8 @@ public final class CastPlayer extends BasePlayer {
}
}
private @Nullable MediaStatus getMediaStatus() {
@Nullable
private MediaStatus getMediaStatus() {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
}
......
......@@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
import java.util.Collections;
import java.util.List;
/**
......@@ -36,7 +37,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider {
@Override
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
return null;
return Collections.emptyList();
}
}
......@@ -31,9 +31,9 @@ android {
}
dependencies {
api 'org.chromium.net:cronet-embedded:73.3683.76'
api 'org.chromium.net:cronet-embedded:75.3770.101'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils-robolectric')
}
......
......@@ -38,7 +38,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
}
......
......@@ -172,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,
......
......@@ -39,7 +39,8 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
......
......@@ -9,6 +9,9 @@
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*;
}
-keep class com.google.android.exoplayer2.util.FlacStreamInfo {
-keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
*;
}
-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
*;
}
......@@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest {
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull();
......@@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest {
decoderJni.setData(input);
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue();
......
......@@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
public class FlacExtractorTest {
@Before
public void setUp() throws Exception {
public void setUp() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
......
......@@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
......@@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
private final FlacDecoderJni decoderJni;
public FlacBinarySearchSeeker(
FlacStreamInfo streamInfo,
FlacStreamMetadata streamMetadata,
long firstFramePosition,
long inputLength,
FlacDecoderJni decoderJni) {
super(
new FlacSeekTimestampConverter(streamInfo),
new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni),
streamInfo.durationUs(),
streamMetadata.durationUs(),
/* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamInfo.totalSamples,
/* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize));
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
}
......@@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
* the timestamp for a stream seek time position.
*/
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
private final FlacStreamInfo streamInfo;
private final FlacStreamMetadata streamMetadata;
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) {
this.streamInfo = streamInfo;
public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
this.streamMetadata = streamMetadata;
}
@Override
public long timeUsToTargetTime(long timeUs) {
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs);
return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
}
}
}
......@@ -15,11 +15,13 @@
*/
package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
......@@ -56,21 +58,20 @@ import java.util.List;
}
decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
FlacStreamInfo streamInfo;
FlacStreamMetadata streamMetadata;
try {
streamInfo = decoderJni.decodeMetadata();
streamMetadata = decoderJni.decodeStreamMetadata();
} catch (ParserException e) {
throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) {
// Never happens.
throw new IllegalStateException(e);
}
if (streamInfo == null) {
throw new FlacDecoderException("Metadata decoding failed");
}
int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
}
@Override
......@@ -94,6 +95,7 @@ import java.util.List;
}
@Override
@Nullable
protected FlacDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
......
......@@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
......@@ -37,14 +40,14 @@ import java.nio.ByteBuffer;
}
}
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.
private final long nativeDecoderContext;
private ByteBuffer byteBufferData;
private ExtractorInput extractorInput;
@Nullable private ByteBuffer byteBufferData;
@Nullable private ExtractorInput extractorInput;
@Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput;
private byte[] tempBuffer;
public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) {
......@@ -57,67 +60,79 @@ import java.nio.ByteBuffer;
}
/**
* Sets data to be parsed by libflac.
* @param byteBufferData Source {@link ByteBuffer}
* Sets the data to be parsed.
*
* @param byteBufferData Source {@link ByteBuffer}.
*/
public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData;
this.extractorInput = null;
this.tempBuffer = null;
}
/**
* Sets data to be parsed by libflac.
* @param extractorInput Source {@link ExtractorInput}
* Sets the data to be parsed.
*
* @param extractorInput Source {@link ExtractorInput}.
*/
public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null;
this.extractorInput = extractorInput;
endOfExtractorInput = false;
if (tempBuffer == null) {
this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
tempBuffer = new byte[TEMP_BUFFER_SIZE];
}
endOfExtractorInput = false;
}
/**
* 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)
......
......@@ -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");
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);
"FlacStreamMetadata");
jmethodID flacStreamMetadataConstructor =
env->GetMethodID(flacStreamMetadataClass, "<init>",
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.min_blocksize, streamInfo.max_blocksize,
streamInfo.min_framesize, streamInfo.max_framesize,
streamInfo.sample_rate, streamInfo.channels,
streamInfo.bits_per_sample, streamInfo.total_samples,
commentList, pictureFrames);
}
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
......
......@@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table;
break;
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
if (!mVorbisCommentsValid) {
FLAC__StreamMetadata_VorbisComment vorbisComment =
metadata->data.vorbis_comment;
for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {
FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =
vorbisComment.comments[i];
if (vorbisCommentEntry.entry != NULL) {
std::string comment(
reinterpret_cast<char *>(vorbisCommentEntry.entry),
vorbisCommentEntry.length);
mVorbisComments.push_back(comment);
}
}
mVorbisCommentsValid = true;
} else {
ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
}
break;
case FLAC__METADATA_TYPE_PICTURE: {
const FLAC__StreamMetadata_Picture *parsedPicture =
&metadata->data.picture;
FlacPicture picture;
picture.mimeType.assign(std::string(parsedPicture->mime_type));
picture.description.assign(
std::string((char *)parsedPicture->description));
picture.data.assign(parsedPicture->data,
parsedPicture->data + parsedPicture->data_length);
picture.width = parsedPicture->width;
picture.height = parsedPicture->height;
picture.depth = parsedPicture->depth;
picture.colors = parsedPicture->colors;
picture.type = parsedPicture->type;
mPictures.push_back(picture);
mPicturesValid = true;
break;
}
default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break;
......@@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source)
mCurrentPos(0LL),
mEOF(false),
mStreamInfoValid(false),
mVorbisCommentsValid(false),
mPicturesValid(false),
mWriteRequested(false),
mWriteCompleted(false),
mWriteBuffer(NULL),
......@@ -266,6 +305,10 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_PICTURE);
FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback,
......
......@@ -19,6 +19,10 @@
#include <stdint.h>
#include <cstdlib>
#include <string>
#include <vector>
// libFLAC parser
#include "FLAC/stream_decoder.h"
......@@ -26,6 +30,17 @@
typedef int status_t;
struct FlacPicture {
int type;
std::string mimeType;
std::string description;
FLAC__uint32 width;
FLAC__uint32 height;
FLAC__uint32 depth;
FLAC__uint32 colors;
std::vector<char> data;
};
class FLACParser {
public:
FLACParser(DataSource *source);
......@@ -44,6 +59,14 @@ class FLACParser {
return mStreamInfo;
}
bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
std::vector<std::string> getVorbisComments() { return mVorbisComments; }
bool arePicturesValid() const { return mPicturesValid; }
const std::vector<FlacPicture> &getPictures() const { return mPictures; }
int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
}
......@@ -71,6 +94,10 @@ class FLACParser {
mEOF = false;
if (newPosition == 0) {
mStreamInfoValid = false;
mVorbisCommentsValid = false;
mPicturesValid = false;
mVorbisComments.clear();
mPictures.clear();
FLAC__stream_decoder_reset(mDecoder);
} else {
FLAC__stream_decoder_flush(mDecoder);
......@@ -116,6 +143,14 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset;
// cached when the VORBIS_COMMENT metadata is parsed by libFLAC
std::vector<std::string> mVorbisComments;
bool mVorbisCommentsValid;
// cached when the PICTURE metadata is parsed by libFLAC
std::vector<FlacPicture> mPictures;
bool mPicturesValid;
// cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested;
bool mWriteCompleted;
......
......@@ -33,7 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
......
......@@ -34,7 +34,7 @@ android {
dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
testImplementation project(modulePrefix + 'testutils-robolectric')
}
......
# ExoPlayer Firebase JobDispatcher extension #
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ##
......@@ -20,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
......@@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util;
*
* @see <a
* href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a>
* @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link
* com.google.android.exoplayer2.scheduler.PlatformScheduler}.
*/
@Deprecated
public final class JobDispatcherScheduler implements Scheduler {
private static final boolean DEBUG = false;
......
......@@ -32,7 +32,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.leanback:leanback:1.0.0'
}
......
......@@ -33,7 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
api 'com.squareup.okhttp3:okhttp:3.12.1'
}
......
......@@ -39,6 +39,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.1.0'
testImplementation project(modulePrefix + 'testutils-robolectric')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.opus;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
......@@ -150,6 +151,7 @@ import java.util.List;
}
@Override
@Nullable
protected OpusDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.opus;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
......@@ -49,9 +50,8 @@ public final class OpusLibrary {
return LOADER.isAvailable();
}
/**
* Returns the version of the underlying library if available, or null otherwise.
*/
/** Returns the version of the underlying library if available, or null otherwise. */
@Nullable
public static String getVersion() {
return isAvailable() ? opusGetVersion() : null;
}
......
......@@ -33,7 +33,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'net.butterflytv.utils:rtmp-client:3.0.1'
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
testImplementation project(modulePrefix + 'testutils-robolectric')
}
......
......@@ -39,7 +39,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
testImplementation project(modulePrefix + 'testutils-robolectric')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.vp9;
import androidx.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
......@@ -120,8 +121,9 @@ import java.nio.ByteBuffer;
}
@Override
protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
boolean reset) {
@Nullable
protected VpxDecoderException decode(
VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) {
ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit();
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.vp9;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
......@@ -49,9 +50,8 @@ public final class VpxLibrary {
return LOADER.isAvailable();
}
/**
* Returns the version of the underlying library if available, or null otherwise.
*/
/** Returns the version of the underlying library if available, or null otherwise. */
@Nullable
public static String getVersion() {
return isAvailable() ? vpxGetVersion() : null;
}
......@@ -60,6 +60,7 @@ public final class VpxLibrary {
* Returns the configuration string with which the underlying library was built if available, or
* null otherwise.
*/
@Nullable
public static String getBuildConfig() {
return isAvailable() ? vpxGetBuildConfig() : null;
}
......
# 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
/*
* 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'
<?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"/>
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.workmanager;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
/** A {@link Scheduler} that uses {@link WorkManager}. */
public final class WorkManagerScheduler implements Scheduler {
private static final boolean DEBUG = false;
private static final String TAG = "WorkManagerScheduler";
private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package";
private static final String KEY_REQUIREMENTS = "requirements";
private final String workName;
/**
* @param workName A name for work scheduled by this instance. If the same name was used by a
* previous instance, anything scheduled by the previous instance will be canceled by this
* instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} are
* called.
*/
public WorkManagerScheduler(String workName) {
this.workName = workName;
}
@Override
public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
Constraints constraints = buildConstraints(requirements);
Data inputData = buildInputData(requirements, servicePackage, serviceAction);
OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData);
logd("Scheduling work: " + workName);
WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest);
return true;
}
@Override
public boolean cancel() {
logd("Canceling work: " + workName);
WorkManager.getInstance().cancelUniqueWork(workName);
return true;
}
private static Constraints buildConstraints(Requirements requirements) {
Constraints.Builder builder = new Constraints.Builder();
if (requirements.isUnmeteredNetworkRequired()) {
builder.setRequiredNetworkType(NetworkType.UNMETERED);
} else if (requirements.isNetworkRequired()) {
builder.setRequiredNetworkType(NetworkType.CONNECTED);
} else {
builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
}
if (requirements.isChargingRequired()) {
builder.setRequiresCharging(true);
}
if (requirements.isIdleRequired() && Util.SDK_INT >= 23) {
setRequiresDeviceIdle(builder);
}
return builder.build();
}
@TargetApi(23)
private static void setRequiresDeviceIdle(Constraints.Builder builder) {
builder.setRequiresDeviceIdle(true);
}
private static Data buildInputData(
Requirements requirements, String servicePackage, String serviceAction) {
Data.Builder builder = new Data.Builder();
builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
builder.putString(KEY_SERVICE_PACKAGE, servicePackage);
builder.putString(KEY_SERVICE_ACTION, serviceAction);
return builder.build();
}
private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) {
OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class);
builder.setConstraints(constraints);
builder.setInputData(inputData);
return builder.build();
}
private static void logd(String message) {
if (DEBUG) {
Log.d(TAG, message);
}
}
/** A {@link Worker} that starts the target service if the requirements are met. */
// This class needs to be public so that WorkManager can instantiate it.
public static final class SchedulerWorker extends Worker {
private final WorkerParameters workerParams;
private final Context context;
public SchedulerWorker(Context context, WorkerParameters workerParams) {
super(context, workerParams);
this.workerParams = workerParams;
this.context = context;
}
@Override
public Result doWork() {
logd("SchedulerWorker is started");
Data inputData = workerParams.getInputData();
Assertions.checkNotNull(inputData, "Work started without input data.");
Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0));
if (requirements.checkRequirements(context)) {
logd("Requirements are met");
String serviceAction = inputData.getString(KEY_SERVICE_ACTION);
String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE);
Assertions.checkNotNull(serviceAction, "Service action missing.");
Assertions.checkNotNull(servicePackage, "Service package missing.");
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
Util.startForegroundService(context, intent);
return Result.success();
} else {
logd("Requirements are not met");
return Result.retry();
}
}
}
}
......@@ -58,7 +58,7 @@ android {
}
dependencies {
implementation 'androidx.annotation:annotation:1.0.2'
implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
......
......@@ -532,7 +532,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
public long getContentPosition() {
if (isPlayingAd()) {
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
return playbackInfo.contentPositionUs == C.TIME_UNSET
? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs()
: period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
} else {
return getCurrentPosition();
}
......
......@@ -1304,8 +1304,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
Pair<Object, Long> defaultPosition =
getPeriodPosition(
timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
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.
......
......@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.10.3";
public static final String VERSION = "2.10.4";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.3";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.4";
/**
* The version of the library expressed as an integer, for example 1002003.
......@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2010003;
public static final int VERSION_INT = 2010004;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
......@@ -29,7 +29,8 @@ import com.google.android.exoplayer2.util.Util;
public final long startPositionUs;
/**
* If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET}
* otherwise.
* if this is not an ad or the next content media period should be played from its default
* position.
*/
public final long contentPositionUs;
/**
......
......@@ -144,7 +144,9 @@ import com.google.android.exoplayer2.util.Assertions;
MediaPeriodInfo info) {
long rendererPositionOffsetUs =
loading == null
? (info.id.isAd() ? info.contentPositionUs : 0)
? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET
? info.contentPositionUs
: 0)
: (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);
MediaPeriodHolder newPeriodHolder =
new MediaPeriodHolder(
......@@ -560,6 +562,7 @@ import com.google.android.exoplayer2.util.Assertions;
}
long startPositionUs;
long contentPositionUs;
int nextWindowIndex =
timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;
Object nextPeriodUid = period.uid;
......@@ -568,6 +571,7 @@ import com.google.android.exoplayer2.util.Assertions;
// We're starting to buffer a new window. When playback transitions to this window we'll
// want it to be from its default start position, so project the default start position
// forward by the duration of the buffer, and start buffering from this point.
contentPositionUs = C.TIME_UNSET;
Pair<Object, Long> defaultPosition =
timeline.getPeriodPosition(
window,
......@@ -587,12 +591,13 @@ import com.google.android.exoplayer2.util.Assertions;
windowSequenceNumber = nextWindowSequenceNumber++;
}
} else {
// We're starting to buffer a new period within the same window.
startPositionUs = 0;
contentPositionUs = 0;
}
MediaPeriodId periodId =
resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber);
return getMediaPeriodInfo(
periodId, /* contentPositionUs= */ startPositionUs, startPositionUs);
return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs);
}
MediaPeriodId currentPeriodId = mediaPeriodInfo.id;
......@@ -616,13 +621,11 @@ import com.google.android.exoplayer2.util.Assertions;
mediaPeriodInfo.contentPositionUs,
currentPeriodId.windowSequenceNumber);
} else {
// Play content from the ad group position. As a special case, if we're transitioning from a
// preroll ad group to content and there are no other ad groups, project the start position
// forward as if this were a transition to a new window. No attempt is made to handle
// midrolls in live streams, as it's unclear what content position should play after an ad
// (server-side dynamic ad insertion is more appropriate for this use case).
// Play content from the ad group position.
long startPositionUs = mediaPeriodInfo.contentPositionUs;
if (period.getAdGroupCount() == 1 && period.getAdGroupTimeUs(0) == 0) {
if (startPositionUs == C.TIME_UNSET) {
// If we're transitioning from an ad group to content starting from its default position,
// project the start position forward as if this were a transition to a new window.
Pair<Object, Long> defaultPosition =
timeline.getPeriodPosition(
window,
......
......@@ -48,7 +48,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
/**
* If {@link #periodId} refers to an ad, the position of the suspended content relative to the
* start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET}
* if {@link #periodId} does not refer to an ad.
* if {@link #periodId} does not refer to an ad or if the suspended content should be played from
* its default position.
*/
public final long contentPositionUs;
/** The current playback state. One of the {@link Player}.STATE_ constants. */
......
......@@ -364,8 +364,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return Collections.singletonList(passthroughDecoderInfo);
}
}
return mediaCodecSelector.getDecoderInfos(
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);
......
......@@ -30,6 +30,7 @@ import java.util.Arrays;
private static final int MINIMUM_PITCH = 65;
private static final int MAXIMUM_PITCH = 400;
private static final int AMDF_FREQUENCY = 4000;
private static final int BYTES_PER_SAMPLE = 2;
private final int inputSampleRateHz;
private final int channelCount;
......@@ -157,9 +158,9 @@ import java.util.Arrays;
maxDiff = 0;
}
/** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */
public int getFramesAvailable() {
return outputFrameCount;
/** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */
public int getOutputSize() {
return outputFrameCount * channelCount * BYTES_PER_SAMPLE;
}
// Internal methods.
......
......@@ -210,7 +210,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
sonic.queueInput(shortBuffer);
inputBuffer.position(inputBuffer.position() + inputSize);
}
int outputSize = sonic.getFramesAvailable() * channelCount * 2;
int outputSize = sonic.getOutputSize();
if (outputSize > 0) {
if (buffer.capacity() < outputSize) {
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
......@@ -243,7 +243,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override
public boolean isEnded() {
return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0);
return inputEnded && (sonic == null || sonic.getOutputSize() == 0);
}
@Override
......
......@@ -301,5 +301,6 @@ public abstract class SimpleDecoder<
* @param reset Whether the decoder must be reset before decoding.
* @return A decoder exception if an error occurred, or null if decoding was successful.
*/
protected abstract @Nullable E decode(I inputBuffer, O outputBuffer, boolean reset);
@Nullable
protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset);
}
......@@ -186,10 +186,6 @@ public final class MpegAudioHeader {
}
}
// Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that
// seeking to a given timestamp and playing from the start up to that timestamp give the same
// results for CBR streams. See also [internal: b/120390268].
bitrate = 8 * frameSize * sampleRate / samplesPerFrame;
String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
......
......@@ -119,7 +119,7 @@ public interface TrackOutput {
* Called to write sample data to the output.
*
* @param data A {@link ParsableByteArray} from which to read the sample data.
* @param length The number of bytes to read.
* @param length The number of bytes to read, starting from {@code data.getPosition()}.
*/
void sampleData(ParsableByteArray data, int length);
......
......@@ -15,8 +15,10 @@
*/
package com.google.android.exoplayer2.extractor.flv;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Date;
......@@ -44,7 +46,7 @@ import java.util.Map;
private long durationUs;
public ScriptTagPayloadReader() {
super(null);
super(new DummyTrackOutput());
durationUs = C.TIME_UNSET;
}
......@@ -138,7 +140,10 @@ import java.util.Map;
ArrayList<Object> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
int type = readAmfType(data);
list.add(readAmfData(data, type));
Object value = readAmfData(data, type);
if (value != null) {
list.add(value);
}
}
return list;
}
......@@ -157,7 +162,10 @@ import java.util.Map;
if (type == AMF_TYPE_END_MARKER) {
break;
}
array.put(key, readAmfData(data, type));
Object value = readAmfData(data, type);
if (value != null) {
array.put(key, value);
}
}
return array;
}
......@@ -174,7 +182,10 @@ import java.util.Map;
for (int i = 0; i < count; i++) {
String key = readAmfString(data);
int type = readAmfType(data);
array.put(key, readAmfData(data, type));
Object value = readAmfData(data, type);
if (value != null) {
array.put(key, value);
}
}
return array;
}
......@@ -191,6 +202,7 @@ import java.util.Map;
return date;
}
@Nullable
private static Object readAmfData(ParsableByteArray data, int type) {
switch (type) {
case AMF_TYPE_NUMBER:
......@@ -208,8 +220,8 @@ import java.util.Map;
case AMF_TYPE_DATE:
return readAmfDate(data);
default:
// We don't log a warning because there are types that we knowingly don't support.
return null;
}
}
}
......@@ -117,6 +117,7 @@ public final class Mp3Extractor implements Extractor {
private Seeker seeker;
private long basisTimeUs;
private long samplesRead;
private long firstSamplePosition;
private int sampleBytesRemaining;
public Mp3Extractor() {
......@@ -214,6 +215,13 @@ public final class Mp3Extractor implements Extractor {
/* selectionFlags= */ 0,
/* language= */ null,
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
firstSamplePosition = input.getPosition();
} else if (firstSamplePosition != 0) {
long inputPosition = input.getPosition();
if (inputPosition < firstSamplePosition) {
// Skip past the seek frame.
input.skipFully((int) (firstSamplePosition - inputPosition));
}
}
return readSample(input);
}
......
......@@ -1140,10 +1140,6 @@ import java.util.List;
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,
language);
} else if (childAtomType == Atom.TYPE_alac) {
initializationData = new byte[childAtomSize];
parent.setPosition(childPosition);
parent.readBytes(initializationData, /* offset= */ 0, childAtomSize);
} else if (childAtomType == Atom.TYPE_dOps) {
// Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic
// Signature and the body of the dOps atom.
......@@ -1152,7 +1148,7 @@ import java.util.List;
System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length);
parent.setPosition(childPosition + Atom.HEADER_SIZE);
parent.readBytes(initializationData, opusMagic.length, childAtomBodySize);
} else if (childAtomSize == Atom.TYPE_dfLa) {
} else if (childAtomSize == Atom.TYPE_dfLa || childAtomType == Atom.TYPE_alac) {
int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE;
initializationData = new byte[childAtomBodySize];
parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE);
......
......@@ -123,6 +123,7 @@ public final class Track {
* @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no
* such entry exists.
*/
@Nullable
public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) {
return sampleDescriptionEncryptionBoxes == null ? null
: sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
......
......@@ -52,7 +52,7 @@ public final class TrackEncryptionBox {
* If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the
* track encryption box or sample group description box. Null otherwise.
*/
public final byte[] defaultInitializationVector;
@Nullable public final byte[] defaultInitializationVector;
/**
* @param isEncrypted See {@link #isEncrypted}.
......
......@@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
......@@ -38,7 +38,7 @@ import java.util.List;
private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
private FlacStreamInfo streamInfo;
private FlacStreamMetadata streamMetadata;
private FlacOggSeeker flacOggSeeker;
public static boolean verifyBitstreamType(ParsableByteArray data) {
......@@ -50,7 +50,7 @@ import java.util.List;
protected void reset(boolean headerData) {
super.reset(headerData);
if (headerData) {
streamInfo = null;
streamMetadata = null;
flacOggSeeker = null;
}
}
......@@ -71,14 +71,24 @@ import java.util.List;
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws IOException, InterruptedException {
byte[] data = packet.data;
if (streamInfo == null) {
streamInfo = new FlacStreamInfo(data, 17);
if (streamMetadata == null) {
streamMetadata = new FlacStreamMetadata(data, 17);
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
List<byte[]> initializationData = Collections.singletonList(metadata);
setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null,
Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate,
initializationData, null, 0, null);
setupData.format =
Format.createAudioSampleFormat(
null,
MimeTypes.AUDIO_FLAC,
null,
Format.NO_VALUE,
streamMetadata.bitRate(),
streamMetadata.channels,
streamMetadata.sampleRate,
initializationData,
null,
0,
null);
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
flacOggSeeker = new FlacOggSeeker();
flacOggSeeker.parseSeekTable(packet);
......@@ -175,11 +185,9 @@ import java.util.List;
}
@Override
public long startSeek(long timeUs) {
long granule = convertTimeToGranule(timeUs);
int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
public void startSeek(long targetGranule) {
int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true);
pendingSeekGranule = seekPointGranules[index];
return granule;
}
@Override
......@@ -211,7 +219,7 @@ import java.util.List;
@Override
public long getDurationUs() {
return streamInfo.durationUs();
return streamMetadata.durationUs();
}
}
......
......@@ -38,7 +38,13 @@ import java.io.IOException;
public int revision;
public int type;
/**
* The absolute granule position of the page. This is the total number of samples from the start
* of the file up to the <em>end</em> of the page. Samples partially in the page that continue on
* the next page do not count.
*/
public long granulePosition;
public long streamSerialNumber;
public long pageSequenceNumber;
public long pageChecksum;
......@@ -72,10 +78,10 @@ import java.io.IOException;
* Peeks an Ogg page header and updates this {@link OggPageHeader}.
*
* @param input The {@link ExtractorInput} to read from.
* @param quiet If {@code true}, no exceptions are thrown but {@code false} is returned if
* something goes wrong.
* @return {@code true} if the read was successful. The read fails if the end of the input is
* encountered without reading data.
* @param quiet Whether to return {@code false} rather than throwing an exception if the header
* cannot be populated.
* @return Whether the read was successful. The read fails if the end of the input is encountered
* without reading data.
* @throws IOException If reading data fails or the stream is invalid.
* @throws InterruptedException If the thread is interrupted.
*/
......
......@@ -33,16 +33,14 @@ import java.io.IOException;
SeekMap createSeekMap();
/**
* Initializes a seek operation.
* Starts a seek operation.
*
* @param timeUs The seek position in microseconds.
* @return The granule position targeted by the seek.
* @param targetGranule The target granule position.
*/
long startSeek(long timeUs);
void startSeek(long targetGranule);
/**
* Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a
* progressive seek.
* Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek.
* <p/>
* If more data is required or if the position of the input needs to be modified then a position
* from which data should be provided is returned. Else a negative value is returned. If a seek
......
......@@ -91,7 +91,8 @@ import java.io.IOException;
reset(!seekMapSet);
} else {
if (state != STATE_READ_HEADERS) {
targetGranule = oggSeeker.startSeek(timeUs);
targetGranule = convertTimeToGranule(timeUs);
oggSeeker.startSeek(targetGranule);
state = STATE_READ_PAYLOAD;
}
}
......@@ -147,9 +148,9 @@ import java.io.IOException;
boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.
oggSeeker =
new DefaultOggSeeker(
this,
payloadStartPosition,
input.getLength(),
this,
firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
firstPayloadPageHeader.granulePosition,
isLastPage);
......@@ -248,13 +249,13 @@ import java.io.IOException;
private static final class UnseekableOggSeeker implements OggSeeker {
@Override
public long read(ExtractorInput input) throws IOException, InterruptedException {
public long read(ExtractorInput input) {
return -1;
}
@Override
public long startSeek(long timeUs) {
return 0;
public void startSeek(long targetGranule) {
// Do nothing.
}
@Override
......
......@@ -87,12 +87,14 @@ public final class WavExtractor implements Extractor {
if (!wavHeader.hasDataBounds()) {
WavHeaderReader.skipToData(input, wavHeader);
extractorOutput.seekMap(wavHeader);
} else if (input.getPosition() == 0) {
input.skipFully(wavHeader.getDataStartPosition());
}
long dataLimit = wavHeader.getDataLimit();
Assertions.checkState(dataLimit != C.POSITION_UNSET);
long dataEndPosition = wavHeader.getDataEndPosition();
Assertions.checkState(dataEndPosition != C.POSITION_UNSET);
long bytesLeft = dataLimit - input.getPosition();
long bytesLeft = dataEndPosition - input.getPosition();
if (bytesLeft <= 0) {
return Extractor.RESULT_END_OF_INPUT;
}
......
......@@ -33,23 +33,29 @@ import com.google.android.exoplayer2.util.Util;
private final int blockAlignment;
/** Bits per sample for the audio data. */
private final int bitsPerSample;
/** The PCM encoding */
@C.PcmEncoding
private final int encoding;
/** Offset to the start of sample data. */
private long dataStartPosition;
/** Total size in bytes of the sample data. */
private long dataSize;
public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment,
int bitsPerSample, @C.PcmEncoding int encoding) {
/** The PCM encoding. */
@C.PcmEncoding private final int encoding;
/** 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) {
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 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 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 end of the sample data (exclusive), in bytes, or {@link
* C#POSITION_UNSET} if the data bounds have not been set.
*/
public long getDataEndPosition() {
return dataEndPosition;
}
/** Returns whether the data start position and size have been set. */
public boolean hasDataBounds() {
return dataStartPosition != 0 && dataSize != 0;
return dataStartPosition != C.POSITION_UNSET;
}
// SeekMap implementation.
......@@ -84,12 +101,13 @@ import com.google.android.exoplayer2.util.Util;
@Override
public long getDurationUs() {
long numFrames = dataSize / blockAlignment;
long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment;
return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
long dataSize = dataEndPosition - dataStartPosition;
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
// Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / blockAlignment) * blockAlignment;
......
......@@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */
......@@ -92,8 +91,8 @@ import java.io.IOException;
// If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ...
input.advancePeekPosition((int) chunkHeader.size - 16);
return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment,
bitsPerSample, encoding);
return new WavHeader(
numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding);
}
/**
......@@ -122,11 +121,13 @@ import java.io.IOException;
ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
// Skip all chunks until we hit the data header.
ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
while (chunkHeader.id != Util.getIntegerCodeForString("data")) {
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() {
......
......@@ -54,8 +54,15 @@ public final class MediaCodecInfo {
public final @Nullable String mimeType;
/**
* The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this
* is a passthrough codec.
* The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this
* is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a
* non-standard MIME type alias.
*/
@Nullable public final String codecMimeType;
/**
* The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not
* known.
*/
public final @Nullable CodecCapabilities capabilities;
......@@ -98,6 +105,7 @@ public final class MediaCodecInfo {
return new MediaCodecInfo(
name,
/* mimeType= */ null,
/* codecMimeType= */ null,
/* capabilities= */ null,
/* passthrough= */ true,
/* forceDisableAdaptive= */ false,
......@@ -109,26 +117,10 @@ public final class MediaCodecInfo {
*
* @param name The name of the {@link MediaCodec}.
* @param mimeType A mime type supported by the {@link MediaCodec}.
* @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
* @return The created instance.
*/
public static MediaCodecInfo newInstance(String name, String mimeType,
CodecCapabilities capabilities) {
return new MediaCodecInfo(
name,
mimeType,
capabilities,
/* passthrough= */ false,
/* forceDisableAdaptive= */ false,
/* forceSecure= */ false);
}
/**
* Creates an instance.
*
* @param name The name of the {@link MediaCodec}.
* @param mimeType A mime type supported by the {@link MediaCodec}.
* @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
* @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}.
* Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias.
* @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or
* {@code null} if not known.
* @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}.
* @param forceSecure Whether {@link #secure} should be forced to {@code true}.
* @return The created instance.
......@@ -136,22 +128,31 @@ public final class MediaCodecInfo {
public static MediaCodecInfo newInstance(
String name,
String mimeType,
CodecCapabilities capabilities,
String codecMimeType,
@Nullable CodecCapabilities capabilities,
boolean forceDisableAdaptive,
boolean forceSecure) {
return new MediaCodecInfo(
name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure);
name,
mimeType,
codecMimeType,
capabilities,
/* passthrough= */ false,
forceDisableAdaptive,
forceSecure);
}
private MediaCodecInfo(
String name,
@Nullable String mimeType,
@Nullable String codecMimeType,
@Nullable CodecCapabilities capabilities,
boolean passthrough,
boolean forceDisableAdaptive,
boolean forceSecure) {
this.name = Assertions.checkNotNull(name);
this.mimeType = mimeType;
this.codecMimeType = codecMimeType;
this.capabilities = capabilities;
this.passthrough = passthrough;
adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities);
......
......@@ -1806,9 +1806,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
String name = codecInfo.name;
return (Util.SDK_INT <= 17
&& ("OMX.rk.video_decoder.avc".equals(name)
|| "OMX.allwinner.video.decoder.avc".equals(name)))
return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name))
|| (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name))
|| ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
}
......
/*
* 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];
}
};
}
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.flac;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
/** A vorbis comment. */
public final class VorbisComment implements Metadata.Entry {
/** The key. */
public final String key;
/** The value. */
public final String value;
/**
* @param key The key.
* @param value The value.
*/
public VorbisComment(String key, String value) {
this.key = key;
this.value = value;
}
/* package */ VorbisComment(Parcel in) {
this.key = castNonNull(in.readString());
this.value = castNonNull(in.readString());
}
@Override
public String toString() {
return "VC: " + key + "=" + value;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
VorbisComment other = (VorbisComment) obj;
return key.equals(other.key) && value.equals(other.value);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + key.hashCode();
result = 31 * result + value.hashCode();
return result;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(key);
dest.writeString(value);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<VorbisComment> CREATOR =
new Parcelable.Creator<VorbisComment>() {
@Override
public VorbisComment createFromParcel(Parcel in) {
return new VorbisComment(in);
}
@Override
public VorbisComment[] newArray(int size) {
return new VorbisComment[size];
}
};
}
......@@ -174,6 +174,7 @@ public abstract class DownloadService extends Service {
@Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater;
@Nullable private final String channelId;
@StringRes private final int channelNameResourceId;
@StringRes private final int channelDescriptionResourceId;
private DownloadManager downloadManager;
private int lastStartId;
......@@ -214,7 +215,23 @@ public abstract class DownloadService extends Service {
foregroundNotificationId,
foregroundNotificationUpdateInterval,
/* channelId= */ null,
/* channelNameResourceId= */ 0);
/* channelNameResourceId= */ 0,
/* channelDescriptionResourceId= */ 0);
}
/** @deprecated Use {@link #DownloadService(int, long, String, int, int)}. */
@Deprecated
protected DownloadService(
int foregroundNotificationId,
long foregroundNotificationUpdateInterval,
@Nullable String channelId,
@StringRes int channelNameResourceId) {
this(
foregroundNotificationId,
foregroundNotificationUpdateInterval,
channelId,
channelNameResourceId,
/* channelDescriptionResourceId= */ 0);
}
/**
......@@ -230,25 +247,33 @@ public abstract class DownloadService extends Service {
* unique per package. The value may be truncated if it's too long. Ignored if {@code
* foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}.
* @param channelNameResourceId A string resource identifier for the user visible name of the
* channel, if {@code channelId} is specified. The recommended maximum length is 40
* characters. The value may be truncated if it is too long. Ignored if {@code
* notification channel. The recommended maximum length is 40 characters. The value may be
* truncated if it's too long. Ignored if {@code channelId} is null or if {@code
* foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}.
* @param channelDescriptionResourceId A string resource identifier for the user visible
* description of the notification channel, or 0 if no description is provided. The
* recommended maximum length is 300 characters. The value may be truncated if it is too long.
* Ignored if {@code channelId} is null or if {@code foregroundNotificationId} is {@link
* #FOREGROUND_NOTIFICATION_ID_NONE}.
*/
protected DownloadService(
int foregroundNotificationId,
long foregroundNotificationUpdateInterval,
@Nullable String channelId,
@StringRes int channelNameResourceId) {
@StringRes int channelNameResourceId,
@StringRes int channelDescriptionResourceId) {
if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) {
this.foregroundNotificationUpdater = null;
this.channelId = null;
this.channelNameResourceId = 0;
this.channelDescriptionResourceId = 0;
} else {
this.foregroundNotificationUpdater =
new ForegroundNotificationUpdater(
foregroundNotificationId, foregroundNotificationUpdateInterval);
this.channelId = channelId;
this.channelNameResourceId = channelNameResourceId;
this.channelDescriptionResourceId = channelDescriptionResourceId;
}
}
......@@ -543,7 +568,11 @@ public abstract class DownloadService extends Service {
public void onCreate() {
if (channelId != null) {
NotificationUtil.createNotificationChannel(
this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW);
this,
channelId,
channelNameResourceId,
channelDescriptionResourceId,
NotificationUtil.IMPORTANCE_LOW);
}
Class<? extends DownloadService> clazz = getClass();
DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz);
......
......@@ -106,13 +106,16 @@ public interface MediaPeriod extends SequenceableLoader {
* Performs a track selection.
*
* <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
* indicating whether the existing {@code SampleStream} can be retained for each selection, and
* indicating whether the existing {@link SampleStream} can be retained for each selection, and
* the existing {@code stream}s themselves. The call will update {@code streams} to reflect the
* provided selections, clearing, setting and replacing entries as required. If an existing sample
* stream is retained but with the requirement that the consuming renderer be reset, then the
* corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
* if a new sample stream is created.
*
* <p>Note that previously received {@link TrackSelection TrackSelections} are no longer valid and
* references need to be replaced even if the corresponding {@link SampleStream} is kept.
*
* <p>This method is only called after the period has been prepared.
*
* @param selections The renderer track selections.
......
......@@ -118,6 +118,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs) {
positionUs = constrainSeekPosition(positionUs);
for (int i = 0; i < selections.length; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
sampleStreams.remove(streams[i]);
......@@ -144,6 +145,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
@Override
public long seekToUs(long positionUs) {
positionUs = constrainSeekPosition(positionUs);
for (int i = 0; i < sampleStreams.size(); i++) {
((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs);
}
......@@ -152,7 +154,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
return positionUs;
return constrainSeekPosition(positionUs);
}
@Override
......@@ -172,6 +174,10 @@ public final class SilenceMediaSource extends BaseMediaSource {
@Override
public void reevaluateBuffer(long positionUs) {}
private long constrainSeekPosition(long positionUs) {
return Util.constrainValue(positionUs, 0, durationUs);
}
}
private static final class SilenceSampleStream implements SampleStream {
......@@ -187,7 +193,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
}
public void seekTo(long positionUs) {
positionBytes = getAudioByteCount(positionUs);
positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes);
}
@Override
......
......@@ -28,9 +28,10 @@ import java.lang.annotation.RetentionPolicy;
*/
public class Cue {
/**
* An unset position or width.
*/
/** The empty cue. */
public static final Cue EMPTY = new Cue("");
/** An unset position or width. */
public static final float DIMEN_UNSET = Float.MIN_VALUE;
/**
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.text;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import java.nio.ByteBuffer;
......@@ -69,6 +70,7 @@ public abstract class SimpleSubtitleDecoder extends
@SuppressWarnings("ByteBufferBackingArray")
@Override
@Nullable
protected final SubtitleDecoderException decode(
SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) {
try {
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.OutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import java.util.List;
/**
......@@ -45,22 +46,22 @@ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti
@Override
public int getEventTimeCount() {
return subtitle.getEventTimeCount();
return Assertions.checkNotNull(subtitle).getEventTimeCount();
}
@Override
public long getEventTime(int index) {
return subtitle.getEventTime(index) + subsampleOffsetUs;
return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs;
}
@Override
public int getNextEventTimeIndex(long timeUs) {
return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs);
return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs);
}
@Override
public List<Cue> getCues(long timeUs) {
return subtitle.getCues(timeUs - subsampleOffsetUs);
return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs);
}
@Override
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.text.pgs;
import android.graphics.Bitmap;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.text.Subtitle;
......@@ -41,7 +42,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
private final ParsableByteArray inflatedBuffer;
private final CueBuilder cueBuilder;
private Inflater inflater;
@Nullable private Inflater inflater;
public PgsDecoder() {
super("PgsDecoder");
......@@ -76,6 +77,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
}
}
@Nullable
private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) {
int limit = buffer.limit();
int sectionType = buffer.readUnsignedByte();
......@@ -197,6 +199,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder {
bitmapY = buffer.readUnsignedShort();
}
@Nullable
public Cue build() {
if (planeWidth == 0
|| planeHeight == 0
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.text.ssa;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
......@@ -49,7 +50,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
private int formatTextIndex;
public SsaDecoder() {
this(null);
this(/* initializationData= */ null);
}
/**
......@@ -58,7 +59,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
* format line. The second must contain an SSA header that will be assumed common to all
* samples.
*/
public SsaDecoder(List<byte[]> initializationData) {
public SsaDecoder(@Nullable List<byte[]> initializationData) {
super("SsaDecoder");
if (initializationData != null && !initializationData.isEmpty()) {
haveInitializationData = true;
......@@ -201,7 +202,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
cues.add(new Cue(text));
cueTimesUs.add(startTimeUs);
if (endTimeUs != C.TIME_UNSET) {
cues.add(null);
cues.add(Cue.EMPTY);
cueTimesUs.add(endTimeUs);
}
}
......
......@@ -32,7 +32,7 @@ import java.util.List;
private final long[] cueTimesUs;
/**
* @param cues The cues in the subtitle. Null entries may be used to represent empty cues.
* @param cues The cues in the subtitle.
* @param cueTimesUs The cue times, in microseconds.
*/
public SsaSubtitle(Cue[] cues, long[] cueTimesUs) {
......@@ -61,7 +61,7 @@ import java.util.List;
@Override
public List<Cue> getCues(long timeUs) {
int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
if (index == -1 || cues[index] == null) {
if (index == -1 || cues[index] == Cue.EMPTY) {
// timeUs is earlier than the start of the first cue, or we have an empty cue.
return Collections.emptyList();
} else {
......
......@@ -111,11 +111,13 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
// Read and parse the text and tags.
textBuilder.setLength(0);
tags.clear();
while (!TextUtils.isEmpty(currentLine = subripData.readLine())) {
currentLine = subripData.readLine();
while (!TextUtils.isEmpty(currentLine)) {
if (textBuilder.length() > 0) {
textBuilder.append("<br>");
}
textBuilder.append(processLine(currentLine, tags));
currentLine = subripData.readLine();
}
Spanned text = Html.fromHtml(textBuilder.toString());
......@@ -132,7 +134,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
cues.add(buildCue(text, alignmentTag));
if (haveEndTimecode) {
cues.add(null);
cues.add(Cue.EMPTY);
}
}
......
......@@ -32,7 +32,7 @@ import java.util.List;
private final long[] cueTimesUs;
/**
* @param cues The cues in the subtitle. Null entries may be used to represent empty cues.
* @param cues The cues in the subtitle.
* @param cueTimesUs The cue times, in microseconds.
*/
public SubripSubtitle(Cue[] cues, long[] cueTimesUs) {
......@@ -61,7 +61,7 @@ import java.util.List;
@Override
public List<Cue> getCues(long timeUs) {
int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false);
if (index == -1 || cues[index] == null) {
if (index == -1 || cues[index] == Cue.EMPTY) {
// timeUs is earlier than the start of the first cue, or we have an empty cue.
return Collections.emptyList();
} else {
......
......@@ -65,6 +65,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f;
private final ParsableByteArray parsableByteArray;
private boolean customVerticalPlacement;
private int defaultFontFace;
private int defaultColorRgba;
......@@ -80,10 +81,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
public Tx3gDecoder(List<byte[]> initializationData) {
super("Tx3gDecoder");
parsableByteArray = new ParsableByteArray();
decodeInitializationData(initializationData);
}
private void decodeInitializationData(List<byte[]> initializationData) {
if (initializationData != null && initializationData.size() == 1
&& (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) {
byte[] initializationBytes = initializationData.get(0);
......@@ -151,8 +149,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
}
parsableByteArray.setPosition(position + atomSize);
}
return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION,
Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET));
return new Tx3gSubtitle(
new Cue(
cueText,
/* textAlignment= */ null,
verticalPlacement,
Cue.LINE_TYPE_FRACTION,
Cue.ANCHOR_TYPE_START,
Cue.DIMEN_UNSET,
Cue.TYPE_UNSET,
Cue.DIMEN_UNSET));
}
private static String readSubtitleText(ParsableByteArray parsableByteArray)
......
......@@ -2318,14 +2318,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
if (TextUtils.equals(format.language, language)) {
return 3;
}
// Partial match where one language is a subset of the other (e.g. "zho-hans" and "zho-hans-hk")
// Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk")
if (format.language.startsWith(language) || language.startsWith(format.language)) {
return 2;
}
// Partial match where only the main language tag is the same (e.g. "fra-fr" and "fra-ca")
if (format.language.length() >= 3
&& language.length() >= 3
&& format.language.substring(0, 3).equals(language.substring(0, 3))) {
// Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca")
String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0];
String queryMainLanguage = Util.splitAtFirst(language, "-")[0];
if (formatMainLanguage.equals(queryMainLanguage)) {
return 1;
}
return 0;
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.upstream;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri;
import androidx.annotation.Nullable;
import android.util.Base64;
......@@ -29,9 +31,10 @@ public final class DataSchemeDataSource extends BaseDataSource {
public static final String SCHEME_DATA = "data";
private @Nullable DataSpec dataSpec;
private int bytesRead;
private @Nullable byte[] data;
@Nullable private DataSpec dataSpec;
@Nullable private byte[] data;
private int endPosition;
private int readPosition;
public DataSchemeDataSource() {
super(/* isNetwork= */ false);
......@@ -41,6 +44,7 @@ public final class DataSchemeDataSource extends BaseDataSource {
public long open(DataSpec dataSpec) throws IOException {
transferInitializing(dataSpec);
this.dataSpec = dataSpec;
readPosition = (int) dataSpec.position;
Uri uri = dataSpec.uri;
String scheme = uri.getScheme();
if (!SCHEME_DATA.equals(scheme)) {
......@@ -61,8 +65,14 @@ public final class DataSchemeDataSource extends BaseDataSource {
// TODO: Add support for other charsets.
data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME));
}
endPosition =
dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length;
if (endPosition > data.length || readPosition > endPosition) {
data = null;
throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE);
}
transferStarted(dataSpec);
return data.length;
return (long) endPosition - readPosition;
}
@Override
......@@ -70,29 +80,29 @@ public final class DataSchemeDataSource extends BaseDataSource {
if (readLength == 0) {
return 0;
}
int remainingBytes = data.length - bytesRead;
int remainingBytes = endPosition - readPosition;
if (remainingBytes == 0) {
return C.RESULT_END_OF_INPUT;
}
readLength = Math.min(readLength, remainingBytes);
System.arraycopy(data, bytesRead, buffer, offset, readLength);
bytesRead += readLength;
System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength);
readPosition += readLength;
bytesTransferred(readLength);
return readLength;
}
@Override
public @Nullable Uri getUri() {
@Nullable
public Uri getUri() {
return dataSpec != null ? dataSpec.uri : null;
}
@Override
public void close() throws IOException {
public void close() {
if (data != null) {
data = null;
transferEnded();
}
dataSpec = null;
}
}
......@@ -15,12 +15,18 @@
*/
package com.google.android.exoplayer2.util;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import java.util.ArrayList;
import java.util.List;
/**
* Holder for FLAC stream info.
*/
public final class FlacStreamInfo {
/** Holder for FLAC metadata. */
public final class FlacStreamMetadata {
private static final String TAG = "FlacStreamMetadata";
public final int minBlockSize;
public final int maxBlockSize;
......@@ -30,16 +36,19 @@ public final class FlacStreamInfo {
public final int channels;
public final int bitsPerSample;
public final long totalSamples;
@Nullable public final Metadata metadata;
private static final String SEPARATOR = "=";
/**
* Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure.
* Parses binary FLAC stream info metadata.
*
* @param data An array holding FLAC stream info metadata structure
* @param offset Offset of the structure in the array
* @param data An array containing binary FLAC stream info metadata.
* @param offset The offset of the stream info metadata in {@code data}.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
*/
public FlacStreamInfo(byte[] data, int offset) {
public FlacStreamMetadata(byte[] data, int offset) {
ParsableBitArray scratch = new ParsableBitArray(data);
scratch.setPosition(offset * 8);
this.minBlockSize = scratch.readBits(16);
......@@ -49,14 +58,11 @@ public final class FlacStreamInfo {
this.sampleRate = scratch.readBits(20);
this.channels = scratch.readBits(3) + 1;
this.bitsPerSample = scratch.readBits(5) + 1;
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32)
| (scratch.readBits(32) & 0xFFFFFFFFL);
// Remaining 16 bytes is md5 value
this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL);
this.metadata = null;
}
/**
* Constructs a FlacStreamInfo given the parameters.
*
* @param minBlockSize Minimum block size of the FLAC stream.
* @param maxBlockSize Maximum block size of the FLAC stream.
* @param minFrameSize Minimum frame size of the FLAC stream.
......@@ -65,10 +71,16 @@ public final class FlacStreamInfo {
* @param channels Number of channels of the FLAC stream.
* @param bitsPerSample Number of bits per sample of the FLAC stream.
* @param totalSamples Total samples of the FLAC stream.
* @param vorbisComments Vorbis comments. Each entry must be in key=value form.
* @param pictureFrames Picture frames.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
* METADATA_BLOCK_STREAMINFO</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_vorbis_comment">FLAC format
* METADATA_BLOCK_VORBIS_COMMENT</a>
* @see <a href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC format
* METADATA_BLOCK_PICTURE</a>
*/
public FlacStreamInfo(
public FlacStreamMetadata(
int minBlockSize,
int maxBlockSize,
int minFrameSize,
......@@ -76,7 +88,9 @@ public final class FlacStreamInfo {
int sampleRate,
int channels,
int bitsPerSample,
long totalSamples) {
long totalSamples,
List<String> vorbisComments,
List<PictureFrame> pictureFrames) {
this.minBlockSize = minBlockSize;
this.maxBlockSize = maxBlockSize;
this.minFrameSize = minFrameSize;
......@@ -85,6 +99,7 @@ public final class FlacStreamInfo {
this.channels = channels;
this.bitsPerSample = bitsPerSample;
this.totalSamples = totalSamples;
this.metadata = buildMetadata(vorbisComments, pictureFrames);
}
/** Returns the maximum size for a decoded frame from the FLAC stream. */
......@@ -126,4 +141,27 @@ public final class FlacStreamInfo {
}
return approxBytesPerFrame;
}
@Nullable
private static Metadata buildMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
return null;
}
ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();
for (int i = 0; i < vorbisComments.size(); i++) {
String vorbisComment = vorbisComments.get(i);
String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);
if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment);
} else {
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry);
}
}
metadataEntries.addAll(pictureFrames);
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
}
}
......@@ -61,6 +61,14 @@ public final class NotificationUtil {
/** @see NotificationManager#IMPORTANCE_HIGH */
public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH;
/** @deprecated Use {@link #createNotificationChannel(Context, String, int, int, int)}. */
@Deprecated
public static void createNotificationChannel(
Context context, String id, @StringRes int nameResourceId, @Importance int importance) {
createNotificationChannel(
context, id, nameResourceId, /* descriptionResourceId= */ 0, importance);
}
/**
* Creates a notification channel that notifications can be posted to. See {@link
* NotificationChannel} and {@link
......@@ -70,21 +78,33 @@ public final class NotificationUtil {
* @param id The id of the channel. Must be unique per package. The value may be truncated if it's
* too long.
* @param nameResourceId A string resource identifier for the user visible name of the channel.
* You can rename this channel when the system locale changes by listening for the {@link
* Intent#ACTION_LOCALE_CHANGED} broadcast. The recommended maximum length is 40 characters.
* The value may be truncated if it is too long.
* The recommended maximum length is 40 characters. The string may be truncated if it's too
* long. You can rename the channel when the system locale changes by listening for the {@link
* Intent#ACTION_LOCALE_CHANGED} broadcast.
* @param descriptionResourceId A string resource identifier for the user visible description of
* the channel, or 0 if no description is provided. The recommended maximum length is 300
* characters. The value may be truncated if it is too long. You can change the description of
* the channel when the system locale changes by listening for the {@link
* Intent#ACTION_LOCALE_CHANGED} broadcast.
* @param importance The importance of the channel. This controls how interruptive notifications
* posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link
* #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link
* #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}.
*/
public static void createNotificationChannel(
Context context, String id, @StringRes int nameResourceId, @Importance int importance) {
Context context,
String id,
@StringRes int nameResourceId,
@StringRes int descriptionResourceId,
@Importance int importance) {
if (Util.SDK_INT >= 26) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel =
new NotificationChannel(id, context.getString(nameResourceId), importance);
if (descriptionResourceId != 0) {
channel.setDescription(context.getString(descriptionResourceId));
}
notificationManager.createNotificationChannel(channel);
}
}
......
......@@ -71,6 +71,7 @@ import java.util.Calendar;
import java.util.Collections;
import java.util.Formatter;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
......@@ -135,6 +136,10 @@ public final class Util {
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})");
// Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter
// ISO 639-2 code back to the corresponding 2-letter code.
@Nullable private static HashMap<String, String> languageTagIso3ToIso2;
private Util() {}
/**
......@@ -450,18 +455,31 @@ public final class Util {
if (language == null) {
return null;
}
try {
Locale locale = getLocaleForLanguageTag(language);
int localeLanguageLength = locale.getLanguage().length();
String normLanguage = locale.getISO3Language();
if (normLanguage.isEmpty()) {
return toLowerInvariant(language);
// Locale data (especially for API < 21) may produce tags with '_' instead of the
// standard-conformant '-'.
String normalizedTag = language.replace('_', '-');
if (Util.SDK_INT >= 21) {
// Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags.
normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag);
}
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);
}
String normTag = getLocaleLanguageTag(locale);
return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength));
} catch (MissingResourceException e) {
return toLowerInvariant(language);
}
return normalizedTag;
}
/**
......@@ -1955,32 +1973,25 @@ public final class Util {
}
private static String[] getSystemLocales() {
Configuration config = Resources.getSystem().getConfiguration();
return SDK_INT >= 24
? getSystemLocalesV24()
: new String[] {getLocaleLanguageTag(Resources.getSystem().getConfiguration().locale)};
? getSystemLocalesV24(config)
: SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()};
}
@TargetApi(24)
private static String[] getSystemLocalesV24() {
return Util.split(Resources.getSystem().getConfiguration().getLocales().toLanguageTags(), ",");
}
private static Locale getLocaleForLanguageTag(String languageTag) {
return Util.SDK_INT >= 21 ? getLocaleForLanguageTagV21(languageTag) : new Locale(languageTag);
private static String[] getSystemLocalesV24(Configuration config) {
return Util.split(config.getLocales().toLanguageTags(), ",");
}
@TargetApi(21)
private static Locale getLocaleForLanguageTagV21(String languageTag) {
return Locale.forLanguageTag(languageTag);
}
private static String getLocaleLanguageTag(Locale locale) {
return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString();
private static String[] getSystemLocaleV21(Configuration config) {
return new String[] {config.locale.toLanguageTag()};
}
@TargetApi(21)
private static String getLocaleLanguageTagV21(Locale locale) {
return locale.toLanguageTag();
private static String normalizeLanguageCodeSyntaxV21(String languageTag) {
return Locale.forLanguageTag(languageTag).toLanguageTag();
}
private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) {
......@@ -2013,6 +2024,54 @@ public final class Util {
}
}
private static HashMap<String, String> createIso3ToIso2Map() {
String[] iso2Languages = Locale.getISOLanguages();
HashMap<String, String> iso3ToIso2 =
new HashMap<>(
/* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length);
for (String iso2 : iso2Languages) {
try {
// This returns the ISO 639-2/T code for the language.
String iso3 = new Locale(iso2).getISO3Language();
if (!TextUtils.isEmpty(iso3)) {
iso3ToIso2.put(iso3, iso2);
}
} catch (MissingResourceException e) {
// Shouldn't happen for list of known languages, but we don't want to throw either.
}
}
// Add additional ISO 639-2/B codes to mapping.
for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) {
iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]);
}
return iso3ToIso2;
}
// See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes.
private static final String[] iso3BibliographicalToIso2 =
new String[] {
"alb", "sq",
"arm", "hy",
"baq", "eu",
"bur", "my",
"tib", "bo",
"chi", "zh",
"cze", "cs",
"dut", "nl",
"ger", "de",
"gre", "el",
"fre", "fr",
"geo", "ka",
"ice", "is",
"mac", "mk",
"mao", "mi",
"may", "ms",
"per", "fa",
"rum", "ro",
"slo", "sk",
"wel", "cy"
};
/**
* Allows the CRC calculation to be done byte by byte instead of bit per bit being the order
* "most significant bit first".
......
......@@ -551,10 +551,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Format format,
MediaCrypto crypto,
float codecOperatingRate) {
String codecMimeType = codecInfo.codecMimeType;
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
MediaFormat mediaFormat =
getMediaFormat(
format,
codecMimeType,
codecMaxValues,
codecOperatingRate,
deviceNeedsNoPostProcessWorkaround,
......@@ -1111,6 +1113,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* Returns the framework {@link MediaFormat} that should be used to configure the decoder.
*
* @param format The format of media.
* @param codecMimeType The MIME type handled by the codec.
* @param codecMaxValues Codec max values that should be used when configuring the decoder.
* @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
* no codec operating rate should be set.
......@@ -1123,13 +1126,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@SuppressLint("InlinedApi")
protected MediaFormat getMediaFormat(
Format format,
String codecMimeType,
CodecMaxValues codecMaxValues,
float codecOperatingRate,
boolean deviceNeedsNoPostProcessWorkaround,
int tunnelingAudioSessionId) {
MediaFormat mediaFormat = new MediaFormat();
// Set format parameters that should always be set.
mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);
mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType);
mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width);
mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height);
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
......@@ -1429,6 +1433,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "1713":
case "1714":
case "A10-70F":
case "A10-70L":
case "A1601":
case "A2016a40":
case "A7000-a":
......
seekMap:
isSeekable = true
duration = 26122
duration = 26125
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
......
seekMap:
isSeekable = true
duration = 26122
duration = 26125
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
......
seekMap:
isSeekable = true
duration = 26122
duration = 26125
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
......
seekMap:
isSeekable = true
duration = 26122
duration = 26125
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
......
......@@ -20,6 +20,7 @@ import static org.junit.Assert.fail;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.net.Uri;
import androidx.annotation.Nullable;
import android.view.Surface;
import androidx.test.core.app.ApplicationProvider;
......@@ -2608,6 +2609,56 @@ public final class ExoPlayerTest {
assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs));
}
@Test
public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception {
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse("https://ad1"))
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse("https://ad2"))
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse("https://ad3"));
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10_000_000,
adPlaybackState));
final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, null);
AtomicReference<Player> playerReference = new AtomicReference<>();
AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET);
EventListener eventListener =
new EventListener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {
contentStartPositionMs.set(playerReference.get().getContentPosition());
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder("contentWithInitialSeekAfterPrerollAd")
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
playerReference.set(player);
player.addListener(eventListener);
}
})
.seek(5_000)
.build();
new ExoPlayerTestRunner.Builder()
.setMediaSource(fakeMediaSource)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(contentStartPositionMs.get()).isAtLeast(5_000L);
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.ogg;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.OggTestData;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.EOFException;
import java.io.IOException;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link DefaultOggSeeker} utility methods. */
@RunWith(AndroidJUnit4.class)
public final class DefaultOggSeekerUtilMethodsTest {
private final Random random = new Random(0);
@Test
public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(4000);
}
@Test
public void testSkipToNextPageOverlap() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(2046);
}
@Test
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
TestUtil.joinByteArrays(
new byte[] {'x', 'O', 'g', 'g', 'S'}
), false);
skipToNextPage(extractorInput);
assertThat(extractorInput.getPosition()).isEqualTo(1);
}
@Test
public void testSkipToNextPageNoMatch() throws Exception {
FakeExtractorInput extractorInput = OggTestData.createInput(
new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);
try {
skipToNextPage(extractorInput);
fail();
} catch (EOFException e) {
// expected
}
}
private static void skipToNextPage(ExtractorInput extractorInput)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ extractorInput.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
oggSeeker.skipToNextPage(extractorInput);
break;
} catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ }
}
}
@Test
public void testSkipToPageOfGranule() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
// expect to be granule of the previous page returned as elapsedSamples
skipToPageOfGranule(input, 54000, 40000);
// expect to be at the start of the third page
assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254)));
}
@Test
public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
skipToPageOfGranule(input, 40000, 20000);
// expect to be at the start of the second page
assertThat(input.getPosition()).isEqualTo(30 + (3 * 254));
}
@Test
public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random);
byte[] data = TestUtil.joinByteArrays(
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet,
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestUtil.createByteArray(254, 254, 254), // Laces.
packet);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
skipToPageOfGranule(input, 10000, -1);
assertThat(input.getPosition()).isEqualTo(0);
}
private void skipToPageOfGranule(ExtractorInput input, long granule,
long elapsedSamplesExpected) throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ input.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1))
.isEqualTo(elapsedSamplesExpected);
return;
} catch (FakeExtractorInput.SimulatedIOException e) {
input.resetPeekPosition();
}
}
}
@Test
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays(
TestUtil.buildTestData(100, random),
OggTestData.buildOggHeader(0x00, 20000, 66, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
OggTestData.buildOggHeader(0x00, 40000, 67, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random),
OggTestData.buildOggHeader(0x05, 60000, 68, 3),
TestUtil.createByteArray(254, 254, 254), // laces
TestUtil.buildTestData(3 * 254, random)
), false);
assertReadGranuleOfLastPage(input, 60000);
}
@Test
public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (EOFException e) {
// ignored
}
}
@Test
public void testReadGranuleOfLastPageWithUnboundedLength()
throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(new byte[0], true);
try {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (IllegalArgumentException e) {
// ignored
}
}
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ input.getLength(),
/* streamReader= */ new FlacReader(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
while (true) {
try {
assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected);
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
}
}
}
}
......@@ -30,35 +30,39 @@ import java.util.Random;
private static final int MAX_GRANULES_IN_PAGE = 100000;
public final byte[] data;
public final long lastGranule;
public final int packetCount;
public final int granuleCount;
public final int pageCount;
public final int firstPayloadPageSize;
public final long firstPayloadPageGranulePosition;
public final int firstPayloadPageGranuleCount;
public final int lastPayloadPageSize;
public final int lastPayloadPageGranuleCount;
private OggTestFile(
byte[] data,
long lastGranule,
int packetCount,
int granuleCount,
int pageCount,
int firstPayloadPageSize,
long firstPayloadPageGranulePosition) {
int firstPayloadPageGranuleCount,
int lastPayloadPageSize,
int lastPayloadPageGranuleCount) {
this.data = data;
this.lastGranule = lastGranule;
this.packetCount = packetCount;
this.granuleCount = granuleCount;
this.pageCount = pageCount;
this.firstPayloadPageSize = firstPayloadPageSize;
this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition;
this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount;
this.lastPayloadPageSize = lastPayloadPageSize;
this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount;
}
public static OggTestFile generate(Random random, int pageCount) {
ArrayList<byte[]> fileData = new ArrayList<>();
int fileSize = 0;
long granule = 0;
int packetLength = -1;
int packetCount = 0;
int granuleCount = 0;
int firstPayloadPageSize = 0;
long firstPayloadPageGranulePosition = 0;
int firstPayloadPageGranuleCount = 0;
int lastPageloadPageSize = 0;
int lastPayloadPageGranuleCount = 0;
int packetLength = -1;
for (int i = 0; i < pageCount; i++) {
int headerType = 0x00;
......@@ -71,17 +75,17 @@ import java.util.Random;
if (i == pageCount - 1) {
headerType |= 4;
}
granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1;
int pageGranuleCount = random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1;
int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT);
byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount);
granuleCount += pageGranuleCount;
byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount);
fileData.add(header);
fileSize += header.length;
int pageSize = header.length;
byte[] laces = new byte[pageSegmentCount];
int bodySize = 0;
for (int j = 0; j < pageSegmentCount; j++) {
if (packetLength < 0) {
packetCount++;
if (i < pageCount - 1) {
packetLength = random.nextInt(MAX_PACKET_LENGTH);
} else {
......@@ -96,14 +100,19 @@ import java.util.Random;
packetLength -= 255;
}
fileData.add(laces);
fileSize += laces.length;
pageSize += laces.length;
byte[] payload = TestUtil.buildTestData(bodySize, random);
fileData.add(payload);
fileSize += payload.length;
pageSize += payload.length;
fileSize += pageSize;
if (i == 0) {
firstPayloadPageSize = header.length + bodySize;
firstPayloadPageGranulePosition = granule;
firstPayloadPageSize = pageSize;
firstPayloadPageGranuleCount = pageGranuleCount;
} else if (i == pageCount - 1) {
lastPageloadPageSize = pageSize;
lastPayloadPageGranuleCount = pageGranuleCount;
}
}
......@@ -115,11 +124,12 @@ import java.util.Random;
}
return new OggTestFile(
file,
granule,
packetCount,
granuleCount,
pageCount,
firstPayloadPageSize,
firstPayloadPageGranulePosition);
firstPayloadPageGranuleCount,
lastPageloadPageSize,
lastPayloadPageGranuleCount);
}
public int findPreviousPageStart(long position) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment