From 1e2ed51f25891e56aef3aeef169f0f3eaca664ab Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 10 Jul 2020 09:40:24 +0100 Subject: [PATCH] Add support for H.263 and MPEG-4 Part 2 in TS The new reader is named H263Reader as it handles H.263 streams, but MPEG-4 Part 2 streams are also intended to be handled. The reader's output format MIME type is video/mp4v as the H.263 streams can be decoded by decoders supporting this MIME type. The implementation is based on the framework implementation for extracting MPEG-4 video in MPEG-TS (https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libstagefright/mpeg2ts/ESQueue.cpp;l=1825;drc=86e363c1fac27302ca4ae33e73296f7797672995) and is similar to the existing H262Reader. Issue: #1603 Issue: #5107 PiperOrigin-RevId: 320565337 --- RELEASENOTES.md | 3 + .../ts/DefaultTsPayloadReaderFactory.java | 2 + .../exoplayer2/extractor/ts/H262Reader.java | 12 +- .../exoplayer2/extractor/ts/H263Reader.java | 477 ++++++++++++++++++ .../exoplayer2/extractor/ts/TsExtractor.java | 1 + .../extractor/ts/TsExtractorTest.java | 5 + testdata/src/test/assets/ts/sample_h263.ts | Bin 0 -> 46624 bytes .../src/test/assets/ts/sample_h263.ts.0.dump | 121 +++++ .../src/test/assets/ts/sample_h263.ts.1.dump | 97 ++++ .../src/test/assets/ts/sample_h263.ts.2.dump | 57 +++ .../src/test/assets/ts/sample_h263.ts.3.dump | 25 + .../ts/sample_h263.ts.unknown_length.dump | 118 +++++ 12 files changed, 913 insertions(+), 5 deletions(-) create mode 100644 library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H263Reader.java create mode 100644 testdata/src/test/assets/ts/sample_h263.ts create mode 100644 testdata/src/test/assets/ts/sample_h263.ts.0.dump create mode 100644 testdata/src/test/assets/ts/sample_h263.ts.1.dump create mode 100644 testdata/src/test/assets/ts/sample_h263.ts.2.dump create mode 100644 testdata/src/test/assets/ts/sample_h263.ts.3.dump create mode 100644 testdata/src/test/assets/ts/sample_h263.ts.unknown_length.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c05b55dc6b..c975d64f89 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -212,6 +212,9 @@ headers mime type in `DefaultExtractorsFactory`. * Add support for partially fragmented MP4s ([#7308](https://github.com/google/ExoPlayer/issues/7308)). + * Add support for MPEG-4 Part 2 and H.263 in MPEG-TS + ([#1603](https://github.com/google/ExoPlayer/issues/1603), + [#5107](https://github.com/google/ExoPlayer/issues/5107)). * Testing * Add `TestExoPlayer`, a utility class with APIs to create `SimpleExoPlayer` instances with fake components for testing. 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 c48c790fbf..514151c2f4 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 @@ -165,6 +165,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new PesReader(new DtsReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader(buildUserDataReader(esInfo))); + case TsExtractor.TS_STREAM_TYPE_H263: + return new PesReader(new H263Reader(buildUserDataReader(esInfo))); case TsExtractor.TS_STREAM_TYPE_H264: return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader(new H264Reader(buildSeiReader(esInfo), diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 012de81297..a7cbd9c5d6 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -22,7 +25,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.extractor.ts.TsPayloadReader.TrackIdGenerator; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -118,7 +120,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { - Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. + checkStateNotNull(output); // Asserts that createTracks has been called. int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -156,7 +158,7 @@ public final class H262Reader implements ElementaryStreamReader { int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. - Pair result = parseCsdBuffer(csdBuffer, formatId); + Pair result = parseCsdBuffer(csdBuffer, checkNotNull(formatId)); output.format(result.first); frameDurationUs = result.second; hasOutputFormat = true; @@ -215,11 +217,11 @@ public final class H262Reader implements ElementaryStreamReader { * Parses the {@link Format} and frame duration from a csd buffer. * * @param csdBuffer The csd buffer. - * @param formatId The id for the generated format. May be null. + * @param formatId The id for the generated format. * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or 0 if * the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H263Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H263Reader.java new file mode 100644 index 0000000000..bbe2f91d6f --- /dev/null +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H263Reader.java @@ -0,0 +1,477 @@ +/* + * Copyright 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.extractor.ts; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.NalUnitUtil; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses an ISO/IEC 14496-2 (MPEG-4 Part 2) or ITU-T Recommendation H.263 byte stream and extracts + * individual frames. + */ +public final class H263Reader implements ElementaryStreamReader { + + private static final String TAG = "H263Reader"; + + private static final int START_CODE_VALUE_VISUAL_OBJECT_SEQUENCE = 0xB0; + private static final int START_CODE_VALUE_USER_DATA = 0xB2; + private static final int START_CODE_VALUE_GROUP_OF_VOP = 0xB3; + private static final int START_CODE_VALUE_VISUAL_OBJECT = 0xB5; + private static final int START_CODE_VALUE_VOP = 0xB6; + private static final int START_CODE_VALUE_MAX_VIDEO_OBJECT = 0x1F; + private static final int START_CODE_VALUE_UNSET = -1; + + // See ISO 14496-2 (2001) table 6-12 for the mapping from aspect_ratio_info to pixel aspect ratio. + private static final float[] PIXEL_WIDTH_HEIGHT_RATIO_BY_ASPECT_RATIO_INFO = + new float[] {1f, 1f, 12 / 11f, 10 / 11f, 16 / 11f, 40 / 33f, 1f}; + private static final int VIDEO_OBJECT_LAYER_SHAPE_RECTANGULAR = 0; + + @Nullable private final UserDataReader userDataReader; + @Nullable private final ParsableByteArray userDataParsable; + + // State that should be reset on seek. + private final boolean[] prefixFlags; + private final CsdBuffer csdBuffer; + @Nullable private final NalUnitTargetBuffer userData; + private H263Reader.@MonotonicNonNull SampleReader sampleReader; + private long totalBytesWritten; + + // State initialized once when tracks are created. + private @MonotonicNonNull String formatId; + private @MonotonicNonNull TrackOutput output; + + // State that should not be reset on seek. + private boolean hasOutputFormat; + + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + + /** Creates a new reader. */ + public H263Reader() { + this(null); + } + + /* package */ H263Reader(@Nullable UserDataReader userDataReader) { + this.userDataReader = userDataReader; + prefixFlags = new boolean[4]; + csdBuffer = new CsdBuffer(128); + if (userDataReader != null) { + userData = new NalUnitTargetBuffer(START_CODE_VALUE_USER_DATA, 128); + userDataParsable = new ParsableByteArray(); + } else { + userData = null; + userDataParsable = null; + } + } + + @Override + public void seek() { + NalUnitUtil.clearPrefixFlags(prefixFlags); + csdBuffer.reset(); + if (sampleReader != null) { + sampleReader.reset(); + } + if (userData != null) { + userData.reset(); + } + totalBytesWritten = 0; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); + sampleReader = new SampleReader(output); + if (userDataReader != null) { + userDataReader.createTracks(extractorOutput, idGenerator); + } + } + + @Override + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + // TODO (Internal b/32267012): Consider using random access indicator. + this.pesTimeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + // Assert that createTracks has been called. + checkStateNotNull(sampleReader); + checkStateNotNull(output); + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + + while (true) { + int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); + + if (startCodeOffset == limit) { + // We've scanned to the end of the data without finding another start code. + if (!hasOutputFormat) { + csdBuffer.onData(dataArray, offset, limit); + } + sampleReader.onData(dataArray, offset, limit); + if (userData != null) { + userData.appendToNalUnit(dataArray, offset, limit); + } + return; + } + + // We've found a start code with the following value. + int startCodeValue = data.data[startCodeOffset + 3] & 0xFF; + // This is the number of bytes from the current offset to the start of the next start + // code. It may be negative if the start code started in the previously consumed data. + int lengthToStartCode = startCodeOffset - offset; + + if (!hasOutputFormat) { + if (lengthToStartCode > 0) { + csdBuffer.onData(dataArray, offset, /* limit= */ startCodeOffset); + } + // This is the number of bytes belonging to the next start code that have already been + // passed to csdBuffer. + int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; + if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { + // The csd data is complete, so we can decode and output the media format. + output.format( + parseCsdBuffer(csdBuffer, csdBuffer.volStartPosition, checkNotNull(formatId))); + hasOutputFormat = true; + } + } + + sampleReader.onData(dataArray, offset, /* limit= */ startCodeOffset); + + if (userData != null) { + int bytesAlreadyPassed = 0; + if (lengthToStartCode > 0) { + userData.appendToNalUnit(dataArray, offset, /* limit= */ startCodeOffset); + } else { + bytesAlreadyPassed = -lengthToStartCode; + } + + if (userData.endNalUnit(bytesAlreadyPassed)) { + int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); + castNonNull(userDataParsable).reset(userData.nalData, unescapedLength); + castNonNull(userDataReader).consume(pesTimeUs, userDataParsable); + } + + if (startCodeValue == START_CODE_VALUE_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { + userData.startNalUnit(startCodeValue); + } + } + + int bytesWrittenPastPosition = limit - startCodeOffset; + long absolutePosition = totalBytesWritten - bytesWrittenPastPosition; + sampleReader.onDataEnd(absolutePosition, bytesWrittenPastPosition, hasOutputFormat); + // Indicate the start of the next chunk. + sampleReader.onStartCode(startCodeValue, pesTimeUs); + // Continue scanning the data. + offset = startCodeOffset + 3; + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Parses a codec-specific data buffer, returning the {@link Format} of the media. + * + * @param csdBuffer The buffer to parse. + * @param volStartPosition The byte offset of the start of the video object layer in the buffer. + * @param formatId The ID for the generated format. + * @return The {@link Format} of the media represented in the buffer. + */ + private static Format parseCsdBuffer(CsdBuffer csdBuffer, int volStartPosition, String formatId) { + byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); + ParsableBitArray buffer = new ParsableBitArray(csdData); + buffer.skipBytes(volStartPosition); + + // Parse the video object layer defined in ISO 14496-2 (2001) subsection 6.2.3. + buffer.skipBytes(4); // video_object_layer_start_code + buffer.skipBit(); // random_accessible_vol + buffer.skipBits(8); // video_object_type_indication + if (buffer.readBit()) { // is_object_layer_identifier + buffer.skipBits(4); // video_object_layer_verid + buffer.skipBits(3); // video_object_layer_priority + } + float pixelWidthHeightRatio; + int aspectRatioInfo = buffer.readBits(4); + if (aspectRatioInfo == 0x0F) { // extended_PAR + int parWidth = buffer.readBits(8); + int parHeight = buffer.readBits(8); + if (parHeight == 0) { + Log.w(TAG, "Invalid aspect ratio"); + pixelWidthHeightRatio = 1f; + } else { + pixelWidthHeightRatio = (float) parWidth / parHeight; + } + } else if (aspectRatioInfo < PIXEL_WIDTH_HEIGHT_RATIO_BY_ASPECT_RATIO_INFO.length) { + pixelWidthHeightRatio = PIXEL_WIDTH_HEIGHT_RATIO_BY_ASPECT_RATIO_INFO[aspectRatioInfo]; + } else { + Log.w(TAG, "Invalid aspect ratio"); + pixelWidthHeightRatio = 1f; + } + if (buffer.readBit()) { // vol_control_parameters + buffer.skipBits(2); // chroma_format + buffer.skipBits(1); // low_delay + if (buffer.readBit()) { // vbv_parameters + buffer.skipBits(15); // first_half_bit_rate + buffer.skipBit(); // marker_bit + buffer.skipBits(15); // latter_half_bit_rate + buffer.skipBit(); // marker_bit + buffer.skipBits(15); // first_half_vbv_buffer_size + buffer.skipBit(); // marker_bit + buffer.skipBits(3); // latter_half_vbv_buffer_size + buffer.skipBits(11); // first_half_vbv_occupancy + buffer.skipBit(); // marker_bit + buffer.skipBits(15); // latter_half_vbv_occupancy + buffer.skipBit(); // marker_bit + } + } + int videoObjectLayerShape = buffer.readBits(2); + if (videoObjectLayerShape != VIDEO_OBJECT_LAYER_SHAPE_RECTANGULAR) { + Log.w(TAG, "Unhandled video object layer shape"); + } + buffer.skipBit(); // marker_bit + int vopTimeIncrementResolution = buffer.readBits(16); + buffer.skipBit(); // marker_bit + if (buffer.readBit()) { // fixed_vop_rate + if (vopTimeIncrementResolution == 0) { + Log.w(TAG, "Invalid vop_increment_time_resolution"); + } else { + vopTimeIncrementResolution--; + int numBits = 0; + while (vopTimeIncrementResolution > 0) { + ++numBits; + vopTimeIncrementResolution >>= 1; + } + buffer.skipBits(numBits); // fixed_vop_time_increment + } + } + buffer.skipBit(); // marker_bit + int videoObjectLayerWidth = buffer.readBits(13); + buffer.skipBit(); // marker_bit + int videoObjectLayerHeight = buffer.readBits(13); + buffer.skipBit(); // marker_bit + buffer.skipBit(); // interlaced + return new Format.Builder() + .setId(formatId) + .setSampleMimeType(MimeTypes.VIDEO_MP4V) + .setWidth(videoObjectLayerWidth) + .setHeight(videoObjectLayerHeight) + .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setInitializationData(Collections.singletonList(csdData)) + .build(); + } + + private static final class CsdBuffer { + + private static final byte[] START_CODE = new byte[] {0, 0, 1}; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_SKIP_TO_VISUAL_OBJECT_SEQUENCE_START, + STATE_EXPECT_VISUAL_OBJECT_START, + STATE_EXPECT_VIDEO_OBJECT_START, + STATE_EXPECT_VIDEO_OBJECT_LAYER_START, + STATE_WAIT_FOR_VOP_START + }) + private @interface State {} + + private static final int STATE_SKIP_TO_VISUAL_OBJECT_SEQUENCE_START = 0; + private static final int STATE_EXPECT_VISUAL_OBJECT_START = 1; + private static final int STATE_EXPECT_VIDEO_OBJECT_START = 2; + private static final int STATE_EXPECT_VIDEO_OBJECT_LAYER_START = 3; + private static final int STATE_WAIT_FOR_VOP_START = 4; + + private boolean isFilling; + @State private int state; + + public int length; + public int volStartPosition; + public byte[] data; + + public CsdBuffer(int initialCapacity) { + data = new byte[initialCapacity]; + } + + public void reset() { + isFilling = false; + length = 0; + state = STATE_SKIP_TO_VISUAL_OBJECT_SEQUENCE_START; + } + + /** + * Called when a start code is encountered in the stream. + * + * @param startCodeValue The start code value. + * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to + * {@link #onData(byte[], int, int)}, or 0. + * @return Whether the csd data is now complete. If true is returned, neither this method nor + * {@link #onData(byte[], int, int)} should be called again without an interleaving call to + * {@link #reset()}. + */ + public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { + switch (state) { + case STATE_SKIP_TO_VISUAL_OBJECT_SEQUENCE_START: + if (startCodeValue == START_CODE_VALUE_VISUAL_OBJECT_SEQUENCE) { + state = STATE_EXPECT_VISUAL_OBJECT_START; + isFilling = true; + } + break; + case STATE_EXPECT_VISUAL_OBJECT_START: + if (startCodeValue != START_CODE_VALUE_VISUAL_OBJECT) { + Log.w(TAG, "Unexpected start code value"); + reset(); + } else { + state = STATE_EXPECT_VIDEO_OBJECT_START; + } + break; + case STATE_EXPECT_VIDEO_OBJECT_START: + if (startCodeValue > START_CODE_VALUE_MAX_VIDEO_OBJECT) { + Log.w(TAG, "Unexpected start code value"); + reset(); + } else { + state = STATE_EXPECT_VIDEO_OBJECT_LAYER_START; + } + break; + case STATE_EXPECT_VIDEO_OBJECT_LAYER_START: + if ((startCodeValue & 0xF0) != 0x20) { + Log.w(TAG, "Unexpected start code value"); + reset(); + } else { + volStartPosition = length; + state = STATE_WAIT_FOR_VOP_START; + } + break; + case STATE_WAIT_FOR_VOP_START: + if (startCodeValue == START_CODE_VALUE_GROUP_OF_VOP + || startCodeValue == START_CODE_VALUE_VISUAL_OBJECT) { + length -= bytesAlreadyPassed; + isFilling = false; + return true; + } + break; + default: + throw new IllegalStateException(); + } + onData(START_CODE, /* offset= */ 0, /* limit= */ START_CODE.length); + return false; + } + + public void onData(byte[] newData, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (data.length < length + readLength) { + data = Arrays.copyOf(data, (length + readLength) * 2); + } + System.arraycopy(newData, offset, data, length, readLength); + length += readLength; + } + } + + private static final class SampleReader { + + /** Byte offset of vop_coding_type after the start code value. */ + private static final int OFFSET_VOP_CODING_TYPE = 1; + /** Value of vop_coding_type for intra video object planes. */ + private static final int VOP_CODING_TYPE_INTRA = 0; + + private final TrackOutput output; + + private boolean readingSample; + private boolean lookingForVopCodingType; + private boolean sampleIsKeyframe; + private int startCodeValue; + private int vopBytesRead; + private long samplePosition; + private long sampleTimeUs; + + public SampleReader(TrackOutput output) { + this.output = output; + } + + public void reset() { + readingSample = false; + lookingForVopCodingType = false; + sampleIsKeyframe = false; + startCodeValue = START_CODE_VALUE_UNSET; + } + + public void onStartCode(int startCodeValue, long pesTimeUs) { + this.startCodeValue = startCodeValue; + sampleIsKeyframe = false; + readingSample = + startCodeValue == START_CODE_VALUE_VOP || startCodeValue == START_CODE_VALUE_GROUP_OF_VOP; + lookingForVopCodingType = startCodeValue == START_CODE_VALUE_VOP; + vopBytesRead = 0; + sampleTimeUs = pesTimeUs; + } + + public void onData(byte[] data, int offset, int limit) { + if (lookingForVopCodingType) { + int headerOffset = offset + OFFSET_VOP_CODING_TYPE - vopBytesRead; + if (headerOffset < limit) { + sampleIsKeyframe = ((data[headerOffset] & 0xC0) >> 6) == VOP_CODING_TYPE_INTRA; + lookingForVopCodingType = false; + } else { + vopBytesRead += limit - offset; + } + } + } + + public void onDataEnd(long position, int bytesWrittenPastPosition, boolean hasOutputFormat) { + if (startCodeValue == START_CODE_VALUE_VOP && hasOutputFormat && readingSample) { + int size = (int) (position - samplePosition); + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + output.sampleMetadata( + sampleTimeUs, flags, size, bytesWrittenPastPosition, /* encryptionData= */ null); + } + // Start a new sample, unless this is a 'group of video object plane' in which case we + // include the data at the start of a 'video object plane' coming next. + if (startCodeValue != START_CODE_VALUE_GROUP_OF_VOP) { + samplePosition = position; + } + } + } +} 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 7b470ad570..9734cab1ca 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 @@ -90,6 +90,7 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_E_AC3 = 0x87; public static final int TS_STREAM_TYPE_AC4 = 0xAC; // DVB/ATSC AC-4 Descriptor public static final int TS_STREAM_TYPE_H262 = 0x02; + public static final int TS_STREAM_TYPE_H263 = 0x10; // MPEG-4 Part 2 and H.263 public static final int TS_STREAM_TYPE_H264 = 0x1B; public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_ID3 = 0x15; 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 5ef5fc9048..4b198f23ed 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 @@ -60,6 +60,11 @@ public final class TsExtractorTest { TsExtractor::new, "ts/sample_h262_mpeg_audio.ts", simulationConfig); } + @Test + public void sampleWithH263() throws Exception { + ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_h263.ts", simulationConfig); + } + @Test public void sampleWithH264AndMpegAudio() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/testdata/src/test/assets/ts/sample_h263.ts b/testdata/src/test/assets/ts/sample_h263.ts new file mode 100644 index 0000000000000000000000000000000000000000..83eecc11e9113be6711b89da5d098cf69c6b5f0c GIT binary patch literal 46624 zcmd?RcU)83wl>A4xtwjLa$0M0zzmCNEZ>LCG-x`5mXRCs?xC_By>R# zQ4m2Ps3;<6P(Y-AIZvE$(ql2)-&fEbB;Nt0hSR3(xwQ5 zz$6gp0DJ%f@8X#anZQteeZN4@b4RQ^gG0UDJu$MEB6Wrj{RnoPN80|E@y=Fo^Mw_uc`45~%eV{_TbSvA}=ME&w~zKfumc z5Gd<^+e)C!yx)`wU_birf4cu7qW^?s06X(vd~$%L*MDOvU}cN9^qj|PHJ3SBgXeqs z2Ku8utcQ#>$`BvW%5LvI=?0_I*2Qz*@QJ#y(tWMC)WQ19vT~wnin3-R2Dl$&82(3UdkCTQ{ zJtoAN(IA+Lpt|F-283ZQ#0`dZ#qyGOqs_0%zFDwaUhKV_q!O9hgpz?*k5CQD0nCS~w(C3qlZNqBIKl@5@lY?|-mW5n=W7GOdroWvmJ#XVGUz-p0`Mi zY|m~;;z{Lp$p;qk}UHR4u!vW`1PB5#p?aEhQgP~MiYC*p` z=OFDN@6+z!0i72te&6|eGA(>0@6FpO&SNun5u>^QcJ!$+F$^K2=eoA4J1` zPvO;QqaBZ5=@UXBI4O>(>^S8(b=-^NJt=?JnUov*-Bh9OS=EW_>w3j1#MsoP!q`)v* z-k9sWgbb)Mq}B>lX0Ge2cH6Kt@{Owm$KYG~*q;`@!bxoG@Wq1A)1G>6UozTPGTQeq zX>4}f|Ej0C|Ku_1O9S7vd=!Cv@Q#VFaPZj>$#(FttDld8;f;;wVWncOKtPY2MD~C( zg*~=*BWD8qER=U$PBO-$JU)@y;%myFx*Mqj$^75$D$|(&*tyJSuVT59yosMKH&G~@ z%g?J$ZhqXJx$F5&DM_3;qDAe&+wb#6A1p*M+GgZL4Ic5c&Dq^DBwKr$7UIy0vFRoY zw?!W(zwEPlcrO)5yhmAD*I1?9uH?5{YN=nXPlYJg9@%eqx)+ugI*oT4sPogCcq(YY zI9_j#giuZA?|getOnljQS;i5HaJ(CZ!7dU{0cy`;@ox1+|F z9y}OVv(#E|4>!kb@zCvQ;sbX(gLKGBS8jxx+GQ&}l(vpa%ed~1QHrt`a|8_o*m>Rz z_4^sOaYNWsFD$FkM5JBx6dj9_s>DBT6NTP}L*6etNSi|kS9gj(yHQ%ie^gqjb&HG8 zMI%s2Pu{7Z@F#ArZF}+5Hr+b=@ayd2aG{l&&U!nB1|)|IgjVtH->xY(d}G5QX(jV> zF1jXF)ppLHm_`(O3`DkahuqWL37d{CYsrY|PrIP(yL?so*tVkzm>6Ua%_5l&yt8yY z@3$-y(R?XZYW4kC?}7&plqQH@%C^WD^x77{&U@UHX1c4N>E;=|m1_>u4iNyvcoZFR z`H`uHB-h8fkPZ`pl(T|o0-4l7WM5;HfzRL@WZCw#^E`8bjqYI~i-cPbpjqa!5)F)$iXe@5>ZAZ{wrA5eqwuHb7 z2T5J8xd7~_-A8Oi`l?nVI=7s3yca~(5p*~0zK-NjA-e?Iu5H&{cjyh*jES#)!GCFD znu3I2G#UF^E55Es$JO<0t~cndrpmKg1xS7Mut27IVU|KPU7hiP+;LNu*o}ArUL6r@13~pJ`r|ke=taK|{_Cof@hdkm z=5U=!_w~n7S0}1UMr)VfkW&axEsj!Ar1c8YbWY(0PVg>m+~3JnWj3N0bk5E?SiGC1 zG?0E)A+P%=_M_Jpmo(cKYO3qgT?|=>e+|g0T)x zP@Rcl9-mpiwg|)7V`p_OwUcDGcKm$(I#v>s-u1fSyfQ zuE1?_Mr|Iva>MP`cjUIW?Mq^LsTImXjHWaM2QCG&WSlYO6MSR=c^dkxq!+D@{EmzD z3PTQ*cSK;pb7H~qB36d>?9-$*#_BHua>a=*FnBJbAS?&(ATJM+_;r~N~Os#1<* zZ{3Sp$e5yQ0t8Tt%9LTIXnVu{#N=2r#WKb7ts@m0ZK~-OtW+ZN5XIVmT+ydIgidqC z7u;JeL+pKR`CP75^VCfDt4_&^oHo(>KW&v?(o`wWAmCynD%OHd2cYzU2lBcLE+hgq zAAIDOXhnG&rBBa&Zs9?+2ID+pZ~=`emS=9`8(wpWfTkIHF4IemP) z7Oj*kqFnAH=|a#DqEeeWgOMW6b^tO&z5Q4YFH-X1otiXAdEou)Kv=!rlHCQj{eKi zR-__!wI~#!(U=<9qTzC&nEC<&Lr7z~Es9@U;N3(M@Tw}Dc+PGK zJ}W%aSI%X~jdX;XBZ>^XV&sF`I74b>9$ERrlLxppcnq{pho#>U=2dp^mzqusV(_== z5WAi5K?|>ra`f4z668Eb&3d)6LiqT7TC>Q*y2=(8u5PRjwJm(rpl?;!Qmntfe7;jG zP;S7@T#>2IQ}0tgoHK5=?dsf_gGV+^+IJiu zb`Dj^$OXSy7{5iIp5g!-nPebPW!?;kQHa^||FU+U*{3)>re84PHjE0Bs0lwpVvgG2 zeVb8mX`c9QE0JI8q`HC^28#FFkESXS%2Q9b;q*#-bl{iF7JqwxT1@iU7G^KYS>)O0 zOSGafa_Q-r0CqG9FDqb+-`a1k>1Jh%y8C8hEK=)-Y>0$Sg3ehm!o*at9X08%aLxHm zz%i}E{*Fb4=ygXO!$h^OKoxUs#GFXPbdz#k?>6W;+ zYxT8{J58XvK4zwjNEnDkXFd=9>0Dd*s!9Kw1k!@S+E=!2$PHj;2Sq1?o*cpTAUwO2 zZ?v{|ylNO*PPs%L^2df^uDS>19jzFi1$DNzV+_o;mc=Kdk(tHg%)or}CCw%d}Eg6{nz zN%))49ldQ~AsI;T)Gev|m*%W$!(U%jc{Tn;bb}+)fEX*7nY5Z^Z>Gy~sdmJ8&0!BB zi$q;0MA^~Nk>W9G`588#2>?5X;p>kZ@^+e!^zAN%Pn@!Fc@qaZYwl0Qdu8s)>5GH3 z&yJ81?ss+WJ5{xx)^5t?qGp_iDjyU&HQgVW@fRC?D`d};2< zn4;vEo6sdMyS3-)`%1H0vjOk?x&d$auN=-i?q2FyYkBv#%GKg)$y4ksK$o) zt;&Y0>?FfJeh7)Ep$1FB>)``KaLVD`0Cvu{+Nch(4#+O=!ThW8FJjfkM+)|vI$=3LbfMg3U2|2j++X<(#V)kocB zVX*t(@)IFrk*Tr%9RzDX12w#X|5pEIjdza;%IQhw)XZ}+SMBdzTSO)rH-x>y6 z_5rYSt!!b}mAk>?pP`^c*hD<*h|OFCs0Gb^8LBY%sJ6S{laPv+HjmeuT-NxHZO2Z% zG{egPferd4prIL>Xh`0}PxSSC5Nbv-#M)!+|y@#!5zvD;<9^~!|GU+BZ>>kx?_UPmEzf9*| z2R?iiJJy;?{+9V&U}m=SPaVAG*v4OEtNsVs=EAX*Kx7tpfy{{G;zr|0l&=eS68Ihx zrl8rPp3L$5;~)AC7#`^iajhV@*!pVZ!sXdP@oZy{5@@di*m*N9>xvpVLvg%JvrL1l z9j)pkk##6{%X<0h^*kFMzdSVN`Q7*&^CF_-F2~PE06Qwp^|UxBU66;0(EhXLavf0{eA$v3dOo#CYlT0KEo-pW z!D!+=sg1_J$0hliBEHvrpn~ho{M@PrU1}~SmkzzY&NgwT`t!Th_G3!3KWBU)f?h29 zP}(z>s}EXLUg;xuXzoX7zzF;6p(u7J176+GRZ)>;fKV`z7tHh$sq5)N14@Sr9GBI$ z(S*d2-Ack5I@tmh!UCyLNRkkegm@5@z$oh)z&vUlUpuYu#7Uk4`AUC3X#-$qJSkh~ zIMUPF&9LD?$HFhNW6r5XCQCu3f#>R(9XSNiBhcKHLrlx_1SI5z35n$+mDe$9zWnj5 z)J@|swusRW$&VEizy5ssM$+YVE8IGg4*?2+1cqud!4{`yIn<<0`=l(M4L8hG1y59* zIlhS|b>%za5{18febHFx;CdMJ2LIpovA*8xO_1n+@K))oVUpY6dtn+57TgFL#E}1%Nsrt z{^395|3aX=2%5Po7%#iEnm!-y^5H{F>Nz>LWvS#N{TwhXS;rNlf|@{*Y_C)k#RnZ; zOh)%v6wSO5K0~kigJ#Es6wg1=cB%dr6(KBi{`l`(E){(c$5aQ5WDfCgy>4TKwDy2cBd`E zKX|f0XK=TQ6pL2p{aHhb)5Wg^(%h4~Lo3T&7R%p$!hQZn$LKlEx7pu^MQ2`W{`D^D z&-7V8Kp@iJRL24A%zvq#%Uv+HVk%C*UPgJzBWr2f1{48N!5CwT`)P``O0y>TKCgNdk^!g9V07)N3Vnrw^r8+tGp~Y z_1d~_^##f~mi|(o><>bA!EJFU1tb!F)*06t^r=8-{DL zI8DFDG6`C`eVh zoXyv|3Kx-Wi*|#x-sHb|)?H_9M(bsoRfoC{`~ioX8Z%6mi`~Kc(b^?Tj4xX6SrM}+ z(Y4yov%qG*{pm*cGdAxHs&?4*Z`(Xe4-m2DllDJW8welhmYxX3qpws5uBI~l-ILEi zT$}NC9M@+2-Nv4=v5OcrcPT_(iB*~g2Sjm3LCC79RbHisc7#tTyPj) z&EWJFyMME>6$#n9tOv|G$~kzOnf9A}bX8tf&A6LzzRDGHFPGmXb@yH9I zJGgth22kI~GgG;coH_zgPR4xRoHBK!7nDgWyHxmkDY6><+)&G>U4W+#i?`ocw#s{b$QOjF$q~IscWxrN7P>9P8|Cr@G{}Yg3 zFRJ(oto`8y*6Wb}u;yo-aJYKMoFaI5mwjM84OPq^?*{}*v;ONVaFqAk3)iV4#X?3K z*rP)N*(yo~+(KEWeYvN;H4Y--@V9S@LPXaWI|uV$yar>; zcs#&6ii!E2>bYa5uC5)0C~%av(7}h*^4lM@%{9B;V~9OMxiv`Fd^E2N^>)fPEU?5p zcnuQ3InczWa;5rIacxR@na%UtxIj}5LDOu{sf&qQUaDU4VoUn{G}ala0n&YDk{=xy zibTWkJz{dQ{m(cJ9qlG-%%Fa(@4#d?R?G#dyvk!z<4e~tg2E@S99k3ugpv;}>gj^S zWpBtwo?fheJ{wjv;(Ht0yKv5x(b7l$Q1*hW={6HLEy&+lOtv?AJIF)0ngT}aKCJeWyKgU95-7T zJ0Zt9Yjlpctst+xC+gjm^YqP%odLJwnL3SZ6XV{WKX+b?e?xJ2ny_Jtod_OyyJYv3 zRU^$%rB3D?KT*H_PKoc=knF(BpqqVa@7F6&SF7#O`0Fb?_EMFll|@u4%>?npQOjag z>N@VCtQ=pbsIZC^0N9y+L@>1y)Ogg@khu(|H5o~48FxzRDcd35(#%sT_~D{Z-p13n z!x+eJRmB_|x6;-)Dc6Vc24r^lYxEIhWP>>o=a z8mcDxfB3M_kFoWNf9cQ@#dXV!N%6(U7rmKP=a{7CS|b@%GKD-Emi8T9Xz<-8PkV5= zJ@NPNc+Lw@D0-HWp@9vvH)deqp2w*>IYmlfa z7aVlQBUq`(Gw;mu1e4O6;?m3n>GZ}*FSJaxs*0uj(ko2a@h`{X4Qt3P3Du|HoOxF; zb5+5=eOGdoL1kFrzU2+nhyXP`DV4R*Ny+6D?3(klb`cD} z;OH6u%I9LAlQ|yPLcS9hCrl=U?=!WZMg!QHO-;F%zZFamJv$NmV)Wuo)CG3`WhK_3 z_jHY&S4sv_dfSU6Uy8(Ki^wrNahFe549_$!xV^mnYN6L#$@Zd0RPST*TY?X_y8+e0 zh-kKmYezd@{X@|qs~M;A%v)z!N(}9}^@{H4T06`z&kn~gMRPV>OJw>qD5})=VW+CM zH7KzzwA9RlTgUef?3g!;yCbHU-N*WyP-74EWP$7nvoUWfNk-Pw1@Dy3lTHsmn#yT; zumDo_PM1pqumd6MqWrV~r8^Y6i&0bd4FlJ&Gt9YrTj{(7dqw7vRa1+(Irt@)9jh1_}F zKarC}@trL#mJ8fJ7Vh)L_*$p1X-c&N{A14mgOA(i(|uzSXTptiYEu$XZes|{?M<=K z#Kkx^b=(P>sOH(~>#EwyhT0{Mr1(V^9QBF5a_q-WW$?6SWzzlxu(Ntd=sMogORw_t zsP@rGaeIv}upYfNJaWd&#nI#KRb1#vQ(4`svE4_kNh9Abq~9^NXK*RxP){?=ZmSaX z&J;i@v=Q*fgK%x8ER#x;x&9xnqZsqcGTNNeP>E0Urys>vc=U#xI2B@EXlk3ft`gl$ zJNGj8-~i~sI;kQ9uB%f~bvc4BThI<{EERvj6GQX!s;-Qee>#2UN^RsPE$>aW30d3+ z5zqQ52_&7Whbtl63T-$NW#VTKU}tk2NFJD0xpT`e$>V&!=Vk*Jvx6LQuZ>d5m0#8< zC?*cizOBoYPLKs@cneOnBU`li1|I+djV|&tG}6)OhtVy0O0ZD^EFZ*e4yh zxjV~cVTX?Q3~I} z9T@2pDnIeN3EwMjP+$;yqi_WzvGtfEOt5+E_2iGE#hfZ_4fH?krJlz^<$9zC66xNr z(l0!OiLM*nd-8#GAO*}w>vrri?JBz9LYl(ZRhY$t9?H$}TV|c? z4GvnDGvaR#Y^PTh=+IotFG{>6OurG*f93jAMbDVcl(dMTmMw}}0JZO29yrTEQ*A|J zKYH1);C1dJa$jJ2M_t=g;867R`xe6zZM)_#9Vr8XC3eWEnyU(|A&l^aqCy6$CXarP zYI6k%M*}KWxCqej0{{o8q9WIpte++8*yc>t0@&G!vO)%(+DaOE{`d<)B$GUTJz^== z7rXRA)5(uDH`COLwos5E)m8w(chNC)w&S5)`(3|V2i=Dq$$~-o438x#Ta_O>uI3bY z?UQ2whw&(Lv;9LZdvoTl*DMA6FdsVxtEsqHTMRcXwBgMMXr((}M;wh3hwKnUA2# z5B(Yo+&|#g1eC%!(V4Q@DrScT0bP6rRSeUD;i<#tey|%5Yk?C=NpJ*}LL5kshn?Du zHIKynB8Ud6Xq5r13|mfHUhHE!nq(De_ahI{J}j~c@Gxs{r=wsgU#^SkTq5p#O|dgm z@-IjL?_8n$3P%q9t1FYh2^B5f@MR)BE@eOntR>&3CUY2MHW{kO(uJucvx2wG+R`UU zBo+HRRPjTN6jp(8v-31FJPblGRU`^zui`Z&>kbRS)a1l|V#P89O3pmC>&M9)?1aV2 z^Cqy$YGzO_!b@OuVA84fA@cz`7THFX`cegOD((v{*0b|y_n~-z)+bt&n=Hv?Hbs&} z!nBHrGHIl94XzT*X9pNq1W6%G_z$uGFEIf26S9A|W)7=$bUR&BI~^0IBN-=Iy`^%t z-B<<<5V&_(qhc}}xmXfWY7GzlX5ZA!+Oiss%>Sa1!=wEHOUTuiu%n7gXMk^jO$~rfx2IK9 zSz{V<4x9I8s^~d}OZ!s{>@%dNhyQ4b{pbj9Yjlh`R6!?j)oj)6@I0WqL#`MN!a*vE z{J@D{nw&%4HC_L1-Vbm1_q?*k{>(!-ya52>pW+>JSXT=re_K}#7Y_Ni{0Hi4%poQ~ z3qTD$JlY?C_<3jo2fzb4Bp!GH7n}d3zf7*y&3bEoJ-ov&uS4EtBmV8Y|Dm}5{`pVx&h%@&dxC%A`ybGI%ptH}^Uc71 zc;?U1eR%%xr=3(WX}vPQF(CfGjq2Z+3h*wU{Qr>mU(EQ!+kYwLmv4Y&f3ZLPRXd|_ zsP{_s-|C&ff5^YpKd<*cAo*`v|HB1<{KG>4?B7d0z`J6?e+2Kq#Ngl0JL|9YeogBa zzW;f>|AE$j1K)qJ65w4q<3ED;UyS|Z+ux{nl|#L^nEqDp_gxS9xBgGpyXx;)?;O9@ z`y}BPzJD)zKmI$`yZWKtNlw4j`&#)S|2F^WdOz_y);rIy^iD@l`i1Y`i{3SVw|d8d z06+3~o3G9EyNzrA-+b+-zQ1W_=)@KP-T#C6+N?N>|2O&CRR6!o*Z$wO5*S?N{iaO7 zkNp2`zV>e{1;WA|kG^)ajC$@bKbH&5pv9gAL$_Ml_4CBM4F$zC*uZJ=$8vyVLf?F` zNXx4JwL6pS^r6yg-jr;92>hgk^tYhp(C;?y53agm!>@LUJruvzQZROk2AsXl8Qs&} zc;&9s=J!i`5{&_m3oaa={!nqUZByyzjl;a$mI8Y3AW3Z=c(i$bidXXM)7?;jca|Fn zG18I_RGKG?xjmap)07iy`=0)rAfXHKE)lyW8@OU5v zaEV!rDPIX;{kWn0L*fl#2IT~qjHX;f{NKFt>bEO7{>Q(~AI_Wnx;6sXSphbi6F}-n z7#N+6M1pF|aOF@KflL-;0+#8rSRYS<2(%62^Z*iry`QB{dVTD7sUGP{yHOF7w$%MI_LXtcT8T5}31t9ww$L5}hQW7z6;*vyn~E92oZiwtQi0L@cw7NWRMznfPS@U% zLFm;?x>$@l^-(^K!sqXrwzW9RhEy$EAg5PoBb~0}cvI+on!i%+{`@pG!Sr<7a){bO z?{g^x9E;v%jI8D_Q0fiEabCZ^!|5~IqFVBvqt$}p6Loh@(Nk#)ww%D*_);LZU2NhQ zZ=3z8Df#6%oTs^^9fT_x2EoSCD!NkA&(srGEsnWy`dzi5g>FCyC(qcVM;A^t*?~sw zNQ02zaTJ240>5DdVCQfnu+^%_YfP{3#T_wit+db3<%RSRdCQ!kSJTL5d6vonqqTe9 zztr}h!KD058k9Vig3^lTQ&%xYZJ`F6E^>m&Wo+(t3|FB#{r4N{QnS}ZUL!gTW9Vs9JHz3d0Z20IGDwk7eD|K64W*t!0e_T;qIDIi=y4C7~jl(`arYGET?!Q<-G zD846+{PD-|@1%&XLswEylo(HDm9f6oX~Ro3apfu=?V+e9dyZztx7M)do0Yu4*-3TG zCw}~%=Y-{u|NP+~PqjkT=Y<*axLTXuDWwZ?P+J0-uNX8W_$fkgy*}&i4~n5hhTd0e zpUBFGOKG_$PIk%w>%HSE?Xx3`Zyn}5b9X9v*B58Qo`>Ew)P62>nLowtvF7n!d0acH!f>8G|#F_?awjFSFvNa)OmN^$HQWF_nF&aBp~OVC$9J4Bjr{Z7HUAj z*+KAiW5(QG5 z0oZvcwTbiQ9ynSXpCaSINs;rz)BYD$m2>y^`AZ6$7Vfc&s#7zI9W5w~Q`~ZT^W_=; znKq^IFfYg}7aOXF)?3fqlPu%ud)lnq`F{u$F>oee?}72e#A1g8Y^e4J3+>${jxC9` z%hi>UO5w@V7a4m{IWVTBEgfDt4Kut&=|`OyuUFk12Mxog?u`~W+tkIXzuq_7xv>B0 z_3PT;;Y9nk*yLINq1k|s!8KOir(Nal+D_I5Jds^7*aWci&eon-GHG9XQVpcEXs&4) zB9Z$=+OV{P*3+9}Falv0jUp}JbWkMrvdH=Uli+bziZaP?Fc%-jCBwv`yapMqyPoU# z(fU;M-9c(p81hYDb{O*QU=KP2PlEKE%4&^#!y6~cEiOY;;++$F8bb92 zx(XpPi7F^CSu=$@MF5*!+CBWCk||eq!?MPZHtdDB;=W>NLBD$LP0`cq*FC8*VQ|U5 zeC%Va3JRukkOg2z^`Nxet;Ufxw-QE=!KmslynXW{*cL+GWYFcxjXygGJaml#tys7^ zg0^f9wUjthRh8`K5Gru1w~qGJr*pGXD7l|z;w$^ILM<*E&Ezl$6M?1z}^w#QPTm8{cXyctBg@UpYE71UU~&!XQYSW2`gwwa0m~S z+C0ux@Tga>Qm3cx%>yJUjoo_2ZEXz0EAnG{KW5WN^G42FId3QviL4m|g_H%Nk@&D| zLHLiw-US5KP{`t&{@y((*dC#T+%Uclz9=!?=zs)o7j50|hJcCYdv-bCNjH@>flhLS z{mw~=2Bjn|)8SWs_QB_t1rVB0I4BmuucU#4xEWP+E6tSY+Vb`zoJK8eQe|RT)#Ncq z@Xw;3_BlWqPAp#2 z#2tjw2D%AJ9@zF3KFH**!B+YSRNDH*x68H4H*D=IwI6XU;#$^peB_rN+-C&p!Fy6H zC>m5CCj@@%M`X|1;_v=2GH@8#0KI2Ui z;-P|N;}~dAN)QR2!dR4ScpzUgq{=|se%l6{>NILwy9hnq-A`+O&@Ng!7yP{YEP$OQ zK8RmMn`!W^>}9-?+Nl|R6Y)~6rViz+i5G{TywQ_lhJervaijxh#E`n?wt%6!%l=(B zP=Q_ILF(gBhQZo+C0NBX;Yw@|JWr*cAB5fdfiAJ0oxIR}ZKx^hcH{IAhF<|jijRa6 zUCG?m;F=OQwmP`Fc~R8yvQ*XLZYZZzuE4?mlTtyZ@i4fwmhRo<9#A|2_FM$ItuHf~ z*;eT^+IL|!+_}z8z)I)YU__Nh!63eu7zSWxh4W#Tlu7z1qU!N56b99X(i1=o`p*@MCZeJjRQ^miIQ}(blzCQsd=F}8d7b%j+Qp;v+CvZh#=omrd1LgTUOZIrAh^Dy zG580y>^9~nYAy9%N_fqReM55{(T&gc6s-jsiDc4UtvLlx7d3pTcoizkwG@8jQv4P2 z*l`)x#M4%GTpdr+W_X_AujT0@;MgZr@D+v>DvqiWLt!>y?;Dxx>Lmbn^oa-u_fC|F ztIfw?V~R7)liZ1Ox1J=ohzN;h;s)S+JkxxY{W0gCn1uy>b=w_#Be6Z~^Wl~vm&Uad zc8^+09n{okIb@5*b>NclS8CAHTdY2iyyxZ-o`GykT6}--0 zig~zKdnq9FMd*tF-%{7RGG8O!=vN2<*x8q!-I>X}k<~smFdIGO#Sjb9rdA+U!3960 zO}nI?6$pExD;0vifWT7I#pYOiN%C)0NyFIeSBr z?mJ5%CXcSCl{d{xQs33Qj0bvC1?r`8zpIw2hW+cLck`X3tDHNrq2`kZ=Ut{sMt|&C z$IOqE04bRPl8uv>=s^_Twm3E+CgM9>h3P<;0h4OYL2lk7;rlnZD93joF@B89i`B72 z*Tf?Lb`DGLOCwUtTvZF&V5}$=3f2mDuh!gTP>ONzyyclO=4{*%yf`{+ESAy!g!x)7 z)EY>E7Zn5cQF#u>uElJou@e>|+|1YSh4!QltsSRXVeQx8s@)A>9vd5b`}S_*nK)wf z0AdKEL(P|nLPJkhY}H(O?pF!XR^TQV5cI=pq=H8=bmSO~sqFEdV9w$S<0Cvu|ICfuN<^6A91P^2rSz-Ax1UbfwXm#?gO`|IJy&Spo&mZ2Xq5y)o=SXyi>|2E{j zE4Wcnb2LDLJxz24=#bTCs!9%wN7TdtB2hL~#xO8m9c={10y&!9lc|OCYsL>3Uiq{~ zO+4y%FJSqo7~@>8O)tyE1KFaS)x`v)U?0Ca)vK)%uM#&@QUgOKda&@e?Kn$6 z|4KCyqk*6WuydJP5GWQ|gkl)m5fG2Y`j4FCCq8bkubJ~M`gNWwZo7G}yEa2}QwhpQ zfHT(;$eQQ8g0mxT7{YWzDq43we5?(Km2u>vH2>6yX1y3t@KNnPxmDdy9!KFVNhW(^ z(a`hur^t< z0+b1>z+Oi0%k2KOvDC=!{l49r%%r3*^QKMAR>bQ3EcA!&Jb<1195{(g;jOEq;Zq`v zs}tqH;NxyuQ8sbyv(qDyPb#~MYB-kg_I{voTq3xPgkUa*N5f*?_Uo))mzK6Ee4O^E zcDG7VYIeT5XMXizyDpOfJ7`=ZkC~r36lTy=?JQhZJDJR=W9*wxkCQCQ0HHU7pxzO2EDsafmF`)*FB?{A&KEq1}_O;m^U zxd0_C0eK7BkM-eMIhE1yI&QLO3AA7zTtBcnf(1G_)q@M_e>lHrO|N z7rDdPbu+s@?;alrx55lOpS~ZWv4{!pIr_K0+imh>%KVQl&Wux3_ zSpfC3of?NzcJLE09`M?k{i;8*t~uZSw zK_EBBNKO7BS+Uui_@!9g?ol$3JSfPm% zj~12|6HC7!(}RYw?cVF|Hx2@@qayukmAwl(y8Ig6e~Gr2Ukr5S0osOW1Z-r&=Pm@+ zJMe?q;!{v4p$pnWC)t)a^)wU}ITT04ZVHkU;UEg{qhz0}e#ek~ff^^g?~dEnN-=i5 z9>K9}IFx_F$nE&7FFj$#Q+S7B^VrKpk;%PfHcCY3=NO*dM_J8x_6j6;U;E2{IWyYc zpfk~Oup$K*t&I?7gCI%N(FlhI%_MC(GYL>WN&kao*rP}G6Hq+xbYt7JNwEjE8O?< zreDSwPFvnNxHPo)^nF48sQr&wD9y2Vd?7#+*Yvi*T&Nu`7Cr7=HXQkqm@8Cm7r4Sh zg|LA07eW|_Uk590ZeYFmBn)qyw%M!_0kAXCIC!UD@<4i@Tj>k+44aNbfXgbFz$G$J zV>Q3{Si{vD?anWYV6pf!PUrI5HA?(GJ2si7dT!n=uwO6%1bHnEd{F7C0RdOZ*6&iA zAzL-hUBt2n)Q1(r{*NvqeIPwAs=Ff19g~e2y4JN zztZApiuY-mvx?G+oOU~Vk_B_8CJXCOiQ}$h%wbCb51n3Th{C&dGcZ*F*pHqXUzF=C zcx`oUM5g_RvXXVs(fvsnK2Ay->rpl@Ewfn*>|4-cqcCP$B1!o|xc{`edXQW1v06w0 z6gbwfT+a>Qc?Bb2`%b)p4B-w5pf{vR zl+NkpJGFV}BTkkNVv#L;x1d|HmjUd|fv#L=Bd7-lM0`7l{hBFJ6sr<_&9Yp|ZbC@X ziuMJean0qJ!s~#xN^W&0BnXEQ1;)ln(va2IWWXbH9p@{I0L?YkSDj?{4Ysemt3-P? zvda7bM%yi{LJDg zL&IuJ0e==dcNiKAcQz?J+Ahd;-e|T&l_J0C*kzgcQ~#>`4;%&M-qwf$<4@QzAC5g( z_7!+oez*n}(q{ay4a5|?=DT#+zMS4Q_^{VCEjOp`fe?m($916aBQ|xAdG|<{T)GLi zt>9j9sjfPg80Fzhg|pC2=X*tRfoBJCy%e<}X!2IcM(;&efwIe0zRRQ+9jKLH)edt!q*{cWa*-yDNyVJ6`1p}Xhk;FjYRRfP!Vp9Y%O9KF$)ZJ% zA?%Bt_q^fi@~~%06Z*Z9liGeM;E;DVs}lMVsPy4pG5antQ57+cd7fsn=AuOTod+kR z`%j#4v`=T*@>PygNheP?zuz~0v2U}*^OkGE{LK8~$l$>2`ClgB_fqG$<0ODCOr04! zDrVydm0~2T3qiybfeF)t(d#k8g^s23akRMy-|cVj$kUIXx$dMP!`*TenXlCFKp^6H z&zb5L{zpwuZc>2RoxXf+b4(u0J>poF4J62xf5>&Ur4zh#vw4$u+tHQ5xPtf&mr; zYlH}te$m$oVS8-bX zCktg)+iK{}i9~g@>9J&z2*wQ57=HpWP*j&Mo?jV^Rim+`mWfcW53%JFu|55O_RXTh znt^KXykmkl(=7|8D%I|tkiU7+U5AE*uG{OrCye0p*Hjr97g)efDQ@0ytr;wC-^mD;gtrFfhVj<+|HKYPe>wpH z>|n&sr_Kc<=(e2YgKM7fs>H3WjCTYQjSqP7N5pt>}USdaFvM^`om7sh@Dk)C5o zw}H^!u-IX2n9C53y}b|rw8J8PVB^M5(^=hXCTV$$?yY(JlsFSC!%ej0#ku>+Uceql zkXuDpY&|$94|J~4klFFQ;}(@PqOeNiu{g=W{h|#{9e&aNf}b_(@%}TEYs%D@i*Kd&6|%@uiLfo*GwGhotvK;*p-oST3vHL zhE|sZ39UQ#uoa;j`izy6`Kk+TxuI#lMVmoI6D7OEWYF7Q`sBhx@q-fy)U8veRR0RPtG*9ukH1Ic z@jy1(1mF~QDoiI*jS*cl)t?4cAi+0X4G5fZ*+&*#J_qgY^?iu;8iNLrO zaU5cN@gj;#nT-cd#Gp}Whx?%LypThE$;`LuU2*EBC>yYVHI{roU^fw^JdhtKHMn37 zIsO(BK8cepqquCeloDp{(Nx{BCUr5KkK;ab`BC-f7w#F&Gmk9i-jrX_<)a~uXRD)% zkV&accG;AGZLW@OyFIbUsP;Y}{r|XW7!r1HIuM}Ahw%2`=Y?DI84}iSoE4sSho#q< z?Zv)EW=)y{;Rlj7LW5^KOcV$fVtjdya7u|&zS1>tD>K3#Srub57f4I;I=2AqykufL zVjRN@^^J%H>2TFSneZyFW!{({H=eA1pDI(}tXTK$Is=Fpi8jD-Y9-x64DoymA`=$S z^svQPWHC3q%su9dSk9P3;5~KN8XqvqAY-(M>NWhA2901DQ8}B6x+raq2Z)ElXT-(h z(sX7;FW-OK2KaMnusDJT&f`w0_ z)xNR+CEB-aoKRzgvjz=X6qu#`C11N(&XyBOiJ$%{Gg-hYu@j_VS!1yI4K_RRDnIfl z6yz#|=4*xHc<6y82MT$SL;TcWbQIn57uJmcojj z#8cA+Ws+RCBQ?2|CD}dEM4e@)6`t?RZawr==SeEx=lH|#9CLo(&-XL$-{<%Je!id2 zOPxbujbX<|kxFuO+aEkV=DI#?!G8Pa+*o~Ay!D;tNG?t}YG86{B(PDqwq>}GAV>=w zaOfG*ttna-LMd-oB!`07mSa|^s-&zN&BL@A`1vl$y1^VYH_w*$7(ja;r@H_GB zpG{c@gED-#k`|{|i3HrK3W=1qvNn#b{&Zs}>@>x|=?u<|*}eVKhwxX5XV}x{%$>2w zV*67cJaQn{siQ6&-_DlymKa_=WdD1(<;u=E1&$d=W<-x(Qc+R3o@5)lmywg_%&%IF zcoHT;s3P07?Gx8wQ?@tfMo9+rs>1>S^f;o7$%|9=5gbQbV>nz(?-c8p3q$_dDMyqo zQ!CPpsDXp6f1YYcprA>dnDp;Yi}X;4@EBk_3BxJf$~6`}My1Cze`hw&wk{-kSl@7F z{JnCcGmUHR{<0a5$w+n9Hs#C9~=H)c%1=1%QqnBgUehXn4TvJplrz!+rS)AdOtefj`_SW$H2Wy?q z{uCc{3VGxFPBc-(XX4|z7EY%#I;PacoVZ?CQB# zLagcCYO(yNS4j7b_2QC)KdsXEzOA}+u2cFWX}*1g({+gfg`3n-$5Fzbx9UTzE^4^O zd#GTQ<8>9-u0h~P~LQdb$nPzGdQ&5YW6*cr-IU8X=Aii!L^4okEUg}_E}pVO5hThhikB8VyE+C z&JZ$EP^^$;p&tbRYkd@$J}H~+>7-_wm9ssoAX)J zSzQSoEySbvdb~?w<|ayM6Lo6i0fT9ky*DsY+~hr>O;eo*{0o8Yn#)5N-lZD^;GuhT zlkcv3O?q9jhe;XO0Rp1-!8*q2L70Mjx{5! zwKW<^lMLunY%Gc^6xo@Ct~haF(onZGlNHeK)+v5@Gb|Q>xImJNU?ofqOW4g(F)8T0 z6mTh7{Rz^(Wi{R0&UHvH*K@{>gJvi#AmvsJzukK?^&oyvgp+0`;757|JXp7m&)Hkf zzk-NRWCjZAe9l@a1KYJW4(I)5Lsm_iJxkL&(NJ*g(*7pH3P~2Fl}Cu@?>JR}V5#gP zpqFQR1tl+<`7q24_;<`I5TA0=!poos;p}6%iOCDf5`9sT|Y*aFNpYWm1+6!4v zFo`}veg4x{cJ>KFJvk?E@nt+&wpUm?(-{3nk%I%xZ6+u%!>`P{r`zodnEHk7^;?FX zAK2O|E{U~39Y}SX=VW<1PwE{%oJA(La@NjHx4;r&uu5#dC9_`SD1F;nST)6tTVn7? zusRs+2j1!hINc-JsOp8fkMvrjMN#)__5aj&+>JBFP1@XS1yFEc{eJV}w&ZOml5>al z2}A6(b>cGDR4Z*PWjh^DYc!FZcEhAP#9ME*)!8pnvT)26B&069h~K%Q@3Moyb#l3X zOF*l{t+_;))L)L7^0^DgvIKmVjh792DaO3vq6x5F$ChD=igV=toZPTVeA6x%&#Myb zKE*Owv^}d@dub)}L3nemnXgCe&H%O%I$#pJ*XmsS0)9B_n~Dr6Au*S_m7JYYX~X87 ze~JPbfIjCK&Etbj!LinXPJR~=#fg5_ip0L9_l>0dmNnnzZgZ|GpId3nUih=*(Dv|v zdR?jPs}l)I=erzS+O~kyOBQ4BDPGKgTGeMrv<0OBjR=fZh(U2|LUz@8D6GW9@a^=^ z3sDML1u`B7#Y4%>m4tqL0J$vbN5(lEg~!U@U=sfiR;Y@9&+a6C>6urqw5J&-_mMeu z1kN>#$hvaZl%qY5)7{N5ds3BxiM;70(wVo-v<4dQ+xFX5CM>H!Q;G(`&qZ2G!>yJe zum~FuMA2wG5vXngwo~igen39UVBl@^`o3UX@i>eLY^RL`0gV`8B3y!5{_@^g0^8~T zamcuu3&wr?-f_ocOkg|X^;bErmVeC6z$~AQVV1ylX2e(PMf{{^SjM>m48}J-{*x9X zWj#WxKB-42a_5_NNO@;}GYTT9yF|e zZ1*$p`y7bS_ z+DN@W6UqGzi{33E*1Hvm??}wqQF>Rww#)qQGO_i}MyNx4uB{I7xi%BxI5TqJq%s{T z*EuY4#suOxBilD2HHt^anFYEs|F50capwPL-=wmhBlkBfdWU!4#72PqDzr6M8qmI* zI0$o$JpwtUc66NC?hbT6MwSTm%dterZ*tfmvlcssoaQHG6+b5D`qpPbjzfD4#}XiO zd*DmY5{&s@ED@czrZa}Ae4RT0zf(pZv!Feb*M#;=9`p!7%t0YQ%t0YS%t0YR z%t29un1cd9kI-^ZG$7`nXhO_E(Sn$RLWY=wq75+zMF(OI3I$>g3Ke1w3Jqco3LRn& z3Ik#e3ISpc3K3!s3JGEkiW