diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java new file mode 100644 index 00000000000..4c493fd8adb --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.metadata.scte35; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import java.nio.ByteBuffer; +import java.util.List; +import junit.framework.TestCase; + +/** + * Test for {@link SpliceInfoDecoder}. + */ +public final class SpliceInfoDecoderTest extends TestCase { + + private SpliceInfoDecoder decoder; + private MetadataInputBuffer inputBuffer; + + @Override + public void setUp() { + decoder = new SpliceInfoDecoder(); + inputBuffer = new MetadataInputBuffer(); + } + + public void testWrappedAroundTimeSignalCommand() throws MetadataDecoderException { + byte[] rawTimeSignalSection = new byte[] { + 0, // table_id. + (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). + 0x14, // section_length(8). + 0x00, // protocol_version. + 0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1). + 0x00, 0x00, 0x00, 0x00, // pts_adjustment(32). + 0x00, // cw_index. + 0x00, // tier(8). + 0x00, // tier(4), splice_command_length(4). + 0x05, // splice_command_length(8). + 0x06, // splice_command_type = time_signal. + // Start of splice_time(). + (byte) 0x80, // time_specified_flag, reserved, pts_time(1). + 0x52, 0x03, 0x02, (byte) 0x8f, // pts_time(32). PTS for a second after playback position. + 0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction). + + // The playback position is 57:15:58.43 approximately. + // With this offset, the playback position pts before wrapping is 0x451ebf851. + Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L); + assertEquals(1, metadata.length()); + assertEquals(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs), + ((TimeSignalCommand) metadata.get(0)).playbackPositionUs); + } + + public void test2SpliceInsertCommands() throws MetadataDecoderException { + byte[] rawSpliceInsertCommand1 = new byte[] { + 0, // table_id. + (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). + 0x19, // section_length(8). + 0x00, // protocol_version. + 0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1). + 0x00, 0x00, 0x00, 0x00, // pts_adjustment(32). + 0x00, // cw_index. + 0x00, // tier(8). + 0x00, // tier(4), splice_command_length(4). + 0x0e, // splice_command_length(8). + 0x05, // splice_command_type = splice_insert. + // Start of splice_insert(). + 0x00, 0x00, 0x00, 0x42, // splice_event_id. + 0x00, // splice_event_cancel_indicator, reserved. + 0x40, // out_of_network_indicator, program_splice_flag, duration_flag, + // splice_immediate_flag, reserved. + // start of splice_time(). + (byte) 0x80, // time_specified_flag, reserved, pts_time(1). + 0x00, 0x00, 0x00, 0x00, // PTS for playback position 3s. + 0x00, 0x10, // unique_program_id. + 0x01, // avail_num. + 0x02, // avails_expected. + 0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction). + + Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000); + assertEquals(1, metadata.length()); + SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0); + assertEquals(66, command.spliceEventId); + assertFalse(command.spliceEventCancelIndicator); + assertFalse(command.outOfNetworkIndicator); + assertTrue(command.programSpliceFlag); + assertFalse(command.spliceImmediateFlag); + assertEquals(3000000, command.programSplicePlaybackPositionUs); + assertEquals(C.TIME_UNSET, command.breakDuration); + assertEquals(16, command.uniqueProgramId); + assertEquals(1, command.availNum); + assertEquals(2, command.availsExpected); + + byte[] rawSpliceInsertCommand2 = new byte[] { + 0, // table_id. + (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). + 0x22, // section_length(8). + 0x00, // protocol_version. + 0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1). + 0x00, 0x00, 0x00, 0x00, // pts_adjustment(32). + 0x00, // cw_index. + 0x00, // tier(8). + 0x00, // tier(4), splice_command_length(4). + 0x13, // splice_command_length(8). + 0x05, // splice_command_type = splice_insert. + // Start of splice_insert(). + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // splice_event_id. + 0x00, // splice_event_cancel_indicator, reserved. + 0x00, // out_of_network_indicator, program_splice_flag, duration_flag, + // splice_immediate_flag, reserved. + 0x02, // component_count. + 0x10, // component_tag. + // start of splice_time(). + (byte) 0x81, // time_specified_flag, reserved, pts_time(1). + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // PTS for playback position 10s. + // start of splice_time(). + 0x11, // component_tag. + 0x00, // time_specified_flag, reserved. + 0x00, 0x20, // unique_program_id. + 0x01, // avail_num. + 0x02, // avails_expected. + 0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction). + + // By changing the subsample offset we force adjuster reconstruction. + long subsampleOffset = 1000011; + metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset); + assertEquals(1, metadata.length()); + command = (SpliceInsertCommand) metadata.get(0); + assertEquals(0xffffffffL, command.spliceEventId); + assertFalse(command.spliceEventCancelIndicator); + assertFalse(command.outOfNetworkIndicator); + assertFalse(command.programSpliceFlag); + assertFalse(command.spliceImmediateFlag); + assertEquals(C.TIME_UNSET, command.programSplicePlaybackPositionUs); + assertEquals(C.TIME_UNSET, command.breakDuration); + List componentSplices = command.componentSpliceList; + assertEquals(2, componentSplices.size()); + assertEquals(16, componentSplices.get(0).componentTag); + assertEquals(1000000, componentSplices.get(0).componentSplicePlaybackPositionUs); + assertEquals(17, componentSplices.get(1).componentTag); + assertEquals(C.TIME_UNSET, componentSplices.get(1).componentSplicePts); + assertEquals(32, command.uniqueProgramId); + assertEquals(1, command.availNum); + assertEquals(2, command.availsExpected); + } + + private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) + throws MetadataDecoderException{ + inputBuffer.clear(); + inputBuffer.data = ByteBuffer.allocate(data.length).put(data); + inputBuffer.timeUs = timeUs; + inputBuffer.subsampleOffsetUs = subsampleOffset; + return decoder.decode(inputBuffer); + } + + private static long removePtsConversionPrecisionError(long timeUs, long offsetUs) { + return TimestampAdjuster.ptsToUs(TimestampAdjuster.usToPts(timeUs - offsetUs)) + offsetUs; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java index a4da5d8e66f..1fc0e1813e0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java @@ -93,6 +93,9 @@ public void reset() { * @return The adjusted timestamp in microseconds. */ public long adjustTsTimestamp(long pts) { + if (pts == C.TIME_UNSET) { + return C.TIME_UNSET; + } if (lastSampleTimestamp != C.TIME_UNSET) { // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), // and we need to snap to the one closest to lastSampleTimestamp. @@ -113,6 +116,9 @@ public long adjustTsTimestamp(long pts) { * @return The adjusted timestamp in microseconds. */ public long adjustSampleTimestamp(long timeUs) { + if (timeUs == C.TIME_UNSET) { + return C.TIME_UNSET; + } // Record the adjusted PTS to adjust for wraparound next time. if (lastSampleTimestamp != C.TIME_UNSET) { lastSampleTimestamp = timeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java index f75a1b46a46..beb4cb9b88e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java @@ -26,7 +26,6 @@ public final class PrivateCommand extends SpliceCommand { public final long ptsAdjustment; public final long identifier; - public final byte[] commandBytes; private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) { diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 6e373a45e7a..dc85788a8b1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.scte35; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; @@ -37,6 +38,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder { private final ParsableByteArray sectionData; private final ParsableBitArray sectionHeader; + private TimestampAdjuster timestampAdjuster; + public SpliceInfoDecoder() { sectionData = new ParsableByteArray(); sectionHeader = new ParsableBitArray(); @@ -44,6 +47,13 @@ public SpliceInfoDecoder() { @Override public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException { + // Internal timestamps adjustment. + if (timestampAdjuster == null + || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { + timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs); + timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); + } + ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); @@ -69,10 +79,11 @@ public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderEx command = SpliceScheduleCommand.parseFromSection(sectionData); break; case TYPE_SPLICE_INSERT: - command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment); + command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment, + timestampAdjuster); break; case TYPE_TIME_SIGNAL: - command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment); + command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment, timestampAdjuster); break; case TYPE_PRIVATE_COMMAND: command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java index 1e025aeb352..07a84bf5d10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java @@ -18,6 +18,7 @@ import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Collections; @@ -34,6 +35,7 @@ public final class SpliceInsertCommand extends SpliceCommand { public final boolean programSpliceFlag; public final boolean spliceImmediateFlag; public final long programSplicePts; + public final long programSplicePlaybackPositionUs; public final List componentSpliceList; public final boolean autoReturn; public final long breakDuration; @@ -43,14 +45,16 @@ public final class SpliceInsertCommand extends SpliceCommand { private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator, boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag, - long programSplicePts, List componentSpliceList, boolean autoReturn, - long breakDuration, int uniqueProgramId, int availNum, int availsExpected) { + long programSplicePts, long programSplicePlaybackPositionUs, + List componentSpliceList, boolean autoReturn, long breakDuration, + int uniqueProgramId, int availNum, int availsExpected) { this.spliceEventId = spliceEventId; this.spliceEventCancelIndicator = spliceEventCancelIndicator; this.outOfNetworkIndicator = outOfNetworkIndicator; this.programSpliceFlag = programSpliceFlag; this.spliceImmediateFlag = spliceImmediateFlag; this.programSplicePts = programSplicePts; + this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs; this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.autoReturn = autoReturn; this.breakDuration = breakDuration; @@ -66,6 +70,7 @@ private SpliceInsertCommand(Parcel in) { programSpliceFlag = in.readByte() == 1; spliceImmediateFlag = in.readByte() == 1; programSplicePts = in.readLong(); + programSplicePlaybackPositionUs = in.readLong(); int componentSpliceListSize = in.readInt(); List componentSpliceList = new ArrayList<>(componentSpliceListSize); for (int i = 0; i < componentSpliceListSize; i++) { @@ -80,7 +85,7 @@ private SpliceInsertCommand(Parcel in) { } /* package */ static SpliceInsertCommand parseFromSection(ParsableByteArray sectionData, - long ptsAdjustment) { + long ptsAdjustment, TimestampAdjuster timestampAdjuster) { long spliceEventId = sectionData.readUnsignedInt(); // splice_event_cancel_indicator(1), reserved(7). boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0; @@ -88,7 +93,7 @@ private SpliceInsertCommand(Parcel in) { boolean programSpliceFlag = false; boolean spliceImmediateFlag = false; long programSplicePts = C.TIME_UNSET; - ArrayList componentSplices = new ArrayList<>(); + List componentSplices = Collections.emptyList(); int uniqueProgramId = 0; int availNum = 0; int availsExpected = 0; @@ -112,7 +117,8 @@ private SpliceInsertCommand(Parcel in) { if (!spliceImmediateFlag) { componentSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment); } - componentSplices.add(new ComponentSplice(componentTag, componentSplicePts)); + componentSplices.add(new ComponentSplice(componentTag, componentSplicePts, + timestampAdjuster.adjustTsTimestamp(componentSplicePts))); } } if (durationFlag) { @@ -125,7 +131,8 @@ private SpliceInsertCommand(Parcel in) { availsExpected = sectionData.readUnsignedByte(); } return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, - programSpliceFlag, spliceImmediateFlag, programSplicePts, componentSplices, autoReturn, + programSpliceFlag, spliceImmediateFlag, programSplicePts, + timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn, duration, uniqueProgramId, availNum, availsExpected); } @@ -136,19 +143,23 @@ public static final class ComponentSplice { public final int componentTag; public final long componentSplicePts; + public final long componentSplicePlaybackPositionUs; - private ComponentSplice(int componentTag, long componentSplicePts) { + private ComponentSplice(int componentTag, long componentSplicePts, + long componentSplicePlaybackPositionUs) { this.componentTag = componentTag; this.componentSplicePts = componentSplicePts; + this.componentSplicePlaybackPositionUs = componentSplicePlaybackPositionUs; } public void writeToParcel(Parcel dest) { dest.writeInt(componentTag); dest.writeLong(componentSplicePts); + dest.writeLong(componentSplicePlaybackPositionUs); } public static ComponentSplice createFromParcel(Parcel in) { - return new ComponentSplice(in.readInt(), in.readLong()); + return new ComponentSplice(in.readInt(), in.readLong(), in.readLong()); } } @@ -163,6 +174,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeByte((byte) (programSpliceFlag ? 1 : 0)); dest.writeByte((byte) (spliceImmediateFlag ? 1 : 0)); dest.writeLong(programSplicePts); + dest.writeLong(programSplicePlaybackPositionUs); int componentSpliceListSize = componentSpliceList.size(); dest.writeInt(componentSpliceListSize); for (int i = 0; i < componentSpliceListSize; i++) { diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java index c31f4dedc87..e21eafbeeb0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java @@ -17,6 +17,7 @@ import android.os.Parcel; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.util.ParsableByteArray; /** @@ -25,14 +26,18 @@ public final class TimeSignalCommand extends SpliceCommand { public final long ptsTime; + public final long playbackPositionUs; - private TimeSignalCommand(long ptsTime) { + private TimeSignalCommand(long ptsTime, long playbackPositionUs) { this.ptsTime = ptsTime; + this.playbackPositionUs = playbackPositionUs; } /* package */ static TimeSignalCommand parseFromSection(ParsableByteArray sectionData, - long ptsAdjustment) { - return new TimeSignalCommand(parseSpliceTime(sectionData, ptsAdjustment)); + long ptsAdjustment, TimestampAdjuster timestampAdjuster) { + long ptsTime = parseSpliceTime(sectionData, ptsAdjustment); + long playbackPositionUs = timestampAdjuster.adjustTsTimestamp(ptsTime); + return new TimeSignalCommand(ptsTime, playbackPositionUs); } /** @@ -61,6 +66,7 @@ private TimeSignalCommand(long ptsTime) { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(ptsTime); + dest.writeLong(playbackPositionUs); } public static final Creator CREATOR = @@ -68,7 +74,7 @@ public void writeToParcel(Parcel dest, int flags) { @Override public TimeSignalCommand createFromParcel(Parcel in) { - return new TimeSignalCommand(in.readLong()); + return new TimeSignalCommand(in.readLong(), in.readLong()); } @Override