Initial hlsmplayer application
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
plugins { id("com.android.library") }
|
||||
|
||||
android {
|
||||
namespace = "androidx.media3.container"
|
||||
compileSdk = 36
|
||||
defaultConfig { minSdk = 23 }
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":lib-common"))
|
||||
implementation(libs.androidx.annotation)
|
||||
compileOnly(libs.checkerframework.qual)
|
||||
compileOnly(libs.errorprone.annotations)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2023 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.
|
||||
-->
|
||||
|
||||
<manifest>
|
||||
<uses-sdk />
|
||||
</manifest>
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 androidx.media3.container;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.ParsableByteArray;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/** Dolby Vision configuration data. */
|
||||
@UnstableApi
|
||||
public final class DolbyVisionConfig {
|
||||
|
||||
/**
|
||||
* Parses Dolby Vision configuration data.
|
||||
*
|
||||
* @param data A {@link ParsableByteArray}, whose position is set to the start of the Dolby Vision
|
||||
* configuration data to parse.
|
||||
* @return The {@link DolbyVisionConfig} corresponding to the configuration, or {@code null} if
|
||||
* the configuration isn't supported.
|
||||
*/
|
||||
@Nullable
|
||||
public static DolbyVisionConfig parse(ParsableByteArray data) {
|
||||
data.skipBytes(2); // dv_version_major, dv_version_minor
|
||||
int profileData = data.readUnsignedByte();
|
||||
int dvProfile = (profileData >> 1);
|
||||
int dvLevel = ((profileData & 0x1) << 5) | ((data.readUnsignedByte() >> 3) & 0x1F);
|
||||
String codecsPrefix;
|
||||
if (dvProfile == 4 || dvProfile == 5 || dvProfile == 7 || dvProfile == 8) {
|
||||
codecsPrefix = "dvhe";
|
||||
} else if (dvProfile == 9) {
|
||||
codecsPrefix = "dvav";
|
||||
} else if (dvProfile == 10) {
|
||||
codecsPrefix = "dav1";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
String codecs =
|
||||
codecsPrefix
|
||||
+ (dvProfile < 10 ? ".0" : ".")
|
||||
+ dvProfile
|
||||
+ (dvLevel < 10 ? ".0" : ".")
|
||||
+ dvLevel;
|
||||
return new DolbyVisionConfig(dvProfile, dvLevel, codecs);
|
||||
}
|
||||
|
||||
/** The profile number. */
|
||||
public final int profile;
|
||||
|
||||
/** The level number. */
|
||||
public final int level;
|
||||
|
||||
/** The RFC 6381 codecs string. */
|
||||
public final String codecs;
|
||||
|
||||
private DolbyVisionConfig(int profile, int level, String codecs) {
|
||||
this.profile = profile;
|
||||
this.level = level;
|
||||
this.codecs = codecs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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 androidx.media3.container;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.util.ParsableByteArray;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format
|
||||
* Specification.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class MdtaMetadataEntry implements Metadata.Entry {
|
||||
|
||||
/** Key for the capture frame rate (in frames per second). */
|
||||
public static final String KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
|
||||
|
||||
// See the MP4 With Auxiliary Tracks Extension (MP4-AT) file format at
|
||||
// https://developer.android.com/media/platform/mp4-at-file-format.
|
||||
/** Key for auxiliary tracks extension box (axte) offset. */
|
||||
public static final String KEY_AUXILIARY_TRACKS_OFFSET = "auxiliary.tracks.offset";
|
||||
|
||||
/** Key for auxiliary tracks extension box (axte) length. */
|
||||
public static final String KEY_AUXILIARY_TRACKS_LENGTH = "auxiliary.tracks.length";
|
||||
|
||||
/** Key for auxiliary tracks map. */
|
||||
public static final String KEY_AUXILIARY_TRACKS_MAP = "auxiliary.tracks.map";
|
||||
|
||||
/** Key for whether auxiliary tracks samples are interleaved. */
|
||||
public static final String KEY_AUXILIARY_TRACKS_INTERLEAVED = "auxiliary.tracks.interleaved";
|
||||
|
||||
/** The auxiliary tracks samples are not interleaved and are in the axte.mdat box. */
|
||||
public static final byte AUXILIARY_TRACKS_SAMPLES_NOT_INTERLEAVED = 0;
|
||||
|
||||
/** The auxiliary tracks samples are interleaved in the primary video track’s mdat box. */
|
||||
public static final byte AUXILIARY_TRACKS_SAMPLES_INTERLEAVED = 1;
|
||||
|
||||
/** The default locale indicator which implies all speakers in all countries. */
|
||||
public static final int DEFAULT_LOCALE_INDICATOR = 0;
|
||||
|
||||
/** The type indicator to use when no type needs to be indicated. */
|
||||
public static final int TYPE_INDICATOR_RESERVED = 0;
|
||||
|
||||
/** The type indicator for UTF-8 string. */
|
||||
public static final int TYPE_INDICATOR_STRING = 1;
|
||||
|
||||
/** The type indicator for Float32. */
|
||||
public static final int TYPE_INDICATOR_FLOAT32 = 23;
|
||||
|
||||
/** The type indicator for 32-bit signed integer. */
|
||||
public static final int TYPE_INDICATOR_INT32 = 67;
|
||||
|
||||
/** The type indicator for an 8-bit unsigned integer. */
|
||||
public static final int TYPE_INDICATOR_8_BIT_UNSIGNED_INT = 75;
|
||||
|
||||
/** The type indicator for 64-bit unsigned integer. */
|
||||
public static final int TYPE_INDICATOR_UNSIGNED_INT64 = 78;
|
||||
|
||||
/** The metadata key name. */
|
||||
public final String key;
|
||||
|
||||
/** The payload. The interpretation of the value depends on {@link #typeIndicator}. */
|
||||
public final byte[] value;
|
||||
|
||||
/** The four byte locale indicator. */
|
||||
public final int localeIndicator;
|
||||
|
||||
/** The four byte type indicator. */
|
||||
public final int typeIndicator;
|
||||
|
||||
/**
|
||||
* Creates a new metadata entry for the specified metadata key/value with {@linkplain
|
||||
* #DEFAULT_LOCALE_INDICATOR default locale indicator}.
|
||||
*/
|
||||
public MdtaMetadataEntry(String key, byte[] value, int typeIndicator) {
|
||||
this(key, value, DEFAULT_LOCALE_INDICATOR, typeIndicator);
|
||||
}
|
||||
|
||||
/** Creates a new metadata entry for the specified metadata key/value. */
|
||||
public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) {
|
||||
validateData(key, value, typeIndicator);
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.localeIndicator = localeIndicator;
|
||||
this.typeIndicator = typeIndicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the auxiliary track types from the {@linkplain #KEY_AUXILIARY_TRACKS_MAP auxiliary
|
||||
* tracks map} metadata.
|
||||
*/
|
||||
public List<Integer> getAuxiliaryTrackTypesFromMap() {
|
||||
checkState(key.equals(KEY_AUXILIARY_TRACKS_MAP), "Metadata is not an auxiliary tracks map");
|
||||
// Value has 1 byte version, 1 byte track count, n bytes track types.
|
||||
int numberOfTracks = value[1];
|
||||
List<Integer> trackTypes = new ArrayList<>();
|
||||
for (int i = 0; i < numberOfTracks; i++) {
|
||||
trackTypes.add(value[i + 2] & 0xFF);
|
||||
}
|
||||
return trackTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MdtaMetadataEntry other = (MdtaMetadataEntry) obj;
|
||||
return key.equals(other.key)
|
||||
&& Arrays.equals(value, other.value)
|
||||
&& localeIndicator == other.localeIndicator
|
||||
&& typeIndicator == other.typeIndicator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + key.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(value);
|
||||
result = 31 * result + localeIndicator;
|
||||
result = 31 * result + typeIndicator;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String formattedValue;
|
||||
switch (typeIndicator) {
|
||||
case TYPE_INDICATOR_STRING:
|
||||
formattedValue = Util.fromUtf8Bytes(value);
|
||||
break;
|
||||
case TYPE_INDICATOR_FLOAT32:
|
||||
formattedValue = String.valueOf(Float.intBitsToFloat(Ints.fromByteArray(value)));
|
||||
break;
|
||||
case TYPE_INDICATOR_INT32:
|
||||
formattedValue = String.valueOf(Ints.fromByteArray(value));
|
||||
break;
|
||||
case TYPE_INDICATOR_8_BIT_UNSIGNED_INT:
|
||||
formattedValue = String.valueOf(Byte.toUnsignedInt(value[0]));
|
||||
break;
|
||||
case TYPE_INDICATOR_UNSIGNED_INT64:
|
||||
formattedValue = String.valueOf(new ParsableByteArray(value).readUnsignedLongToLong());
|
||||
break;
|
||||
case TYPE_INDICATOR_RESERVED:
|
||||
if (key.equals(KEY_AUXILIARY_TRACKS_MAP)) {
|
||||
formattedValue = getFormattedValueForAuxiliaryTracksMap(getAuxiliaryTrackTypesFromMap());
|
||||
break;
|
||||
}
|
||||
// fall through
|
||||
default:
|
||||
formattedValue = Util.toHexString(value);
|
||||
}
|
||||
|
||||
return "mdta: key=" + key + ", value=" + formattedValue;
|
||||
}
|
||||
|
||||
private static void validateData(String key, byte[] value, int typeIndicator) {
|
||||
switch (key) {
|
||||
case KEY_ANDROID_CAPTURE_FPS:
|
||||
checkArgument(typeIndicator == TYPE_INDICATOR_FLOAT32 && value.length == 4);
|
||||
break;
|
||||
case KEY_AUXILIARY_TRACKS_OFFSET:
|
||||
case KEY_AUXILIARY_TRACKS_LENGTH:
|
||||
checkArgument(typeIndicator == TYPE_INDICATOR_UNSIGNED_INT64 && value.length == 8);
|
||||
break;
|
||||
case KEY_AUXILIARY_TRACKS_MAP:
|
||||
checkArgument(typeIndicator == TYPE_INDICATOR_RESERVED);
|
||||
break;
|
||||
case KEY_AUXILIARY_TRACKS_INTERLEAVED:
|
||||
checkArgument(
|
||||
typeIndicator == TYPE_INDICATOR_8_BIT_UNSIGNED_INT
|
||||
&& value.length == 1
|
||||
&& (value[0] == AUXILIARY_TRACKS_SAMPLES_NOT_INTERLEAVED
|
||||
|| value[0] == AUXILIARY_TRACKS_SAMPLES_INTERLEAVED));
|
||||
break;
|
||||
default:
|
||||
// Ignore custom keys.
|
||||
}
|
||||
}
|
||||
|
||||
private static String getFormattedValueForAuxiliaryTracksMap(List<Integer> trackTypes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("track types = ");
|
||||
Joiner.on(',').appendTo(sb, trackTypes);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.container;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/** Stores MP4 {@code alternate_group} info parsed from a {@code tkhd} box. */
|
||||
@UnstableApi
|
||||
public final class Mp4AlternateGroupData implements Metadata.Entry {
|
||||
|
||||
public final int alternateGroup;
|
||||
|
||||
public Mp4AlternateGroupData(int alternateGroup) {
|
||||
this.alternateGroup = alternateGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof Mp4AlternateGroupData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Mp4AlternateGroupData other = (Mp4AlternateGroupData) obj;
|
||||
return alternateGroup == other.alternateGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return alternateGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Mp4AlternateGroup: " + alternateGroup;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
/*
|
||||
* 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 androidx.media3.container;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.ParsableByteArray;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/** A representation of an MP4 box (aka atom). */
|
||||
@SuppressWarnings("ConstantField")
|
||||
@UnstableApi
|
||||
public abstract class Mp4Box {
|
||||
|
||||
/** Size of a box header, in bytes. */
|
||||
public static final int HEADER_SIZE = 8;
|
||||
|
||||
/** Size of a full box header, in bytes. */
|
||||
public static final int FULL_HEADER_SIZE = 12;
|
||||
|
||||
/** Size of a long box header, in bytes. */
|
||||
public static final int LONG_HEADER_SIZE = 16;
|
||||
|
||||
/** Value for the size field in a box that defines its size in the largesize field. */
|
||||
public static final int DEFINES_LARGE_SIZE = 1;
|
||||
|
||||
/** Value for the size field in a box that extends to the end of the file. */
|
||||
public static final int EXTENDS_TO_END_SIZE = 0;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_ftyp = 0x66747970;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_apv1 = 0x61707631;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_apvC = 0x61707643;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_avc1 = 0x61766331;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_avc3 = 0x61766333;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_avcC = 0x61766343;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_hvc1 = 0x68766331;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_hev1 = 0x68657631;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_hvcC = 0x68766343;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_lhvC = 0x6C687643;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_vvc1 = 0x76766331;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_vvi1 = 0x76766931;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_vvcC = 0x76766343;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_vexu = 0x76657875;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_eyes = 0x65796573;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stri = 0x73747269;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_vp08 = 0x76703038;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_vp09 = 0x76703039;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_vpcC = 0x76706343;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_av01 = 0x61763031;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_av1C = 0x61763143;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_colr = 0x636f6c72;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dvav = 0x64766176;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dva1 = 0x64766131;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dvhe = 0x64766865;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dvh1 = 0x64766831;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dvcC = 0x64766343;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dav1 = 0x64617631;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dvvC = 0x64767643;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dvwC = 0x64767743;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_s263 = 0x73323633;
|
||||
|
||||
public static final int TYPE_H263 = 0x48323633;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_h263 = 0x68323633;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_d263 = 0x64323633;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mdat = 0x6d646174;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mp4a = 0x6d703461;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE__mp2 = 0x2e6d7032;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE__mp3 = 0x2e6d7033;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mha1 = 0x6d686131;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mhm1 = 0x6d686d31;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mhaC = 0x6d686143;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mhaP = 0x6d686150;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_wave = 0x77617665;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_lpcm = 0x6c70636d;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_sowt = 0x736f7774;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_ac_3 = 0x61632d33;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dac3 = 0x64616333;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_ec_3 = 0x65632d33;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dec3 = 0x64656333;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_ac_4 = 0x61632d34;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dac4 = 0x64616334;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mlpa = 0x6d6c7061;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dmlp = 0x646d6c70;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dtsc = 0x64747363;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dtsh = 0x64747368;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dtsl = 0x6474736c;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dtse = 0x64747365;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dtsx = 0x64747378;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_ddts = 0x64647473;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_udts = 0x75647473;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_tfdt = 0x74666474;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_tfhd = 0x74666864;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_tfra = 0x74667261;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_mfro = 0x6d66726f;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_mfra = 0x6d667261;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_trex = 0x74726578;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_trun = 0x7472756e;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_sidx = 0x73696478;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_moov = 0x6d6f6f76;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mpvd = 0x6d707664;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mvhd = 0x6d766864;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_trak = 0x7472616b;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mdia = 0x6d646961;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_minf = 0x6d696e66;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stbl = 0x7374626c;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_esds = 0x65736473;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_btrt = 0x62747274;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_moof = 0x6d6f6f66;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_traf = 0x74726166;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mvex = 0x6d766578;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mehd = 0x6d656864;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_tkhd = 0x746b6864;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_edts = 0x65647473;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_elst = 0x656c7374;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mdhd = 0x6d646864;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_hdlr = 0x68646c72;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stsd = 0x73747364;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_pssh = 0x70737368;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_sinf = 0x73696e66;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_schm = 0x7363686d;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_schi = 0x73636869;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_tenc = 0x74656e63;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_encv = 0x656e6376;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_enca = 0x656e6361;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_frma = 0x66726d61;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_saiz = 0x7361697a;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_saio = 0x7361696f;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_sbgp = 0x73626770;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_sgpd = 0x73677064;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_uuid = 0x75756964;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_senc = 0x73656e63;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_pasp = 0x70617370;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_TTML = 0x54544d4c;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_m1v_ = 0x6d317620;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mp4v = 0x6d703476;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mp4s = 0x6D703473;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stts = 0x73747473;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stss = 0x73747373;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_ctts = 0x63747473;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stsc = 0x73747363;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stsz = 0x7374737a;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stz2 = 0x73747a32;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stco = 0x7374636f;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_co64 = 0x636f3634;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_tx3g = 0x74783367;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_wvtt = 0x77767474;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_stpp = 0x73747070;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_c608 = 0x63363038;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_samr = 0x73616d72;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_sawb = 0x73617762;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_udta = 0x75647461;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_meta = 0x6d657461;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_xyz = 0xa978797a;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_smta = 0x736d7461;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_chpl = 0x6368706c;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_tref = 0x74726566;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_chap = 0x63686170;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_saut = 0x73617574;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_srfr = 0x73726672;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_keys = 0x6b657973;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_ilst = 0x696c7374;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mean = 0x6d65616e;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_name = 0x6e616d65;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_data = 0x64617461;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_emsg = 0x656d7367;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_st3d = 0x73743364;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_sv3d = 0x73763364;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_proj = 0x70726f6a;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_camm = 0x63616d6d;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mett = 0x6d657474;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_it35 = 0x69743335;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_alac = 0x616c6163;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_alaw = 0x616c6177;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_ulaw = 0x756c6177;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_Opus = 0x4f707573;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dOps = 0x644f7073;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_fLaC = 0x664c6143;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dfLa = 0x64664c61;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_twos = 0x74776f73;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_clli = 0x636c6c69;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_mdcv = 0x6d646376;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_iamf = 0x69616d66;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_iacb = 0x69616362;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_axte = 0x61787465;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_ipcm = 0x6970636d;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_fpcm = 0x6670636d;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_pcmC = 0x70636d43;
|
||||
|
||||
@SuppressWarnings({"ConstantCaseForConstants", "IdentifierName"})
|
||||
public static final int TYPE_free = 0x66726565;
|
||||
|
||||
public final int type;
|
||||
|
||||
// private to only allow sub-classing from within this file.
|
||||
private Mp4Box(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getBoxTypeString(type);
|
||||
}
|
||||
|
||||
/** An MP4 box that is a leaf. */
|
||||
public static final class LeafBox extends Mp4Box {
|
||||
|
||||
/** The box data. */
|
||||
public final ParsableByteArray data;
|
||||
|
||||
/**
|
||||
* @param type The type of the box.
|
||||
* @param data The box data.
|
||||
*/
|
||||
public LeafBox(int type, ParsableByteArray data) {
|
||||
super(type);
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/** An MP4 box that has child boxes. */
|
||||
public static final class ContainerBox extends Mp4Box {
|
||||
|
||||
public final long endPosition;
|
||||
public final List<LeafBox> leafChildren;
|
||||
public final List<ContainerBox> containerChildren;
|
||||
|
||||
/**
|
||||
* @param type The type of the box.
|
||||
* @param endPosition The position of the first byte after the end of the box.
|
||||
*/
|
||||
public ContainerBox(int type, long endPosition) {
|
||||
super(type);
|
||||
this.endPosition = endPosition;
|
||||
leafChildren = new ArrayList<>();
|
||||
containerChildren = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child leaf to this container.
|
||||
*
|
||||
* @param box The child to add.
|
||||
*/
|
||||
public void add(LeafBox box) {
|
||||
leafChildren.add(box);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child container to this container.
|
||||
*
|
||||
* @param box The child to add.
|
||||
*/
|
||||
public void add(ContainerBox box) {
|
||||
containerChildren.add(box);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child leaf of the given type.
|
||||
*
|
||||
* <p>If no child exists with the given type then null is returned. If multiple children exist
|
||||
* with the given type then the first one to have been added is returned.
|
||||
*
|
||||
* @param type The leaf type.
|
||||
* @return The child leaf of the given type, or null if no such child exists.
|
||||
*/
|
||||
@Nullable
|
||||
public LeafBox getLeafBoxOfType(int type) {
|
||||
int childrenSize = leafChildren.size();
|
||||
for (int i = 0; i < childrenSize; i++) {
|
||||
LeafBox box = leafChildren.get(i);
|
||||
if (box.type == type) {
|
||||
return box;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child container of the given type.
|
||||
*
|
||||
* <p>If no child exists with the given type then null is returned. If multiple children exist
|
||||
* with the given type then the first one to have been added is returned.
|
||||
*
|
||||
* @param type The container type.
|
||||
* @return The child container of the given type, or null if no such child exists.
|
||||
*/
|
||||
@Nullable
|
||||
public ContainerBox getContainerBoxOfType(int type) {
|
||||
int childrenSize = containerChildren.size();
|
||||
for (int i = 0; i < childrenSize; i++) {
|
||||
ContainerBox box = containerChildren.get(i);
|
||||
if (box.type == type) {
|
||||
return box;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getBoxTypeString(type)
|
||||
+ " leaves: "
|
||||
+ Arrays.toString(leafChildren.toArray())
|
||||
+ " containers: "
|
||||
+ Arrays.toString(containerChildren.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a numeric box type to the corresponding four character string.
|
||||
*
|
||||
* @param type The numeric box type.
|
||||
* @return The corresponding four character string.
|
||||
*/
|
||||
public static String getBoxTypeString(int type) {
|
||||
return ""
|
||||
+ (char) ((type >> 24) & 0xFF)
|
||||
+ (char) ((type >> 16) & 0xFF)
|
||||
+ (char) ((type >> 8) & 0xFF)
|
||||
+ (char) (type & 0xFF);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 androidx.media3.container;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import androidx.annotation.FloatRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.primitives.Floats;
|
||||
|
||||
/**
|
||||
* Stores MP4 location data.
|
||||
*
|
||||
* <p>The location data is typically read/written in the "udta" box (user data box, defined in
|
||||
* ISO/IEC 14496-12).
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class Mp4LocationData implements Metadata.Entry {
|
||||
|
||||
/** The latitude, in degrees. */
|
||||
public final float latitude;
|
||||
|
||||
/** The longitude, in degrees. */
|
||||
public final float longitude;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param latitude The latitude, in degrees. Its value must be in the range [-90, 90].
|
||||
* @param longitude The longitude, in degrees. Its value must be in the range [-180, 180].
|
||||
*/
|
||||
public Mp4LocationData(
|
||||
@FloatRange(from = -90.0, to = 90.0) float latitude,
|
||||
@FloatRange(from = -180.0, to = 180.0) float longitude) {
|
||||
checkArgument(
|
||||
latitude >= -90.0f && latitude <= 90.0f && longitude >= -180.0f && longitude <= 180.0f,
|
||||
"Invalid latitude or longitude");
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Mp4LocationData other = (Mp4LocationData) obj;
|
||||
return latitude == other.latitude && longitude == other.longitude;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + Floats.hashCode(latitude);
|
||||
result = 31 * result + Floats.hashCode(longitude);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "xyz: latitude=" + latitude + ", longitude=" + longitude;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2024 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 androidx.media3.container;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/**
|
||||
* Stores the orientation hint for the video playback.
|
||||
*
|
||||
* <p>The orientation hint is typically read/written in the "tkhd" box (track header box, defined in
|
||||
* ISO/IEC 14496-12).
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class Mp4OrientationData implements Metadata.Entry {
|
||||
|
||||
/** The orientation, in degrees. */
|
||||
public final int orientation;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param orientation The orientation, in degrees. The supported values are 0, 90, 180 and 270
|
||||
* (degrees).
|
||||
*/
|
||||
public Mp4OrientationData(int orientation) {
|
||||
checkArgument(
|
||||
orientation == 0 || orientation == 90 || orientation == 180 || orientation == 270,
|
||||
"Unsupported orientation");
|
||||
this.orientation = orientation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof Mp4OrientationData)) {
|
||||
return false;
|
||||
}
|
||||
Mp4OrientationData other = (Mp4OrientationData) obj;
|
||||
return orientation == other.orientation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + Integer.hashCode(orientation);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Orientation= " + orientation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2023 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 androidx.media3.container;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
/** Stores MP4 timestamp data. */
|
||||
@UnstableApi
|
||||
public final class Mp4TimestampData implements Metadata.Entry {
|
||||
/** Represents an unset or unknown timescale. */
|
||||
public static final int TIMESCALE_UNSET = -1;
|
||||
|
||||
/**
|
||||
* The delta between a Unix epoch timestamp (in milliseconds since midnight, January 1, 1970) and
|
||||
* an MP4 timestamp (in seconds since midnight, January 1, 1904).
|
||||
*/
|
||||
private static final int UNIX_EPOCH_TO_MP4_TIME_DELTA_SECONDS =
|
||||
((1970 - 1904) * 365 + 17 /* leap year */) * (24 * 60 * 60);
|
||||
|
||||
/** The creation timestamp. */
|
||||
public final long creationTimestampSeconds;
|
||||
|
||||
/** The modification timestamp. */
|
||||
public final long modificationTimestampSeconds;
|
||||
|
||||
/** The timescale of the movie. */
|
||||
public final long timescale;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>The {@link #timescale} is set to {@link Mp4TimestampData#TIMESCALE_UNSET}.
|
||||
*
|
||||
* @param creationTimestampSeconds The creation time UTC in seconds since midnight, January 1,
|
||||
* 1904.
|
||||
* @param modificationTimestampSeconds The modification time UTC in seconds since midnight,
|
||||
* January 1, 1904.
|
||||
*/
|
||||
public Mp4TimestampData(long creationTimestampSeconds, long modificationTimestampSeconds) {
|
||||
this.creationTimestampSeconds = creationTimestampSeconds;
|
||||
this.modificationTimestampSeconds = modificationTimestampSeconds;
|
||||
this.timescale = TIMESCALE_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param creationTimestampSeconds The creation time UTC in seconds since midnight, January 1,
|
||||
* 1904.
|
||||
* @param modificationTimestampSeconds The modification time UTC in seconds since midnight,
|
||||
* January 1, 1904.
|
||||
* @param timescale The timescale of the movie.
|
||||
*/
|
||||
public Mp4TimestampData(
|
||||
long creationTimestampSeconds, long modificationTimestampSeconds, long timescale) {
|
||||
this.creationTimestampSeconds = creationTimestampSeconds;
|
||||
this.modificationTimestampSeconds = modificationTimestampSeconds;
|
||||
this.timescale = timescale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an MP4 timestamp (in seconds since midnight, January 1, 1904) from a Unix epoch
|
||||
* timestamp (in milliseconds since midnight, January 1, 1970).
|
||||
*/
|
||||
public static long unixTimeToMp4TimeSeconds(long unixTimestampMs) {
|
||||
return (unixTimestampMs / 1_000L) + UNIX_EPOCH_TO_MP4_TIME_DELTA_SECONDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!(obj instanceof Mp4TimestampData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Mp4TimestampData other = (Mp4TimestampData) obj;
|
||||
|
||||
return creationTimestampSeconds == other.creationTimestampSeconds
|
||||
&& modificationTimestampSeconds == other.modificationTimestampSeconds
|
||||
&& timescale == other.timescale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + Longs.hashCode(creationTimestampSeconds);
|
||||
result = 31 * result + Longs.hashCode(modificationTimestampSeconds);
|
||||
result = 31 * result + Longs.hashCode(timescale);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Mp4Timestamp: creation time="
|
||||
+ creationTimestampSeconds
|
||||
+ ", modification time="
|
||||
+ modificationTimestampSeconds
|
||||
+ ", timescale="
|
||||
+ timescale;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* Copyright 2025 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.container;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.ParsableBitArray;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility methods for parsing AV1 Open Bitstream Units or OBUs.
|
||||
*
|
||||
* <p><a href=https://aomediacodec.github.io/av1-spec/>AV1 Bitstream and Decoding Process
|
||||
* Specification</a>
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class ObuParser {
|
||||
|
||||
/** OBU type sequence header. */
|
||||
public static final int OBU_SEQUENCE_HEADER = 1;
|
||||
|
||||
/** OBU type temporal delimiter. */
|
||||
public static final int OBU_TEMPORAL_DELIMITER = 2;
|
||||
|
||||
/** OBU type frame header. */
|
||||
public static final int OBU_FRAME_HEADER = 3;
|
||||
|
||||
/** OBU type metadata. */
|
||||
public static final int OBU_METADATA = 5;
|
||||
|
||||
/** OBU type frame. */
|
||||
public static final int OBU_FRAME = 6;
|
||||
|
||||
/** OBU type padding. */
|
||||
public static final int OBU_PADDING = 15;
|
||||
|
||||
/** Open bitstream unit. */
|
||||
public static final class Obu {
|
||||
|
||||
/** The OBU type. See {@code obu_type} in the AV1 spec. */
|
||||
public final int type;
|
||||
|
||||
/** The OBU header. */
|
||||
public final ByteBuffer header;
|
||||
|
||||
/** The OBU data, excluding the header. */
|
||||
public final ByteBuffer payload;
|
||||
|
||||
private Obu(int type, ByteBuffer header, ByteBuffer payload) {
|
||||
this.type = type;
|
||||
this.header = header;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the input sample into a list of OBUs.
|
||||
*
|
||||
* <p>Expects the AV1 sample format specified by the <a
|
||||
* href=https://aomediacodec.github.io/av1-isobmff/#sampleformat>AV1 Codec ISO Media File Format
|
||||
* Binding</a>. That is, each OBU has the {@code obu_has_size_field} set to 1 except for the last
|
||||
* OBU in the sample, for which {@code obu_has_size_field} may be set to 0.
|
||||
*
|
||||
* <p>If the provided sample is truncated, only returns the OBUs that are fully contained in the
|
||||
* sample.
|
||||
*
|
||||
* @param sample The sample data.
|
||||
* @return The list of OBUs contained within the sample data.
|
||||
*/
|
||||
public static List<Obu> split(ByteBuffer sample) {
|
||||
// Create a read-only buffer that shares content with the sample to avoid modifying the
|
||||
// input buffer's position, mark or limit.
|
||||
ByteBuffer readOnlySample = sample.asReadOnlyBuffer();
|
||||
List<Obu> obuList = new ArrayList<>();
|
||||
while (readOnlySample.hasRemaining()) {
|
||||
int obuType;
|
||||
int obuSize;
|
||||
ByteBuffer header = readOnlySample.duplicate();
|
||||
try {
|
||||
int headerByte = readOnlySample.get();
|
||||
obuType = (headerByte >> 3) & 0xF;
|
||||
int extensionFlag = (headerByte >> 2) & 0x1;
|
||||
if (extensionFlag != 0) {
|
||||
readOnlySample.get(); // skip obu_extension_header()
|
||||
}
|
||||
int obuHasSizeField = (headerByte >> 1) & 0x1;
|
||||
if (obuHasSizeField != 0) {
|
||||
obuSize = leb128(readOnlySample);
|
||||
} else {
|
||||
// Only the last sample is allowed to have obu_has_size_field == 0, and the size is
|
||||
// assumed to fill the remainder of the sample.
|
||||
obuSize = readOnlySample.remaining();
|
||||
}
|
||||
} catch (BufferUnderflowException ignored) {
|
||||
// Intentionally ignoring this exception because this method supports truncated input
|
||||
// which means the contents are cut off.
|
||||
// ByteBuffer reading fails with underflow exception if the input sample is truncated.
|
||||
break;
|
||||
}
|
||||
if (readOnlySample.position() + obuSize > readOnlySample.limit()) {
|
||||
// The input sample was truncated and doesn't hold the full OBU.
|
||||
break;
|
||||
}
|
||||
header.limit(readOnlySample.position());
|
||||
ByteBuffer payload = readOnlySample.duplicate();
|
||||
payload.limit(readOnlySample.position() + obuSize);
|
||||
obuList.add(new Obu(obuType, header, payload));
|
||||
readOnlySample.position(readOnlySample.position() + obuSize);
|
||||
}
|
||||
return obuList;
|
||||
}
|
||||
|
||||
private static int leb128(ByteBuffer data) {
|
||||
int value = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int leb128Byte = data.get();
|
||||
value |= ((leb128Byte & 0x7F) << (i * 7));
|
||||
if ((leb128Byte & 0x80) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** An AV1 Sequence header. */
|
||||
public static final class SequenceHeader {
|
||||
/** See {@code reduced_still_picture_header}. */
|
||||
public final boolean reducedStillPictureHeader;
|
||||
|
||||
/** See {@code decoder_model_info_present_flag}. */
|
||||
public final boolean decoderModelInfoPresentFlag;
|
||||
|
||||
/** See {@code frame_id_numbers_present_flag}. */
|
||||
public final boolean frameIdNumbersPresentFlag;
|
||||
|
||||
/** See {@code seq_force_screen_content_tools}. */
|
||||
public final boolean seqForceScreenContentTools;
|
||||
|
||||
/** See {@code seq_force_integer_mv}. */
|
||||
public final boolean seqForceIntegerMv;
|
||||
|
||||
/** See {@code OrderHintBits}. */
|
||||
public final int orderHintBits;
|
||||
|
||||
/** See {@code seq_profile}. */
|
||||
public final int seqProfile;
|
||||
|
||||
/** See {@code seq_level_idx}. */
|
||||
public final int seqLevelIdx0;
|
||||
|
||||
/** See {@code seq_tier}. */
|
||||
public final int seqTier0;
|
||||
|
||||
/** See {@code initial_display_delay_present}. */
|
||||
public final boolean initialDisplayDelayPresentFlag;
|
||||
|
||||
/** See {@code initial_display_delay_minus_one}. */
|
||||
public final int initialDisplayDelayMinus1;
|
||||
|
||||
/** See {@code high_bitdepth}. */
|
||||
public final boolean highBitdepth;
|
||||
|
||||
/** See {@code twelve_bit}. */
|
||||
public final boolean twelveBit;
|
||||
|
||||
/** See {@code mono_chrome}. */
|
||||
public final boolean monochrome;
|
||||
|
||||
/** See {@code subsampling_x}. */
|
||||
public final boolean subsamplingX;
|
||||
|
||||
/** See {@code subsampling_Y}. */
|
||||
public final boolean subsamplingY;
|
||||
|
||||
/** See {@code chroma_sample_position}. */
|
||||
public final int chromaSamplePosition;
|
||||
|
||||
/** See {@code color_primaries}. */
|
||||
public final byte colorPrimaries;
|
||||
|
||||
/** See {@code transfer_characteristics}. */
|
||||
public final byte transferCharacteristics;
|
||||
|
||||
/** See {@code matrix_coefficients}. */
|
||||
public final byte matrixCoefficients;
|
||||
|
||||
/**
|
||||
* Returns a {@link SequenceHeader} parsed from the input OBU, or {@code null} if the AV1
|
||||
* bitstream is not yet supported.
|
||||
*
|
||||
* @param obu The input OBU with type {@link #OBU_SEQUENCE_HEADER}.
|
||||
*/
|
||||
@Nullable
|
||||
public static SequenceHeader parse(Obu obu) {
|
||||
try {
|
||||
return new SequenceHeader(obu);
|
||||
} catch (NotYetImplementedException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses a {@link #OBU_SEQUENCE_HEADER} and creates an instance. */
|
||||
private SequenceHeader(Obu obu) throws NotYetImplementedException {
|
||||
int seqLevelIdx0 = 0;
|
||||
int seqTier0 = 0;
|
||||
int initialDisplayDelayMinus1 = 0;
|
||||
checkArgument(obu.type == OBU_SEQUENCE_HEADER);
|
||||
byte[] data = new byte[obu.payload.remaining()];
|
||||
// Do not modify obu.payload while reading it.
|
||||
obu.payload.asReadOnlyBuffer().get(data);
|
||||
ParsableBitArray obuData = new ParsableBitArray(data);
|
||||
seqProfile = obuData.readBits(3);
|
||||
obuData.skipBit(); // still_picture
|
||||
reducedStillPictureHeader = obuData.readBit();
|
||||
if (reducedStillPictureHeader) {
|
||||
seqLevelIdx0 = obuData.readBits(5);
|
||||
decoderModelInfoPresentFlag = false;
|
||||
initialDisplayDelayPresentFlag = false;
|
||||
} else {
|
||||
boolean timingInfoPresentFlag = obuData.readBit();
|
||||
if (timingInfoPresentFlag) {
|
||||
skipTimingInfo(obuData);
|
||||
decoderModelInfoPresentFlag = obuData.readBit();
|
||||
if (decoderModelInfoPresentFlag) {
|
||||
// skip decoder_model_info()
|
||||
obuData.skipBits(47);
|
||||
}
|
||||
} else {
|
||||
decoderModelInfoPresentFlag = false;
|
||||
}
|
||||
initialDisplayDelayPresentFlag = obuData.readBit();
|
||||
int operatingPointsCntMinus1 = obuData.readBits(5);
|
||||
for (int i = 0; i <= operatingPointsCntMinus1; i++) {
|
||||
obuData.skipBits(12); // operating_point_idc[ i ]
|
||||
if (i == 0) {
|
||||
seqLevelIdx0 = obuData.readBits(5);
|
||||
if (seqLevelIdx0 > 7) {
|
||||
seqTier0 = obuData.readBit() ? 1 : 0;
|
||||
}
|
||||
} else {
|
||||
int seqLevelIdx = obuData.readBits(5);
|
||||
if (seqLevelIdx > 7) {
|
||||
obuData.skipBit(); // seq_tier[ i ]
|
||||
}
|
||||
}
|
||||
if (decoderModelInfoPresentFlag) {
|
||||
obuData.skipBit(); // decoder_model_present_for_this_op
|
||||
}
|
||||
if (initialDisplayDelayPresentFlag) {
|
||||
boolean initialDisplayDelayPresentForThisOpFlag = obuData.readBit();
|
||||
if (initialDisplayDelayPresentForThisOpFlag) {
|
||||
if (i == 0) {
|
||||
initialDisplayDelayMinus1 = obuData.readBits(4);
|
||||
} else {
|
||||
obuData.skipBits(4); // initial_display_delay_minus_1[ i ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int frameWidthBitsMinus1 = obuData.readBits(4);
|
||||
int frameHeightBitsMinus1 = obuData.readBits(4);
|
||||
obuData.skipBits(frameWidthBitsMinus1 + 1); // max_frame_width_minus_1
|
||||
obuData.skipBits(frameHeightBitsMinus1 + 1); // max_frame_height_minus_1
|
||||
if (!reducedStillPictureHeader) {
|
||||
frameIdNumbersPresentFlag = obuData.readBit();
|
||||
} else {
|
||||
frameIdNumbersPresentFlag = false;
|
||||
}
|
||||
if (frameIdNumbersPresentFlag) {
|
||||
obuData.skipBits(4); // delta_frame_id_length_minus_2
|
||||
obuData.skipBits(3); // additional_frame_id_length_minus_1
|
||||
}
|
||||
// use_128x128_superblock, enable_filter_intra, and enable_intra_edge_filter
|
||||
obuData.skipBits(3);
|
||||
if (reducedStillPictureHeader) {
|
||||
seqForceIntegerMv = true;
|
||||
seqForceScreenContentTools = true;
|
||||
orderHintBits = 0;
|
||||
} else {
|
||||
// enable_interintra_compound, enable_masked_compound, enable_warped_motion, and
|
||||
// enable_dual_filter
|
||||
obuData.skipBits(4);
|
||||
boolean enableOrderHint = obuData.readBit();
|
||||
if (enableOrderHint) {
|
||||
obuData.skipBits(2); // enable_jnt_comp and enable_ref_frame_mvs
|
||||
}
|
||||
boolean seqChooseScreenContentTools = obuData.readBit();
|
||||
if (seqChooseScreenContentTools) {
|
||||
seqForceScreenContentTools = true;
|
||||
} else {
|
||||
seqForceScreenContentTools = obuData.readBit();
|
||||
}
|
||||
if (seqForceScreenContentTools) {
|
||||
boolean seqChooseIntegerMv = obuData.readBit();
|
||||
if (seqChooseIntegerMv) {
|
||||
seqForceIntegerMv = true;
|
||||
} else {
|
||||
seqForceIntegerMv = obuData.readBit();
|
||||
}
|
||||
} else {
|
||||
seqForceIntegerMv = true;
|
||||
}
|
||||
if (enableOrderHint) {
|
||||
int orderHintBitsMinus1 = obuData.readBits(3);
|
||||
orderHintBits = orderHintBitsMinus1 + 1;
|
||||
} else {
|
||||
orderHintBits = 0;
|
||||
}
|
||||
}
|
||||
this.seqLevelIdx0 = seqLevelIdx0;
|
||||
this.seqTier0 = seqTier0;
|
||||
this.initialDisplayDelayMinus1 = initialDisplayDelayMinus1;
|
||||
// enable_superres, enable_cdef, enable_restoration
|
||||
obuData.skipBits(3);
|
||||
// Begin Color Config
|
||||
highBitdepth = obuData.readBit();
|
||||
if (seqProfile == 2 && highBitdepth) {
|
||||
twelveBit = obuData.readBit();
|
||||
} else {
|
||||
twelveBit = false;
|
||||
}
|
||||
if (seqProfile != 1) {
|
||||
monochrome = obuData.readBit();
|
||||
} else {
|
||||
monochrome = false;
|
||||
}
|
||||
boolean colorDescriptionPresent = obuData.readBit();
|
||||
if (colorDescriptionPresent) {
|
||||
colorPrimaries = (byte) obuData.readBits(8);
|
||||
transferCharacteristics = (byte) obuData.readBits(8);
|
||||
matrixCoefficients = (byte) obuData.readBits(8);
|
||||
} else {
|
||||
colorPrimaries = 0;
|
||||
transferCharacteristics = 0;
|
||||
matrixCoefficients = 0;
|
||||
}
|
||||
if (monochrome) {
|
||||
obuData.skipBit(); // color_range
|
||||
subsamplingX = false;
|
||||
subsamplingY = false;
|
||||
chromaSamplePosition = 0;
|
||||
} else if (colorPrimaries == 0x1 /* CP_BT_709 */
|
||||
&& transferCharacteristics == 13 /* TC_SRGB */
|
||||
&& matrixCoefficients == 0x0 /* MC_IDENTITY */) {
|
||||
// Nothing to read from obu.
|
||||
subsamplingX = false;
|
||||
subsamplingY = false;
|
||||
chromaSamplePosition = 0;
|
||||
} else {
|
||||
obuData.skipBit(); // color_range
|
||||
if (seqProfile == 0) {
|
||||
subsamplingX = true;
|
||||
subsamplingY = true;
|
||||
} else if (seqProfile == 1) {
|
||||
subsamplingX = false;
|
||||
subsamplingY = false;
|
||||
} else {
|
||||
if (twelveBit) {
|
||||
subsamplingX = obuData.readBit();
|
||||
if (subsamplingX) {
|
||||
subsamplingY = obuData.readBit();
|
||||
} else {
|
||||
subsamplingY = false;
|
||||
}
|
||||
} else {
|
||||
subsamplingX = true;
|
||||
subsamplingY = false;
|
||||
}
|
||||
}
|
||||
if (subsamplingX && subsamplingY) {
|
||||
chromaSamplePosition = obuData.readBits(2);
|
||||
} else {
|
||||
chromaSamplePosition = 0;
|
||||
}
|
||||
}
|
||||
obuData.skipBit(); // separate_uv_delta_q
|
||||
}
|
||||
|
||||
/** Advances the bit array by skipping the {@code timing_info()} syntax element. */
|
||||
private static void skipTimingInfo(ParsableBitArray parsableBitArray) {
|
||||
parsableBitArray.skipBits(64); // num_units_in_display_tick and time_scale
|
||||
boolean equalPictureInterval = parsableBitArray.readBit();
|
||||
if (equalPictureInterval) {
|
||||
skipUvlc(parsableBitArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Advances the bit array by skipping the {@code uvlc()} process. */
|
||||
private static void skipUvlc(ParsableBitArray parsableBitArray) {
|
||||
int leadingZeros = 0;
|
||||
while (true) {
|
||||
boolean done = parsableBitArray.readBit();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
leadingZeros++;
|
||||
}
|
||||
// 32 or more leading zeros returns (1 << 32) - 1 from the uvlc() process without reading more
|
||||
// bits.
|
||||
if (leadingZeros < 32) {
|
||||
parsableBitArray.skipBits(leadingZeros);
|
||||
}
|
||||
}
|
||||
|
||||
/** An AV1 Frame Header. */
|
||||
public static final class FrameHeader {
|
||||
private static final int PROBE_BYTES = 4;
|
||||
|
||||
private static final int FRAME_TYPE_KEY_FRAME = 0;
|
||||
private static final int FRAME_TYPE_INTRA_ONLY_FRAME = 2;
|
||||
private static final int FRAME_TYPE_SWITCH_FRAME = 3;
|
||||
|
||||
private final boolean isDependedOn;
|
||||
|
||||
/** Returns whether the frame header is depended on by subsequent frames. */
|
||||
public boolean isDependedOn() {
|
||||
return isDependedOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link FrameHeader} parsed from the input OBU, or {@code null} if the AV1 bitstream
|
||||
* is not yet supported.
|
||||
*
|
||||
* @param sequenceHeader The most recent sequence header before the frame header.
|
||||
* @param obu The input OBU with type {@link #OBU_FRAME} or {@link #OBU_FRAME_HEADER}.
|
||||
*/
|
||||
@Nullable
|
||||
public static FrameHeader parse(SequenceHeader sequenceHeader, Obu obu) {
|
||||
try {
|
||||
return new FrameHeader(sequenceHeader, obu);
|
||||
} catch (NotYetImplementedException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private FrameHeader(SequenceHeader sequenceHeader, Obu obu) throws NotYetImplementedException {
|
||||
checkArgument(obu.type == OBU_FRAME || obu.type == OBU_FRAME_HEADER);
|
||||
byte[] bytes = new byte[min(PROBE_BYTES, obu.payload.remaining())];
|
||||
// Do not modify obu.payload while reading it.
|
||||
obu.payload.asReadOnlyBuffer().get(bytes);
|
||||
ParsableBitArray obuData = new ParsableBitArray(bytes);
|
||||
throwWhenFeatureRequired(sequenceHeader.reducedStillPictureHeader);
|
||||
boolean showExistingFrame = obuData.readBit();
|
||||
if (showExistingFrame) {
|
||||
isDependedOn = false;
|
||||
return;
|
||||
}
|
||||
int frameType = obuData.readBits(2);
|
||||
boolean showFrame = obuData.readBit();
|
||||
throwWhenFeatureRequired(sequenceHeader.decoderModelInfoPresentFlag);
|
||||
if (!showFrame) {
|
||||
// show_frame equal to 0 specifies that this frame should not be immediately output.
|
||||
// If a frame is output later, then it is depended on.
|
||||
isDependedOn = true;
|
||||
return;
|
||||
}
|
||||
boolean errorResilientMode;
|
||||
if (frameType == FRAME_TYPE_SWITCH_FRAME || (frameType == FRAME_TYPE_KEY_FRAME)) {
|
||||
errorResilientMode = true;
|
||||
} else {
|
||||
errorResilientMode = obuData.readBit();
|
||||
}
|
||||
obuData.skipBit(); // disable_cdf_update
|
||||
throwWhenFeatureRequired(!sequenceHeader.seqForceScreenContentTools);
|
||||
boolean allowScreenContentTools = obuData.readBit();
|
||||
if (allowScreenContentTools) {
|
||||
throwWhenFeatureRequired(!sequenceHeader.seqForceIntegerMv);
|
||||
obuData.skipBit(); // force_integer_mv
|
||||
}
|
||||
throwWhenFeatureRequired(sequenceHeader.frameIdNumbersPresentFlag);
|
||||
if (frameType != FRAME_TYPE_SWITCH_FRAME) {
|
||||
obuData.skipBit(); // frame_size_override_flag
|
||||
}
|
||||
obuData.skipBits(sequenceHeader.orderHintBits); // order_hint
|
||||
if (frameType != FRAME_TYPE_INTRA_ONLY_FRAME
|
||||
&& frameType != FRAME_TYPE_KEY_FRAME
|
||||
&& !errorResilientMode) {
|
||||
obuData.skipBits(3); // primary_ref_frame
|
||||
}
|
||||
int refreshFrameFlags;
|
||||
if (frameType == FRAME_TYPE_SWITCH_FRAME || (frameType == FRAME_TYPE_KEY_FRAME)) {
|
||||
refreshFrameFlags = (1 << 8) - 1;
|
||||
} else {
|
||||
refreshFrameFlags = obuData.readBits(8);
|
||||
}
|
||||
isDependedOn = refreshFrameFlags != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** An AV1 Metadata OBU */
|
||||
public static final class Metadata {
|
||||
/** Metadata type HDR Content Light Level. */
|
||||
public static final int METADATA_TYPE_HDR_CLL = 1;
|
||||
|
||||
/** Metadata type HDR Mastering Display Color Volume. */
|
||||
public static final int METADATA_TYPE_HDR_MDCV = 2;
|
||||
|
||||
/** Metadata type Scalability. */
|
||||
public static final int METADATA_TYPE_SCALABILITY = 3;
|
||||
|
||||
/** Metadata type ITUT T35. */
|
||||
public static final int METADATA_TYPE_ITUT_T35 = 4;
|
||||
|
||||
/** Metadata type Timecode. */
|
||||
public static final int METADATA_TYPE_TIMECODE = 5;
|
||||
|
||||
/** The Metadata type. See {@code metadata_type} in the AV1 spec. */
|
||||
public final int type;
|
||||
|
||||
/** The Metadata payload. */
|
||||
public final ByteBuffer payload;
|
||||
|
||||
/**
|
||||
* Returns a {@link Metadata} parsed from the input OBU.
|
||||
*
|
||||
* @param obu The input OBU with type {@link #OBU_METADATA}.
|
||||
*/
|
||||
public static Metadata parse(Obu obu) {
|
||||
checkArgument(obu.type == OBU_METADATA);
|
||||
ByteBuffer buffer = obu.payload.asReadOnlyBuffer();
|
||||
int type = leb128(buffer);
|
||||
return new Metadata(type, buffer);
|
||||
}
|
||||
|
||||
private Metadata(int type, ByteBuffer payload) {
|
||||
this.type = type;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
/** Full AV1 bitstream parsing is not yet implemented. */
|
||||
private static void throwWhenFeatureRequired(boolean expression)
|
||||
throws NotYetImplementedException {
|
||||
if (expression) {
|
||||
throw new NotYetImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private static class NotYetImplementedException extends Exception {}
|
||||
|
||||
private ObuParser() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright (C) 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 androidx.media3.container;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Utility methods for handling Opus audio streams. */
|
||||
@UnstableApi
|
||||
public class OpusUtil {
|
||||
|
||||
/** Opus streams are always 48000 Hz. */
|
||||
public static final int SAMPLE_RATE = 48_000;
|
||||
|
||||
/** Maximum achievable Opus bitrate. */
|
||||
public static final int MAX_BYTES_PER_SECOND = 510 * 1000 / 8; // See RFC 6716. Section 2.1.1
|
||||
|
||||
private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
|
||||
private static final int FULL_CODEC_INITIALIZATION_DATA_BUFFER_COUNT = 3;
|
||||
|
||||
private OpusUtil() {} // Prevents instantiation.
|
||||
|
||||
/**
|
||||
* Parses the channel count from an Opus Identification Header.
|
||||
*
|
||||
* @param header An Opus Identification Header, as defined by RFC 7845.
|
||||
* @return The parsed channel count.
|
||||
*/
|
||||
public static int getChannelCount(byte[] header) {
|
||||
return header[9] & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds codec initialization data from an Opus Identification Header.
|
||||
*
|
||||
* @param header An Opus Identification Header, as defined by RFC 7845.
|
||||
* @return Codec initialization data suitable for an Opus <a
|
||||
* href="https://developer.android.com/reference/android/media/MediaCodec#initialization">MediaCodec</a>.
|
||||
*/
|
||||
public static List<byte[]> buildInitializationData(byte[] header) {
|
||||
int preSkipSamples = getPreSkipSamples(header);
|
||||
long preSkipNanos = sampleCountToNanoseconds(preSkipSamples);
|
||||
long seekPreRollNanos = sampleCountToNanoseconds(DEFAULT_SEEK_PRE_ROLL_SAMPLES);
|
||||
|
||||
List<byte[]> initializationData = new ArrayList<>(FULL_CODEC_INITIALIZATION_DATA_BUFFER_COUNT);
|
||||
initializationData.add(header);
|
||||
initializationData.add(buildNativeOrderByteArray(preSkipNanos));
|
||||
initializationData.add(buildNativeOrderByteArray(seekPreRollNanos));
|
||||
return initializationData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of audio samples in the given Ogg encapuslated Opus packet.
|
||||
*
|
||||
* <p>The buffer's position is not modified.
|
||||
*
|
||||
* @param buffer The audio packet.
|
||||
* @return Returns the number of audio samples in the packet.
|
||||
*/
|
||||
public static int parseOggPacketAudioSampleCount(ByteBuffer buffer) {
|
||||
// RFC 3433 section 6 - The Ogg page format.
|
||||
int preAudioPacketByteCount = parseOggPacketForPreAudioSampleByteCount(buffer);
|
||||
int numPageSegments = buffer.get(/* index= */ 26 + preAudioPacketByteCount);
|
||||
// Skip Ogg header + segment table.
|
||||
int indexFirstOpusPacket = 27 + numPageSegments + preAudioPacketByteCount;
|
||||
long packetDurationUs =
|
||||
getPacketDurationUs(
|
||||
buffer.get(indexFirstOpusPacket),
|
||||
buffer.limit() - indexFirstOpusPacket > 1 ? buffer.get(indexFirstOpusPacket + 1) : 0);
|
||||
return (int) (packetDurationUs * SAMPLE_RATE / C.MICROS_PER_SECOND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the offset from the start of the buffer to audio sample Ogg packets.
|
||||
*
|
||||
* @param buffer containing the Ogg Encapsulated Opus audio bitstream.
|
||||
* @return the offset before the Ogg packet containing audio samples.
|
||||
*/
|
||||
public static int parseOggPacketForPreAudioSampleByteCount(ByteBuffer buffer) {
|
||||
// Parse Ogg Packet Type from Header at index 5
|
||||
if ((buffer.get(/* index= */ 5) & 0x02) == 0) {
|
||||
// Ogg Page packet header type is not beginning of logical stream. Must be an Audio page.
|
||||
return 0;
|
||||
}
|
||||
// ID Header Page size is Ogg packet header size + sum(lacing values: 1..number_page_segments).
|
||||
int idHeaderPageSize = 28;
|
||||
int idHeaderPageNumOfSegments = buffer.get(/* index= */ 26);
|
||||
for (int i = 0; i < idHeaderPageNumOfSegments; i++) {
|
||||
idHeaderPageSize += buffer.get(/* index= */ 27 + i);
|
||||
}
|
||||
// Comment Header Page size is Ogg packet header size + sum(lacing values:
|
||||
// 1..number_page_segments).
|
||||
int commentHeaderPageSize = 28;
|
||||
int commentHeaderPageSizeNumOfSegments = buffer.get(/* index= */ idHeaderPageSize + 26);
|
||||
for (int i = 0; i < commentHeaderPageSizeNumOfSegments; i++) {
|
||||
commentHeaderPageSize += buffer.get(/* index= */ idHeaderPageSize + 27 + i);
|
||||
}
|
||||
return idHeaderPageSize + commentHeaderPageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of audio samples in the given audio packet.
|
||||
*
|
||||
* <p>The buffer's position is not modified.
|
||||
*
|
||||
* @param buffer The audio packet.
|
||||
* @return Returns the number of audio samples in the packet.
|
||||
*/
|
||||
public static int parsePacketAudioSampleCount(ByteBuffer buffer) {
|
||||
long packetDurationUs =
|
||||
getPacketDurationUs(buffer.get(0), buffer.limit() > 1 ? buffer.get(1) : 0);
|
||||
return (int) (packetDurationUs * SAMPLE_RATE / C.MICROS_PER_SECOND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration of the given audio packet.
|
||||
*
|
||||
* @param buffer The audio packet.
|
||||
* @return Returns the duration of the given audio packet, in microseconds.
|
||||
*/
|
||||
public static long getPacketDurationUs(byte[] buffer) {
|
||||
return getPacketDurationUs(buffer[0], buffer.length > 1 ? buffer[1] : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of pre-skip samples specified by the given Opus codec initialization data.
|
||||
*
|
||||
* @param header The Opus Identification header.
|
||||
* @return The number of pre-skip samples.
|
||||
*/
|
||||
public static int getPreSkipSamples(byte[] header) {
|
||||
return ((header[11] & 0xFF) << 8) | (header[10] & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an Opus frame should be sent to the decoder as it is either past the start
|
||||
* position or within the seek-preroll duration.
|
||||
*
|
||||
* <p>The measure of whether an Opus frame should not be decoded is if its time precedes the start
|
||||
* position by more than the default seek-preroll value.
|
||||
*
|
||||
* @param startTimeUs The time to start playing at.
|
||||
* @param frameTimeUs The time of the Opus sample.
|
||||
* @return Whether the frame should be decoded.
|
||||
*/
|
||||
public static boolean needToDecodeOpusFrame(long startTimeUs, long frameTimeUs) {
|
||||
// Divide by 1000 in rhs value to convert nanoseconds to microseconds.
|
||||
return (startTimeUs - frameTimeUs)
|
||||
<= (sampleCountToNanoseconds(DEFAULT_SEEK_PRE_ROLL_SAMPLES) / 1000);
|
||||
}
|
||||
|
||||
private static long getPacketDurationUs(byte packetByte0, byte packetByte1) {
|
||||
// See RFC6716, Sections 3.1 and 3.2.
|
||||
int toc = packetByte0 & 0xFF;
|
||||
int frames;
|
||||
switch (toc & 0x3) {
|
||||
case 0:
|
||||
frames = 1;
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
frames = 2;
|
||||
break;
|
||||
default:
|
||||
frames = packetByte1 & 0x3F;
|
||||
break;
|
||||
}
|
||||
|
||||
int config = toc >> 3;
|
||||
int length = config & 0x3;
|
||||
int frameDurationUs;
|
||||
if (config >= 16) {
|
||||
frameDurationUs = 2500 << length;
|
||||
} else if (config >= 12) {
|
||||
frameDurationUs = 10000 << (length & 0x1);
|
||||
} else if (length == 3) {
|
||||
frameDurationUs = 60000;
|
||||
} else {
|
||||
frameDurationUs = 10000 << length;
|
||||
}
|
||||
return (long) frames * frameDurationUs;
|
||||
}
|
||||
|
||||
private static byte[] buildNativeOrderByteArray(long value) {
|
||||
return ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(value).array();
|
||||
}
|
||||
|
||||
private static long sampleCountToNanoseconds(long sampleCount) {
|
||||
return (sampleCount * C.NANOS_PER_SECOND) / SAMPLE_RATE;
|
||||
}
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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 androidx.media3.container;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/**
|
||||
* Wraps a byte array, providing methods that allow it to be read as a NAL unit bitstream.
|
||||
*
|
||||
* <p>Whenever the byte sequence [0, 0, 3] appears in the wrapped byte array, it is treated as [0,
|
||||
* 0] for all reading/skipping operations, which makes the bitstream appear to be unescaped.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class ParsableNalUnitBitArray {
|
||||
|
||||
private byte[] data;
|
||||
private int byteLimit;
|
||||
// The start offset which defines the range of data to be considered.
|
||||
private int startOffset;
|
||||
|
||||
// The byte offset is never equal to the offset of the 3rd byte in a subsequence [0, 0, 3].
|
||||
private int byteOffset;
|
||||
private int bitOffset;
|
||||
|
||||
/**
|
||||
* @param data The data to wrap.
|
||||
* @param offset The byte offset in {@code data} to start reading from.
|
||||
* @param limit The byte offset of the end of the bitstream in {@code data}.
|
||||
*/
|
||||
@SuppressWarnings({"initialization.fields.uninitialized", "nullness:method.invocation"})
|
||||
public ParsableNalUnitBitArray(byte[] data, int offset, int limit) {
|
||||
reset(data, offset, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the wrapped data, limit and offset.
|
||||
*
|
||||
* @param data The data to wrap.
|
||||
* @param offset The byte offset in {@code data} to start reading from.
|
||||
* @param limit The byte offset of the end of the bitstream in {@code data}.
|
||||
*/
|
||||
public void reset(byte[] data, int offset, int limit) {
|
||||
this.data = data;
|
||||
startOffset = offset;
|
||||
byteOffset = offset;
|
||||
byteLimit = limit;
|
||||
bitOffset = 0;
|
||||
assertValidOffset();
|
||||
}
|
||||
|
||||
/** Skips a single bit. */
|
||||
public void skipBit() {
|
||||
if (++bitOffset == 8) {
|
||||
bitOffset = 0;
|
||||
byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
|
||||
}
|
||||
assertValidOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips bits and moves current reading position forward.
|
||||
*
|
||||
* @param numBits The number of bits to skip.
|
||||
*/
|
||||
public void skipBits(int numBits) {
|
||||
int oldByteOffset = byteOffset;
|
||||
int numBytes = numBits / 8;
|
||||
byteOffset += numBytes;
|
||||
bitOffset += numBits - (numBytes * 8);
|
||||
if (bitOffset > 7) {
|
||||
byteOffset++;
|
||||
bitOffset -= 8;
|
||||
}
|
||||
for (int i = oldByteOffset + 1; i <= byteOffset; i++) {
|
||||
if (shouldSkipByte(i)) {
|
||||
// Skip the byte and move forward to check three bytes ahead.
|
||||
byteOffset++;
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
assertValidOffset();
|
||||
}
|
||||
|
||||
/** Skips bits until at a byte alignment; if already byte aligned, then simply returns. */
|
||||
public void byteAlign() {
|
||||
if (bitOffset > 0) {
|
||||
skipBits(8 - bitOffset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether it's possible to read {@code n} bits starting from the current offset. The
|
||||
* offset is not modified.
|
||||
*
|
||||
* @param numBits The number of bits.
|
||||
* @return Whether it is possible to read {@code n} bits.
|
||||
*/
|
||||
public boolean canReadBits(int numBits) {
|
||||
int oldByteOffset = byteOffset;
|
||||
int numBytes = numBits / 8;
|
||||
int newByteOffset = byteOffset + numBytes;
|
||||
int newBitOffset = bitOffset + numBits - (numBytes * 8);
|
||||
if (newBitOffset > 7) {
|
||||
newByteOffset++;
|
||||
newBitOffset -= 8;
|
||||
}
|
||||
for (int i = oldByteOffset + 1; i <= newByteOffset && newByteOffset <= byteLimit; i++) {
|
||||
if (shouldSkipByte(i)) {
|
||||
// Skip the byte and move forward to check three bytes ahead.
|
||||
newByteOffset++;
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
return newByteOffset < byteLimit || (newByteOffset == byteLimit && newBitOffset == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single bit.
|
||||
*
|
||||
* @return Whether the bit is set.
|
||||
*/
|
||||
public boolean readBit() {
|
||||
boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0;
|
||||
skipBit();
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads up to 32 bits.
|
||||
*
|
||||
* @param numBits The number of bits to read.
|
||||
* @return An integer whose bottom n bits hold the read data.
|
||||
*/
|
||||
public int readBits(int numBits) {
|
||||
int returnValue = 0;
|
||||
bitOffset += numBits;
|
||||
while (bitOffset > 8) {
|
||||
bitOffset -= 8;
|
||||
returnValue |= (data[byteOffset] & 0xFF) << bitOffset;
|
||||
byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
|
||||
}
|
||||
returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
|
||||
returnValue &= 0xFFFFFFFF >>> (32 - numBits);
|
||||
if (bitOffset == 8) {
|
||||
bitOffset = 0;
|
||||
byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1;
|
||||
}
|
||||
assertValidOffset();
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether it is possible to read an Exp-Golomb-coded integer starting from the current
|
||||
* offset. The offset is not modified.
|
||||
*
|
||||
* @return Whether it is possible to read an Exp-Golomb-coded integer.
|
||||
*/
|
||||
public boolean canReadExpGolombCodedNum() {
|
||||
int initialByteOffset = byteOffset;
|
||||
int initialBitOffset = bitOffset;
|
||||
int leadingZeros = 0;
|
||||
while (byteOffset < byteLimit && !readBit()) {
|
||||
leadingZeros++;
|
||||
}
|
||||
boolean hitLimit = byteOffset == byteLimit;
|
||||
byteOffset = initialByteOffset;
|
||||
bitOffset = initialBitOffset;
|
||||
return !hitLimit && canReadBits(leadingZeros * 2 + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned Exp-Golomb-coded format integer.
|
||||
*
|
||||
* @return The value of the parsed Exp-Golomb-coded integer.
|
||||
*/
|
||||
public int readUnsignedExpGolombCodedInt() {
|
||||
return readExpGolombCodeNum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an signed Exp-Golomb-coded format integer.
|
||||
*
|
||||
* @return The value of the parsed Exp-Golomb-coded integer.
|
||||
*/
|
||||
public int readSignedExpGolombCodedInt() {
|
||||
int codeNum = readExpGolombCodeNum();
|
||||
return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);
|
||||
}
|
||||
|
||||
private int readExpGolombCodeNum() {
|
||||
int leadingZeros = 0;
|
||||
while (!readBit()) {
|
||||
leadingZeros++;
|
||||
}
|
||||
return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
|
||||
}
|
||||
|
||||
private boolean shouldSkipByte(int offset) {
|
||||
// Do not read outside of [startOffset, byteLimit) bounds.
|
||||
return startOffset <= offset - 2
|
||||
&& offset < byteLimit
|
||||
&& data[offset] == (byte) 0x03
|
||||
&& data[offset - 2] == (byte) 0x00
|
||||
&& data[offset - 1] == (byte) 0x00;
|
||||
}
|
||||
|
||||
private void assertValidOffset() {
|
||||
// It is fine for position to be at the end of the array, but no further.
|
||||
checkState(
|
||||
byteOffset >= 0 && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.container;
|
||||
|
||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
||||
import static androidx.media3.common.util.Util.castNonNull;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.ParsableByteArray;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.PriorityQueue;
|
||||
|
||||
/** A queue of buffers, ordered by presentation timestamp. */
|
||||
@RestrictTo(LIBRARY_GROUP)
|
||||
public final class ReorderingBufferQueue {
|
||||
|
||||
/** Functional interface to handle a buffer that is being removed from the queue. */
|
||||
public interface OutputConsumer {
|
||||
/** Handles a buffer that is being removed from the queue. */
|
||||
void consume(long presentationTimeUs, ParsableByteArray buffer);
|
||||
}
|
||||
|
||||
private final OutputConsumer outputConsumer;
|
||||
|
||||
/** Pool of re-usable {@link ParsableByteArray} objects to avoid repeated allocations. */
|
||||
private final ArrayDeque<ParsableByteArray> unusedParsableByteArrays;
|
||||
|
||||
/** Pool of re-usable {@link BuffersWithTimestamp} objects to avoid repeated allocations. */
|
||||
private final ArrayDeque<BuffersWithTimestamp> unusedBuffersWithTimestamp;
|
||||
|
||||
private final PriorityQueue<BuffersWithTimestamp> pendingBuffers;
|
||||
|
||||
private int reorderingQueueSize;
|
||||
@Nullable private BuffersWithTimestamp lastQueuedBuffer;
|
||||
|
||||
/**
|
||||
* Creates an instance, initially with no max size.
|
||||
*
|
||||
* @param outputConsumer Callback to invoke when buffers are removed from the head of the queue,
|
||||
* either due to exceeding the {@linkplain #setMaxSize(int) max queue size} during a call to
|
||||
* {@link #add(long, ParsableByteArray)}, or due to {@link #flush()}.
|
||||
*/
|
||||
public ReorderingBufferQueue(OutputConsumer outputConsumer) {
|
||||
this.outputConsumer = outputConsumer;
|
||||
unusedParsableByteArrays = new ArrayDeque<>();
|
||||
unusedBuffersWithTimestamp = new ArrayDeque<>();
|
||||
pendingBuffers = new PriorityQueue<>();
|
||||
reorderingQueueSize = C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the max size of the re-ordering queue.
|
||||
*
|
||||
* <p>The size is defined in terms of the number of unique presentation timestamps, rather than
|
||||
* the number of buffers. This ensures that properties like H.264's {@code
|
||||
* max_number_reorder_frames} can be used to set this max size in the case of multiple SEI
|
||||
* messages per sample (where multiple SEI messages therefore have the same presentation
|
||||
* timestamp).
|
||||
*
|
||||
* <p>When the queue exceeds this size during a call to {@link #add(long, ParsableByteArray)}, the
|
||||
* buffers associated with the least timestamp are passed to the {@link OutputConsumer} provided
|
||||
* during construction.
|
||||
*
|
||||
* <p>If the new size is larger than the number of elements currently in the queue, items are
|
||||
* removed from the head of the queue (least first) and passed to the {@link OutputConsumer}
|
||||
* provided during construction.
|
||||
*/
|
||||
public void setMaxSize(int reorderingQueueSize) {
|
||||
checkState(reorderingQueueSize >= 0);
|
||||
this.reorderingQueueSize = reorderingQueueSize;
|
||||
flushQueueDownToSize(reorderingQueueSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum size of this queue, or {@link C#LENGTH_UNSET} if it is unbounded.
|
||||
*
|
||||
* <p>See {@link #setMaxSize(int)} for details on how size is defined.
|
||||
*/
|
||||
public int getMaxSize() {
|
||||
return reorderingQueueSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a buffer to the queue.
|
||||
*
|
||||
* <p>If this causes the queue to exceed its {@linkplain #setMaxSize(int) max size}, buffers
|
||||
* associated with the least timestamp (which may be the buffer passed to this method) are passed
|
||||
* to the {@link OutputConsumer} provided during construction.
|
||||
*
|
||||
* <p>buffers with matching timestamps must be added consecutively (this will naturally happen
|
||||
* when parsing buffers from a container).
|
||||
*
|
||||
* @param presentationTimeUs The presentation time of the buffer. {@link C#TIME_UNSET} will cause
|
||||
* the buffer to be emitted immediately.
|
||||
* @param buffer The buffer data. The data will be copied, so the provided object can be re-used
|
||||
* after this method returns.
|
||||
*/
|
||||
public void add(long presentationTimeUs, ParsableByteArray buffer) {
|
||||
if (presentationTimeUs == C.TIME_UNSET
|
||||
|| reorderingQueueSize == 0
|
||||
|| (reorderingQueueSize != C.LENGTH_UNSET
|
||||
&& pendingBuffers.size() >= reorderingQueueSize
|
||||
&& presentationTimeUs < castNonNull(pendingBuffers.peek()).presentationTimeUs)) {
|
||||
outputConsumer.consume(presentationTimeUs, buffer);
|
||||
return;
|
||||
}
|
||||
// Make a local copy of the buffer data so we can store it in the queue and allow the buffer
|
||||
// parameter to be safely re-used after this add() method returns.
|
||||
ParsableByteArray bufferCopy = copy(buffer);
|
||||
if (lastQueuedBuffer != null && presentationTimeUs == lastQueuedBuffer.presentationTimeUs) {
|
||||
lastQueuedBuffer.nalBuffers.add(bufferCopy);
|
||||
return;
|
||||
}
|
||||
BuffersWithTimestamp buffersWithTimestamp =
|
||||
unusedBuffersWithTimestamp.isEmpty()
|
||||
? new BuffersWithTimestamp()
|
||||
: unusedBuffersWithTimestamp.pop();
|
||||
buffersWithTimestamp.init(presentationTimeUs, bufferCopy);
|
||||
pendingBuffers.add(buffersWithTimestamp);
|
||||
lastQueuedBuffer = buffersWithTimestamp;
|
||||
if (reorderingQueueSize != C.LENGTH_UNSET) {
|
||||
flushQueueDownToSize(reorderingQueueSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies {@code input} into a {@link ParsableByteArray} instance from {@link
|
||||
* #unusedParsableByteArrays}, or a new instance if that is empty.
|
||||
*/
|
||||
private ParsableByteArray copy(ParsableByteArray input) {
|
||||
ParsableByteArray result =
|
||||
unusedParsableByteArrays.isEmpty()
|
||||
? new ParsableByteArray()
|
||||
: unusedParsableByteArrays.pop();
|
||||
result.reset(input.bytesLeft());
|
||||
System.arraycopy(
|
||||
/* src= */ input.getData(),
|
||||
/* srcPos= */ input.getPosition(),
|
||||
/* dest= */ result.getData(),
|
||||
/* destPos= */ 0,
|
||||
/* length= */ result.bytesLeft());
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Empties the queue, discarding all previously {@linkplain #add added} buffers. */
|
||||
public void clear() {
|
||||
pendingBuffers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties the queue, passing all buffers (least first) to the {@link OutputConsumer} provided
|
||||
* during construction.
|
||||
*/
|
||||
public void flush() {
|
||||
flushQueueDownToSize(0);
|
||||
}
|
||||
|
||||
private void flushQueueDownToSize(int targetSize) {
|
||||
while (pendingBuffers.size() > targetSize) {
|
||||
BuffersWithTimestamp buffersWithTimestamp = castNonNull(pendingBuffers.poll());
|
||||
for (int i = 0; i < buffersWithTimestamp.nalBuffers.size(); i++) {
|
||||
outputConsumer.consume(
|
||||
buffersWithTimestamp.presentationTimeUs, buffersWithTimestamp.nalBuffers.get(i));
|
||||
unusedParsableByteArrays.push(buffersWithTimestamp.nalBuffers.get(i));
|
||||
}
|
||||
buffersWithTimestamp.nalBuffers.clear();
|
||||
if (lastQueuedBuffer != null
|
||||
&& lastQueuedBuffer.presentationTimeUs == buffersWithTimestamp.presentationTimeUs) {
|
||||
lastQueuedBuffer = null;
|
||||
}
|
||||
unusedBuffersWithTimestamp.push(buffersWithTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds the presentation timestamp of a sample and its associated buffers. */
|
||||
private static final class BuffersWithTimestamp implements Comparable<BuffersWithTimestamp> {
|
||||
|
||||
public final List<ParsableByteArray> nalBuffers;
|
||||
public long presentationTimeUs;
|
||||
|
||||
public BuffersWithTimestamp() {
|
||||
presentationTimeUs = C.TIME_UNSET;
|
||||
nalBuffers = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void init(long presentationTimeUs, ParsableByteArray nalBuffer) {
|
||||
checkArgument(presentationTimeUs != C.TIME_UNSET);
|
||||
checkState(this.nalBuffers.isEmpty());
|
||||
this.presentationTimeUs = presentationTimeUs;
|
||||
this.nalBuffers.add(nalBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(BuffersWithTimestamp other) {
|
||||
return Long.compare(this.presentationTimeUs, other.presentationTimeUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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 androidx.media3.container;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
|
||||
/**
|
||||
* Wraps a byte array, providing methods that allow it to be read as a Vorbis bitstream.
|
||||
*
|
||||
* <p>See the <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis
|
||||
* bitpacking specification</a>
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class VorbisBitArray {
|
||||
|
||||
private final byte[] data;
|
||||
private final int byteLimit;
|
||||
|
||||
private int byteOffset;
|
||||
private int bitOffset;
|
||||
|
||||
/**
|
||||
* Creates a new instance that wraps an existing array.
|
||||
*
|
||||
* @param data the array to wrap.
|
||||
*/
|
||||
public VorbisBitArray(byte[] data) {
|
||||
this.data = data;
|
||||
byteLimit = data.length;
|
||||
}
|
||||
|
||||
/** Resets the reading position to zero. */
|
||||
public void reset() {
|
||||
byteOffset = 0;
|
||||
bitOffset = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single bit.
|
||||
*
|
||||
* @return {@code true} if the bit is set, {@code false} otherwise.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public boolean readBit() {
|
||||
boolean returnValue = (((data[byteOffset] & 0xFF) >> bitOffset) & 0x01) == 1;
|
||||
skipBits(1);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads up to 32 bits.
|
||||
*
|
||||
* @param numBits The number of bits to read.
|
||||
* @return An integer whose bottom {@code numBits} bits hold the read data.
|
||||
*/
|
||||
public int readBits(int numBits) {
|
||||
int tempByteOffset = byteOffset;
|
||||
int bitsRead = min(numBits, 8 - bitOffset);
|
||||
int returnValue = ((data[tempByteOffset++] & 0xFF) >> bitOffset) & (0xFF >> (8 - bitsRead));
|
||||
while (bitsRead < numBits) {
|
||||
returnValue |= (data[tempByteOffset++] & 0xFF) << bitsRead;
|
||||
bitsRead += 8;
|
||||
}
|
||||
returnValue &= 0xFFFFFFFF >>> (32 - numBits);
|
||||
skipBits(numBits);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips {@code numberOfBits} bits.
|
||||
*
|
||||
* @param numBits The number of bits to skip.
|
||||
*/
|
||||
public void skipBits(int numBits) {
|
||||
int numBytes = numBits / 8;
|
||||
byteOffset += numBytes;
|
||||
bitOffset += numBits - (numBytes * 8);
|
||||
if (bitOffset > 7) {
|
||||
byteOffset++;
|
||||
bitOffset -= 8;
|
||||
}
|
||||
assertValidOffset();
|
||||
}
|
||||
|
||||
/** Returns the reading position in bits. */
|
||||
public int getPosition() {
|
||||
return byteOffset * 8 + bitOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reading position in bits.
|
||||
*
|
||||
* @param position The new reading position in bits.
|
||||
*/
|
||||
public void setPosition(int position) {
|
||||
byteOffset = position / 8;
|
||||
bitOffset = position - (byteOffset * 8);
|
||||
assertValidOffset();
|
||||
}
|
||||
|
||||
/** Returns the number of remaining bits. */
|
||||
public int bitsLeft() {
|
||||
return (byteLimit - byteOffset) * 8 - bitOffset;
|
||||
}
|
||||
|
||||
private void assertValidOffset() {
|
||||
// It is fine for position to be at the end of the array, but no further.
|
||||
checkState(
|
||||
byteOffset >= 0 && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* 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 androidx.media3.container;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.ParserException;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.ParsableByteArray;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Utility methods for parsing Vorbis streams. */
|
||||
@UnstableApi
|
||||
public final class VorbisUtil {
|
||||
|
||||
/** Vorbis comment header. */
|
||||
public static final class CommentHeader {
|
||||
|
||||
public final String vendor;
|
||||
public final String[] comments;
|
||||
public final int length;
|
||||
|
||||
public CommentHeader(String vendor, String[] comments, int length) {
|
||||
this.vendor = vendor;
|
||||
this.comments = comments;
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vorbis identification header.
|
||||
*
|
||||
* <p>See the <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis
|
||||
* spec/Identification header</a>
|
||||
*/
|
||||
public static final class VorbisIdHeader {
|
||||
|
||||
/** The {@code vorbis_version} field. */
|
||||
public final int version;
|
||||
|
||||
/** The {@code audio_channels} field. */
|
||||
public final int channels;
|
||||
|
||||
/** The {@code audio_sample_rate} field. */
|
||||
public final int sampleRate;
|
||||
|
||||
/** The {@code bitrate_maximum} field, or {@link Format#NO_VALUE} if not greater than zero. */
|
||||
public final int bitrateMaximum;
|
||||
|
||||
/** The {@code bitrate_nominal} field, or {@link Format#NO_VALUE} if not greater than zero. */
|
||||
public final int bitrateNominal;
|
||||
|
||||
/** The {@code bitrate_minimum} field, or {@link Format#NO_VALUE} if not greater than zero. */
|
||||
public final int bitrateMinimum;
|
||||
|
||||
/** The {@code blocksize_0} field. */
|
||||
public final int blockSize0;
|
||||
|
||||
/** The {@code blocksize_1} field. */
|
||||
public final int blockSize1;
|
||||
|
||||
/** The {@code framing_flag} field. */
|
||||
public final boolean framingFlag;
|
||||
|
||||
/** The raw header data. */
|
||||
public final byte[] data;
|
||||
|
||||
/**
|
||||
* @param version See {@link #version}.
|
||||
* @param channels See {@link #channels}.
|
||||
* @param sampleRate See {@link #sampleRate}.
|
||||
* @param bitrateMaximum See {@link #bitrateMaximum}.
|
||||
* @param bitrateNominal See {@link #bitrateNominal}.
|
||||
* @param bitrateMinimum See {@link #bitrateMinimum}.
|
||||
* @param blockSize0 See {@link #version}.
|
||||
* @param blockSize1 See {@link #blockSize1}.
|
||||
* @param framingFlag See {@link #framingFlag}.
|
||||
* @param data See {@link #data}.
|
||||
*/
|
||||
public VorbisIdHeader(
|
||||
int version,
|
||||
int channels,
|
||||
int sampleRate,
|
||||
int bitrateMaximum,
|
||||
int bitrateNominal,
|
||||
int bitrateMinimum,
|
||||
int blockSize0,
|
||||
int blockSize1,
|
||||
boolean framingFlag,
|
||||
byte[] data) {
|
||||
this.version = version;
|
||||
this.channels = channels;
|
||||
this.sampleRate = sampleRate;
|
||||
this.bitrateMaximum = bitrateMaximum;
|
||||
this.bitrateNominal = bitrateNominal;
|
||||
this.bitrateMinimum = bitrateMinimum;
|
||||
this.blockSize0 = blockSize0;
|
||||
this.blockSize1 = blockSize1;
|
||||
this.framingFlag = framingFlag;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/** Vorbis setup header modes. */
|
||||
public static final class Mode {
|
||||
|
||||
public final boolean blockFlag;
|
||||
public final int windowType;
|
||||
public final int transformType;
|
||||
public final int mapping;
|
||||
|
||||
public Mode(boolean blockFlag, int windowType, int transformType, int mapping) {
|
||||
this.blockFlag = blockFlag;
|
||||
this.windowType = windowType;
|
||||
this.transformType = transformType;
|
||||
this.mapping = mapping;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TAG = "VorbisUtil";
|
||||
|
||||
/**
|
||||
* Returns ilog(x), which is the index of the highest set bit in {@code x}.
|
||||
*
|
||||
* <p>See the <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-1190009.2.1">Vorbis
|
||||
* spec</a>
|
||||
*
|
||||
* @param x the value of which the ilog should be calculated.
|
||||
* @return ilog(x)
|
||||
*/
|
||||
public static int iLog(int x) {
|
||||
int val = 0;
|
||||
while (x > 0) {
|
||||
val++;
|
||||
x >>>= 1;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a Vorbis identification header from {@code headerData}.
|
||||
*
|
||||
* <p>See the <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-630004.2.2">Vorbis
|
||||
* spec/Identification header</a>
|
||||
*
|
||||
* @param headerData a {@link ParsableByteArray} wrapping the header data.
|
||||
* @return a {@link VorbisUtil.VorbisIdHeader} with meta data.
|
||||
* @throws ParserException thrown if invalid capture pattern is detected.
|
||||
*/
|
||||
public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData)
|
||||
throws ParserException {
|
||||
|
||||
verifyVorbisHeaderCapturePattern(0x01, headerData, false);
|
||||
|
||||
int version = headerData.readLittleEndianUnsignedIntToInt();
|
||||
int channels = headerData.readUnsignedByte();
|
||||
int sampleRate = headerData.readLittleEndianUnsignedIntToInt();
|
||||
int bitrateMaximum = headerData.readLittleEndianInt();
|
||||
if (bitrateMaximum <= 0) {
|
||||
bitrateMaximum = Format.NO_VALUE;
|
||||
}
|
||||
int bitrateNominal = headerData.readLittleEndianInt();
|
||||
if (bitrateNominal <= 0) {
|
||||
bitrateNominal = Format.NO_VALUE;
|
||||
}
|
||||
int bitrateMinimum = headerData.readLittleEndianInt();
|
||||
if (bitrateMinimum <= 0) {
|
||||
bitrateMinimum = Format.NO_VALUE;
|
||||
}
|
||||
int blockSize = headerData.readUnsignedByte();
|
||||
int blockSize0 = (int) Math.pow(2, blockSize & 0x0F);
|
||||
int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4);
|
||||
|
||||
boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0;
|
||||
// raw data of Vorbis setup header has to be passed to decoder as CSD buffer #1
|
||||
byte[] data = Arrays.copyOf(headerData.getData(), headerData.limit());
|
||||
|
||||
return new VorbisIdHeader(
|
||||
version,
|
||||
channels,
|
||||
sampleRate,
|
||||
bitrateMaximum,
|
||||
bitrateNominal,
|
||||
bitrateMinimum,
|
||||
blockSize0,
|
||||
blockSize1,
|
||||
framingFlag,
|
||||
data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a Vorbis comment header.
|
||||
*
|
||||
* <p>See the <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis
|
||||
* spec/Comment header</a>
|
||||
*
|
||||
* @param headerData A {@link ParsableByteArray} wrapping the header data.
|
||||
* @return A {@link VorbisUtil.CommentHeader} with all the comments.
|
||||
* @throws ParserException If an error occurs parsing the comment header.
|
||||
*/
|
||||
public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
|
||||
throws ParserException {
|
||||
return readVorbisCommentHeader(
|
||||
headerData, /* hasMetadataHeader= */ true, /* hasFramingBit= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a Vorbis comment header.
|
||||
*
|
||||
* <p>The data provided may not contain the Vorbis metadata common header and the framing bit.
|
||||
*
|
||||
* <p>See the <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3">Vorbis
|
||||
* spec/Comment header</a>
|
||||
*
|
||||
* @param headerData A {@link ParsableByteArray} wrapping the header data.
|
||||
* @param hasMetadataHeader Whether the {@code headerData} contains a Vorbis metadata common
|
||||
* header preceding the comment header.
|
||||
* @param hasFramingBit Whether the {@code headerData} contains a framing bit.
|
||||
* @return A {@link VorbisUtil.CommentHeader} with all the comments.
|
||||
* @throws ParserException If an error occurs parsing the comment header.
|
||||
*/
|
||||
public static CommentHeader readVorbisCommentHeader(
|
||||
ParsableByteArray headerData, boolean hasMetadataHeader, boolean hasFramingBit)
|
||||
throws ParserException {
|
||||
|
||||
if (hasMetadataHeader) {
|
||||
verifyVorbisHeaderCapturePattern(/* headerType= */ 0x03, headerData, /* quiet= */ false);
|
||||
}
|
||||
int length = 7;
|
||||
|
||||
int len = (int) headerData.readLittleEndianUnsignedInt();
|
||||
length += 4;
|
||||
String vendor = headerData.readString(len);
|
||||
length += vendor.length();
|
||||
|
||||
long commentListLen = headerData.readLittleEndianUnsignedInt();
|
||||
String[] comments = new String[(int) commentListLen];
|
||||
length += 4;
|
||||
for (int i = 0; i < commentListLen; i++) {
|
||||
len = (int) headerData.readLittleEndianUnsignedInt();
|
||||
length += 4;
|
||||
comments[i] = headerData.readString(len);
|
||||
length += comments[i].length();
|
||||
}
|
||||
if (hasFramingBit && (headerData.readUnsignedByte() & 0x01) == 0) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"framing bit expected to be set", /* cause= */ null);
|
||||
}
|
||||
length += 1;
|
||||
return new CommentHeader(vendor, comments, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether the next bytes in {@code header} are a Vorbis header of the given {@code
|
||||
* headerType}.
|
||||
*
|
||||
* @param headerType the type of the header expected.
|
||||
* @param header the alleged header bytes.
|
||||
* @param quiet if {@code true} no exceptions are thrown. Instead {@code false} is returned.
|
||||
* @return Whether the header is a Vorbis header.
|
||||
* @throws ParserException thrown if header type or capture pattern is not as expected.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public static boolean verifyVorbisHeaderCapturePattern(
|
||||
int headerType, ParsableByteArray header, boolean quiet) throws ParserException {
|
||||
if (header.bytesLeft() < 7) {
|
||||
if (quiet) {
|
||||
return false;
|
||||
} else {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"too short header: " + header.bytesLeft(), /* cause= */ null);
|
||||
}
|
||||
}
|
||||
|
||||
if (header.readUnsignedByte() != headerType) {
|
||||
if (quiet) {
|
||||
return false;
|
||||
} else {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"expected header type " + Integer.toHexString(headerType), /* cause= */ null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(header.readUnsignedByte() == 'v'
|
||||
&& header.readUnsignedByte() == 'o'
|
||||
&& header.readUnsignedByte() == 'r'
|
||||
&& header.readUnsignedByte() == 'b'
|
||||
&& header.readUnsignedByte() == 'i'
|
||||
&& header.readUnsignedByte() == 's')) {
|
||||
if (quiet) {
|
||||
return false;
|
||||
} else {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"expected characters 'vorbis'", /* cause= */ null);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the block size of the given packet.
|
||||
*
|
||||
* @param firstByteOfAudioPacket The first byte of the audio packet.
|
||||
* @param idHeader The Vorbis identification header.
|
||||
* @param modes The Vorbis modes.
|
||||
* @return The block size of the packet.
|
||||
*/
|
||||
public static int getPacketBlockSize(
|
||||
byte firstByteOfAudioPacket, VorbisIdHeader idHeader, Mode[] modes) {
|
||||
// Read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
|
||||
int iLogModes = iLog(modes.length - 1);
|
||||
int modeNumber = readBits(firstByteOfAudioPacket, iLogModes, 1);
|
||||
return modes[modeNumber].blockFlag ? idHeader.blockSize1 : idHeader.blockSize0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of samples in the given packet.
|
||||
*
|
||||
* @param packetBlockSize The block size of the packet.
|
||||
* @param previousPacketBlockSize The block size of the previous packet.
|
||||
* @param seenFirstAudioPacket Whether the first audio packet has been seen.
|
||||
* @return The number of samples in the packet.
|
||||
*/
|
||||
public static int getSampleCountInPacket(
|
||||
int packetBlockSize, int previousPacketBlockSize, boolean seenFirstAudioPacket) {
|
||||
// A packet contains samples produced from overlapping the previous and current frame data.
|
||||
// See
|
||||
// (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
|
||||
return seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an int of {@code length} bits from {@code src} starting at {@code
|
||||
* leastSignificantBitIndex}.
|
||||
*
|
||||
* @param src the {@code byte} to read from.
|
||||
* @param length the length in bits of the int to read.
|
||||
* @param leastSignificantBitIndex the index of the least significant bit of the int to read.
|
||||
* @return the int value read.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
/* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) {
|
||||
return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method reads the modes which are located at the very end of the Vorbis setup header.
|
||||
* That's why we need to partially decode or at least read the entire setup header to know where
|
||||
* to start reading the modes.
|
||||
*
|
||||
* <p>See the <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-650004.2.4">Vorbis
|
||||
* spec/Setup header</a>
|
||||
*
|
||||
* @param headerData a {@link ParsableByteArray} containing setup header data.
|
||||
* @param channels the number of channels.
|
||||
* @return an array of {@link Mode}s.
|
||||
* @throws ParserException thrown if bit stream is invalid.
|
||||
*/
|
||||
public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels)
|
||||
throws ParserException {
|
||||
|
||||
verifyVorbisHeaderCapturePattern(0x05, headerData, false);
|
||||
|
||||
int numberOfBooks = headerData.readUnsignedByte() + 1;
|
||||
|
||||
VorbisBitArray bitArray = new VorbisBitArray(headerData.getData());
|
||||
bitArray.skipBits(headerData.getPosition() * 8);
|
||||
|
||||
for (int i = 0; i < numberOfBooks; i++) {
|
||||
skipBook(bitArray);
|
||||
}
|
||||
|
||||
int timeCount = bitArray.readBits(6) + 1;
|
||||
for (int i = 0; i < timeCount; i++) {
|
||||
if (bitArray.readBits(16) != 0x00) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"placeholder of time domain transforms not zeroed out", /* cause= */ null);
|
||||
}
|
||||
}
|
||||
readFloors(bitArray);
|
||||
readResidues(bitArray);
|
||||
readMappings(channels, bitArray);
|
||||
|
||||
Mode[] modes = readModes(bitArray);
|
||||
if (!bitArray.readBit()) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"framing bit after modes not set as expected", /* cause= */ null);
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
private static Mode[] readModes(VorbisBitArray bitArray) {
|
||||
int modeCount = bitArray.readBits(6) + 1;
|
||||
Mode[] modes = new Mode[modeCount];
|
||||
for (int i = 0; i < modeCount; i++) {
|
||||
boolean blockFlag = bitArray.readBit();
|
||||
int windowType = bitArray.readBits(16);
|
||||
int transformType = bitArray.readBits(16);
|
||||
int mapping = bitArray.readBits(8);
|
||||
modes[i] = new Mode(blockFlag, windowType, transformType, mapping);
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
private static void readMappings(int channels, VorbisBitArray bitArray) throws ParserException {
|
||||
int mappingsCount = bitArray.readBits(6) + 1;
|
||||
for (int i = 0; i < mappingsCount; i++) {
|
||||
int mappingType = bitArray.readBits(16);
|
||||
if (mappingType != 0) {
|
||||
Log.e(TAG, "mapping type other than 0 not supported: " + mappingType);
|
||||
continue;
|
||||
}
|
||||
int submaps;
|
||||
if (bitArray.readBit()) {
|
||||
submaps = bitArray.readBits(4) + 1;
|
||||
} else {
|
||||
submaps = 1;
|
||||
}
|
||||
int couplingSteps;
|
||||
if (bitArray.readBit()) {
|
||||
couplingSteps = bitArray.readBits(8) + 1;
|
||||
for (int j = 0; j < couplingSteps; j++) {
|
||||
bitArray.skipBits(iLog(channels - 1)); // magnitude
|
||||
bitArray.skipBits(iLog(channels - 1)); // angle
|
||||
}
|
||||
} /*else {
|
||||
couplingSteps = 0;
|
||||
}*/
|
||||
if (bitArray.readBits(2) != 0x00) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"to reserved bits must be zero after mapping coupling steps", /* cause= */ null);
|
||||
}
|
||||
if (submaps > 1) {
|
||||
for (int j = 0; j < channels; j++) {
|
||||
bitArray.skipBits(4); // mappingMux
|
||||
}
|
||||
}
|
||||
for (int j = 0; j < submaps; j++) {
|
||||
bitArray.skipBits(8); // discard
|
||||
bitArray.skipBits(8); // submapFloor
|
||||
bitArray.skipBits(8); // submapResidue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void readResidues(VorbisBitArray bitArray) throws ParserException {
|
||||
int residueCount = bitArray.readBits(6) + 1;
|
||||
for (int i = 0; i < residueCount; i++) {
|
||||
int residueType = bitArray.readBits(16);
|
||||
if (residueType > 2) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"residueType greater than 2 is not decodable", /* cause= */ null);
|
||||
} else {
|
||||
bitArray.skipBits(24); // begin
|
||||
bitArray.skipBits(24); // end
|
||||
bitArray.skipBits(24); // partitionSize (add one)
|
||||
int classifications = bitArray.readBits(6) + 1;
|
||||
bitArray.skipBits(8); // classbook
|
||||
int[] cascade = new int[classifications];
|
||||
for (int j = 0; j < classifications; j++) {
|
||||
int highBits = 0;
|
||||
int lowBits = bitArray.readBits(3);
|
||||
if (bitArray.readBit()) {
|
||||
highBits = bitArray.readBits(5);
|
||||
}
|
||||
cascade[j] = highBits * 8 + lowBits;
|
||||
}
|
||||
for (int j = 0; j < classifications; j++) {
|
||||
for (int k = 0; k < 8; k++) {
|
||||
if ((cascade[j] & (0x01 << k)) != 0) {
|
||||
bitArray.skipBits(8); // discard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void readFloors(VorbisBitArray bitArray) throws ParserException {
|
||||
int floorCount = bitArray.readBits(6) + 1;
|
||||
for (int i = 0; i < floorCount; i++) {
|
||||
int floorType = bitArray.readBits(16);
|
||||
switch (floorType) {
|
||||
case 0:
|
||||
bitArray.skipBits(8); // order
|
||||
bitArray.skipBits(16); // rate
|
||||
bitArray.skipBits(16); // barkMapSize
|
||||
bitArray.skipBits(6); // amplitudeBits
|
||||
bitArray.skipBits(8); // amplitudeOffset
|
||||
int floorNumberOfBooks = bitArray.readBits(4) + 1;
|
||||
for (int j = 0; j < floorNumberOfBooks; j++) {
|
||||
bitArray.skipBits(8);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
int partitions = bitArray.readBits(5);
|
||||
int maximumClass = -1;
|
||||
int[] partitionClassList = new int[partitions];
|
||||
for (int j = 0; j < partitions; j++) {
|
||||
partitionClassList[j] = bitArray.readBits(4);
|
||||
if (partitionClassList[j] > maximumClass) {
|
||||
maximumClass = partitionClassList[j];
|
||||
}
|
||||
}
|
||||
int[] classDimensions = new int[maximumClass + 1];
|
||||
for (int j = 0; j < classDimensions.length; j++) {
|
||||
classDimensions[j] = bitArray.readBits(3) + 1;
|
||||
int classSubclasses = bitArray.readBits(2);
|
||||
if (classSubclasses > 0) {
|
||||
bitArray.skipBits(8); // classMasterbooks
|
||||
}
|
||||
for (int k = 0; k < (1 << classSubclasses); k++) {
|
||||
bitArray.skipBits(8); // subclassBook (subtract 1)
|
||||
}
|
||||
}
|
||||
bitArray.skipBits(2); // multiplier (add one)
|
||||
int rangeBits = bitArray.readBits(4);
|
||||
int count = 0;
|
||||
for (int j = 0, k = 0; j < partitions; j++) {
|
||||
int idx = partitionClassList[j];
|
||||
count += classDimensions[idx];
|
||||
for (; k < count; k++) {
|
||||
bitArray.skipBits(rangeBits); // floorValue
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"floor type greater than 1 not decodable: " + floorType, /* cause= */ null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void skipBook(VorbisBitArray bitArray) throws ParserException {
|
||||
if (bitArray.readBits(24) != 0x564342) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"expected code book to start with [0x56, 0x43, 0x42] at " + bitArray.getPosition(),
|
||||
/* cause= */ null);
|
||||
}
|
||||
int dimensions = bitArray.readBits(16);
|
||||
int entries = bitArray.readBits(24);
|
||||
|
||||
boolean isOrdered = bitArray.readBit();
|
||||
if (!isOrdered) {
|
||||
boolean isSparse = bitArray.readBit();
|
||||
for (int i = 0; i < entries; i++) {
|
||||
if (isSparse) {
|
||||
if (bitArray.readBit()) {
|
||||
bitArray.skipBits(5); // lengthMap entry
|
||||
}
|
||||
} else { // not sparse
|
||||
bitArray.skipBits(5); // lengthMap entry
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bitArray.skipBits(5); // length
|
||||
for (int i = 0; i < entries; ) {
|
||||
i += bitArray.readBits(iLog(entries - i)); // num
|
||||
}
|
||||
}
|
||||
|
||||
int lookupType = bitArray.readBits(4);
|
||||
if (lookupType > 2) {
|
||||
throw ParserException.createForMalformedContainer(
|
||||
"lookup type greater than 2 not decodable: " + lookupType, /* cause= */ null);
|
||||
} else if (lookupType == 1 || lookupType == 2) {
|
||||
bitArray.skipBits(32); // minimumValue
|
||||
bitArray.skipBits(32); // deltaValue
|
||||
int valueBits = bitArray.readBits(4) + 1;
|
||||
bitArray.skipBits(1); // sequenceP
|
||||
long lookupValuesCount;
|
||||
if (lookupType == 1) {
|
||||
if (dimensions != 0) {
|
||||
lookupValuesCount = mapType1QuantValues(entries, dimensions);
|
||||
} else {
|
||||
lookupValuesCount = 0;
|
||||
}
|
||||
} else {
|
||||
lookupValuesCount = (long) entries * dimensions;
|
||||
}
|
||||
// discard (no decoding required yet)
|
||||
bitArray.skipBits((int) (lookupValuesCount * valueBits));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See the <a
|
||||
* href="http://svn.xiph.org/trunk/vorbis/lib/sharedbook.c">_book_maptype1_quantvals</a>
|
||||
*/
|
||||
private static long mapType1QuantValues(long entries, long dimension) {
|
||||
return (long) Math.floor(Math.pow(entries, 1.d / dimension));
|
||||
}
|
||||
|
||||
private VorbisUtil() {
|
||||
// Prevent instantiation.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2023 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 androidx.media3.container;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Stores XMP data. */
|
||||
@UnstableApi
|
||||
public final class XmpData implements Metadata.Entry {
|
||||
public final byte[] data;
|
||||
|
||||
/** Creates an instance. */
|
||||
public XmpData(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Arrays.equals(data, ((XmpData) obj).data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "XMP: " + Util.toHexString(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@NonNullApi
|
||||
package androidx.media3.container;
|
||||
|
||||
import androidx.media3.common.util.NonNullApi;
|
||||
Reference in New Issue
Block a user