diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4c4aa230a3..fd50e7bdbb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -82,6 +82,89 @@ [GL demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/gl) to show how to render video to a `GLSurfaceView` while applying a GL shader. ([#6920](https://github.com/google/ExoPlayer/issues/6920)). +* Core library: + * Add API in `AnalyticsListener` to report video frame processing offset. + `MediaCodecVideoRenderer` reports the event. + * Add fields `videoFrameProcessingOffsetUsSum` and + `videoFrameProcessingOffsetUsCount` in `DecoderCounters` to compute the + average video frame processing offset. + * Add playlist API + ([#6161](https://github.com/google/ExoPlayer/issues/6161)). + * Add `play` and `pause` methods to `Player`. + * Add `Player.getCurrentLiveOffset` to conveniently return the live + offset. + * Add `Player.onPlayWhenReadyChanged` with reasons. + * Add `Player.onPlaybackStateChanged` and deprecate + `Player.onPlayerStateChanged`. + * Deprecate and rename `getPlaybackError` to `getPlayerError` for + consistency. + * Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for + consistency. + * Make `MediaSourceEventListener.LoadEventInfo` and + `MediaSourceEventListener.MediaLoadData` top-level classes. + * Rename `MediaCodecRenderer.onOutputFormatChanged` to + `MediaCodecRenderer.onOutputMediaFormatChanged`, further clarifying the + distinction between `Format` and `MediaFormat`. + * Move player message-related constants from `C` to `Renderer`, to avoid + having the constants class depend on player/renderer classes. + * Split out `common` and `extractor` submodules. + * Allow to explicitly send `PlayerMessage`s at the end of a stream. + * Add `DataSpec.Builder` and deprecate most `DataSpec` constructors. + * Add `DataSpec.customData` to allow applications to pass custom data + through `DataSource` chains. + * Add a sample count parameter to `MediaCodecRenderer.processOutputBuffer` + and `AudioSink.handleBuffer` to allow batching multiple encoded frames + in one buffer. + * Add a `Format.Builder` and deprecate all `Format.create*` methods and + most `Format.copyWith*` methods. + * Split `Format.bitrate` into `Format.averageBitrate` and + `Format.peakBitrate` + ([#2863](https://github.com/google/ExoPlayer/issues/2863)). +* Text: + * Parse `` and `` tags in WebVTT subtitles (rendering is coming + later). + * Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT + subtitles (rendering is coming later). + * Parse `tts:combineText` property (i.e. tate-chu-yoko) in TTML subtitles + (rendering is coming later). + * Fix `SubtitlePainter` to render `EDGE_TYPE_OUTLINE` using the correct + color ([#6724](https://github.com/google/ExoPlayer/pull/6724)). + * Add support for WebVTT default + [text](https://www.w3.org/TR/webvtt1/#default-text-color) and + [background](https://www.w3.org/TR/webvtt1/#default-text-background) + colors ([PR #4178](https://github.com/google/ExoPlayer/pull/4178), + [issue #6581](https://github.com/google/ExoPlayer/issues/6581)). + * Catch-and-log all fatal exceptions in `TextRenderer` instead of + re-throwing, allowing playback to continue even if subtitles fail + ([#6885](https://github.com/google/ExoPlayer/issues/6885)). + * Parse `tts:ruby` and `tts:rubyPosition` properties in TTML subtitles + (rendering is coming later). +* DRM: + * Add support for attaching DRM sessions to clear content in the demo app. + * Remove `DrmSessionManager` references from all renderers. + `DrmSessionManager` must be injected into the MediaSources using the + MediaSources factories. +* Downloads: Merge downloads in `SegmentDownloader` to improve overall + download speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). +* MP3: Add `IndexSeeker` for accurate seeks in VBR streams + ([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker is + enabled by passing `FLAG_ENABLE_INDEX_SEEKING` to the `Mp3Extractor`. It may + require to scan a significant portion of the file for seeking, which may be + costly on large files. +* MP4: Store the Android capture frame rate only in `Format.metadata`. + `Format.frameRate` now stores the calculated frame rate. +* Testing + * Upgrade Truth dependency from 0.44 to 1.0. + * Upgrade to JUnit 4.13-rc-2. +* UI + * Move logic of prev, next, fast forward and rewind to ControlDispatcher + ([#6926](https://github.com/google/ExoPlayer/issues/6926)). +* Demo apps: Add + [GL demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/gl) to + show how to render video to a `GLSurfaceView` while applying a GL shader. + ([#6920](https://github.com/google/ExoPlayer/issues/6920)). +* Metadata: Add minimal DVB Application Information Table (AIT) support + ([#6922](https://github.com/google/ExoPlayer/pull/6922)). ### 2.11.3 (2020-02-19) ### diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e61ab83777..43335cf51c 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -97,6 +97,7 @@ public final class MimeTypes { public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy"; + public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait"; private static final ArrayList customMimeTypes = new ArrayList<>(); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index fc1bc653c6..963e43fc7e 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.C; +import java.nio.charset.Charset; + /** * Wraps a byte array, providing methods that allow it to be read as a bitstream. */ @@ -277,6 +280,31 @@ public final class ParsableBitArray { assertValidOffset(); } + /** + * Reads the next {@code length} bytes as a UTF-8 string. Must only be called when the position is + * byte aligned. + * + * @param length The number of bytes to read. + * @return The string encoded by the bytes in UTF-8. + */ + public String readBytesAsString(int length) { + return readBytesAsString(length, Charset.forName(C.UTF8_NAME)); + } + + /** + * Reads the next {@code length} bytes as a string encoded in {@link Charset}. Must only be called + * when the position is byte aligned. + * + * @param length The number of bytes to read. + * @param charset The character set of the encoded characters. + * @return The string encoded by the bytes in the specified character set. + */ + public String readBytesAsString(int length, Charset charset) { + byte[] bytes = new byte[length]; + readBytes(bytes, 0, length); + return new String(bytes, charset); + } + /** * Overwrites {@code numBits} from this array using the {@code numBits} least significant bits * from {@code value}. Bits are written in order from most significant to least significant. The diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java index f031468461..93655dd35e 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -16,9 +16,12 @@ package com.google.android.exoplayer2.util; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; +import java.nio.charset.Charset; import org.junit.Test; import org.junit.runner.RunWith; @@ -276,6 +279,35 @@ public final class ParsableBitArrayTest { assertThat(testArray.readBits(8)).isEqualTo(0x5F); } + @Test + public void testReadBytesAsStringDefaultsToUtf8() { + byte[] testData = "a non-åscii strìng".getBytes(Charset.forName(C.UTF8_NAME)); + ParsableBitArray testArray = new ParsableBitArray(testData); + + testArray.skipBytes(2); + assertThat(testArray.readBytesAsString(testData.length - 2)).isEqualTo("non-åscii strìng"); + } + + @Test + public void testReadBytesAsStringExplicitCharset() { + byte[] testData = "a non-åscii strìng".getBytes(Charset.forName(C.UTF16_NAME)); + ParsableBitArray testArray = new ParsableBitArray(testData); + + testArray.skipBytes(6); + assertThat(testArray.readBytesAsString(testData.length - 6, Charset.forName(C.UTF16_NAME))) + .isEqualTo("non-åscii strìng"); + } + + @Test + public void testReadBytesNotByteAligned() { + String testString = "test string"; + byte[] testData = testString.getBytes(Charset.forName(C.UTF8_NAME)); + ParsableBitArray testArray = new ParsableBitArray(testData); + + testArray.skipBit(); + assertThrows(IllegalStateException.class, () -> testArray.readBytesAsString(2)); + } + @Test public void testPutBitsWithinByte() { ParsableBitArray output = new ParsableBitArray(new byte[4]); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java index 0b653830a3..fae53a5d09 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.metadata.dvbsi.AppInfoTableDecoder; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.icy.IcyDecoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; @@ -67,7 +68,8 @@ public interface MetadataDecoderFactory { return MimeTypes.APPLICATION_ID3.equals(mimeType) || MimeTypes.APPLICATION_EMSG.equals(mimeType) || MimeTypes.APPLICATION_SCTE35.equals(mimeType) - || MimeTypes.APPLICATION_ICY.equals(mimeType); + || MimeTypes.APPLICATION_ICY.equals(mimeType) + || MimeTypes.APPLICATION_AIT.equals(mimeType); } @Override @@ -83,6 +85,8 @@ public interface MetadataDecoderFactory { return new SpliceInfoDecoder(); case MimeTypes.APPLICATION_ICY: return new IcyDecoder(); + case MimeTypes.APPLICATION_AIT: + return new AppInfoTableDecoder(); default: break; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTable.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTable.java new file mode 100644 index 0000000000..cdfb15f15b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTable.java @@ -0,0 +1,80 @@ +/* + * 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.dvbsi; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Assertions; + +/** + * A representation of a DVB Application Information Table (AIT). + * + *

For more info on the AIT see section 5.3.4 of the + * DVB ETSI TS 102 809 v1.1.1 spec. + */ +public final class AppInfoTable implements Metadata.Entry { + /** + * The application shall be started when the service is selected, unless the application is + * already running. + */ + public static final int CONTROL_CODE_AUTOSTART = 0x01; + /** + * The application is allowed to run while the service is selected, however it shall not start + * automatically when the service becomes selected. + */ + public static final int CONTROL_CODE_PRESENT = 0x02; + + public final int controlCode; + public final String url; + + public AppInfoTable(int controlCode, String url) { + this.controlCode = controlCode; + this.url = url; + } + + @Override + public String toString() { + return "Ait(controlCode=" + controlCode + ",url=" + url + ")"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(url); + parcel.writeInt(controlCode); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public AppInfoTable createFromParcel(Parcel in) { + String url = Assertions.checkNotNull(in.readString()); + int controlCode = in.readInt(); + return new AppInfoTable(controlCode, url); + } + + @Override + public AppInfoTable[] newArray(int size) { + return new AppInfoTable[size]; + } + }; +} 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 new file mode 100644 index 0000000000..10325b34ec --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoder.java @@ -0,0 +1,140 @@ +/* + * 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.dvbsi; + +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.util.ParsableBitArray; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; + +/** + * Decoder for the DVB Application Information Table (AIT). + * + *

For more info on the AIT see section 5.3.4 of the + * DVB ETSI TS 102 809 v1.1.1 spec. + */ +public final class AppInfoTableDecoder implements MetadataDecoder { + + /** See section 5.3.6. */ + private static final int DESCRIPTOR_TRANSPORT_PROTOCOL = 0x02; + /** See section 5.3.7. */ + private static final int DESCRIPTOR_SIMPLE_APPLICATION_LOCATION = 0x15; + + /** See table 29 in section 5.3.6. */ + private static final int TRANSPORT_PROTOCOL_HTTP = 3; + + /** See table 16 in section 5.3.4.6. */ + public static final int APPLICATION_INFORMATION_TABLE_ID = 0x74; + + @Override + @Nullable + @SuppressWarnings("ByteBufferBackingArray") + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + int tableId = buffer.get(); + return tableId == APPLICATION_INFORMATION_TABLE_ID + ? parseAit(new ParsableBitArray(buffer.array(), buffer.limit())) + : null; + } + + @Nullable + private static Metadata parseAit(ParsableBitArray sectionData) { + // tableId, section_syntax_indication, reserved_future_use, reserved + sectionData.skipBits(12); + int sectionLength = sectionData.readBits(12); + int endOfSection = sectionData.getBytePosition() + sectionLength - 4 /* Ignore leading CRC */; + + // test_application_flag, application_type, reserved, version_number, current_next_indicator, + // section_number, last_section_number, reserved_future_use + sectionData.skipBits(44); + + int commonDescriptorsLength = sectionData.readBits(12); + + // Since we currently only keep URL and control code, which are unique per application, + // there is no useful information in common descriptor. + sectionData.skipBytes(commonDescriptorsLength); + + // reserved_future_use, application_loop_length + sectionData.skipBits(16); + + ArrayList appInfoTables = new ArrayList<>(); + while (sectionData.getBytePosition() < endOfSection) { + @Nullable String urlBase = null; + @Nullable String urlExtension = null; + + // application_identifier + sectionData.skipBits(48); + + int controlCode = sectionData.readBits(8); + + // reserved_future_use + sectionData.skipBits(4); + + int applicationDescriptorsLoopLength = sectionData.readBits(12); + int positionOfNextApplication = + sectionData.getBytePosition() + applicationDescriptorsLoopLength; + while (sectionData.getBytePosition() < positionOfNextApplication) { + int descriptorTag = sectionData.readBits(8); + int descriptorLength = sectionData.readBits(8); + int positionOfNextDescriptor = sectionData.getBytePosition() + descriptorLength; + + if (descriptorTag == DESCRIPTOR_TRANSPORT_PROTOCOL) { + // See section 5.3.6. + int protocolId = sectionData.readBits(16); + // label + sectionData.skipBits(8); + + if (protocolId == TRANSPORT_PROTOCOL_HTTP) { + // See section 5.3.6.2. + while (sectionData.getBytePosition() < positionOfNextDescriptor) { + int urlBaseLength = sectionData.readBits(8); + urlBase = sectionData.readBytesAsString(urlBaseLength, Charset.forName(C.ASCII_NAME)); + + int extensionCount = sectionData.readBits(8); + for (int urlExtensionIndex = 0; + urlExtensionIndex < extensionCount; + urlExtensionIndex++) { + int urlExtensionLength = sectionData.readBits(8); + sectionData.skipBytes(urlExtensionLength); + } + } + } + } else if (descriptorTag == DESCRIPTOR_SIMPLE_APPLICATION_LOCATION) { + // See section 5.3.7. + urlExtension = + sectionData.readBytesAsString(descriptorLength, Charset.forName(C.ASCII_NAME)); + } + + sectionData.setPosition(positionOfNextDescriptor * 8); + } + + sectionData.setPosition(positionOfNextApplication * 8); + + if (urlBase != null && urlExtension != null) { + appInfoTables.add(new AppInfoTable(controlCode, urlBase + urlExtension)); + } + } + + return appInfoTables.isEmpty() ? null : new Metadata(appInfoTables); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/package-info.java new file mode 100644 index 0000000000..33efd262fe --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/dvbsi/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + * + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.dvbsi; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoderTest.java new file mode 100644 index 0000000000..2befb4ef8b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/dvbsi/AppInfoTableDecoderTest.java @@ -0,0 +1,83 @@ +/* + * 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.dvbsi; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link AppInfoTableDecoder}. */ +@RunWith(AndroidJUnit4.class) +public final class AppInfoTableDecoderTest { + + private static final String TYPICAL_FILE = "dvbsi/ait_typical.bin"; + private static final String NO_URL_BASE_FILE = "dvbsi/ait_no_url_base.bin"; + private static final String NO_URL_PATH_FILE = "dvbsi/ait_no_url_path.bin"; + + @Test + public void decode_typical() throws Exception { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + Metadata metadata = decoder.decode(createMetadataInputBuffer(readTestFile(TYPICAL_FILE))); + + assertThat(metadata.length()).isEqualTo(2); + Metadata.Entry firstEntry = metadata.get(0); + assertThat(firstEntry).isInstanceOf(AppInfoTable.class); + assertThat(((AppInfoTable) firstEntry).controlCode) + .isEqualTo(AppInfoTable.CONTROL_CODE_AUTOSTART); + assertThat(((AppInfoTable) firstEntry).url).isEqualTo("http://example.com/path/foo"); + Metadata.Entry secondEntry = metadata.get(1); + assertThat(secondEntry).isInstanceOf(AppInfoTable.class); + assertThat(((AppInfoTable) secondEntry).controlCode) + .isEqualTo(AppInfoTable.CONTROL_CODE_PRESENT); + assertThat(((AppInfoTable) secondEntry).url).isEqualTo("http://google.com/path/bar"); + } + + @Test + public void decode_noUrlBase() throws Exception { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + Metadata metadata = decoder.decode(createMetadataInputBuffer(readTestFile(NO_URL_BASE_FILE))); + + assertThat(metadata).isNull(); + } + + @Test + public void decode_noUrlPath() throws Exception { + AppInfoTableDecoder decoder = new AppInfoTableDecoder(); + Metadata metadata = decoder.decode(createMetadataInputBuffer(readTestFile(NO_URL_PATH_FILE))); + + assertThat(metadata).isNull(); + } + + private static MetadataInputBuffer createMetadataInputBuffer(byte[] data) { + MetadataInputBuffer inputBuffer = new MetadataInputBuffer(); + inputBuffer.data = ByteBuffer.allocate(data.length); + inputBuffer.data.put(data); + inputBuffer.data.flip(); + return inputBuffer; + } + + private static byte[] readTestFile(String name) throws IOException { + return TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), name); + } +} diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index fa56ad6b48..ee7c14dbc1 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -80,7 +80,10 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * delimiters (AUDs). */ public static final int FLAG_DETECT_ACCESS_UNITS = 1 << 3; - /** Prevents the creation of {@link SpliceInfoSectionReader} instances. */ + /** + * Prevents the creation of {@link SectionPayloadReader}s for splice information sections + * (SCTE-35). + */ public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 1 << 4; /** * Whether the list of {@code closedCaptionFormats} passed to {@link @@ -170,22 +173,25 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new PesReader(new H265Reader(buildSeiReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) - ? null : new SectionReader(new SpliceInfoSectionReader()); + ? null + : new SectionReader(new PassthroughSectionPayloadReader(MimeTypes.APPLICATION_SCTE35)); case TsExtractor.TS_STREAM_TYPE_ID3: return new PesReader(new Id3Reader()); case TsExtractor.TS_STREAM_TYPE_DVBSUBS: return new PesReader( new DvbSubtitleReader(esInfo.dvbSubtitleInfos)); + case TsExtractor.TS_STREAM_TYPE_AIT: + return new SectionReader(new PassthroughSectionPayloadReader(MimeTypes.APPLICATION_AIT)); default: return null; } } /** - * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for - * {@link #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a - * {@link SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor - * is not present. + * If {@link #FLAG_OVERRIDE_CAPTION_DESCRIPTORS} is set, returns a {@link SeiReader} for {@link + * #closedCaptionFormats}. If unset, parses the PMT descriptor information and returns a {@link + * SeiReader} for the declared formats, or {@link #closedCaptionFormats} if the descriptor is not + * present. * * @param esInfo The {@link EsInfo} passed to {@link #createPayloadReader(int, EsInfo)}. * @return A {@link SeiReader} for closed caption tracks. diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java similarity index 69% rename from library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java rename to library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java index e8caa4be7b..af374f6eae 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java @@ -20,7 +20,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; @@ -28,16 +27,30 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * Parses splice info sections as defined by SCTE35. + * A {@link SectionPayloadReader} that directly outputs the section bytes as sample data. + * + *

Timestamp adjustment is provided through {@link Format#subsampleOffsetUs}. */ -public final class SpliceInfoSectionReader implements SectionPayloadReader { +public final class PassthroughSectionPayloadReader implements SectionPayloadReader { + private final String mimeType; private @MonotonicNonNull TimestampAdjuster timestampAdjuster; private @MonotonicNonNull TrackOutput output; private boolean formatDeclared; + /** + * Create a new PassthroughSectionPayloadReader. + * + * @param mimeType The MIME type set as {@link Format#sampleMimeType} on the created output track. + */ + public PassthroughSectionPayloadReader(String mimeType) { + this.mimeType = mimeType; + } + @Override - public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + public void init( + TimestampAdjuster timestampAdjuster, + ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { this.timestampAdjuster = timestampAdjuster; idGenerator.generateNewId(); @@ -53,14 +66,20 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { return; } output.format( - Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35) - .copyWithSubsampleOffsetUs(timestampAdjuster.getTimestampOffsetUs())); + new Format.Builder() + .setSampleMimeType(mimeType) + .setSubsampleOffsetUs(timestampAdjuster.getTimestampOffsetUs()) + .build()); formatDeclared = true; } int sampleSize = sectionData.bytesLeft(); output.sampleData(sectionData, sampleSize); - output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME, - sampleSize, 0, null); + output.sampleMetadata( + timestampAdjuster.getLastAdjustedTimestampUs(), + C.BUFFER_FLAG_KEY_FRAME, + sampleSize, + 0, + null); } @EnsuresNonNull({"timestampAdjuster", "output"}) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 35e8806a6f..3d0d6a18ea 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -96,6 +96,9 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_SPLICE_INFO = 0x86; public static final int TS_STREAM_TYPE_DVBSUBS = 0x59; + // Stream types that aren't defined by the MPEG-2 TS specification. + public static final int TS_STREAM_TYPE_AIT = 0x101; + public static final int TS_PACKET_SIZE = 188; public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. @@ -494,6 +497,7 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_REGISTRATION = 0x05; private static final int TS_PMT_DESC_ISO639_LANG = 0x0A; private static final int TS_PMT_DESC_AC3 = 0x6A; + private static final int TS_PMT_DESC_AIT = 0x6F; private static final int TS_PMT_DESC_EAC3 = 0x7A; private static final int TS_PMT_DESC_DTS = 0x7B; private static final int TS_PMT_DESC_DVB_EXT = 0x7F; @@ -578,7 +582,7 @@ public final class TsExtractor implements Extractor { pmtScratch.skipBits(4); // reserved int esInfoLength = pmtScratch.readBits(12); // ES_info_length. EsInfo esInfo = readEsInfo(sectionData, esInfoLength); - if (streamType == 0x06) { + if (streamType == 0x06 || streamType == 0x05) { streamType = esInfo.streamType; } remainingEntriesLength -= esInfoLength + 5; @@ -688,6 +692,8 @@ public final class TsExtractor implements Extractor { dvbSubtitleInfos.add(new DvbSubtitleInfo(dvbLanguage, dvbSubtitlingType, initializationData)); } + } else if (descriptorTag == TS_PMT_DESC_AIT) { + streamType = TS_STREAM_TYPE_AIT; } // Skip unused bytes of current descriptor. data.skipBytes(positionOfNextDescriptor - data.getPosition()); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 55bea3bc4b..3b8f72ff31 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -53,6 +53,11 @@ public final class TsExtractorTest { ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_scte35.ts"); } + @Test + public void testAit() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ait.ts"); + } + @Test public void testStreamWithJunkData() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/testdata/src/test/assets/dvbsi/README.md b/testdata/src/test/assets/dvbsi/README.md new file mode 100644 index 0000000000..b17512aaa9 --- /dev/null +++ b/testdata/src/test/assets/dvbsi/README.md @@ -0,0 +1,15 @@ +# DVB Test Data + +The `.bin` files in this directory are generated from the `.xml` files using +`tstabcomp` from [TSDuck](https://tsduck.io/). + +The XML files are kept to make it clear where the values in the test assertions +are coming from, and to make it easier to change or add data in future. When +adding new files, or making changes to existing ones, you should regenerate the +`.bin` files using the command above before committing. + +To regenerate all the `.bin` files: + +```shell +$ tstabcomp -c testdata/src/test/assets/dvbsi/*.xml +``` diff --git a/testdata/src/test/assets/dvbsi/ait_no_url_base.bin b/testdata/src/test/assets/dvbsi/ait_no_url_base.bin new file mode 100644 index 0000000000..b1215d5d94 Binary files /dev/null and b/testdata/src/test/assets/dvbsi/ait_no_url_base.bin differ diff --git a/testdata/src/test/assets/dvbsi/ait_no_url_base.xml b/testdata/src/test/assets/dvbsi/ait_no_url_base.xml new file mode 100644 index 0000000000..0b54643345 --- /dev/null +++ b/testdata/src/test/assets/dvbsi/ait_no_url_base.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/dvbsi/ait_no_url_path.bin b/testdata/src/test/assets/dvbsi/ait_no_url_path.bin new file mode 100644 index 0000000000..4fedaf6c76 Binary files /dev/null and b/testdata/src/test/assets/dvbsi/ait_no_url_path.bin differ diff --git a/testdata/src/test/assets/dvbsi/ait_no_url_path.xml b/testdata/src/test/assets/dvbsi/ait_no_url_path.xml new file mode 100644 index 0000000000..0bc17fc616 --- /dev/null +++ b/testdata/src/test/assets/dvbsi/ait_no_url_path.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/dvbsi/ait_typical.bin b/testdata/src/test/assets/dvbsi/ait_typical.bin new file mode 100644 index 0000000000..0ab2d3f1fe Binary files /dev/null and b/testdata/src/test/assets/dvbsi/ait_typical.bin differ diff --git a/testdata/src/test/assets/dvbsi/ait_typical.xml b/testdata/src/test/assets/dvbsi/ait_typical.xml new file mode 100644 index 0000000000..1da4178bdd --- /dev/null +++ b/testdata/src/test/assets/dvbsi/ait_typical.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/ts/sample_ait.ts b/testdata/src/test/assets/ts/sample_ait.ts new file mode 100644 index 0000000000..eb3678e00e Binary files /dev/null and b/testdata/src/test/assets/ts/sample_ait.ts differ diff --git a/testdata/src/test/assets/ts/sample_ait.ts.0.dump b/testdata/src/test/assets/ts/sample_ait.ts.0.dump new file mode 100644 index 0000000000..3af64e70cd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ait.ts.0.dump @@ -0,0 +1,146 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 330: + total output bytes = 9928 + sample count = 19 + format 0: + averageBitrate = -1 + peakBitrate = -1 + id = 1031/330 + containerMimeType = null + sampleMimeType = audio/eac3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = fr + drmInitData = - + metadata = null + initializationData: + sample 0: + time = 0 + flags = 1 + data = length 512, hash E47547D4 + sample 1: + time = 32000 + flags = 1 + data = length 512, hash F6A537AC + sample 2: + time = 64000 + flags = 1 + data = length 512, hash 97391682 + sample 3: + time = 96000 + flags = 1 + data = length 512, hash CFD3B665 + sample 4: + time = 128000 + flags = 1 + data = length 512, hash 2E79A3AF + sample 5: + time = 160000 + flags = 1 + data = length 512, hash 2C24E2A3 + sample 6: + time = 192000 + flags = 1 + data = length 512, hash 5BCB9661 + sample 7: + time = 224000 + flags = 1 + data = length 512, hash 943ACBF2 + sample 8: + time = 256000 + flags = 1 + data = length 512, hash B248E943 + sample 9: + time = 288000 + flags = 1 + data = length 512, hash EC2DD86F + sample 10: + time = 320000 + flags = 1 + data = length 512, hash A659332F + sample 11: + time = 352000 + flags = 1 + data = length 512, hash CB641607 + sample 12: + time = 384000 + flags = 1 + data = length 512, hash 157489A0 + sample 13: + time = 416000 + flags = 1 + data = length 512, hash A37CB66E + sample 14: + time = 448000 + flags = 1 + data = length 512, hash 932F07D4 + sample 15: + time = 480000 + flags = 1 + data = length 512, hash 91F50161 + sample 16: + time = 512000 + flags = 1 + data = length 512, hash 7F9D6CCB + sample 17: + time = 544000 + flags = 1 + data = length 512, hash 3955F015 + sample 18: + time = 576000 + flags = 1 + data = length 512, hash A8E5C938 +track 370: + total output bytes = 1413 + sample count = 3 + format 0: + averageBitrate = -1 + peakBitrate = -1 + id = null + containerMimeType = null + sampleMimeType = application/vnd.dvb.ait + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = -43622564033 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + sample 0: + time = 0 + flags = 1 + data = length 471, hash B189052F + sample 1: + time = 192000 + flags = 1 + data = length 471, hash B189052F + sample 2: + time = 384000 + flags = 1 + data = length 471, hash B189052F +tracksEnded = true diff --git a/testdata/src/test/assets/ts/sample_ait.ts.unklen.dump b/testdata/src/test/assets/ts/sample_ait.ts.unklen.dump new file mode 100644 index 0000000000..3af64e70cd --- /dev/null +++ b/testdata/src/test/assets/ts/sample_ait.ts.unklen.dump @@ -0,0 +1,146 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 330: + total output bytes = 9928 + sample count = 19 + format 0: + averageBitrate = -1 + peakBitrate = -1 + id = 1031/330 + containerMimeType = null + sampleMimeType = audio/eac3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = fr + drmInitData = - + metadata = null + initializationData: + sample 0: + time = 0 + flags = 1 + data = length 512, hash E47547D4 + sample 1: + time = 32000 + flags = 1 + data = length 512, hash F6A537AC + sample 2: + time = 64000 + flags = 1 + data = length 512, hash 97391682 + sample 3: + time = 96000 + flags = 1 + data = length 512, hash CFD3B665 + sample 4: + time = 128000 + flags = 1 + data = length 512, hash 2E79A3AF + sample 5: + time = 160000 + flags = 1 + data = length 512, hash 2C24E2A3 + sample 6: + time = 192000 + flags = 1 + data = length 512, hash 5BCB9661 + sample 7: + time = 224000 + flags = 1 + data = length 512, hash 943ACBF2 + sample 8: + time = 256000 + flags = 1 + data = length 512, hash B248E943 + sample 9: + time = 288000 + flags = 1 + data = length 512, hash EC2DD86F + sample 10: + time = 320000 + flags = 1 + data = length 512, hash A659332F + sample 11: + time = 352000 + flags = 1 + data = length 512, hash CB641607 + sample 12: + time = 384000 + flags = 1 + data = length 512, hash 157489A0 + sample 13: + time = 416000 + flags = 1 + data = length 512, hash A37CB66E + sample 14: + time = 448000 + flags = 1 + data = length 512, hash 932F07D4 + sample 15: + time = 480000 + flags = 1 + data = length 512, hash 91F50161 + sample 16: + time = 512000 + flags = 1 + data = length 512, hash 7F9D6CCB + sample 17: + time = 544000 + flags = 1 + data = length 512, hash 3955F015 + sample 18: + time = 576000 + flags = 1 + data = length 512, hash A8E5C938 +track 370: + total output bytes = 1413 + sample count = 3 + format 0: + averageBitrate = -1 + peakBitrate = -1 + id = null + containerMimeType = null + sampleMimeType = application/vnd.dvb.ait + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = -43622564033 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + sample 0: + time = 0 + flags = 1 + data = length 471, hash B189052F + sample 1: + time = 192000 + flags = 1 + data = length 471, hash B189052F + sample 2: + time = 384000 + flags = 1 + data = length 471, hash B189052F +tracksEnded = true