Commit fe7e5b81 by Andrew Lewis

Merge pull request #9864 from OxygenCobalt:vorbis-comments

PiperOrigin-RevId: 424355325
parents 651fa0db c2d0649e
Showing with 257 additions and 94 deletions
......@@ -56,6 +56,8 @@
* Extractors:
* Fix inconsistency with spec in H.265 SPS nal units parsing
([#9719](https://github.com/google/ExoPlayer/issues/9719)).
* Parse Vorbis Comments (including `METADATA_BLOCK_PICTURE`) in Ogg Opus
and Vorbis files.
* Text:
* Add a `MediaItem.SubtitleConfiguration#id` field which is propagated to
the `Format#id` field of the subtitle track created from the
......
......@@ -24,10 +24,9 @@ import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
......@@ -168,9 +167,12 @@ public final class FlacMetadataReader {
metadataHolder.flacStreamMetadata =
flacStreamMetadata.copyWithVorbisComments(vorbisComments);
} else if (type == FlacConstants.METADATA_TYPE_PICTURE) {
PictureFrame pictureFrame = readPictureMetadataBlock(input, length);
ParsableByteArray pictureBlock = new ParsableByteArray(length);
input.readFully(pictureBlock.getData(), 0, length);
pictureBlock.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
PictureFrame pictureFrame = PictureFrame.fromPictureBlock(pictureBlock);
metadataHolder.flacStreamMetadata =
flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame));
flacStreamMetadata.copyWithPictureFrames(ImmutableList.of(pictureFrame));
} else {
input.skipFully(length);
}
......@@ -268,28 +270,5 @@ public final class FlacMetadataReader {
return Arrays.asList(commentHeader.comments);
}
private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length)
throws IOException {
ParsableByteArray scratch = new ParsableByteArray(length);
input.readFully(scratch.getData(), 0, length);
scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
int pictureType = scratch.readInt();
int mimeTypeLength = scratch.readInt();
String mimeType = scratch.readString(mimeTypeLength, Charsets.US_ASCII);
int descriptionLength = scratch.readInt();
String description = scratch.readString(descriptionLength);
int width = scratch.readInt();
int height = scratch.readInt();
int depth = scratch.readInt();
int colors = scratch.readInt();
int pictureDataLength = scratch.readInt();
byte[] pictureData = new byte[pictureDataLength];
scratch.readBytes(pictureData, 0, pictureDataLength);
return new PictureFrame(
pictureType, mimeType, description, width, height, depth, colors, pictureData);
}
private FlacMetadataReader() {}
}
......@@ -15,13 +15,13 @@
*/
package com.google.android.exoplayer2.extractor;
import static com.google.android.exoplayer2.extractor.VorbisUtil.parseVorbisComments;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.Util;
......@@ -60,8 +60,6 @@ public final class FlacStreamMetadata {
/** Indicates that a value is not in the corresponding lookup table. */
public static final int NOT_IN_LOOKUP_TABLE = -1;
/** Separator between the field name of a Vorbis comment and the corresponding value. */
private static final String SEPARATOR = "=";
/** Minimum number of samples per block. */
public final int minBlockSizeSamples;
......@@ -149,7 +147,7 @@ public final class FlacStreamMetadata {
bitsPerSample,
totalSamples,
/* seekTable= */ null,
buildMetadata(vorbisComments, pictureFrames));
concatenateVorbisMetadata(vorbisComments, pictureFrames));
}
private FlacStreamMetadata(
......@@ -274,8 +272,7 @@ public final class FlacStreamMetadata {
public FlacStreamMetadata copyWithVorbisComments(List<String> vorbisComments) {
@Nullable
Metadata appendedMetadata =
getMetadataCopyWithAppendedEntriesFrom(
buildMetadata(vorbisComments, Collections.emptyList()));
getMetadataCopyWithAppendedEntriesFrom(parseVorbisComments(vorbisComments));
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
......@@ -292,9 +289,7 @@ public final class FlacStreamMetadata {
/** Returns a copy of {@code this} with the given picture frames added to the metadata. */
public FlacStreamMetadata copyWithPictureFrames(List<PictureFrame> pictureFrames) {
@Nullable
Metadata appendedMetadata =
getMetadataCopyWithAppendedEntriesFrom(
buildMetadata(Collections.emptyList(), pictureFrames));
Metadata appendedMetadata = getMetadataCopyWithAppendedEntriesFrom(new Metadata(pictureFrames));
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
......@@ -308,6 +303,20 @@ public final class FlacStreamMetadata {
appendedMetadata);
}
/**
* Returns a new {@link Metadata} instance created from {@code vorbisComments} and {@code
* pictureFrames}.
*/
@Nullable
private static Metadata concatenateVorbisMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
@Nullable Metadata parsedVorbisComments = parseVorbisComments(vorbisComments);
if (parsedVorbisComments == null && pictureFrames.isEmpty()) {
return null;
}
return new Metadata(pictureFrames).copyWithAppendedEntriesFrom(parsedVorbisComments);
}
private static int getSampleRateLookupKey(int sampleRate) {
switch (sampleRate) {
case 88200:
......@@ -353,27 +362,4 @@ public final class FlacStreamMetadata {
return NOT_IN_LOOKUP_TABLE;
}
}
@Nullable
private static Metadata buildMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
return null;
}
ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();
for (int i = 0; i < vorbisComments.size(); i++) {
String vorbisComment = vorbisComments.get(i);
String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);
if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment);
} else {
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry);
}
}
metadataEntries.addAll(pictureFrames);
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
}
}
......@@ -15,11 +15,20 @@
*/
package com.google.android.exoplayer2.extractor;
import android.util.Base64;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.Metadata.Entry;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** Utility methods for parsing Vorbis streams. */
public final class VorbisUtil {
......@@ -249,6 +258,45 @@ public final class VorbisUtil {
}
/**
* Builds a {@link Metadata} instance from a list of Vorbis Comments.
*
* <p>METADATA_BLOCK_PICTURE comments will be transformed into {@link PictureFrame} entries. All
* others will be transformed into {@link VorbisComment} entries.
*
* @param vorbisComments The raw input of comments, as a key-value pair KEY=VAL.
* @return The fully parsed Metadata instance. Null if no vorbis comments could be parsed.
*/
@Nullable
public static Metadata parseVorbisComments(List<String> vorbisComments) {
List<Entry> metadataEntries = new ArrayList<>();
for (int i = 0; i < vorbisComments.size(); i++) {
String vorbisComment = vorbisComments.get(i);
String[] keyAndValue = Util.splitAtFirst(vorbisComment, "=");
if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment);
continue;
}
if (keyAndValue[0].equals("METADATA_BLOCK_PICTURE")) {
// This tag is a special cover art tag, outlined by
// https://wiki.xiph.org/index.php/VorbisComment#Cover_art.
// Decode it from Base64 and transform it into a PictureFrame.
try {
byte[] decoded = Base64.decode(keyAndValue[1], Base64.DEFAULT);
metadataEntries.add(PictureFrame.fromPictureBlock(new ParsableByteArray(decoded)));
} catch (RuntimeException e) {
Log.w(TAG, "Failed to parse vorbis picture", e);
}
} else {
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry);
}
}
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
}
/**
* Verifies whether the next bytes in {@code header} are a Vorbis header of the given {@code
* headerType}.
*
......
......@@ -15,12 +15,18 @@
*/
package com.google.android.exoplayer2.extractor.ogg;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.OpusUtil;
import com.google.android.exoplayer2.extractor.VorbisUtil;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
......@@ -28,26 +34,13 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
/** {@link StreamReader} to extract Opus data out of Ogg byte stream. */
/* package */ final class OpusReader extends StreamReader {
private static final int OPUS_CODE = 0x4f707573;
private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
private boolean headerRead;
private static final byte[] OPUS_ID_HEADER_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
private static final byte[] OPUS_COMMENT_HEADER_SIGNATURE = {
'O', 'p', 'u', 's', 'T', 'a', 'g', 's'
};
public static boolean verifyBitstreamType(ParsableByteArray data) {
if (data.bytesLeft() < OPUS_SIGNATURE.length) {
return false;
}
byte[] header = new byte[OPUS_SIGNATURE.length];
data.readBytes(header, 0, OPUS_SIGNATURE.length);
return Arrays.equals(header, OPUS_SIGNATURE);
}
@Override
protected void reset(boolean headerData) {
super.reset(headerData);
if (headerData) {
headerRead = false;
}
return peekPacketStartsWith(data, OPUS_ID_HEADER_SIGNATURE);
}
@Override
......@@ -57,11 +50,16 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
@Override
@EnsuresNonNullIf(expression = "#3.format", result = false)
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
if (!headerRead) {
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws ParserException {
if (peekPacketStartsWith(packet, OPUS_ID_HEADER_SIGNATURE)) {
byte[] headerBytes = Arrays.copyOf(packet.getData(), packet.limit());
int channelCount = OpusUtil.getChannelCount(headerBytes);
List<byte[]> initializationData = OpusUtil.buildInitializationData(headerBytes);
// The ID header must come at the start of the file:
// https://datatracker.ietf.org/doc/html/rfc7845#section-3
checkState(setupData.format == null);
setupData.format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_OPUS)
......@@ -69,13 +67,33 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
.setSampleRate(OpusUtil.SAMPLE_RATE)
.setInitializationData(initializationData)
.build();
headerRead = true;
return true;
} else if (peekPacketStartsWith(packet, OPUS_COMMENT_HEADER_SIGNATURE)) {
// The comment header must come immediately after the ID header, so the format will already
// be populated: https://datatracker.ietf.org/doc/html/rfc7845#section-3
checkStateNotNull(setupData.format);
packet.skipBytes(OPUS_COMMENT_HEADER_SIGNATURE.length);
VorbisUtil.CommentHeader commentHeader =
VorbisUtil.readVorbisCommentHeader(
packet, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false);
@Nullable
Metadata vorbisMetadata =
VorbisUtil.parseVorbisComments(ImmutableList.copyOf(commentHeader.comments));
if (vorbisMetadata == null) {
return true;
}
setupData.format =
setupData
.format
.buildUpon()
.setMetadata(vorbisMetadata.copyWithAppendedEntriesFrom(setupData.format.metadata))
.build();
return true;
} else {
checkNotNull(setupData.format); // Has been set when the header was read.
boolean headerPacket = packet.readInt() == OPUS_CODE;
packet.setPosition(0);
return headerPacket;
// The ID header must come at the start of the file, so the format must already be populated:
// https://datatracker.ietf.org/doc/html/rfc7845#section-3
checkStateNotNull(setupData.format);
return false;
}
}
......@@ -114,4 +132,22 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
}
return (long) frames * length;
}
/**
* Returns true if the given {@link ParsableByteArray} starts with {@code expectedPrefix}. Does
* not change the {@link ParsableByteArray#getPosition() position} of {@code packet}.
*
* @param packet The packet data.
* @return True if the packet starts with {@code expectedPrefix}, false if not.
*/
private static boolean peekPacketStartsWith(ParsableByteArray packet, byte[] expectedPrefix) {
if (packet.bytesLeft() < expectedPrefix.length) {
return false;
}
int startPosition = packet.getPosition();
byte[] header = new byte[expectedPrefix.length];
packet.readBytes(header, 0, expectedPrefix.length);
packet.setPosition(startPosition);
return Arrays.equals(header, expectedPrefix);
}
}
......@@ -24,8 +24,10 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.VorbisUtil;
import com.google.android.exoplayer2.extractor.VorbisUtil.Mode;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -111,6 +113,10 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
codecInitializationData.add(idHeader.data);
codecInitializationData.add(vorbisSetup.setupHeaderData);
@Nullable
Metadata metadata =
VorbisUtil.parseVorbisComments(ImmutableList.copyOf(vorbisSetup.commentHeader.comments));
setupData.format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_VORBIS)
......@@ -119,6 +125,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
.setChannelCount(idHeader.channels)
.setSampleRate(idHeader.sampleRate)
.setInitializationData(codecInitializationData)
.setMetadata(metadata)
.build();
return true;
}
......
......@@ -22,9 +22,11 @@ import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.base.Charsets;
import java.util.Arrays;
/** A picture parsed from a FLAC file. */
/** A picture parsed from a Vorbis Comment or a FLAC picture block. */
public final class PictureFrame implements Metadata.Entry {
/** The type of the picture. */
......@@ -134,6 +136,35 @@ public final class PictureFrame implements Metadata.Entry {
return 0;
}
/**
* Parses a {@code METADATA_BLOCK_PICTURE} into a {@code PictureFrame} instance.
*
* <p>{@code pictureBlock} may be read directly from a <a
* href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC file</a>, or decoded from
* the base64 content of a <a
* href="https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE">Vorbis Comment</a>.
*
* @param pictureBlock The data of the {@code METADATA_BLOCK_PICTURE}, not including any headers.
* @return A {@code PictureFrame} parsed from {@code pictureBlock}.
*/
public static PictureFrame fromPictureBlock(ParsableByteArray pictureBlock) {
int pictureType = pictureBlock.readInt();
int mimeTypeLength = pictureBlock.readInt();
String mimeType = pictureBlock.readString(mimeTypeLength, Charsets.US_ASCII);
int descriptionLength = pictureBlock.readInt();
String description = pictureBlock.readString(descriptionLength);
int width = pictureBlock.readInt();
int height = pictureBlock.readInt();
int depth = pictureBlock.readInt();
int colors = pictureBlock.readInt();
int pictureDataLength = pictureBlock.readInt();
byte[] pictureData = new byte[pictureDataLength];
pictureBlock.readBytes(pictureData, 0, pictureDataLength);
return new PictureFrame(
pictureType, mimeType, description, width, height, depth, colors, pictureData);
}
public static final Parcelable.Creator<PictureFrame> CREATOR =
new Parcelable.Creator<PictureFrame>() {
......
......@@ -23,8 +23,9 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.metadata.Metadata;
/** A vorbis comment. */
public final class VorbisComment implements Metadata.Entry {
/** @deprecated Use {@link com.google.android.exoplayer2.metadata.vorbis.VorbisComment} instead. */
@Deprecated
public class VorbisComment implements Metadata.Entry {
/** The key. */
public final String key;
......@@ -41,7 +42,7 @@ public final class VorbisComment implements Metadata.Entry {
this.value = value;
}
/* package */ VorbisComment(Parcel in) {
protected VorbisComment(Parcel in) {
this.key = castNonNull(in.readString());
this.value = castNonNull(in.readString());
}
......
/*
* 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 com.google.android.exoplayer2.metadata.vorbis;
import android.os.Parcel;
/** A vorbis comment, extracted from a FLAC or Ogg file. */
@SuppressWarnings("deprecation") // Extending deprecated type for backwards compatibility.
public final class VorbisComment extends com.google.android.exoplayer2.metadata.flac.VorbisComment {
/**
* @param key The key.
* @param value The value.
*/
public VorbisComment(String key, String value) {
super(key, value);
}
/* package */ VorbisComment(Parcel in) {
super(in);
}
public static final Creator<VorbisComment> CREATOR =
new Creator<VorbisComment>() {
@Override
public VorbisComment createFromParcel(Parcel in) {
return new VorbisComment(in);
}
@Override
public VorbisComment[] newArray(int size) {
return new VorbisComment[size];
}
};
}
/*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.metadata.vorbis;
import com.google.android.exoplayer2.util.NonNullApi;
......@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMeta
import com.google.android.exoplayer2.extractor.flac.FlacConstants;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray;
......
......@@ -21,7 +21,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.flac.FlacConstants;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import java.util.ArrayList;
......
......@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.flac;
package com.google.android.exoplayer2.metadata.vorbis;
import static com.google.common.truth.Truth.assertThat;
......
......@@ -23,7 +23,6 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.dvbsi.AppInfoTable;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.metadata.icy.IcyInfo;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
......@@ -32,6 +31,7 @@ import com.google.android.exoplayer2.metadata.mp4.MotionPhotoMetadata;
import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry;
import com.google.android.exoplayer2.metadata.scte35.SpliceCommand;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
import com.google.android.exoplayer2.testutil.Dumper;
import com.google.android.exoplayer2.text.Cue;
......
......@@ -13,6 +13,7 @@ track 0:
sampleMimeType = audio/opus
channelCount = 2
sampleRate = 48000
metadata = entries=[VC: encoder=Lavf54.20.4]
initializationData:
data = length 19, hash BFE794DB
data = length 8, hash CA22068C
......
......@@ -13,6 +13,7 @@ track 0:
sampleMimeType = audio/opus
channelCount = 2
sampleRate = 48000
metadata = entries=[VC: encoder=Lavf54.20.4]
initializationData:
data = length 19, hash BFE794DB
data = length 8, hash CA22068C
......
......@@ -13,6 +13,7 @@ track 0:
sampleMimeType = audio/opus
channelCount = 2
sampleRate = 48000
metadata = entries=[VC: encoder=Lavf54.20.4]
initializationData:
data = length 19, hash BFE794DB
data = length 8, hash CA22068C
......
......@@ -13,6 +13,7 @@ track 0:
sampleMimeType = audio/opus
channelCount = 2
sampleRate = 48000
metadata = entries=[VC: encoder=Lavf54.20.4]
initializationData:
data = length 19, hash BFE794DB
data = length 8, hash CA22068C
......
......@@ -10,6 +10,7 @@ track 0:
sampleMimeType = audio/opus
channelCount = 2
sampleRate = 48000
metadata = entries=[VC: encoder=Lavf54.20.4]
initializationData:
data = length 19, hash BFE794DB
data = length 8, hash CA22068C
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment