diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1ebd1f35cb..f664ee97bb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -44,6 +44,8 @@ Previously we incorrectly parsed any number of decimal places but always assumed the value was in milliseconds, leading to incorrect timestamps ([#1997](https://github.com/androidx/media/issues/1997)). + * Add support for VobSub subtitles + ([#8260](https://github.com/google/ExoPlayer/issues/8260)). * Metadata: * Image: * DataSource: diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 1b9e00935f..2691406d13 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -212,6 +212,8 @@ public final class Util { private static final String ISM_HLS_FORMAT_EXTENSION = "format=m3u8-aapl"; private static final String ISM_DASH_FORMAT_EXTENSION = "format=mpd-time-csf"; + private static final int ZLIB_INFLATE_HEADER = 0x78; + // Replacement map of ISO language codes used for normalization. @Nullable private static HashMap languageTagReplacementMap; @@ -3102,6 +3104,26 @@ public final class Util { } } + /** + * Uncompresses the data in {@code input} if it starts with the zlib marker {@code 0x78}. + * + * @param input Wraps the compressed input data. + * @param output Wraps an output buffer to be used to store the uncompressed data. If {@code + * output.data} isn't big enough to hold the uncompressed data, a new array is created. If + * {@code true} is returned then the output's position will be set to 0 and its limit will be + * set to the length of the uncompressed data. + * @param inflater If not null, used to uncompress the input. Otherwise a new {@link Inflater} is + * created. + * @return Whether the input is uncompressed successfully. + */ + @UnstableApi + public static boolean maybeInflate( + ParsableByteArray input, ParsableByteArray output, @Nullable Inflater inflater) { + return input.bytesLeft() > 0 + && input.peekUnsignedByte() == ZLIB_INFLATE_HEADER + && inflate(input, output, inflater); + } + /** * Returns whether the app is running on a TV device. * diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MkvPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MkvPlaybackTest.java index 0f1498dfa8..7bd42ba864 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MkvPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/MkvPlaybackTest.java @@ -15,6 +15,8 @@ */ package androidx.media3.exoplayer.e2etest; +import static org.robolectric.annotation.GraphicsMode.Mode.NATIVE; + import android.content.Context; import android.graphics.SurfaceTexture; import android.view.Surface; @@ -35,9 +37,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; +import org.robolectric.annotation.GraphicsMode; /** End-to-end tests using MKV samples. */ @RunWith(ParameterizedRobolectricTestRunner.class) +@GraphicsMode(NATIVE) public final class MkvPlaybackTest { @Parameters(name = "{0}") public static ImmutableList mediaSamples() { @@ -51,7 +55,8 @@ public final class MkvPlaybackTest { "sample_with_null_terminated_srt.mkv", "sample_with_overlapping_srt.mkv", "sample_with_vtt_subtitles.mkv", - "sample_with_null_terminated_vtt_subtitles.mkv"); + "sample_with_null_terminated_vtt_subtitles.mkv", + "sample_with_vobsub.mkv"); } @ParameterizedRobolectricTestRunner.Parameter public String inputFile; diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/DefaultSubtitleParserFactory.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/DefaultSubtitleParserFactory.java index b2a271956b..b2413e3be5 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/DefaultSubtitleParserFactory.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/DefaultSubtitleParserFactory.java @@ -26,6 +26,7 @@ import androidx.media3.extractor.text.ssa.SsaParser; import androidx.media3.extractor.text.subrip.SubripParser; import androidx.media3.extractor.text.ttml.TtmlParser; import androidx.media3.extractor.text.tx3g.Tx3gParser; +import androidx.media3.extractor.text.vobsub.VobsubParser; import androidx.media3.extractor.text.webvtt.Mp4WebvttParser; import androidx.media3.extractor.text.webvtt.WebvttParser; import java.util.Objects; @@ -58,6 +59,7 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor || Objects.equals(mimeType, MimeTypes.APPLICATION_SUBRIP) || Objects.equals(mimeType, MimeTypes.APPLICATION_TX3G) || Objects.equals(mimeType, MimeTypes.APPLICATION_PGS) + || Objects.equals(mimeType, MimeTypes.APPLICATION_VOBSUB) || Objects.equals(mimeType, MimeTypes.APPLICATION_DVBSUBS) || Objects.equals(mimeType, MimeTypes.APPLICATION_TTML); } @@ -79,6 +81,8 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor return Tx3gParser.CUE_REPLACEMENT_BEHAVIOR; case MimeTypes.APPLICATION_PGS: return PgsParser.CUE_REPLACEMENT_BEHAVIOR; + case MimeTypes.APPLICATION_VOBSUB: + return VobsubParser.CUE_REPLACEMENT_BEHAVIOR; case MimeTypes.APPLICATION_DVBSUBS: return DvbParser.CUE_REPLACEMENT_BEHAVIOR; case MimeTypes.APPLICATION_TTML: @@ -107,6 +111,8 @@ public final class DefaultSubtitleParserFactory implements SubtitleParser.Factor return new Tx3gParser(format.initializationData); case MimeTypes.APPLICATION_PGS: return new PgsParser(); + case MimeTypes.APPLICATION_VOBSUB: + return new VobsubParser(format.initializationData); case MimeTypes.APPLICATION_DVBSUBS: return new DvbParser(format.initializationData); case MimeTypes.APPLICATION_TTML: diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/pgs/PgsParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/pgs/PgsParser.java index 0e7ff2d1a7..0c196c3dae 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/pgs/PgsParser.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/pgs/PgsParser.java @@ -49,8 +49,6 @@ public final class PgsParser implements SubtitleParser { private static final int SECTION_TYPE_IDENTIFIER = 0x16; private static final int SECTION_TYPE_END = 0x80; - private static final byte INFLATE_HEADER = 0x78; - private final ParsableByteArray buffer; private final ParsableByteArray inflatedBuffer; private final CueBuilder cueBuilder; @@ -76,7 +74,12 @@ public final class PgsParser implements SubtitleParser { Consumer output) { buffer.reset(data, /* limit= */ offset + length); buffer.setPosition(offset); - maybeInflateData(buffer); + if (inflater == null) { + inflater = new Inflater(); + } + if (Util.maybeInflate(buffer, inflatedBuffer, inflater)) { + buffer.reset(inflatedBuffer.getData(), inflatedBuffer.limit()); + } cueBuilder.reset(); ArrayList cues = new ArrayList<>(); while (buffer.bytesLeft() >= 3) { @@ -89,17 +92,6 @@ public final class PgsParser implements SubtitleParser { new CuesWithTiming(cues, /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET)); } - private void maybeInflateData(ParsableByteArray buffer) { - if (buffer.bytesLeft() > 0 && buffer.peekUnsignedByte() == INFLATE_HEADER) { - if (inflater == null) { - inflater = new Inflater(); - } - if (Util.inflate(buffer, inflatedBuffer, inflater)) { - buffer.reset(inflatedBuffer.getData(), inflatedBuffer.limit()); - } // else assume data is not compressed. - } - } - @Nullable private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/vobsub/VobsubParser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/vobsub/VobsubParser.java new file mode 100644 index 0000000000..b8cadd2a72 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/vobsub/VobsubParser.java @@ -0,0 +1,391 @@ +/* + * Copyright 2025 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 + * + * https://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 androidx.media3.extractor.text.vobsub; + +import static java.lang.Math.min; +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.Format.CueReplacementBehavior; +import androidx.media3.common.text.Cue; +import androidx.media3.common.util.Consumer; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableBitArray; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import androidx.media3.extractor.text.CuesWithTiming; +import androidx.media3.extractor.text.SubtitleParser; +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.Inflater; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** A {@link SubtitleParser} for Vobsub subtitles. */ +@UnstableApi +public final class VobsubParser implements SubtitleParser { + + /** + * The {@link CueReplacementBehavior} for consecutive {@link CuesWithTiming} emitted by this + * implementation. + */ + public static final @CueReplacementBehavior int CUE_REPLACEMENT_BEHAVIOR = + Format.CUE_REPLACEMENT_BEHAVIOR_REPLACE; + + private static final String TAG = "VobsubParser"; + private static final int DEFAULT_DURATION_US = 5_000_000; + + private final ParsableByteArray scratch; + private final ParsableByteArray inflatedScratch; + private final CueBuilder cueBuilder; + @Nullable private Inflater inflater; + + public VobsubParser(List initializationData) { + scratch = new ParsableByteArray(); + inflatedScratch = new ParsableByteArray(); + cueBuilder = new CueBuilder(); + cueBuilder.parseIdx(new String(initializationData.get(0), UTF_8)); + } + + @Override + public @CueReplacementBehavior int getCueReplacementBehavior() { + return CUE_REPLACEMENT_BEHAVIOR; + } + + @Override + public void parse( + byte[] data, + int offset, + int length, + OutputOptions outputOptions, + Consumer output) { + scratch.reset(data, offset + length); + scratch.setPosition(offset); + @Nullable Cue cue = parse(); + output.accept( + new CuesWithTiming( + cue != null ? ImmutableList.of(cue) : ImmutableList.of(), + /* startTimeUs= */ C.TIME_UNSET, + /* durationUs= */ DEFAULT_DURATION_US)); + } + + @Nullable + private Cue parse() { + if (inflater == null) { + inflater = new Inflater(); + } + if (Util.maybeInflate(scratch, inflatedScratch, inflater)) { + scratch.reset(inflatedScratch.getData(), inflatedScratch.limit()); + } + cueBuilder.reset(); + int bytesLeft = scratch.bytesLeft(); + if (bytesLeft < 2 || scratch.readUnsignedShort() != bytesLeft) { + return null; + } + cueBuilder.parseSpu(scratch); + return cueBuilder.build(scratch); + } + + private static final class CueBuilder { + + private static final int CMD_COLORS = 3; + private static final int CMD_ALPHA = 4; + private static final int CMD_AREA = 5; + private static final int CMD_OFFSETS = 6; + private static final int CMD_END = 255; + + private final int[] colors; + + private boolean hasPlane; + private boolean hasColors; + private int @MonotonicNonNull [] palette; + private int planeWidth; + private int planeHeight; + @Nullable private Rect boundingBox; + private int dataOffset0; + private int dataOffset1; + + public CueBuilder() { + colors = new int[4]; + dataOffset0 = C.INDEX_UNSET; + dataOffset1 = C.INDEX_UNSET; + } + + public void parseIdx(String idx) { + for (String line : Util.split(idx.trim(), "\\r?\\n")) { + if (line.startsWith("palette: ")) { + String[] values = Util.split(line.substring("palette: ".length()), ","); + palette = new int[values.length]; + + for (int i = 0; i < values.length; i++) { + palette[i] = parseColor(values[i].trim()); + } + } else if (line.startsWith("size: ")) { + // We need this line to calculate the relative positions and size required when building + // the Cue below. + String[] sizes = Util.split(line.substring("size: ".length()).trim(), "x"); + + if (sizes.length == 2) { + try { + planeWidth = Integer.parseInt(sizes[0]); + planeHeight = Integer.parseInt(sizes[1]); + hasPlane = true; + } catch (RuntimeException e) { + Log.w(TAG, "Parsing IDX failed", e); + } + } + } + } + } + + private static int parseColor(String value) { + try { + return Integer.parseInt(value, 16); + } catch (RuntimeException e) { + return 0; + } + } + + public void parseSpu(ParsableByteArray buffer) { + if (palette == null || !hasPlane) { + // Give up if we don't have the color palette or the video size. + return; + } + int[] palette = this.palette; + buffer.skipBytes(buffer.readUnsignedShort() - 2); + int end = buffer.readUnsignedShort(); + parseControl(palette, buffer, end); + } + + private void parseControl(int[] palette, ParsableByteArray buffer, int end) { + while (buffer.getPosition() < end && buffer.bytesLeft() > 0) { + switch (buffer.readUnsignedByte()) { + case CMD_COLORS: + if (!parseControlColors(palette, buffer)) { + return; + } + break; + case CMD_ALPHA: + if (!parseControlAlpha(buffer)) { + return; + } + break; + case CMD_AREA: + if (!parseControlArea(buffer)) { + return; + } + break; + case CMD_OFFSETS: + if (!parseControlOffsets(buffer)) { + return; + } + break; + case CMD_END: + default: + return; + } + } + } + + private boolean parseControlColors(int[] palette, ParsableByteArray buffer) { + if (buffer.bytesLeft() < 2) { + return false; + } + + int byte0 = buffer.readUnsignedByte(); + int byte1 = buffer.readUnsignedByte(); + + colors[3] = getColor(palette, byte0 >> 4); + colors[2] = getColor(palette, byte0 & 0xf); + colors[1] = getColor(palette, byte1 >> 4); + colors[0] = getColor(palette, byte1 & 0xf); + hasColors = true; + + return true; + } + + private static int getColor(int[] palette, int index) { + return index >= 0 && index < palette.length ? palette[index] : palette[0]; + } + + private boolean parseControlAlpha(ParsableByteArray buffer) { + + if (buffer.bytesLeft() < 2 || !hasColors) { + return false; + } + + int byte0 = buffer.readUnsignedByte(); + int byte1 = buffer.readUnsignedByte(); + + colors[3] = setAlpha(colors[3], (byte0 >> 4)); + colors[2] = setAlpha(colors[2], (byte0 & 0xf)); + colors[1] = setAlpha(colors[1], (byte1 >> 4)); + colors[0] = setAlpha(colors[0], (byte1 & 0xf)); + + return true; + } + + private static int setAlpha(int color, int alpha) { + return ((color & 0x00ffffff) | ((alpha * 17) << 24)); + } + + private boolean parseControlArea(ParsableByteArray buffer) { + if (buffer.bytesLeft() < 6) { + return false; + } + + int byte0 = buffer.readUnsignedByte(); + int byte1 = buffer.readUnsignedByte(); + int byte2 = buffer.readUnsignedByte(); + + int left = (byte0 << 4) | (byte1 >> 4); + int right = ((byte1 & 0xf) << 8) | byte2; + + int byte3 = buffer.readUnsignedByte(); + int byte4 = buffer.readUnsignedByte(); + int byte5 = buffer.readUnsignedByte(); + + int top = (byte3 << 4) | (byte4 >> 4); + int bottom = ((byte4 & 0xf) << 8) | byte5; + + boundingBox = new Rect(left, top, right + 1, bottom + 1); + + return true; + } + + private boolean parseControlOffsets(ParsableByteArray buffer) { + if (buffer.bytesLeft() < 4) { + return false; + } + + dataOffset0 = buffer.readUnsignedShort(); + dataOffset1 = buffer.readUnsignedShort(); + + return true; + } + + @Nullable + public Cue build(ParsableByteArray buffer) { + if (palette == null + || !hasPlane + || !hasColors + || boundingBox == null + || dataOffset0 == C.INDEX_UNSET + || dataOffset1 == C.INDEX_UNSET + || boundingBox.width() < 2 + || boundingBox.height() < 2) { + return null; + } + Rect boundingBox = this.boundingBox; + int[] bitmapData = new int[boundingBox.width() * boundingBox.height()]; + ParsableBitArray bitBuffer = new ParsableBitArray(); + + buffer.setPosition(dataOffset0); + bitBuffer.reset(buffer); + parseRleData(bitBuffer, /* evenInterlace= */ true, boundingBox, bitmapData); + buffer.setPosition(dataOffset1); + bitBuffer.reset(buffer); + parseRleData(bitBuffer, /* evenInterlace= */ false, boundingBox, bitmapData); + + Bitmap bitmap = + Bitmap.createBitmap( + bitmapData, boundingBox.width(), boundingBox.height(), Bitmap.Config.ARGB_8888); + + return new Cue.Builder() + .setBitmap(bitmap) + .setPosition((float) boundingBox.left / planeWidth) + .setPositionAnchor(Cue.ANCHOR_TYPE_START) + .setLine((float) boundingBox.top / planeHeight, Cue.LINE_TYPE_FRACTION) + .setLineAnchor(Cue.ANCHOR_TYPE_START) + .setSize((float) boundingBox.width() / planeWidth) + .setBitmapHeight((float) boundingBox.height() / planeHeight) + .build(); + } + + /** + * Parse run-length encoded data into the {@code bitmapData} array. The subtitle bitmap is + * encoded in two blocks of interlaced lines, {@code y} gives the index of the starting line (0 + * or 1). + * + * @param bitBuffer The RLE encoded data. + * @param evenInterlace Whether to decode the even or odd interlaced lines. + * @param bitmapData Output array. + */ + private void parseRleData( + ParsableBitArray bitBuffer, boolean evenInterlace, Rect boundingBox, int[] bitmapData) { + int width = boundingBox.width(); + int height = boundingBox.height(); + int x = 0; + int y = evenInterlace ? 0 : 1; + int outIndex = y * width; + Run run = new Run(); + + while (true) { + parseRun(bitBuffer, width, run); + + int length = min(run.length, width - x); + if (length > 0) { + Arrays.fill(bitmapData, outIndex, outIndex + length, colors[run.colorIndex]); + outIndex += length; + x += length; + } + if (x >= width) { + y += 2; + if (y >= height) { + break; + } + x = 0; + outIndex = y * width; + bitBuffer.byteAlign(); + } + } + } + + private static void parseRun(ParsableBitArray bitBuffer, int width, Run output) { + int value = 0; + int test = 1; + + while (value < test && test <= 0x40) { + if (bitBuffer.bitsLeft() < 4) { + output.colorIndex = C.INDEX_UNSET; + output.length = 0; + return; + } + value = (value << 4) | bitBuffer.readBits(4); + test <<= 2; + } + output.colorIndex = value & 3; + output.length = value < 4 ? width : (value >> 2); + } + + public void reset() { + hasColors = false; + boundingBox = null; + dataOffset0 = C.INDEX_UNSET; + dataOffset1 = C.INDEX_UNSET; + } + + private static final class Run { + public int colorIndex; + public int length; + } + } +} diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/vobsub/package-info.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/vobsub/package-info.java new file mode 100644 index 0000000000..4bfb52af18 --- /dev/null +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/vobsub/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2025 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 androidx.media3.extractor.text.vobsub; + +import androidx.media3.common.util.NonNullApi; diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/DefaultSubtitleParserFactoryTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/DefaultSubtitleParserFactoryTest.java index 471214afac..b8bef9fc7f 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/DefaultSubtitleParserFactoryTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/DefaultSubtitleParserFactoryTest.java @@ -50,6 +50,9 @@ public class DefaultSubtitleParserFactoryTest { if (fieldValue.equals(MimeTypes.APPLICATION_DVBSUBS)) { formatBuilder.setInitializationData(ImmutableList.of(new byte[] {1, 2, 3, 4})); } + if (fieldValue.equals(MimeTypes.APPLICATION_VOBSUB)) { + formatBuilder.setInitializationData(ImmutableList.of(new byte[] {1, 2, 3, 4})); + } Format format = formatBuilder.build(); if (factory.supportsFormat(format)) { try { diff --git a/libraries/test_data/src/test/assets/media/mkv/sample_with_vobsub.mkv b/libraries/test_data/src/test/assets/media/mkv/sample_with_vobsub.mkv new file mode 100644 index 0000000000..a6722d08d1 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mkv/sample_with_vobsub.mkv differ diff --git a/libraries/test_data/src/test/assets/playbackdumps/mkv/sample_with_vobsub.mkv.dump b/libraries/test_data/src/test/assets/playbackdumps/mkv/sample_with_vobsub.mkv.dump new file mode 100644 index 0000000000..2334374aa6 --- /dev/null +++ b/libraries/test_data/src/test/assets/playbackdumps/mkv/sample_with_vobsub.mkv.dump @@ -0,0 +1,539 @@ +MediaCodecAdapter (exotest.audio.ac3): + inputBuffers: + count = 30 + input buffer #0: + timeUs = 1000000000000 + contents = length 416, hash 211F2286 + input buffer #1: + timeUs = 1000000034000 + contents = length 418, hash 77425A86 + input buffer #2: + timeUs = 1000000069000 + contents = length 418, hash A0FE5CA1 + input buffer #3: + timeUs = 1000000104000 + contents = length 418, hash 2309B066 + input buffer #4: + timeUs = 1000000139000 + contents = length 418, hash 928A653B + input buffer #5: + timeUs = 1000000173000 + contents = length 418, hash 3422F0CB + input buffer #6: + timeUs = 1000000208000 + contents = length 418, hash EFF43D5B + input buffer #7: + timeUs = 1000000243000 + contents = length 418, hash FC8093C7 + input buffer #8: + timeUs = 1000000279000 + contents = length 418, hash CCC08A16 + input buffer #9: + timeUs = 1000000313000 + contents = length 418, hash 2A6EE863 + input buffer #10: + timeUs = 1000000348000 + contents = length 418, hash D69A9251 + input buffer #11: + timeUs = 1000000383000 + contents = length 418, hash BCFB758D + input buffer #12: + timeUs = 1000000418000 + contents = length 418, hash 11B66799 + input buffer #13: + timeUs = 1000000452000 + contents = length 418, hash C824D392 + input buffer #14: + timeUs = 1000000487000 + contents = length 418, hash C167D872 + input buffer #15: + timeUs = 1000000522000 + contents = length 418, hash 4221C855 + input buffer #16: + timeUs = 1000000557000 + contents = length 418, hash 4D4FF934 + input buffer #17: + timeUs = 1000000591000 + contents = length 418, hash 984AA025 + input buffer #18: + timeUs = 1000000626000 + contents = length 418, hash BB788B46 + input buffer #19: + timeUs = 1000000661000 + contents = length 418, hash 9EFBFD97 + input buffer #20: + timeUs = 1000000696000 + contents = length 418, hash DF1A460C + input buffer #21: + timeUs = 1000000730000 + contents = length 418, hash 2BDB56A + input buffer #22: + timeUs = 1000000765000 + contents = length 418, hash CA230060 + input buffer #23: + timeUs = 1000000800000 + contents = length 418, hash D2F19F41 + input buffer #24: + timeUs = 1000000836000 + contents = length 418, hash AF392D79 + input buffer #25: + timeUs = 1000000870000 + contents = length 418, hash C5D7F2A3 + input buffer #26: + timeUs = 1000000905000 + contents = length 418, hash 733A35AE + input buffer #27: + timeUs = 1000000940000 + contents = length 418, hash DE46E5D3 + input buffer #28: + timeUs = 1000000975000 + contents = length 418, hash 56AB8D37 + input buffer #29: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + outputBuffers: + count = 29 + output buffer #0: + timeUs = 1000000000000 + size = 0 + rendered = false + output buffer #1: + timeUs = 1000000034000 + size = 0 + rendered = false + output buffer #2: + timeUs = 1000000069000 + size = 0 + rendered = false + output buffer #3: + timeUs = 1000000104000 + size = 0 + rendered = false + output buffer #4: + timeUs = 1000000139000 + size = 0 + rendered = false + output buffer #5: + timeUs = 1000000173000 + size = 0 + rendered = false + output buffer #6: + timeUs = 1000000208000 + size = 0 + rendered = false + output buffer #7: + timeUs = 1000000243000 + size = 0 + rendered = false + output buffer #8: + timeUs = 1000000279000 + size = 0 + rendered = false + output buffer #9: + timeUs = 1000000313000 + size = 0 + rendered = false + output buffer #10: + timeUs = 1000000348000 + size = 0 + rendered = false + output buffer #11: + timeUs = 1000000383000 + size = 0 + rendered = false + output buffer #12: + timeUs = 1000000418000 + size = 0 + rendered = false + output buffer #13: + timeUs = 1000000452000 + size = 0 + rendered = false + output buffer #14: + timeUs = 1000000487000 + size = 0 + rendered = false + output buffer #15: + timeUs = 1000000522000 + size = 0 + rendered = false + output buffer #16: + timeUs = 1000000557000 + size = 0 + rendered = false + output buffer #17: + timeUs = 1000000591000 + size = 0 + rendered = false + output buffer #18: + timeUs = 1000000626000 + size = 0 + rendered = false + output buffer #19: + timeUs = 1000000661000 + size = 0 + rendered = false + output buffer #20: + timeUs = 1000000696000 + size = 0 + rendered = false + output buffer #21: + timeUs = 1000000730000 + size = 0 + rendered = false + output buffer #22: + timeUs = 1000000765000 + size = 0 + rendered = false + output buffer #23: + timeUs = 1000000800000 + size = 0 + rendered = false + output buffer #24: + timeUs = 1000000836000 + size = 0 + rendered = false + output buffer #25: + timeUs = 1000000870000 + size = 0 + rendered = false + output buffer #26: + timeUs = 1000000905000 + size = 0 + rendered = false + output buffer #27: + timeUs = 1000000940000 + size = 0 + rendered = false + output buffer #28: + timeUs = 1000000975000 + size = 0 + rendered = false +MediaCodecAdapter (exotest.video.avc): + inputBuffers: + count = 31 + input buffer #0: + timeUs = 1000000000000 + contents = length 36517, hash B334DF25 + input buffer #1: + timeUs = 1000000003000 + contents = length 5341, hash 40B85E2 + input buffer #2: + timeUs = 1000000002000 + contents = length 596, hash 357B4D92 + input buffer #3: + timeUs = 1000000010000 + contents = length 7704, hash A39EDA06 + input buffer #4: + timeUs = 1000000007000 + contents = length 989, hash 2813C72D + input buffer #5: + timeUs = 1000000005000 + contents = length 721, hash C50D1C73 + input buffer #6: + timeUs = 1000000008000 + contents = length 519, hash 65FE1911 + input buffer #7: + timeUs = 1000000017000 + contents = length 6160, hash E1CAC0EC + input buffer #8: + timeUs = 1000000013000 + contents = length 953, hash 7160C661 + input buffer #9: + timeUs = 1000000012000 + contents = length 620, hash 7A7AE07C + input buffer #10: + timeUs = 1000000015000 + contents = length 405, hash 5CC7F4E7 + input buffer #11: + timeUs = 1000000022000 + contents = length 4852, hash 9DB6979D + input buffer #12: + timeUs = 1000000020000 + contents = length 547, hash E31A6979 + input buffer #13: + timeUs = 1000000018000 + contents = length 570, hash FEC40D00 + input buffer #14: + timeUs = 1000000028000 + contents = length 5525, hash 7C478F7E + input buffer #15: + timeUs = 1000000025000 + contents = length 1082, hash DA07059A + input buffer #16: + timeUs = 1000000023000 + contents = length 807, hash 93478E6B + input buffer #17: + timeUs = 1000000027000 + contents = length 744, hash 9A8E6026 + input buffer #18: + timeUs = 1000000035000 + contents = length 4732, hash C73B23C0 + input buffer #19: + timeUs = 1000000032000 + contents = length 1004, hash 8A19A228 + input buffer #20: + timeUs = 1000000030000 + contents = length 794, hash 8126022C + input buffer #21: + timeUs = 1000000033000 + contents = length 645, hash F08300E5 + input buffer #22: + timeUs = 1000000042000 + contents = length 2684, hash 727FE378 + input buffer #23: + timeUs = 1000000038000 + contents = length 787, hash 419A7821 + input buffer #24: + timeUs = 1000000037000 + contents = length 649, hash 5C159346 + input buffer #25: + timeUs = 1000000040000 + contents = length 509, hash F912D655 + input buffer #26: + timeUs = 1000000048000 + contents = length 1226, hash 29815C21 + input buffer #27: + timeUs = 1000000045000 + contents = length 898, hash D997AD0A + input buffer #28: + timeUs = 1000000043000 + contents = length 476, hash A0423645 + input buffer #29: + timeUs = 1000000047000 + contents = length 486, hash DDF32CBB + input buffer #30: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + outputBuffers: + count = 30 + output buffer #0: + timeUs = 1000000000000 + size = 36517 + rendered = true + output buffer #1: + timeUs = 1000000003000 + size = 5341 + rendered = true + output buffer #2: + timeUs = 1000000002000 + size = 596 + rendered = true + output buffer #3: + timeUs = 1000000010000 + size = 7704 + rendered = true + output buffer #4: + timeUs = 1000000007000 + size = 989 + rendered = true + output buffer #5: + timeUs = 1000000005000 + size = 721 + rendered = true + output buffer #6: + timeUs = 1000000008000 + size = 519 + rendered = true + output buffer #7: + timeUs = 1000000017000 + size = 6160 + rendered = true + output buffer #8: + timeUs = 1000000013000 + size = 953 + rendered = true + output buffer #9: + timeUs = 1000000012000 + size = 620 + rendered = true + output buffer #10: + timeUs = 1000000015000 + size = 405 + rendered = true + output buffer #11: + timeUs = 1000000022000 + size = 4852 + rendered = true + output buffer #12: + timeUs = 1000000020000 + size = 547 + rendered = true + output buffer #13: + timeUs = 1000000018000 + size = 570 + rendered = true + output buffer #14: + timeUs = 1000000028000 + size = 5525 + rendered = true + output buffer #15: + timeUs = 1000000025000 + size = 1082 + rendered = true + output buffer #16: + timeUs = 1000000023000 + size = 807 + rendered = true + output buffer #17: + timeUs = 1000000027000 + size = 744 + rendered = true + output buffer #18: + timeUs = 1000000035000 + size = 4732 + rendered = true + output buffer #19: + timeUs = 1000000032000 + size = 1004 + rendered = true + output buffer #20: + timeUs = 1000000030000 + size = 794 + rendered = true + output buffer #21: + timeUs = 1000000033000 + size = 645 + rendered = true + output buffer #22: + timeUs = 1000000042000 + size = 2684 + rendered = true + output buffer #23: + timeUs = 1000000038000 + size = 787 + rendered = true + output buffer #24: + timeUs = 1000000037000 + size = 649 + rendered = true + output buffer #25: + timeUs = 1000000040000 + size = 509 + rendered = true + output buffer #26: + timeUs = 1000000048000 + size = 1226 + rendered = true + output buffer #27: + timeUs = 1000000045000 + size = 898 + rendered = true + output buffer #28: + timeUs = 1000000043000 + size = 476 + rendered = true + output buffer #29: + timeUs = 1000000047000 + size = 486 + rendered = true +AudioSink: + buffer count = 29 + config: + pcmEncoding = 2 + channelCount = 1 + sampleRate = 44100 + buffer #0: + time = 1000000000000 + data = 1 + buffer #1: + time = 1000000034000 + data = 1 + buffer #2: + time = 1000000069000 + data = 1 + buffer #3: + time = 1000000104000 + data = 1 + buffer #4: + time = 1000000139000 + data = 1 + buffer #5: + time = 1000000173000 + data = 1 + buffer #6: + time = 1000000208000 + data = 1 + buffer #7: + time = 1000000243000 + data = 1 + buffer #8: + time = 1000000279000 + data = 1 + buffer #9: + time = 1000000313000 + data = 1 + buffer #10: + time = 1000000348000 + data = 1 + buffer #11: + time = 1000000383000 + data = 1 + buffer #12: + time = 1000000418000 + data = 1 + buffer #13: + time = 1000000452000 + data = 1 + buffer #14: + time = 1000000487000 + data = 1 + buffer #15: + time = 1000000522000 + data = 1 + buffer #16: + time = 1000000557000 + data = 1 + buffer #17: + time = 1000000591000 + data = 1 + buffer #18: + time = 1000000626000 + data = 1 + buffer #19: + time = 1000000661000 + data = 1 + buffer #20: + time = 1000000696000 + data = 1 + buffer #21: + time = 1000000730000 + data = 1 + buffer #22: + time = 1000000765000 + data = 1 + buffer #23: + time = 1000000800000 + data = 1 + buffer #24: + time = 1000000836000 + data = 1 + buffer #25: + time = 1000000870000 + data = 1 + buffer #26: + time = 1000000905000 + data = 1 + buffer #27: + time = 1000000940000 + data = 1 + buffer #28: + time = 1000000975000 + data = 1 +TextOutput: + Subtitle[0]: + presentationTimeUs = 0 + Cues = [] + Subtitle[1]: + presentationTimeUs = 0 + Cue[0]: + bitmap = length 296960, hash 18F91C99 + line = 0.88611114 + lineType = 0 + lineAnchor = 0 + position = 0.0 + positionAnchor = 0 + size = 1.0 + bitmapHeight = 0.08055556