diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java index dee0db5a8e..46501ce002 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java @@ -31,7 +31,8 @@ public interface MetadataDecoder { * ByteBuffer#hasArray()} is true. * * @param inputBuffer The input buffer to decode. - * @return The decoded metadata object, or null if the metadata could not be decoded. + * @return The decoded metadata object, or {@code null} if the metadata could not be decoded or if + * {@link MetadataInputBuffer#isDecodeOnly()} was set on the input buffer. */ @Nullable Metadata decode(MetadataInputBuffer inputBuffer); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/SimpleMetadataDecoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/SimpleMetadataDecoder.java new file mode 100644 index 0000000000..cf3954b7c5 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/SimpleMetadataDecoder.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 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; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import java.nio.ByteBuffer; + +/** + * A {@link MetadataDecoder} base class that validates input buffers and discards any for which + * {@link MetadataInputBuffer#isDecodeOnly()} is {@code true}. + */ +public abstract class SimpleMetadataDecoder implements MetadataDecoder { + + @Override + @Nullable + public final Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + Assertions.checkArgument( + buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + return inputBuffer.isDecodeOnly() ? null : decode(inputBuffer, buffer); + } + + /** + * Called by {@link #decode(MetadataInputBuffer)} after input buffer validation has been + * performed, except in the case that {@link MetadataInputBuffer#isDecodeOnly()} is {@code true}. + * + * @param inputBuffer The input buffer to decode. + * @param buffer The input buffer's {@link MetadataInputBuffer#data data buffer}, for convenience. + * Validation by {@link #decode} guarantees that {@link ByteBuffer#hasArray()}, {@link + * ByteBuffer#position()} and {@link ByteBuffer#arrayOffset()} are {@code true}, {@code 0} and + * {@code 0} respectively. + * @return The decoded metadata object, or {@code null} if the metadata could not be decoded. + */ + @Nullable + protected abstract Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer); +} diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index c03a5cb038..8d4f52ab48 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -16,21 +16,19 @@ package com.google.android.exoplayer2.metadata.emsg; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; import java.util.Arrays; /** Decodes data encoded by {@link EventMessageEncoder}. */ -public final class EventMessageDecoder implements MetadataDecoder { +public final class EventMessageDecoder extends SimpleMetadataDecoder { @Override - public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); - Assertions.checkArgument( - buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + @SuppressWarnings("ByteBufferBackingArray") // Buffer validated by SimpleMetadataDecoder.decode + protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) { return new Metadata(decode(new ParsableByteArray(buffer.array(), buffer.limit()))); } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 19b970e726..904ac20739 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -18,9 +18,8 @@ package com.google.android.exoplayer2.metadata.id3; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -32,10 +31,8 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; -/** - * Decodes ID3 tags. - */ -public final class Id3Decoder implements MetadataDecoder { +/** Decodes ID3 tags. */ +public final class Id3Decoder extends SimpleMetadataDecoder { /** * A predicate for determining whether individual frames should be decoded. @@ -98,10 +95,8 @@ public final class Id3Decoder implements MetadataDecoder { @Override @Nullable - public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); - Assertions.checkArgument( - buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + @SuppressWarnings("ByteBufferBackingArray") // Buffer validated by SimpleMetadataDecoder.decode + protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) { return decode(buffer.array(), buffer.limit()); } @@ -118,7 +113,7 @@ public final class Id3Decoder implements MetadataDecoder { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); - Id3Header id3Header = decodeHeader(id3Data); + @Nullable Id3Header id3Header = decodeHeader(id3Data); if (id3Header == null) { return null; } @@ -142,8 +137,14 @@ public final class Id3Decoder implements MetadataDecoder { } while (id3Data.bytesLeft() >= frameHeaderSize) { - Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize, framePredicate); + @Nullable + Id3Frame frame = + decodeFrame( + id3Header.majorVersion, + id3Data, + unsignedIntFrameSizeHack, + frameHeaderSize, + framePredicate); if (frame != null) { id3Frames.add(frame); } @@ -660,8 +661,10 @@ public final class Id3Decoder implements MetadataDecoder { ArrayList subFrames = new ArrayList<>(); int limit = framePosition + frameSize; while (id3Data.getPosition() < limit) { - Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack, - frameHeaderSize, framePredicate); + @Nullable + Id3Frame frame = + decodeFrame( + majorVersion, id3Data, unsignedIntFrameSizeHack, frameHeaderSize, framePredicate); if (frame != null) { subFrames.add(frame); } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/metadata/SimpleMetadataDecoderTest.java b/library/common/src/test/java/com/google/android/exoplayer2/metadata/SimpleMetadataDecoderTest.java new file mode 100644 index 0000000000..be969fc031 --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/metadata/SimpleMetadataDecoderTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2020 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; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.annotation.Nullable; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SimpleMetadataDecoder}. */ +@RunWith(AndroidJUnit4.class) +public class SimpleMetadataDecoderTest { + + @Test + public void decode_nullDataInputBuffer_throwsNullPointerException() { + TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder(); + MetadataInputBuffer nullDataInputBuffer = new MetadataInputBuffer(); + nullDataInputBuffer.data = null; + + assertThrows(NullPointerException.class, () -> decoder.decode(nullDataInputBuffer)); + assertThat(decoder.decodeWasCalled).isFalse(); + } + + @Test + public void decode_directDataInputBuffer_throwsIllegalArgumentException() { + TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder(); + MetadataInputBuffer directDataInputBuffer = new MetadataInputBuffer(); + directDataInputBuffer.data = ByteBuffer.allocateDirect(8); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(directDataInputBuffer)); + assertThat(decoder.decodeWasCalled).isFalse(); + } + + @Test + public void decode_nonZeroPositionDataInputBuffer_throwsIllegalArgumentException() { + TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder(); + MetadataInputBuffer nonZeroPositionDataInputBuffer = new MetadataInputBuffer(); + nonZeroPositionDataInputBuffer.data = ByteBuffer.wrap(new byte[8]); + nonZeroPositionDataInputBuffer.data.position(1); + + assertThrows( + IllegalArgumentException.class, () -> decoder.decode(nonZeroPositionDataInputBuffer)); + assertThat(decoder.decodeWasCalled).isFalse(); + } + + @Test + public void decode_nonZeroOffsetDataInputBuffer_throwsIllegalArgumentException() { + TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder(); + MetadataInputBuffer directDataInputBuffer = new MetadataInputBuffer(); + directDataInputBuffer.data = ByteBuffer.wrap(new byte[8], /* offset= */ 4, /* length= */ 4); + + assertThrows(IllegalArgumentException.class, () -> decoder.decode(directDataInputBuffer)); + assertThat(decoder.decodeWasCalled).isFalse(); + } + + @Test + public void decode_decodeOnlyBuffer_notPassedToDecodeInternal() { + TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder(); + MetadataInputBuffer decodeOnlyBuffer = new MetadataInputBuffer(); + decodeOnlyBuffer.data = ByteBuffer.wrap(new byte[8]); + decodeOnlyBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY); + + assertThat(decoder.decode(decodeOnlyBuffer)).isNull(); + assertThat(decoder.decodeWasCalled).isFalse(); + } + + @Test + public void decode_returnsDecodeInternalResult() { + TestSimpleMetadataDecoder decoder = new TestSimpleMetadataDecoder(); + MetadataInputBuffer buffer = new MetadataInputBuffer(); + buffer.data = ByteBuffer.wrap(new byte[8]); + + assertThat(decoder.decode(buffer)).isSameInstanceAs(decoder.result); + assertThat(decoder.decodeWasCalled).isTrue(); + } + + private static final class TestSimpleMetadataDecoder extends SimpleMetadataDecoder { + + public final Metadata result; + + public boolean decodeWasCalled; + + public TestSimpleMetadataDecoder() { + result = new Metadata(); + } + + @Nullable + @Override + protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) { + decodeWasCalled = true; + return result; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 238d515caf..02e55070b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -129,10 +129,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { inputStreamEnded = true; - } else if (buffer.isDecodeOnly()) { - // Do nothing. Note this assumes that all metadata buffers can be decoded independently. - // If we ever need to support a metadata format where this is not the case, we'll need to - // pass the buffer to the decoder and discard the output. } else { buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoder.java index 15f633f67f..fb16945d82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoder.java @@ -17,9 +17,8 @@ package com.google.android.exoplayer2.metadata.dvbsi; import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.common.base.Charsets; import java.nio.ByteBuffer; @@ -32,7 +31,7 @@ import java.util.ArrayList; * href="https://www.etsi.org/deliver/etsi_ts/102800_102899/102809/01.01.01_60/ts_102809v010101p.pdf"> * DVB ETSI TS 102 809 v1.1.1 spec. */ -public final class AppInfoTableDecoder implements MetadataDecoder { +public final class AppInfoTableDecoder extends SimpleMetadataDecoder { /** See section 5.3.6. */ private static final int DESCRIPTOR_TRANSPORT_PROTOCOL = 0x02; @@ -47,10 +46,8 @@ public final class AppInfoTableDecoder implements MetadataDecoder { @Override @Nullable - public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); - Assertions.checkArgument( - buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + @SuppressWarnings("ByteBufferBackingArray") // Buffer validated by SimpleMetadataDecoder.decode + protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) { int tableId = buffer.get(); return tableId == APPLICATION_INFORMATION_TABLE_ID ? parseAit(new ParsableBitArray(buffer.array(), buffer.limit())) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index aa5e83a682..8f0254d83f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -17,9 +17,8 @@ package com.google.android.exoplayer2.metadata.icy; import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder; import com.google.android.exoplayer2.util.Util; import com.google.common.base.Charsets; import java.nio.ByteBuffer; @@ -29,7 +28,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** Decodes ICY stream information. */ -public final class IcyDecoder implements MetadataDecoder { +public final class IcyDecoder extends SimpleMetadataDecoder { private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; @@ -44,10 +43,7 @@ public final class IcyDecoder implements MetadataDecoder { } @Override - public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); - Assertions.checkArgument( - buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); + protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) { @Nullable String icyString = decodeToString(buffer); byte[] icyBytes = new byte[buffer.limit()]; buffer.get(icyBytes); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 647e1296a9..fbcf9da6f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -17,19 +17,16 @@ package com.google.android.exoplayer2.metadata.scte35; import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.metadata.SimpleMetadataDecoder; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * Decodes splice info sections and produces splice commands. - */ -public final class SpliceInfoDecoder implements MetadataDecoder { +/** Decodes splice info sections and produces splice commands. */ +public final class SpliceInfoDecoder extends SimpleMetadataDecoder { private static final int TYPE_SPLICE_NULL = 0x00; private static final int TYPE_SPLICE_SCHEDULE = 0x04; @@ -48,11 +45,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder { } @Override - public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); - Assertions.checkArgument( - buffer.position() == 0 && buffer.hasArray() && buffer.arrayOffset() == 0); - + @SuppressWarnings("ByteBufferBackingArray") // Buffer validated by SimpleMetadataDecoder.decode + protected Metadata decode(MetadataInputBuffer inputBuffer, ByteBuffer buffer) { // Internal timestamps adjustment. if (timestampAdjuster == null || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index 02b0dd3b52..94c908c98a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -361,12 +361,15 @@ public final class PlayerEmsgHandler implements Handler.Callback { private void parseAndDiscardSamples() { while (sampleQueue.isReady(/* loadingFinished= */ false)) { - MetadataInputBuffer inputBuffer = dequeueSample(); + @Nullable MetadataInputBuffer inputBuffer = dequeueSample(); if (inputBuffer == null) { continue; } long eventTimeUs = inputBuffer.timeUs; - Metadata metadata = decoder.decode(inputBuffer); + @Nullable Metadata metadata = decoder.decode(inputBuffer); + if (metadata == null) { + continue; + } EventMessage eventMessage = (EventMessage) metadata.get(0); if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { parsePlayerEmsgEvent(eventTimeUs, eventMessage);