Initial hlsmplayer application

This commit is contained in:
root
2026-09-27 17:13:02 +08:00
commit be5a40bfe3
1214 changed files with 315753 additions and 0 deletions
+18
View File
@@ -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();
}
}
@@ -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;
}
}
@@ -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)));
}
}
@@ -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;