Play H.264 in the old VfW MKV wrapper (Dragon Ball GT): Orbit's copy of Media3's Matroska reader

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-05 08:14:42 +00:00
committed by Claude
co-authored by Claude Opus 5.5
parent 368b6f7023
commit 6a262dac19
11 changed files with 3486 additions and 3 deletions
@@ -101,7 +101,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl
val player = remember(session, plan) {
if (runningPlayer != null) return@remember runningPlayer
val factory = DataSource.Factory { SecureMediaDataSource(session) }
ExoPlayer.Builder(context, programmeRenderers(context)).setMediaSourceFactory(DefaultMediaSourceFactory(factory))
ExoPlayer.Builder(context, programmeRenderers(context)).setMediaSourceFactory(DefaultMediaSourceFactory(factory, OrbitExtractorsFactory()))
.setSeekBackIncrementMs(10_000).setSeekForwardIncrementMs(10_000).build().apply {
setMediaItem(channelMediaItem(plan))
if (scheduled != null) seekTo(scheduled.programme.offsetAt(scheduled.nowMs()))
@@ -118,7 +118,7 @@ private fun GuideLivePlayer(session: EmbySampleSession, plan: EmbyPlaybackPlan,
val player = remember(session, plan) {
if (runningPlayer != null) return@remember runningPlayer
val factory = DataSource.Factory { SecureMediaDataSource(session) }
ExoPlayer.Builder(context, programmeRenderers(context)).setMediaSourceFactory(DefaultMediaSourceFactory(factory)).build().apply {
ExoPlayer.Builder(context, programmeRenderers(context)).setMediaSourceFactory(DefaultMediaSourceFactory(factory, OrbitExtractorsFactory())).build().apply {
setMediaItem(channelMediaItem(plan))
seekTo(programme.offsetAt(System.currentTimeMillis()))
trackSelectionParameters = trackSelectionParameters.buildUpon().setPreferredAudioLanguage("en").build()
@@ -78,7 +78,7 @@ internal class LoadedNext(val startMs: Long, val plan: EmbyPlaybackPlan, private
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
internal fun loadNextPlayer(context: Context, session: EmbySampleSession, plan: EmbyPlaybackPlan, audio: String?): ExoPlayer {
val factory = DataSource.Factory { SecureMediaDataSource(session) }
return ExoPlayer.Builder(context, programmeRenderers(context)).setMediaSourceFactory(DefaultMediaSourceFactory(factory)).setSeekBackIncrementMs(10_000).setSeekForwardIncrementMs(10_000).build().apply {
return ExoPlayer.Builder(context, programmeRenderers(context)).setMediaSourceFactory(DefaultMediaSourceFactory(factory, OrbitExtractorsFactory())).setSeekBackIncrementMs(10_000).setSeekForwardIncrementMs(10_000).build().apply {
setMediaItem(channelMediaItem(plan))
trackSelectionParameters = trackSelectionParameters.buildUpon().setPreferredAudioLanguage(audio ?: "en").build()
prepare(); playWhenReady = false
@@ -0,0 +1,43 @@
package tv.orbit.classic
import android.net.Uri
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorsFactory
import androidx.media3.extractor.text.DefaultSubtitleParserFactory
import androidx.media3.extractor.text.SubtitleParser
/**
* Media3's own extractors, with its Matroska reader swapped for Orbit's copy ([tv.orbit.classic.mkv.MatroskaExtractor]),
* which also reads H.264 in the old Video for Windows wrapper (Dragon Ball GT's files, 4 November 2026). Everything else,
* subtitles included, is configured exactly as Media3 would.
*/
@OptIn(UnstableApi::class)
internal class OrbitExtractorsFactory : ExtractorsFactory {
private val media3 = DefaultExtractorsFactory()
private var subtitles: SubtitleParser.Factory = DefaultSubtitleParserFactory()
private var transcoding = true
internal fun swap(extractors: Array<Extractor>): Array<Extractor> = extractors.map {
if (it is androidx.media3.extractor.mkv.MatroskaExtractor) tv.orbit.classic.mkv.MatroskaExtractor(subtitles,
if (transcoding) 0 else tv.orbit.classic.mkv.MatroskaExtractor.FLAG_EMIT_RAW_SUBTITLE_DATA) else it
}.toTypedArray()
override fun createExtractors(): Array<Extractor> = swap(media3.createExtractors())
override fun createExtractors(uri: Uri, responseHeaders: Map<String, List<String>>): Array<Extractor> = swap(media3.createExtractors(uri, responseHeaders))
@Deprecated("Media3's own deprecation")
override fun experimentalSetTextTrackTranscodingEnabled(textTrackTranscodingEnabled: Boolean): ExtractorsFactory {
transcoding = textTrackTranscodingEnabled; media3.experimentalSetTextTrackTranscodingEnabled(textTrackTranscodingEnabled); return this
}
override fun setSubtitleParserFactory(subtitleParserFactory: SubtitleParser.Factory): ExtractorsFactory {
subtitles = subtitleParserFactory; media3.setSubtitleParserFactory(subtitleParserFactory); return this
}
override fun experimentalSetCodecsToParseWithinGopSampleDependencies(codecsToParseWithinGopSampleDependencies: Int): ExtractorsFactory {
media3.experimentalSetCodecsToParseWithinGopSampleDependencies(codecsToParseWithinGopSampleDependencies); return this
}
}
@@ -0,0 +1,256 @@
/*
* 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 tv.orbit.classic.mkv;
import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.media3.common.C;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.Assertions;
import androidx.media3.extractor.ExtractorInput;
import java.io.EOFException;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayDeque;
/** Default implementation of {@link EbmlReader}. */
/* package */ final class DefaultEbmlReader implements EbmlReader {
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({ELEMENT_STATE_READ_ID, ELEMENT_STATE_READ_CONTENT_SIZE, ELEMENT_STATE_READ_CONTENT})
private @interface ElementState {}
private static final int ELEMENT_STATE_READ_ID = 0;
private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1;
private static final int ELEMENT_STATE_READ_CONTENT = 2;
private static final int MAX_ID_BYTES = 4;
private static final int MAX_LENGTH_BYTES = 8;
private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
private final byte[] scratch;
private final ArrayDeque<MasterElement> masterElementsStack;
private final VarintReader varintReader;
private EbmlProcessor processor;
private @ElementState int elementState;
private int elementId;
private long elementContentSize;
public DefaultEbmlReader() {
scratch = new byte[8];
masterElementsStack = new ArrayDeque<>();
varintReader = new VarintReader();
}
@Override
public void init(EbmlProcessor processor) {
this.processor = processor;
}
@Override
public void reset() {
elementState = ELEMENT_STATE_READ_ID;
masterElementsStack.clear();
varintReader.reset();
}
@Override
public boolean read(ExtractorInput input) throws IOException {
Assertions.checkStateNotNull(processor);
while (true) {
MasterElement head = masterElementsStack.peek();
if (head != null && input.getPosition() >= head.elementEndPosition) {
processor.endMasterElement(masterElementsStack.pop().elementId);
return true;
}
if (elementState == ELEMENT_STATE_READ_ID) {
long result = varintReader.readUnsignedVarint(input, true, false, MAX_ID_BYTES);
if (result == C.RESULT_MAX_LENGTH_EXCEEDED) {
result = maybeResyncToNextLevel1Element(input);
}
if (result == C.RESULT_END_OF_INPUT) {
return false;
}
// Element IDs are at most 4 bytes, so we can cast to integers.
elementId = (int) result;
elementState = ELEMENT_STATE_READ_CONTENT_SIZE;
}
if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) {
elementContentSize = varintReader.readUnsignedVarint(input, false, true, MAX_LENGTH_BYTES);
elementState = ELEMENT_STATE_READ_CONTENT;
}
@EbmlProcessor.ElementType int type = processor.getElementType(elementId);
switch (type) {
case EbmlProcessor.ELEMENT_TYPE_MASTER:
long elementContentPosition = input.getPosition();
long elementEndPosition = elementContentPosition + elementContentSize;
masterElementsStack.push(new MasterElement(elementId, elementEndPosition));
processor.startMasterElement(elementId, elementContentPosition, elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
return true;
case EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw ParserException.createForMalformedContainer(
"Invalid integer size: " + elementContentSize, /* cause= */ null);
}
processor.integerElement(elementId, readInteger(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case EbmlProcessor.ELEMENT_TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
throw ParserException.createForMalformedContainer(
"Invalid float size: " + elementContentSize, /* cause= */ null);
}
processor.floatElement(elementId, readFloat(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case EbmlProcessor.ELEMENT_TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) {
throw ParserException.createForMalformedContainer(
"String element size: " + elementContentSize, /* cause= */ null);
}
processor.stringElement(elementId, readString(input, (int) elementContentSize));
elementState = ELEMENT_STATE_READ_ID;
return true;
case EbmlProcessor.ELEMENT_TYPE_BINARY:
processor.binaryElement(elementId, (int) elementContentSize, input);
elementState = ELEMENT_STATE_READ_ID;
return true;
case EbmlProcessor.ELEMENT_TYPE_UNKNOWN:
input.skipFully((int) elementContentSize);
elementState = ELEMENT_STATE_READ_ID;
break;
default:
throw ParserException.createForMalformedContainer(
"Invalid element type " + type, /* cause= */ null);
}
}
}
/**
* Does a byte by byte search to try and find the next level 1 element. This method is called if
* some invalid data is encountered in the parser.
*
* @param input The {@link ExtractorInput} from which data has to be read.
* @return id of the next level 1 element that has been found.
* @throws EOFException If the end of input was encountered when searching for the next level 1
* element.
* @throws IOException If an error occurs reading from the input.
*/
private long maybeResyncToNextLevel1Element(ExtractorInput input) throws IOException {
input.resetPeekPosition();
while (true) {
input.peekFully(scratch, 0, MAX_ID_BYTES);
int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]);
if (varintLength != C.LENGTH_UNSET && varintLength <= MAX_ID_BYTES) {
int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false);
if (processor.isLevel1Element(potentialId)) {
input.skipFully(varintLength);
return potentialId;
}
}
input.skipFully(1);
}
}
/**
* Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the integer being read.
* @return The read integer value.
* @throws IOException If an error occurs reading from the input.
*/
private long readInteger(ExtractorInput input, int byteLength) throws IOException {
input.readFully(scratch, 0, byteLength);
long value = 0;
for (int i = 0; i < byteLength; i++) {
value = (value << 8) | (scratch[i] & 0xFF);
}
return value;
}
/**
* Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the float being read.
* @return The read float value.
* @throws IOException If an error occurs reading from the input.
*/
private double readFloat(ExtractorInput input, int byteLength) throws IOException {
long integerValue = readInteger(input, byteLength);
double floatValue;
if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
floatValue = Float.intBitsToFloat((int) integerValue);
} else {
floatValue = Double.longBitsToDouble(integerValue);
}
return floatValue;
}
/**
* Reads a string of length {@code byteLength} from the {@link ExtractorInput}. Zero padding is
* removed, so the returned string may be shorter than {@code byteLength}.
*
* @param input The {@link ExtractorInput} from which to read.
* @param byteLength The length of the string being read, including zero padding.
* @return The read string value.
* @throws IOException If an error occurs reading from the input.
*/
private static String readString(ExtractorInput input, int byteLength) throws IOException {
if (byteLength == 0) {
return "";
}
byte[] stringBytes = new byte[byteLength];
input.readFully(stringBytes, 0, byteLength);
// Remove zero padding.
int trimmedLength = byteLength;
while (trimmedLength > 0 && stringBytes[trimmedLength - 1] == 0) {
trimmedLength--;
}
return new String(stringBytes, 0, trimmedLength);
}
/**
* Used in {@link #masterElementsStack} to track when the current master element ends, so that
* {@link EbmlProcessor#endMasterElement(int)} can be called.
*/
private static final class MasterElement {
private final int elementId;
private final long elementEndPosition;
private MasterElement(int elementId, long elementEndPosition) {
this.elementId = elementId;
this.elementEndPosition = elementEndPosition;
}
}
}
@@ -0,0 +1,159 @@
/*
* Copyright (C) 2019 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 tv.orbit.classic.mkv;
import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.extractor.ExtractorInput;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Defines EBML element IDs/types and processes events. */
@UnstableApi
public interface EbmlProcessor {
/**
* EBML element types. One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link
* #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} or
* {@link #ELEMENT_TYPE_FLOAT}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
ELEMENT_TYPE_UNKNOWN,
ELEMENT_TYPE_MASTER,
ELEMENT_TYPE_UNSIGNED_INT,
ELEMENT_TYPE_STRING,
ELEMENT_TYPE_BINARY,
ELEMENT_TYPE_FLOAT
})
@interface ElementType {}
/** Type for unknown elements. */
int ELEMENT_TYPE_UNKNOWN = 0;
/** Type for elements that contain child elements. */
int ELEMENT_TYPE_MASTER = 1;
/** Type for integer value elements of up to 8 bytes. */
int ELEMENT_TYPE_UNSIGNED_INT = 2;
/** Type for string elements. */
int ELEMENT_TYPE_STRING = 3;
/** Type for binary elements. */
int ELEMENT_TYPE_BINARY = 4;
/** Type for IEEE floating point value elements of either 4 or 8 bytes. */
int ELEMENT_TYPE_FLOAT = 5;
/**
* Maps an element ID to a corresponding type.
*
* <p>If {@link #ELEMENT_TYPE_UNKNOWN} is returned then the element is skipped. Note that all
* children of a skipped element are also skipped.
*
* @param id The element ID to map.
* @return One of {@link #ELEMENT_TYPE_UNKNOWN}, {@link #ELEMENT_TYPE_MASTER}, {@link
* #ELEMENT_TYPE_UNSIGNED_INT}, {@link #ELEMENT_TYPE_STRING}, {@link #ELEMENT_TYPE_BINARY} and
* {@link #ELEMENT_TYPE_FLOAT}.
*/
@ElementType
int getElementType(int id);
/**
* Checks if the given id is that of a level 1 element.
*
* @param id The element ID.
* @return Whether the given id is that of a level 1 element.
*/
boolean isLevel1Element(int id);
/**
* Called when the start of a master element is encountered.
*
* <p>Following events should be considered as taking place within this element until a matching
* call to {@link #endMasterElement(int)} is made.
*
* <p>Note that it is possible for another master element of the same element ID to be nested
* within itself.
*
* @param id The element ID.
* @param contentPosition The position of the start of the element's content in the stream.
* @param contentSize The size of the element's content in bytes.
* @throws ParserException If a parsing error occurs.
*/
void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException;
/**
* Called when the end of a master element is encountered.
*
* @param id The element ID.
* @throws ParserException If a parsing error occurs.
*/
void endMasterElement(int id) throws ParserException;
/**
* Called when an integer element is encountered.
*
* @param id The element ID.
* @param value The integer value that the element contains.
* @throws ParserException If a parsing error occurs.
*/
void integerElement(int id, long value) throws ParserException;
/**
* Called when a float element is encountered.
*
* @param id The element ID.
* @param value The float value that the element contains
* @throws ParserException If a parsing error occurs.
*/
void floatElement(int id, double value) throws ParserException;
/**
* Called when a string element is encountered.
*
* @param id The element ID.
* @param value The string value that the element contains.
* @throws ParserException If a parsing error occurs.
*/
void stringElement(int id, String value) throws ParserException;
/**
* Called when a binary element is encountered.
*
* <p>The element header (containing the element ID and content size) will already have been read.
* Implementations are required to consume the whole remainder of the element, which is {@code
* contentSize} bytes in length, before returning. Implementations are permitted to fail (by
* throwing an exception) having partially consumed the data, however if they do this, they must
* consume the remainder of the content when called again.
*
* @param id The element ID.
* @param contentsSize The element's content size.
* @param input The {@link ExtractorInput} from which data should be read.
* @throws ParserException If a parsing error occurs.
* @throws IOException If an error occurs reading from the input.
*/
void binaryElement(int id, int contentsSize, ExtractorInput input) throws IOException;
}
@@ -0,0 +1,55 @@
/*
* 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 tv.orbit.classic.mkv;
import androidx.media3.common.ParserException;
import androidx.media3.extractor.ExtractorInput;
import java.io.IOException;
/**
* Event-driven EBML reader that delivers events to an {@link EbmlProcessor}.
*
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. It was
* originally designed for the Matroska container format. More information about EBML and Matroska
* is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
*/
/* package */ interface EbmlReader {
/**
* Initializes the extractor with an {@link EbmlProcessor}.
*
* @param processor An {@link EbmlProcessor} to process events.
*/
void init(EbmlProcessor processor);
/**
* Resets the state of the reader.
*
* <p>Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure
* from scratch.
*/
void reset();
/**
* Reads from an {@link ExtractorInput}, invoking an event callback if possible.
*
* @param input The {@link ExtractorInput} from which data should be read.
* @return True if data can continue to be read. False if the end of the input was encountered.
* @throws ParserException If parsing fails.
* @throws IOException If an error occurs reading from the input.
*/
boolean read(ExtractorInput input) throws IOException;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,112 @@
/*
* 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 tv.orbit.classic.mkv;
import androidx.media3.common.C;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import java.io.IOException;
/**
* Utility class that peeks from the input stream in order to determine whether it appears to be
* compatible input for this extractor.
*/
/* package */ final class Sniffer {
/** The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}. */
private static final int SEARCH_LENGTH = 1024;
private static final int ID_EBML = 0x1A45DFA3;
private final ParsableByteArray scratch;
private int peekLength;
public Sniffer() {
scratch = new ParsableByteArray(8);
}
/** See {@link Extractor#sniff(ExtractorInput)}. */
public boolean sniff(ExtractorInput input) throws IOException {
long inputLength = input.getLength();
int bytesToSearch =
(int)
(inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH
? SEARCH_LENGTH
: inputLength);
// Find four bytes equal to ID_EBML near the start of the input.
input.peekFully(scratch.getData(), 0, 4);
long tag = scratch.readUnsignedInt();
peekLength = 4;
while (tag != ID_EBML) {
if (++peekLength == bytesToSearch) {
return false;
}
input.peekFully(scratch.getData(), 0, 1);
tag = (tag << 8) & 0xFFFFFF00;
tag |= scratch.getData()[0] & 0xFF;
}
// Read the size of the EBML header and make sure it is within the stream.
long headerSize = readUint(input);
long headerStart = peekLength;
if (headerSize == Long.MIN_VALUE
|| (inputLength != C.LENGTH_UNSET && headerStart + headerSize >= inputLength)) {
return false;
}
// Read the payload elements in the EBML header.
while (peekLength < headerStart + headerSize) {
long id = readUint(input);
if (id == Long.MIN_VALUE) {
return false;
}
long size = readUint(input);
if (size < 0 || size > Integer.MAX_VALUE) {
return false;
}
if (size != 0) {
int sizeInt = (int) size;
input.advancePeekPosition(sizeInt);
peekLength += sizeInt;
}
}
return peekLength == headerStart + headerSize;
}
/** Peeks a variable-length unsigned EBML integer from the input. */
private long readUint(ExtractorInput input) throws IOException {
input.peekFully(scratch.getData(), 0, 1);
int value = scratch.getData()[0] & 0xFF;
if (value == 0) {
return Long.MIN_VALUE;
}
int mask = 0x80;
int length = 0;
while ((value & mask) == 0) {
mask >>= 1;
length++;
}
value &= ~mask;
input.peekFully(scratch.getData(), 1, length);
for (int i = 0; i < length; i++) {
value <<= 8;
value += scratch.getData()[i + 1] & 0xFF;
}
peekLength += length + 1;
return value;
}
}
@@ -0,0 +1,150 @@
/*
* 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 tv.orbit.classic.mkv;
import androidx.media3.common.C;
import androidx.media3.extractor.ExtractorInput;
import java.io.EOFException;
import java.io.IOException;
/** Reads EBML variable-length integers (varints) from an {@link ExtractorInput}. */
/* package */ final class VarintReader {
private static final int STATE_BEGIN_READING = 0;
private static final int STATE_READ_CONTENTS = 1;
/**
* The first byte of a variable-length integer (varint) will have one of these bit masks
* indicating the total length in bytes.
*
* <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
*/
private static final long[] VARINT_LENGTH_MASKS =
new long[] {0x80L, 0x40L, 0x20L, 0x10L, 0x08L, 0x04L, 0x02L, 0x01L};
private final byte[] scratch;
private int state;
private int length;
public VarintReader() {
scratch = new byte[8];
}
/** Resets the reader to start reading a new variable-length integer. */
public void reset() {
state = STATE_BEGIN_READING;
length = 0;
}
/**
* Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that reading
* can be resumed later if an error occurs having read only some of it.
*
* <p>If an value is successfully read, then the reader will automatically reset itself ready to
* read another value.
*
* <p>If an {@link IOException} is thrown, the read can be resumed later by calling this method
* again, passing an {@link ExtractorInput} providing data starting where the previous one left
* off.
*
* @param input The {@link ExtractorInput} from which the integer should be read.
* @param allowEndOfInput True if encountering the end of the input having read no data is
* allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
* should be considered an error, causing an {@link EOFException} to be thrown.
* @param removeLengthMask Removes the variable-length integer length mask from the value.
* @param maximumAllowedLength Maximum allowed length of the variable integer to be read.
* @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true
* and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the
* length of the varint exceeded maximumAllowedLength.
* @throws IOException If an error occurs reading from the input.
*/
public long readUnsignedVarint(
ExtractorInput input,
boolean allowEndOfInput,
boolean removeLengthMask,
int maximumAllowedLength)
throws IOException {
if (state == STATE_BEGIN_READING) {
// Read the first byte to establish the length.
if (!input.readFully(scratch, 0, 1, allowEndOfInput)) {
return C.RESULT_END_OF_INPUT;
}
int firstByte = scratch[0] & 0xFF;
length = parseUnsignedVarintLength(firstByte);
if (length == C.LENGTH_UNSET) {
throw new IllegalStateException("No valid varint length mask found");
}
state = STATE_READ_CONTENTS;
}
if (length > maximumAllowedLength) {
state = STATE_BEGIN_READING;
return C.RESULT_MAX_LENGTH_EXCEEDED;
}
if (length != 1) {
// Read the remaining bytes.
input.readFully(scratch, 1, length - 1);
}
state = STATE_BEGIN_READING;
return assembleVarint(scratch, length, removeLengthMask);
}
/** Returns the number of bytes occupied by the most recently parsed varint. */
public int getLastLength() {
return length;
}
/**
* Parses and the length of the varint given the first byte.
*
* @param firstByte First byte of the varint.
* @return Length of the varint beginning with the given byte if it was valid, {@link
* C#LENGTH_UNSET} otherwise.
*/
public static int parseUnsignedVarintLength(int firstByte) {
int varIntLength = C.LENGTH_UNSET;
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
varIntLength = i + 1;
break;
}
}
return varIntLength;
}
/**
* Assemble a varint from the given byte array.
*
* @param varintBytes Bytes that make up the varint.
* @param varintLength Length of the varint to assemble.
* @param removeLengthMask Removes the variable-length integer length mask from the value.
* @return Parsed and assembled varint.
*/
public static long assembleVarint(
byte[] varintBytes, int varintLength, boolean removeLengthMask) {
long varint = varintBytes[0] & 0xFFL;
if (removeLengthMask) {
varint &= ~VARINT_LENGTH_MASKS[varintLength - 1];
}
for (int i = 1; i < varintLength; i++) {
varint = (varint << 8) | (varintBytes[i] & 0xFFL);
}
return varint;
}
}
@@ -0,0 +1,29 @@
package tv.orbit.classic
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.nio.ByteBuffer
import java.nio.ByteOrder
/** Orbit's Matroska reader: H.264 in the Video for Windows wrapper gets a picture (Dragon Ball GT, 4 November 2026); nothing else changes. */
class OrbitMatroskaTest {
private fun code(fourCc: String) = ByteBuffer.wrap(fourCc.toByteArray(Charsets.US_ASCII)).order(ByteOrder.LITTLE_ENDIAN).int.toLong() and 0xFFFFFFFFL
@Test fun h264InTheOldWrapperIsReadAsH264() {
for (fourCc in listOf("H264", "h264", "X264", "x264", "AVC1", "avc1", "DAVC", "VSSH")) assertTrue(fourCc, tv.orbit.classic.mkv.MatroskaExtractor.h264FourCc(code(fourCc)))
}
@Test fun otherFourCcsAreLeftToMedia3() {
for (fourCc in listOf("XVID", "DIVX", "H263", "WVC1", "ABCD")) assertFalse(fourCc, tv.orbit.classic.mkv.MatroskaExtractor.h264FourCc(code(fourCc)))
}
@Test fun onlyTheMatroskaReaderIsSwapped() {
val factory = OrbitExtractorsFactory()
val media3 = arrayOf<androidx.media3.extractor.Extractor>(androidx.media3.extractor.mp4.Mp4Extractor(androidx.media3.extractor.text.DefaultSubtitleParserFactory()),
androidx.media3.extractor.mkv.MatroskaExtractor(androidx.media3.extractor.text.DefaultSubtitleParserFactory()))
val ours = factory.swap(media3)
assertTrue(ours[0] === media3[0])
assertTrue(ours[1] is tv.orbit.classic.mkv.MatroskaExtractor)
}
}