Opus: Add utility for handling header and initialization data

PiperOrigin-RevId: 325202386
This commit is contained in:
olly 2020-08-06 12:09:17 +01:00 committed by kim-vde
parent 94fb9adec1
commit 5de56cd618
5 changed files with 248 additions and 69 deletions

View File

@ -24,6 +24,7 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.AudioSink.SinkFormatSupport; import com.google.android.exoplayer2.audio.AudioSink.SinkFormatSupport;
import com.google.android.exoplayer2.audio.DecoderAudioRenderer; import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
import com.google.android.exoplayer2.audio.OpusUtil;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.TraceUtil;
@ -125,6 +126,6 @@ public class LibopusAudioRenderer extends DecoderAudioRenderer<OpusDecoder> {
protected Format getOutputFormat(OpusDecoder decoder) { protected Format getOutputFormat(OpusDecoder decoder) {
@C.PcmEncoding @C.PcmEncoding
int pcmEncoding = decoder.outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; int pcmEncoding = decoder.outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
return Util.getPcmFormat(pcmEncoding, decoder.channelCount, OpusDecoder.SAMPLE_RATE); return Util.getPcmFormat(pcmEncoding, decoder.channelCount, OpusUtil.SAMPLE_RATE);
} }
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.opus;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.OpusUtil;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
@ -26,18 +27,12 @@ import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List; import java.util.List;
/** Opus decoder. */ /** Opus decoder. */
/* package */ final class OpusDecoder /* package */ final class OpusDecoder
extends SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, OpusDecoderException> { extends SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, OpusDecoderException> {
private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
/** Opus streams are always decoded at 48000 Hz. */
public static final int SAMPLE_RATE = 48_000;
private static final int NO_ERROR = 0; private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = -1; private static final int DECODE_ERROR = -1;
private static final int DRM_ERROR = -2; private static final int DRM_ERROR = -2;
@ -46,9 +41,8 @@ import java.util.List;
public final int channelCount; public final int channelCount;
@Nullable private final ExoMediaCrypto exoMediaCrypto; @Nullable private final ExoMediaCrypto exoMediaCrypto;
private final int preSkipSamples;
private final int headerSkipSamples; private final int seekPreRollSamples;
private final int headerSeekPreRollSamples;
private final long nativeDecoderContext; private final long nativeDecoderContext;
private int skipSamples; private int skipSamples;
@ -77,21 +71,31 @@ import java.util.List;
throws OpusDecoderException { throws OpusDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!OpusLibrary.isAvailable()) { if (!OpusLibrary.isAvailable()) {
throw new OpusDecoderException("Failed to load decoder native libraries."); throw new OpusDecoderException("Failed to load decoder native libraries");
} }
this.exoMediaCrypto = exoMediaCrypto; this.exoMediaCrypto = exoMediaCrypto;
if (exoMediaCrypto != null && !OpusLibrary.opusIsSecureDecodeSupported()) { if (exoMediaCrypto != null && !OpusLibrary.opusIsSecureDecodeSupported()) {
throw new OpusDecoderException("Opus decoder does not support secure decode."); throw new OpusDecoderException("Opus decoder does not support secure decode");
} }
int initializationDataSize = initializationData.size();
if (initializationDataSize != 1 && initializationDataSize != 3) {
throw new OpusDecoderException("Invalid initialization data size");
}
if (initializationDataSize == 3
&& (initializationData.get(1).length != 8 || initializationData.get(2).length != 8)) {
throw new OpusDecoderException("Invalid pre-skip or seek pre-roll");
}
preSkipSamples = OpusUtil.getPreSkipSamples(initializationData);
seekPreRollSamples = OpusUtil.getSeekPreRollSamples(initializationData);
byte[] headerBytes = initializationData.get(0); byte[] headerBytes = initializationData.get(0);
if (headerBytes.length < 19) { if (headerBytes.length < 19) {
throw new OpusDecoderException("Header size is too small."); throw new OpusDecoderException("Invalid header length");
} }
channelCount = headerBytes[9] & 0xFF; channelCount = OpusUtil.getChannelCount(headerBytes);
if (channelCount > 8) { if (channelCount > 8) {
throw new OpusDecoderException("Invalid channel count: " + channelCount); throw new OpusDecoderException("Invalid channel count: " + channelCount);
} }
int preskip = readUnsignedLittleEndian16(headerBytes, 10);
int gain = readSignedLittleEndian16(headerBytes, 16); int gain = readSignedLittleEndian16(headerBytes, 16);
byte[] streamMap = new byte[8]; byte[] streamMap = new byte[8];
@ -100,7 +104,7 @@ import java.util.List;
if (headerBytes[18] == 0) { // Channel mapping if (headerBytes[18] == 0) { // Channel mapping
// If there is no channel mapping, use the defaults. // If there is no channel mapping, use the defaults.
if (channelCount > 2) { // Maximum channel count with default layout. if (channelCount > 2) { // Maximum channel count with default layout.
throw new OpusDecoderException("Invalid Header, missing stream map."); throw new OpusDecoderException("Invalid header, missing stream map");
} }
numStreams = 1; numStreams = 1;
numCoupled = (channelCount == 2) ? 1 : 0; numCoupled = (channelCount == 2) ? 1 : 0;
@ -108,29 +112,15 @@ import java.util.List;
streamMap[1] = 1; streamMap[1] = 1;
} else { } else {
if (headerBytes.length < 21 + channelCount) { if (headerBytes.length < 21 + channelCount) {
throw new OpusDecoderException("Header size is too small."); throw new OpusDecoderException("Invalid header length");
} }
// Read the channel mapping. // Read the channel mapping.
numStreams = headerBytes[19] & 0xFF; numStreams = headerBytes[19] & 0xFF;
numCoupled = headerBytes[20] & 0xFF; numCoupled = headerBytes[20] & 0xFF;
System.arraycopy(headerBytes, 21, streamMap, 0, channelCount); System.arraycopy(headerBytes, 21, streamMap, 0, channelCount);
} }
if (initializationData.size() == 3) {
if (initializationData.get(1).length != 8 || initializationData.get(2).length != 8) {
throw new OpusDecoderException("Invalid Codec Delay or Seek Preroll");
}
long codecDelayNs =
ByteBuffer.wrap(initializationData.get(1)).order(ByteOrder.nativeOrder()).getLong();
long seekPreRollNs =
ByteBuffer.wrap(initializationData.get(2)).order(ByteOrder.nativeOrder()).getLong();
headerSkipSamples = nsToSamples(codecDelayNs);
headerSeekPreRollSamples = nsToSamples(seekPreRollNs);
} else {
headerSkipSamples = preskip;
headerSeekPreRollSamples = DEFAULT_SEEK_PRE_ROLL_SAMPLES;
}
nativeDecoderContext = nativeDecoderContext =
opusInit(SAMPLE_RATE, channelCount, numStreams, numCoupled, gain, streamMap); opusInit(OpusUtil.SAMPLE_RATE, channelCount, numStreams, numCoupled, gain, streamMap);
if (nativeDecoderContext == 0) { if (nativeDecoderContext == 0) {
throw new OpusDecoderException("Failed to initialize decoder"); throw new OpusDecoderException("Failed to initialize decoder");
} }
@ -170,7 +160,7 @@ import java.util.List;
opusReset(nativeDecoderContext); opusReset(nativeDecoderContext);
// When seeking to 0, skip number of samples as specified in opus header. When seeking to // When seeking to 0, skip number of samples as specified in opus header. When seeking to
// any other time, skip number of samples as specified by seek preroll. // any other time, skip number of samples as specified by seek preroll.
skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples; skipSamples = (inputBuffer.timeUs == 0) ? preSkipSamples : seekPreRollSamples;
} }
ByteBuffer inputData = Util.castNonNull(inputBuffer.data); ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
@ -182,7 +172,7 @@ import java.util.List;
inputData, inputData,
inputData.limit(), inputData.limit(),
outputBuffer, outputBuffer,
SAMPLE_RATE, OpusUtil.SAMPLE_RATE,
exoMediaCrypto, exoMediaCrypto,
cryptoInfo.mode, cryptoInfo.mode,
Assertions.checkNotNull(cryptoInfo.key), Assertions.checkNotNull(cryptoInfo.key),
@ -231,18 +221,10 @@ import java.util.List;
opusClose(nativeDecoderContext); opusClose(nativeDecoderContext);
} }
private static int nsToSamples(long ns) { private static int readSignedLittleEndian16(byte[] input, int offset) {
return (int) (ns * SAMPLE_RATE / 1000000000);
}
private static int readUnsignedLittleEndian16(byte[] input, int offset) {
int value = input[offset] & 0xFF; int value = input[offset] & 0xFF;
value |= (input[offset + 1] & 0xFF) << 8; value |= (input[offset + 1] & 0xFF) << 8;
return value; return (short) value;
}
private static int readSignedLittleEndian16(byte[] input, int offset) {
return (short) readUnsignedLittleEndian16(input, offset);
} }
private native long opusInit( private native long opusInit(

View File

@ -0,0 +1,112 @@
/*
* 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 com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.C;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
/** Utility methods for handling Opus audio streams. */
public class OpusUtil {
/** Opus streams are always 48000 Hz. */
public static final int SAMPLE_RATE = 48_000;
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 pre-skip samples specified by the given Opus codec initialization data.
*
* @param initializationData The codec initialization data.
* @return The number of pre-skip samples.
*/
public static int getPreSkipSamples(List<byte[]> initializationData) {
if (initializationData.size() == FULL_CODEC_INITIALIZATION_DATA_BUFFER_COUNT) {
long codecDelayNs =
ByteBuffer.wrap(initializationData.get(1)).order(ByteOrder.nativeOrder()).getLong();
return (int) nanosecondsToSampleCount(codecDelayNs);
}
// Fall back to parsing directly from the Opus Identification header.
return getPreSkipSamples(initializationData.get(0));
}
/**
* Returns the number of seek per-roll samples specified by the given Opus codec initialization
* data.
*
* @param initializationData The codec initialization data.
* @return The number of seek pre-roll samples.
*/
public static int getSeekPreRollSamples(List<byte[]> initializationData) {
if (initializationData.size() == FULL_CODEC_INITIALIZATION_DATA_BUFFER_COUNT) {
long seekPreRollNs =
ByteBuffer.wrap(initializationData.get(2)).order(ByteOrder.nativeOrder()).getLong();
return (int) nanosecondsToSampleCount(seekPreRollNs);
}
// Fall back to returning the default seek pre-roll.
return DEFAULT_SEEK_PRE_ROLL_SAMPLES;
}
private static int getPreSkipSamples(byte[] header) {
return ((header[11] & 0xFF) << 8) | (header[10] & 0xFF);
}
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;
}
private static long nanosecondsToSampleCount(long nanoseconds) {
return (nanoseconds * SAMPLE_RATE) / C.NANOS_PER_SECOND;
}
}

View File

@ -0,0 +1,104 @@
/*
* 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 com.google.android.exoplayer2.audio;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link OpusUtil}. */
@RunWith(AndroidJUnit4.class)
public final class OpusUtilTest {
private static final byte[] HEADER =
new byte[] {79, 112, 117, 115, 72, 101, 97, 100, 0, 2, 1, 56, 0, 0, -69, -128, 0, 0, 0};
private static final int HEADER_PRE_SKIP_SAMPLES = 14337;
private static final byte[] HEADER_PRE_SKIP_BYTES =
buildNativeOrderByteArray(sampleCountToNanoseconds(HEADER_PRE_SKIP_SAMPLES));
private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
private static final byte[] DEFAULT_SEEK_PRE_ROLL_BYTES =
buildNativeOrderByteArray(sampleCountToNanoseconds(DEFAULT_SEEK_PRE_ROLL_SAMPLES));
private static final ImmutableList<byte[]> HEADER_ONLY_INITIALIZATION_DATA =
ImmutableList.of(HEADER);
private static final long CUSTOM_PRE_SKIP_SAMPLES = 28674;
private static final byte[] CUSTOM_PRE_SKIP_BYTES =
buildNativeOrderByteArray(sampleCountToNanoseconds(CUSTOM_PRE_SKIP_SAMPLES));
private static final long CUSTOM_SEEK_PRE_ROLL_SAMPLES = 7680;
private static final byte[] CUSTOM_SEEK_PRE_ROLL_BYTES =
buildNativeOrderByteArray(sampleCountToNanoseconds(CUSTOM_SEEK_PRE_ROLL_SAMPLES));
private static final ImmutableList<byte[]> FULL_INITIALIZATION_DATA =
ImmutableList.of(HEADER, CUSTOM_PRE_SKIP_BYTES, CUSTOM_SEEK_PRE_ROLL_BYTES);
@Test
public void buildInitializationData() {
List<byte[]> initializationData = OpusUtil.buildInitializationData(HEADER);
assertThat(initializationData).hasSize(3);
assertThat(initializationData.get(0)).isEqualTo(HEADER);
assertThat(initializationData.get(1)).isEqualTo(HEADER_PRE_SKIP_BYTES);
assertThat(initializationData.get(2)).isEqualTo(DEFAULT_SEEK_PRE_ROLL_BYTES);
}
@Test
public void getChannelCount() {
int channelCount = OpusUtil.getChannelCount(HEADER);
assertThat(channelCount).isEqualTo(2);
}
@Test
public void getPreSkipSamples_fullInitializationData_returnsOverrideValue() {
int preSkipSamples = OpusUtil.getPreSkipSamples(FULL_INITIALIZATION_DATA);
assertThat(preSkipSamples).isEqualTo(CUSTOM_PRE_SKIP_SAMPLES);
}
@Test
public void getPreSkipSamples_headerOnlyInitializationData_returnsHeaderValue() {
int preSkipSamples = OpusUtil.getPreSkipSamples(HEADER_ONLY_INITIALIZATION_DATA);
assertThat(preSkipSamples).isEqualTo(HEADER_PRE_SKIP_SAMPLES);
}
@Test
public void getSeekPreRollSamples_fullInitializationData_returnsInitializationDataValue() {
int seekPreRollSamples = OpusUtil.getSeekPreRollSamples(FULL_INITIALIZATION_DATA);
assertThat(seekPreRollSamples).isEqualTo(CUSTOM_SEEK_PRE_ROLL_SAMPLES);
}
@Test
public void getSeekPreRollSamples_headerOnlyInitializationData_returnsDefaultValue() {
int seekPreRollSamples = OpusUtil.getSeekPreRollSamples(HEADER_ONLY_INITIALIZATION_DATA);
assertThat(seekPreRollSamples).isEqualTo(DEFAULT_SEEK_PRE_ROLL_SAMPLES);
}
private static long sampleCountToNanoseconds(long sampleCount) {
return (sampleCount * C.NANOS_PER_SECOND) / OpusUtil.SAMPLE_RATE;
}
private static byte[] buildNativeOrderByteArray(long value) {
return ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(value).array();
}
}

View File

@ -15,13 +15,10 @@
*/ */
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.OpusUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -30,11 +27,6 @@ import java.util.List;
*/ */
/* package */ final class OpusReader extends StreamReader { /* package */ final class OpusReader extends StreamReader {
private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
/** Opus streams are always decoded at 48000 Hz. */
private static final int SAMPLE_RATE = 48_000;
private static final int OPUS_CODE = 0x4f707573; private static final int OPUS_CODE = 0x4f707573;
private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
@ -65,20 +57,14 @@ import java.util.List;
@Override @Override
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
if (!headerRead) { if (!headerRead) {
byte[] metadata = Arrays.copyOf(packet.getData(), packet.limit()); byte[] headerBytes = Arrays.copyOf(packet.getData(), packet.limit());
int channelCount = metadata[9] & 0xFF; int channelCount = OpusUtil.getChannelCount(headerBytes);
int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF); List<byte[]> initializationData = OpusUtil.buildInitializationData(headerBytes);
List<byte[]> initializationData = new ArrayList<>(3);
initializationData.add(metadata);
putNativeOrderLong(initializationData, preskip);
putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES);
setupData.format = setupData.format =
new Format.Builder() new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_OPUS) .setSampleMimeType(MimeTypes.AUDIO_OPUS)
.setChannelCount(channelCount) .setChannelCount(channelCount)
.setSampleRate(SAMPLE_RATE) .setSampleRate(OpusUtil.SAMPLE_RATE)
.setInitializationData(initializationData) .setInitializationData(initializationData)
.build(); .build();
headerRead = true; headerRead = true;
@ -90,12 +76,6 @@ import java.util.List;
return true; return true;
} }
private void putNativeOrderLong(List<byte[]> initializationData, int samples) {
long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE;
byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array();
initializationData.add(array);
}
/** /**
* Returns the duration of the given audio packet. * Returns the duration of the given audio packet.
* *