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:
committed by
Claude
co-authored by
Claude Opus 5.5
parent
368b6f7023
commit
6a262dac19
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user