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,
diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle
index 06a5888404..c67de27697 100644
--- a/extensions/flac/build.gradle
+++ b/extensions/flac/build.gradle
@@ -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')
diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt
index ee0a9fa5b5..3e52f643e7 100644
--- a/extensions/flac/proguard-rules.txt
+++ b/extensions/flac/proguard-rules.txt
@@ -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 {
*;
}
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
index 435279fc45..a3770afc78 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java
@@ -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();
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
index d9cbac6ad5..97f152cea4 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
@@ -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.");
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
index b9c6ea06dd..4bfcc003ec 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java
@@ -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);
}
}
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
index 2d74bce5f1..50eb048d98 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
@@ -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) {
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
index de038921aa..f454e28c68 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java
@@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer2.ext.flac;
+import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -37,14 +40,14 @@ import java.nio.ByteBuffer;
}
}
- private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has
+ private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.
private final long nativeDecoderContext;
- private ByteBuffer byteBufferData;
- private ExtractorInput extractorInput;
+ @Nullable private ByteBuffer byteBufferData;
+ @Nullable private ExtractorInput extractorInput;
+ @Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput;
- private byte[] tempBuffer;
public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) {
@@ -57,67 +60,79 @@ import java.nio.ByteBuffer;
}
/**
- * Sets data to be parsed by libflac.
- * @param byteBufferData Source {@link ByteBuffer}
+ * Sets the data to be parsed.
+ *
+ * @param byteBufferData Source {@link ByteBuffer}.
*/
public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData;
this.extractorInput = null;
- this.tempBuffer = null;
}
/**
- * Sets data to be parsed by libflac.
- * @param extractorInput Source {@link ExtractorInput}
+ * Sets the data to be parsed.
+ *
+ * @param extractorInput Source {@link ExtractorInput}.
*/
public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null;
this.extractorInput = extractorInput;
- if (tempBuffer == null) {
- this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
- }
endOfExtractorInput = false;
+ if (tempBuffer == null) {
+ tempBuffer = new byte[TEMP_BUFFER_SIZE];
+ }
}
+ /**
+ * Returns whether the end of the data to be parsed has been reached, or true if no data was set.
+ */
public boolean isEndOfData() {
if (byteBufferData != null) {
return byteBufferData.remaining() == 0;
} else if (extractorInput != null) {
return endOfExtractorInput;
+ } else {
+ return true;
}
- return true;
+ }
+
+ /** Clears the data to be parsed. */
+ public void clearData() {
+ byteBufferData = null;
+ extractorInput = null;
}
/**
* Reads up to {@code length} bytes from the data source.
- *
- * This method blocks until at least one byte of data can be read, the end of the input is
+ *
+ *
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.
- *
- * 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)
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index bb72e114fe..cd91b06288 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -21,7 +21,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
-import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
+import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@@ -33,7 +33,8 @@ import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
-import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
@@ -42,6 +43,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Arrays;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Facilitates the extraction of data from the FLAC container format.
@@ -74,23 +78,20 @@ public final class FlacExtractor implements Extractor {
*/
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
+ private final ParsableByteArray outputBuffer;
private final Id3Peeker id3Peeker;
- private final boolean isId3MetadataDisabled;
+ private final boolean id3MetadataDisabled;
- private FlacDecoderJni decoderJni;
+ @Nullable private FlacDecoderJni decoderJni;
+ private @MonotonicNonNull ExtractorOutput extractorOutput;
+ private @MonotonicNonNull TrackOutput trackOutput;
- private ExtractorOutput extractorOutput;
- private TrackOutput trackOutput;
+ private boolean streamMetadataDecoded;
+ private @MonotonicNonNull FlacStreamMetadata streamMetadata;
+ private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
- private ParsableByteArray outputBuffer;
- private ByteBuffer outputByteBuffer;
- private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
- private FlacStreamInfo streamInfo;
-
- private Metadata id3Metadata;
- private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
-
- private boolean readPastStreamInfo;
+ @Nullable private Metadata id3Metadata;
+ @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
/** Constructs an instance with flags = 0. */
public FlacExtractor() {
@@ -103,8 +104,9 @@ public final class FlacExtractor implements Extractor {
* @param flags Flags that control the extractor's behavior.
*/
public FlacExtractor(int flags) {
+ outputBuffer = new ParsableByteArray();
id3Peeker = new Id3Peeker();
- isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
+ id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
}
@Override
@@ -130,48 +132,53 @@ public final class FlacExtractor implements Extractor {
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
- if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
+ if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input);
}
- decoderJni.setData(input);
- readPastStreamInfo(input);
-
- if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
- return handlePendingSeek(input, seekPosition);
- }
-
- long lastDecodePosition = decoderJni.getDecodePosition();
+ FlacDecoderJni decoderJni = initDecoderJni(input);
try {
- decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
- } catch (FlacDecoderJni.FlacFrameDecodeException e) {
- throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
- }
- int outputSize = outputByteBuffer.limit();
- if (outputSize == 0) {
- return RESULT_END_OF_INPUT;
- }
+ decodeStreamMetadata(input);
- writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
- return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
+ return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
+ }
+
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
+ long lastDecodePosition = decoderJni.getDecodePosition();
+ try {
+ decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
+ } catch (FlacDecoderJni.FlacFrameDecodeException e) {
+ throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
+ }
+ int outputSize = outputByteBuffer.limit();
+ if (outputSize == 0) {
+ return RESULT_END_OF_INPUT;
+ }
+
+ outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
+ return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+ } finally {
+ decoderJni.clearData();
+ }
}
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
- readPastStreamInfo = false;
+ streamMetadataDecoded = false;
}
if (decoderJni != null) {
decoderJni.reset(position);
}
- if (flacBinarySearchSeeker != null) {
- flacBinarySearchSeeker.setSeekTargetUs(timeUs);
+ if (binarySearchSeeker != null) {
+ binarySearchSeeker.setSeekTargetUs(timeUs);
}
}
@Override
public void release() {
- flacBinarySearchSeeker = null;
+ binarySearchSeeker = null;
if (decoderJni != null) {
decoderJni.release();
decoderJni = null;
@@ -179,123 +186,141 @@ public final class FlacExtractor implements Extractor {
}
/**
- * Peeks ID3 tag data (if present) at the beginning of the input.
+ * Peeks ID3 tag data at the beginning of the input.
*
- * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
- * present in the input.
+ * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
*/
@Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
Id3Decoder.FramePredicate id3FramePredicate =
- isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
+ id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
+ @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
+ @SuppressWarnings({"contracts.postcondition.not.satisfied"})
+ private FlacDecoderJni initDecoderJni(ExtractorInput input) {
+ FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);
+ decoderJni.setData(input);
+ return decoderJni;
+ }
+
+ @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
+ @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
+ @SuppressWarnings({"contracts.postcondition.not.satisfied"})
+ private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
+ if (streamMetadataDecoded) {
+ return;
+ }
+
+ FlacStreamMetadata streamMetadata;
+ try {
+ streamMetadata = decoderJni.decodeStreamMetadata();
+ } catch (IOException e) {
+ decoderJni.reset(/* newPosition= */ 0);
+ input.setRetryPosition(/* position= */ 0, e);
+ throw e;
+ }
+
+ streamMetadataDecoded = true;
+ if (this.streamMetadata == null) {
+ this.streamMetadata = streamMetadata;
+ binarySearchSeeker =
+ outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
+ Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
+ if (streamMetadata.metadata != null) {
+ metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
+ }
+ outputFormat(streamMetadata, metadata, trackOutput);
+ outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
+ outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
+ }
+ }
+
+ @RequiresNonNull("binarySearchSeeker")
+ private int handlePendingSeek(
+ ExtractorInput input,
+ PositionHolder seekPosition,
+ ParsableByteArray outputBuffer,
+ OutputFrameHolder outputFrameHolder,
+ TrackOutput trackOutput)
+ throws InterruptedException, IOException {
+ int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
+ ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
+ if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
+ outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
+ }
+ return seekResult;
+ }
+
/**
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
*
* @return Whether the input begins with {@link #FLAC_SIGNATURE}.
*/
- private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
+ private static boolean peekFlacSignature(ExtractorInput input)
+ throws IOException, InterruptedException {
byte[] header = new byte[FLAC_SIGNATURE.length];
- input.peekFully(header, 0, FLAC_SIGNATURE.length);
+ input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);
return Arrays.equals(header, FLAC_SIGNATURE);
}
- private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
- if (readPastStreamInfo) {
- return;
- }
-
- FlacStreamInfo streamInfo = decodeStreamInfo(input);
- readPastStreamInfo = true;
- if (this.streamInfo == null) {
- updateFlacStreamInfo(input, streamInfo);
- }
- }
-
- private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
- this.streamInfo = streamInfo;
- outputSeekMap(input, streamInfo);
- outputFormat(streamInfo);
- outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
- outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
- outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
- }
-
- private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
- throws InterruptedException, IOException {
- try {
- FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
- if (streamInfo == null) {
- throw new IOException("Metadata decoding failed");
- }
- return streamInfo;
- } catch (IOException e) {
- decoderJni.reset(0);
- input.setRetryPosition(0, e);
- throw e;
- }
- }
-
- private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
- boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
- SeekMap seekMap =
- hasSeekTable
- ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
- : getSeekMapForNonSeekTableFlac(input, streamInfo);
- extractorOutput.seekMap(seekMap);
- }
-
- private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
- long inputLength = input.getLength();
- if (inputLength != C.LENGTH_UNSET) {
+ /**
+ * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
+ * handle seeks.
+ */
+ @Nullable
+ private static FlacBinarySearchSeeker outputSeekMap(
+ FlacDecoderJni decoderJni,
+ FlacStreamMetadata streamMetadata,
+ long streamLength,
+ ExtractorOutput output) {
+ boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
+ FlacBinarySearchSeeker binarySearchSeeker = null;
+ SeekMap seekMap;
+ if (hasSeekTable) {
+ seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
+ } else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
- flacBinarySearchSeeker =
- new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
- return flacBinarySearchSeeker.getSeekMap();
- } else { // can't seek at all, because there's no SeekTable and the input length is unknown.
- return new SeekMap.Unseekable(streamInfo.durationUs());
+ binarySearchSeeker =
+ new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
+ seekMap = binarySearchSeeker.getSeekMap();
+ } else {
+ seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
}
+ output.seekMap(seekMap);
+ return binarySearchSeeker;
}
- private void outputFormat(FlacStreamInfo streamInfo) {
+ private static void outputFormat(
+ FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
Format mediaFormat =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
- streamInfo.bitRate(),
- streamInfo.maxDecodedFrameSize(),
- streamInfo.channels,
- streamInfo.sampleRate,
- getPcmEncoding(streamInfo.bitsPerSample),
+ streamMetadata.bitRate(),
+ streamMetadata.maxDecodedFrameSize(),
+ streamMetadata.channels,
+ streamMetadata.sampleRate,
+ getPcmEncoding(streamMetadata.bitsPerSample),
/* encoderDelay= */ 0,
/* encoderPadding= */ 0,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null,
- isId3MetadataDisabled ? null : id3Metadata);
- trackOutput.format(mediaFormat);
+ metadata);
+ output.format(mediaFormat);
}
- private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
- throws InterruptedException, IOException {
- int seekResult =
- flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
- ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
- if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
- writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
- }
- return seekResult;
- }
-
- private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
- outputBuffer.setPosition(0);
- trackOutput.sampleData(outputBuffer, size);
- trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
+ private static void outputSample(
+ ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
+ sampleData.setPosition(0);
+ output.sampleData(sampleData, size);
+ output.sampleMetadata(
+ timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
}
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc
index 298719d48d..d60a7cead2 100644
--- a/extensions/flac/src/main/jni/flac_jni.cc
+++ b/extensions/flac/src/main/jni/flac_jni.cc
@@ -14,9 +14,12 @@
* limitations under the License.
*/
-#include
#include
+#include
+
#include
+#include
+
#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, "", "()V");
+ jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
+ jmethodID arrayListAddMethod =
+ env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+
+ if (context->parser->areVorbisCommentsValid()) {
+ std::vector vorbisComments =
+ context->parser->getVorbisComments();
+ for (std::vector::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 pictures = context->parser->getPictures();
+ jclass pictureFrameClass = env->FindClass(
+ "com/google/android/exoplayer2/metadata/flac/PictureFrame");
+ jmethodID pictureFrameConstructor =
+ env->GetMethodID(pictureFrameClass, "",
+ "(ILjava/lang/String;Ljava/lang/String;IIII[B)V");
+ for (std::vector::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, "", "(IIIIIIIJ)V");
+ "FlacStreamMetadata");
+ jmethodID flacStreamMetadataConstructor =
+ env->GetMethodID(flacStreamMetadataClass, "",
+ "(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
- return env->NewObject(cls, constructor, streamInfo.min_blocksize,
- streamInfo.max_blocksize, streamInfo.min_framesize,
- streamInfo.max_framesize, streamInfo.sample_rate,
- streamInfo.channels, streamInfo.bits_per_sample,
- streamInfo.total_samples);
+ return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
+ streamInfo.min_blocksize, streamInfo.max_blocksize,
+ streamInfo.min_framesize, streamInfo.max_framesize,
+ streamInfo.sample_rate, streamInfo.channels,
+ streamInfo.bits_per_sample, streamInfo.total_samples,
+ commentList, pictureFrames);
}
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc
index 83d3367415..830f3e2178 100644
--- a/extensions/flac/src/main/jni/flac_parser.cc
+++ b/extensions/flac/src/main/jni/flac_parser.cc
@@ -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(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,
diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h
index cea7fbe33b..14ba9e8725 100644
--- a/extensions/flac/src/main/jni/include/flac_parser.h
+++ b/extensions/flac/src/main/jni/include/flac_parser.h
@@ -19,6 +19,10 @@
#include
+#include
+#include
+#include
+
// 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 data;
+};
+
class FLACParser {
public:
FLACParser(DataSource *source);
@@ -44,6 +59,14 @@ class FLACParser {
return mStreamInfo;
}
+ bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
+
+ std::vector getVorbisComments() { return mVorbisComments; }
+
+ bool arePicturesValid() const { return mPicturesValid; }
+
+ const std::vector &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 mVorbisComments;
+ bool mVorbisCommentsValid;
+
+ // cached when the PICTURE metadata is parsed by libFLAC
+ std::vector mPictures;
+ bool mPicturesValid;
+
// cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested;
bool mWriteCompleted;
diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle
index 50acd6c040..1031d6f4b7 100644
--- a/extensions/gvr/build.gradle
+++ b/extensions/gvr/build.gradle
@@ -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
}
diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle
index 2df9448d08..0ef9f281c9 100644
--- a/extensions/ima/build.gradle
+++ b/extensions/ima/build.gradle
@@ -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')
}
diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md
index f70125ba38..a6f0c3966a 100644
--- a/extensions/jobdispatcher/README.md
+++ b/extensions/jobdispatcher/README.md
@@ -1,7 +1,11 @@
# ExoPlayer Firebase JobDispatcher extension #
+**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
+
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
+[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
+[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ##
@@ -20,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
-
diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
index d79dead0d7..c8975275f1 100644
--- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
+++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java
@@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util;
*
* @see GoogleApiAvailability
+ * @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;
diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle
index c6f5a216ce..ecaa78e25b 100644
--- a/extensions/leanback/build.gradle
+++ b/extensions/leanback/build.gradle
@@ -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'
}
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index db2e073c8a..68bd422185 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -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'
}
diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle
index 56acbdb7d3..28f7b05465 100644
--- a/extensions/opus/build.gradle
+++ b/extensions/opus/build.gradle
@@ -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
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
index f8ec477b88..dbce33b923 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
@@ -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) {
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
index 285be96388..2c2c8f6972 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
@@ -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;
}
diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle
index ca734c3657..b74be659ee 100644
--- a/extensions/rtmp/build.gradle
+++ b/extensions/rtmp/build.gradle
@@ -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')
}
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index 02b68b831d..92450f0381 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -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
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
index 57e5481b55..0e13e82630 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
@@ -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;
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
index 5a65fc56ff..db056d5110 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java
@@ -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;
}
diff --git a/extensions/workmanager/README.md b/extensions/workmanager/README.md
new file mode 100644
index 0000000000..bd2dbc71ad
--- /dev/null
+++ b/extensions/workmanager/README.md
@@ -0,0 +1,22 @@
+# ExoPlayer WorkManager extension
+
+This extension provides a Scheduler implementation which uses [WorkManager][].
+
+[WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html
+
+## Getting the extension
+
+The easiest way to use the extension is to add it as a gradle dependency:
+
+```gradle
+implementation 'com.google.android.exoplayer:extension-workmanager:2.X.X'
+```
+
+where `2.X.X` is the version, which must match the version of the ExoPlayer
+library being used.
+
+Alternatively, you can clone the ExoPlayer repository and depend on the module
+locally. Instructions for doing this can be found in ExoPlayer's
+[top level README][].
+
+[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle
new file mode 100644
index 0000000000..ea7564316f
--- /dev/null
+++ b/extensions/workmanager/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+apply from: '../../constants.gradle'
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion project.ext.compileSdkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ minSdkVersion project.ext.minSdkVersion
+ targetSdkVersion project.ext.targetSdkVersion
+ }
+
+ testOptions.unitTests.includeAndroidResources = true
+}
+
+dependencies {
+ implementation project(modulePrefix + 'library-core')
+ implementation 'androidx.work:work-runtime:2.1.0'
+}
+
+ext {
+ javadocTitle = 'WorkManager extension'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'extension-workmanager'
+ releaseDescription = 'WorkManager extension for ExoPlayer.'
+}
+apply from: '../../publish.gradle'
diff --git a/extensions/workmanager/src/main/AndroidManifest.xml b/extensions/workmanager/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..1daf50bd00
--- /dev/null
+++ b/extensions/workmanager/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java
new file mode 100644
index 0000000000..01801c9897
--- /dev/null
+++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.workmanager;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import androidx.work.Constraints;
+import androidx.work.Data;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.NetworkType;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import com.google.android.exoplayer2.scheduler.Requirements;
+import com.google.android.exoplayer2.scheduler.Scheduler;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+
+/** A {@link Scheduler} that uses {@link WorkManager}. */
+public final class WorkManagerScheduler implements Scheduler {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "WorkManagerScheduler";
+ private static final String KEY_SERVICE_ACTION = "service_action";
+ private static final String KEY_SERVICE_PACKAGE = "service_package";
+ private static final String KEY_REQUIREMENTS = "requirements";
+
+ private final String workName;
+
+ /**
+ * @param workName A name for work scheduled by this instance. If the same name was used by a
+ * previous instance, anything scheduled by the previous instance will be canceled by this
+ * instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} are
+ * called.
+ */
+ public WorkManagerScheduler(String workName) {
+ this.workName = workName;
+ }
+
+ @Override
+ public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
+ Constraints constraints = buildConstraints(requirements);
+ Data inputData = buildInputData(requirements, servicePackage, serviceAction);
+ OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData);
+ logd("Scheduling work: " + workName);
+ WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest);
+ return true;
+ }
+
+ @Override
+ public boolean cancel() {
+ logd("Canceling work: " + workName);
+ WorkManager.getInstance().cancelUniqueWork(workName);
+ return true;
+ }
+
+ private static Constraints buildConstraints(Requirements requirements) {
+ Constraints.Builder builder = new Constraints.Builder();
+
+ if (requirements.isUnmeteredNetworkRequired()) {
+ builder.setRequiredNetworkType(NetworkType.UNMETERED);
+ } else if (requirements.isNetworkRequired()) {
+ builder.setRequiredNetworkType(NetworkType.CONNECTED);
+ } else {
+ builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
+ }
+
+ if (requirements.isChargingRequired()) {
+ builder.setRequiresCharging(true);
+ }
+
+ if (requirements.isIdleRequired() && Util.SDK_INT >= 23) {
+ setRequiresDeviceIdle(builder);
+ }
+
+ return builder.build();
+ }
+
+ @TargetApi(23)
+ private static void setRequiresDeviceIdle(Constraints.Builder builder) {
+ builder.setRequiresDeviceIdle(true);
+ }
+
+ private static Data buildInputData(
+ Requirements requirements, String servicePackage, String serviceAction) {
+ Data.Builder builder = new Data.Builder();
+
+ builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements());
+ builder.putString(KEY_SERVICE_PACKAGE, servicePackage);
+ builder.putString(KEY_SERVICE_ACTION, serviceAction);
+
+ return builder.build();
+ }
+
+ private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) {
+ OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class);
+
+ builder.setConstraints(constraints);
+ builder.setInputData(inputData);
+
+ return builder.build();
+ }
+
+ private static void logd(String message) {
+ if (DEBUG) {
+ Log.d(TAG, message);
+ }
+ }
+
+ /** A {@link Worker} that starts the target service if the requirements are met. */
+ // This class needs to be public so that WorkManager can instantiate it.
+ public static final class SchedulerWorker extends Worker {
+
+ private final WorkerParameters workerParams;
+ private final Context context;
+
+ public SchedulerWorker(Context context, WorkerParameters workerParams) {
+ super(context, workerParams);
+ this.workerParams = workerParams;
+ this.context = context;
+ }
+
+ @Override
+ public Result doWork() {
+ logd("SchedulerWorker is started");
+ Data inputData = workerParams.getInputData();
+ Assertions.checkNotNull(inputData, "Work started without input data.");
+ Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0));
+ if (requirements.checkRequirements(context)) {
+ logd("Requirements are met");
+ String serviceAction = inputData.getString(KEY_SERVICE_ACTION);
+ String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE);
+ Assertions.checkNotNull(serviceAction, "Service action missing.");
+ Assertions.checkNotNull(servicePackage, "Service package missing.");
+ Intent intent = new Intent(serviceAction).setPackage(servicePackage);
+ logd("Starting service action: " + serviceAction + " package: " + servicePackage);
+ Util.startForegroundService(context, intent);
+ return Result.success();
+ } else {
+ logd("Requirements are not met");
+ return Result.retry();
+ }
+ }
+ }
+}
diff --git a/library/core/build.gradle b/library/core/build.gradle
index 68ff8cc977..e633e12057 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -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
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index c004058082..a10416fac8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -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();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index a9fe73371a..65a6866a9f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -1304,8 +1304,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
Pair