From 192f566a1bcbaae98bf0a4584a203c2463ba7137 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 12 Apr 2016 08:55:44 -0700 Subject: [PATCH] Copy opus extension v1->v2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=119643009 --- .../exoplayer/ext/flac/FlacDecoder.java | 5 +- extensions/opus/README.md | 77 ++++ extensions/opus/build.gradle | 45 ++ extensions/opus/src/main/.classpath | 10 + extensions/opus/src/main/.cproject | 57 +++ extensions/opus/src/main/.project | 97 +++++ .../main/.settings/org.eclipse.jdt.core.prefs | 12 + extensions/opus/src/main/AndroidManifest.xml | 22 + .../ext/opus/LibopusAudioTrackRenderer.java | 397 ++++++++++++++++++ .../exoplayer/ext/opus/OpusDecoder.java | 217 ++++++++++ .../ext/opus/OpusDecoderException.java | 27 ++ .../exoplayer/ext/opus/OpusOutputBuffer.java | 56 +++ extensions/opus/src/main/jni/Android.mk | 33 ++ extensions/opus/src/main/jni/Application.mk | 20 + .../opus/src/main/jni/convert_android_asm.sh | 47 +++ extensions/opus/src/main/jni/libopus.mk | 50 +++ extensions/opus/src/main/jni/opus_jni.cc | 111 +++++ extensions/opus/src/main/proguard.cfg | 6 + extensions/opus/src/main/project.properties | 16 + extensions/opus/src/main/res/.README.txt | 2 + .../exoplayer/text/SubtitleParser.java | 2 +- .../util/extensions/SimpleDecoder.java | 15 +- settings.gradle | 2 + 23 files changed, 1318 insertions(+), 8 deletions(-) create mode 100644 extensions/opus/README.md create mode 100644 extensions/opus/build.gradle create mode 100644 extensions/opus/src/main/.classpath create mode 100644 extensions/opus/src/main/.cproject create mode 100644 extensions/opus/src/main/.project create mode 100644 extensions/opus/src/main/.settings/org.eclipse.jdt.core.prefs create mode 100644 extensions/opus/src/main/AndroidManifest.xml create mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/LibopusAudioTrackRenderer.java create mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoder.java create mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderException.java create mode 100644 extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusOutputBuffer.java create mode 100644 extensions/opus/src/main/jni/Android.mk create mode 100644 extensions/opus/src/main/jni/Application.mk create mode 100755 extensions/opus/src/main/jni/convert_android_asm.sh create mode 100644 extensions/opus/src/main/jni/libopus.mk create mode 100644 extensions/opus/src/main/jni/opus_jni.cc create mode 100644 extensions/opus/src/main/proguard.cfg create mode 100644 extensions/opus/src/main/project.properties create mode 100644 extensions/opus/src/main/res/.README.txt diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java index 1ffd1d397e..2f4e14fe7f 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoder.java @@ -74,7 +74,10 @@ import java.util.List; @Override public FlacDecoderException decode(DecoderInputBuffer inputBuffer, - FlacOutputBuffer outputBuffer) { + FlacOutputBuffer outputBuffer, boolean reset) { + if (reset) { + decoder.flush(); + } ByteBuffer data = inputBuffer.data; outputBuffer.timestampUs = inputBuffer.timeUs; data.limit(data.position()); diff --git a/extensions/opus/README.md b/extensions/opus/README.md new file mode 100644 index 0000000000..8b1409c423 --- /dev/null +++ b/extensions/opus/README.md @@ -0,0 +1,77 @@ +# ExoPlayer Opus Extension # + +## Description ## + +The Opus Extension is a [TrackRenderer][] implementation that helps you bundle +libopus (the Opus decoding library) into your app and use it along with +ExoPlayer to play Opus audio on Android devices. + +[TrackRenderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/TrackRenderer.html + +## Build Instructions ## + +* Checkout ExoPlayer along with Extensions: + +``` +git clone https://github.com/google/ExoPlayer.git +``` + +* Set the following environment variables: + +``` +cd "" +EXOPLAYER_ROOT="$(pwd)" +OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main" +``` + +* Download the [Android NDK][] and set its location in an environment variable: + +``` +NDK_PATH="" +``` + +* Fetch libopus: + +``` +cd "${OPUS_EXT_PATH}/jni" && \ +git clone git://git.opus-codec.org/opus.git libopus +``` + +* Run the script to convert arm assembly to NDK compatible format: + +``` +cd ${OPUS_EXT_PATH}/jni && ./convert_android_asm.sh +``` + +* Build the JNI native libraries from the command line: + +``` +cd "${OPUS_EXT_PATH}"/jni && \ +${NDK_PATH}/ndk-build APP_ABI=all -j4 +``` + +* In your project, you can add a dependency to the Opus Extension by using a +rule like this: + +``` +// in settings.gradle +include ':..:ExoPlayer:library' +include ':..:ExoPlayer:extension-opus' + +// in build.gradle +dependencies { + compile project(':..:ExoPlayer:library') + compile project(':..:ExoPlayer:extension-opus') +} +``` + +* Now, when you build your app, the Opus extension will be built and the native + libraries will be packaged along with the APK. + +## Notes ## + +* Every time there is a change to the libopus checkout: + * Arm assembly should be converted by running `convert_android_asm.sh` + * Clean and re-build the project. +* If you want to use your own version of libopus, place it in + `${OPUS_EXT_PATH}/jni/libopus`. diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle new file mode 100644 index 0000000000..9ccc1862aa --- /dev/null +++ b/extensions/opus/build.gradle @@ -0,0 +1,45 @@ +// Copyright (C) 2014 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. +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 23 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + + lintOptions { + abortOnError false + } + + sourceSets.main { + jniLibs.srcDir 'src/main/libs' + jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + } +} + +dependencies { + compile project(':library') +} + diff --git a/extensions/opus/src/main/.classpath b/extensions/opus/src/main/.classpath new file mode 100644 index 0000000000..503bb38b67 --- /dev/null +++ b/extensions/opus/src/main/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/opus/src/main/.cproject b/extensions/opus/src/main/.cproject new file mode 100644 index 0000000000..22cc11ab57 --- /dev/null +++ b/extensions/opus/src/main/.cproject @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/opus/src/main/.project b/extensions/opus/src/main/.project new file mode 100644 index 0000000000..9b65339b9c --- /dev/null +++ b/extensions/opus/src/main/.project @@ -0,0 +1,97 @@ + + + ExoPlayerExt-Opus + + + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + ?children? + ?name?=outputEntries\|?children?=?name?=entry\\\\\\\|\\\|?name?=entry\\\\\\\|\\\|\|| + + + ?name? + + + + org.eclipse.cdt.make.core.append_environment + true + + + org.eclipse.cdt.make.core.buildArguments + + + + org.eclipse.cdt.make.core.buildCommand + ndk-build + + + org.eclipse.cdt.make.core.cleanBuildTarget + clean + + + org.eclipse.cdt.make.core.contents + org.eclipse.cdt.make.core.activeConfigSettings + + + org.eclipse.cdt.make.core.enableAutoBuild + false + + + org.eclipse.cdt.make.core.enableCleanBuild + true + + + org.eclipse.cdt.make.core.enableFullBuild + true + + + org.eclipse.cdt.make.core.stopOnError + true + + + org.eclipse.cdt.make.core.useDefaultBuildCmd + true + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + + diff --git a/extensions/opus/src/main/.settings/org.eclipse.jdt.core.prefs b/extensions/opus/src/main/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..d17b6724d1 --- /dev/null +++ b/extensions/opus/src/main/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/extensions/opus/src/main/AndroidManifest.xml b/extensions/opus/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..7c26ae79f3 --- /dev/null +++ b/extensions/opus/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/LibopusAudioTrackRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/LibopusAudioTrackRenderer.java new file mode 100644 index 0000000000..e30a502b85 --- /dev/null +++ b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/LibopusAudioTrackRenderer.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2014 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.exoplayer.ext.opus; + +import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.DecoderInputBuffer; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.ExoPlayer; +import com.google.android.exoplayer.Format; +import com.google.android.exoplayer.FormatHolder; +import com.google.android.exoplayer.MediaClock; +import com.google.android.exoplayer.SampleSourceTrackRenderer; +import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.TrackStream; +import com.google.android.exoplayer.audio.AudioTrack; +import com.google.android.exoplayer.util.MimeTypes; + +import android.os.Handler; + +import java.util.List; + +/** + * Decodes and renders audio using the native Opus decoder. + */ +public final class LibopusAudioTrackRenderer extends SampleSourceTrackRenderer + implements MediaClock { + + /** + * Interface definition for a callback to be notified of {@link LibopusAudioTrackRenderer} events. + */ + public interface EventListener { + + /** + * Invoked when the {@link AudioTrack} fails to initialize. + * + * @param e The corresponding exception. + */ + void onAudioTrackInitializationError(AudioTrack.InitializationException e); + + /** + * Invoked when an {@link AudioTrack} write fails. + * + * @param e The corresponding exception. + */ + void onAudioTrackWriteError(AudioTrack.WriteException e); + + /** + * Invoked when decoding fails. + * + * @param e The corresponding exception. + */ + void onDecoderError(OpusDecoderException e); + + } + + /** + * The type of a message that can be passed to an instance of this class via + * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object + * should be a {@link Float} with 0 being silence and 1 being unity gain. + */ + public static final int MSG_SET_VOLUME = 1; + + private static final int NUM_BUFFERS = 16; + private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + + public final CodecCounters codecCounters = new CodecCounters(); + + private final Handler eventHandler; + private final EventListener eventListener; + private final AudioTrack audioTrack; + private final FormatHolder formatHolder; + + private Format format; + private OpusDecoder decoder; + private DecoderInputBuffer inputBuffer; + private OpusOutputBuffer outputBuffer; + + private long currentPositionUs; + private boolean allowPositionDiscontinuity; + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private boolean sourceIsReady; + + private int audioSessionId; + + public LibopusAudioTrackRenderer() { + this(null, null); + } + + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public LibopusAudioTrackRenderer(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioTrack = new AudioTrack(); + formatHolder = new FormatHolder(); + } + + /** + * Returns whether the underlying libopus library is available. + */ + public static boolean isLibopusAvailable() { + return OpusDecoder.IS_AVAILABLE; + } + + /** + * Returns the version of the underlying libopus library if available, otherwise {@code null}. + */ + public static String getLibopusVersion() { + return isLibopusAvailable() ? OpusDecoder.getLibopusVersion() : null; + } + + @Override + protected MediaClock getMediaClock() { + return this; + } + + @Override + protected int supportsFormat(Format format) { + return MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType) + ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + protected void render(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) + throws ExoPlaybackException { + if (outputStreamEnded) { + return; + } + this.sourceIsReady = sourceIsReady; + + // Try and read a format if we don't have one already. + if (format == null && !readFormat()) { + // We can't make progress without one. + return; + } + + // If we don't have a decoder yet, we need to instantiate one. + if (decoder == null) { + // For opus, the format can contain upto 3 entries in initializationData in the following + // exact order: + // 1) Opus Header Information (required) + // 2) Codec Delay in nanoseconds (required if Seek Preroll is present) + // 3) Seek Preroll in nanoseconds (required if Codec Delay is present) + List initializationData = format.initializationData; + if (initializationData.size() < 1) { + throw ExoPlaybackException.createForRenderer( + new IllegalStateException("Missing initialization data"), getIndex()); + } + try { + decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + initializationData); + } catch (OpusDecoderException e) { + notifyDecoderError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + decoder.start(); + codecCounters.codecInitCount++; + } + + // Rendering loop. + try { + renderBuffer(); + while (feedInputBuffer()) {} + } catch (AudioTrack.InitializationException e) { + notifyAudioTrackInitializationError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (AudioTrack.WriteException e) { + notifyAudioTrackWriteError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } catch (OpusDecoderException e) { + notifyDecoderError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + codecCounters.ensureUpdated(); + } + + private void renderBuffer() throws OpusDecoderException, AudioTrack.InitializationException, + AudioTrack.WriteException { + if (outputStreamEnded) { + return; + } + + if (outputBuffer == null) { + outputBuffer = decoder.dequeueOutputBuffer(); + if (outputBuffer == null) { + return; + } + } + + if (outputBuffer.isEndOfStream()) { + outputStreamEnded = true; + audioTrack.handleEndOfStream(); + outputBuffer.release(); + outputBuffer = null; + return; + } + + if (!audioTrack.isInitialized()) { + if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) { + audioTrack.initialize(audioSessionId); + } else { + audioSessionId = audioTrack.initialize(); + } + if (getState() == TrackRenderer.STATE_STARTED) { + audioTrack.play(); + } + } + + int handleBufferResult; + handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.data.position(), + outputBuffer.data.remaining(), outputBuffer.timestampUs); + + // If we are out of sync, allow currentPositionUs to jump backwards. + if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { + allowPositionDiscontinuity = true; + } + + // Release the buffer if it was consumed. + if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { + codecCounters.renderedOutputBufferCount++; + outputBuffer.release(); + outputBuffer = null; + } + } + + private boolean feedInputBuffer() throws OpusDecoderException { + if (inputStreamEnded) { + return false; + } + + if (inputBuffer == null) { + inputBuffer = decoder.dequeueInputBuffer(); + if (inputBuffer == null) { + return false; + } + } + + int result = readSource(formatHolder, inputBuffer); + if (result == TrackStream.NOTHING_READ) { + return false; + } + if (result == TrackStream.FORMAT_READ) { + format = formatHolder.format; + return true; + } + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + } + + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + return true; + } + + private void flushDecoder() { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; + } + decoder.flush(); + } + + @Override + protected boolean isEnded() { + return outputStreamEnded && !audioTrack.hasPendingData(); + } + + @Override + protected boolean isReady() { + return audioTrack.hasPendingData() + || (format != null && (sourceIsReady || outputBuffer != null)); + } + + @Override + public long getPositionUs() { + long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { + currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; + } + return currentPositionUs; + } + + @Override + protected void reset(long positionUs) { + audioTrack.reset(); + currentPositionUs = positionUs; + allowPositionDiscontinuity = true; + inputStreamEnded = false; + outputStreamEnded = false; + sourceIsReady = false; + if (decoder != null) { + flushDecoder(); + } + } + + @Override + protected void onStarted() { + audioTrack.play(); + } + + @Override + protected void onStopped() { + audioTrack.pause(); + } + + @Override + protected void onDisabled() { + inputBuffer = null; + outputBuffer = null; + format = null; + audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + try { + if (decoder != null) { + decoder.release(); + decoder = null; + codecCounters.codecReleaseCount++; + } + audioTrack.release(); + } finally { + super.onDisabled(); + } + } + + private boolean readFormat() { + int result = readSource(formatHolder, null); + if (result == TrackStream.FORMAT_READ) { + format = formatHolder.format; + audioTrack.configure(format.getFrameworkMediaFormatV16(), false); + return true; + } + return false; + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_SET_VOLUME) { + audioTrack.setVolume((Float) message); + } else { + super.handleMessage(messageType, message); + } + } + + private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackInitializationError(e); + } + }); + } + } + + private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackWriteError(e); + } + }); + } + } + + private void notifyDecoderError(final OpusDecoderException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDecoderError(e); + } + }); + } + } + +} diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoder.java new file mode 100644 index 0000000000..e987d3299f --- /dev/null +++ b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoder.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2014 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.exoplayer.ext.opus; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.DecoderInputBuffer; +import com.google.android.exoplayer.util.extensions.SimpleDecoder; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; + +/** + * JNI wrapper for the libopus Opus decoder. + */ +/* package */ final class OpusDecoder extends + SimpleDecoder { + + /** + * Whether the underlying libopus library is available. + */ + public static final boolean IS_AVAILABLE; + static { + boolean isAvailable; + try { + System.loadLibrary("opus"); + System.loadLibrary("opusJNI"); + isAvailable = true; + } catch (UnsatisfiedLinkError exception) { + isAvailable = false; + } + IS_AVAILABLE = isAvailable; + } + + /** + * Returns the version string of the underlying libopus decoder. + */ + public static native String getLibopusVersion(); + + private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840; + + /** + * Opus streams are always decoded at 48000 Hz. + */ + private static final int SAMPLE_RATE = 48000; + + private final int channelCount; + private final int headerSkipSamples; + private final int headerSeekPreRollSamples; + private final long nativeDecoderContext; + + private int skipSamples; + + /** + * Creates an Opus decoder. + * + * @param numInputBuffers The number of input buffers. + * @param numOutputBuffers The number of output buffers. + * @param initialInputBufferSize The initial size of each input buffer. + * @param initializationData Codec-specific initialization data. The first element must contain an + * opus header. Optionally, the list may contain two additional buffers, which must contain + * the encoder delay and seek pre roll values in nanoseconds, encoded as longs. + * @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder. + */ + public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, + List initializationData) throws OpusDecoderException { + super(new DecoderInputBuffer[numInputBuffers], new OpusOutputBuffer[numOutputBuffers]); + byte[] headerBytes = initializationData.get(0); + if (headerBytes.length < 19) { + throw new OpusDecoderException("Header size is too small."); + } + channelCount = headerBytes[9] & 0xFF; + if (channelCount > 8) { + throw new OpusDecoderException("Invalid channel count: " + channelCount); + } + int preskip = readLittleEndian16(headerBytes, 10); + int gain = readLittleEndian16(headerBytes, 16); + + byte[] streamMap = new byte[8]; + int numStreams, numCoupled; + if (headerBytes[18] == 0) { // Channel mapping + // If there is no channel mapping, use the defaults. + if (channelCount > 2) { // Maximum channel count with default layout. + throw new OpusDecoderException("Invalid Header, missing stream map."); + } + numStreams = 1; + numCoupled = (channelCount == 2) ? 1 : 0; + streamMap[0] = 0; + streamMap[1] = 1; + } else { + if (headerBytes.length < 21 + channelCount) { + throw new OpusDecoderException("Header size is too small."); + } + // Read the channel mapping. + numStreams = headerBytes[19] & 0xFF; + numCoupled = headerBytes[20] & 0xFF; + for (int i = 0; i < channelCount; i++) { + streamMap[i] = headerBytes[21 + i]; + } + } + 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.LITTLE_ENDIAN).getLong(); + long seekPreRollNs = + ByteBuffer.wrap(initializationData.get(2)).order(ByteOrder.LITTLE_ENDIAN).getLong(); + headerSkipSamples = nsToSamples(codecDelayNs); + headerSeekPreRollSamples = nsToSamples(seekPreRollNs); + } else { + headerSkipSamples = preskip; + headerSeekPreRollSamples = DEFAULT_SEEK_PRE_ROLL_SAMPLES; + } + nativeDecoderContext = opusInit(SAMPLE_RATE, channelCount, numStreams, numCoupled, gain, + streamMap); + if (nativeDecoderContext == 0) { + throw new OpusDecoderException("Failed to initialize decoder"); + } + setInitialInputBufferSize(initialInputBufferSize); + } + + @Override + public DecoderInputBuffer createInputBuffer() { + return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + } + + @Override + public OpusOutputBuffer createOutputBuffer() { + return new OpusOutputBuffer(this); + } + + @Override + protected void releaseOutputBuffer(OpusOutputBuffer buffer) { + super.releaseOutputBuffer(buffer); + } + + @Override + public OpusDecoderException decode(DecoderInputBuffer inputBuffer, + OpusOutputBuffer outputBuffer, boolean reset) { + if (reset) { + opusReset(nativeDecoderContext); + // 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. + skipSamples = + (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples; + } + outputBuffer.timestampUs = inputBuffer.timeUs; + inputBuffer.data.position(inputBuffer.data.position() - inputBuffer.size); + int requiredOutputBufferSize = + opusGetRequiredOutputBufferSize(inputBuffer.data, inputBuffer.size, SAMPLE_RATE); + if (requiredOutputBufferSize < 0) { + return new OpusDecoderException("Error when computing required output buffer size."); + } + outputBuffer.init(requiredOutputBufferSize); + int result = opusDecode(nativeDecoderContext, inputBuffer.data, inputBuffer.size, + outputBuffer.data, outputBuffer.data.capacity()); + if (result < 0) { + return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result)); + } + outputBuffer.data.position(0); + outputBuffer.data.limit(result); + if (skipSamples > 0) { + int bytesPerSample = channelCount * 2; + int skipBytes = skipSamples * bytesPerSample; + if (result <= skipBytes) { + skipSamples -= result / bytesPerSample; + outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + outputBuffer.data.position(result); + } else { + skipSamples = 0; + outputBuffer.data.position(skipBytes); + } + } + return null; + } + + @Override + public void release() { + super.release(); + opusClose(nativeDecoderContext); + } + + private native long opusInit(int sampleRate, int channelCount, int numStreams, int numCoupled, + int gain, byte[] streamMap); + private native int opusDecode(long decoder, ByteBuffer inputBuffer, int inputSize, + ByteBuffer outputBuffer, int outputSize); + private native int opusGetRequiredOutputBufferSize( + ByteBuffer inputBuffer, int inputSize, int sampleRate); + private native void opusClose(long decoder); + private native void opusReset(long decoder); + private native String opusGetErrorMessage(int errorCode); + + private static int nsToSamples(long ns) { + return (int) (ns * SAMPLE_RATE / 1000000000); + } + + private static int readLittleEndian16(byte[] input, int offset) { + int value = input[offset] & 0xFF; + value |= (input[offset + 1] & 0xFF) << 8; + return value; + } + +} diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderException.java b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderException.java new file mode 100644 index 0000000000..a3ff1b0688 --- /dev/null +++ b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusDecoderException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2014 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.exoplayer.ext.opus; + +/** + * Thrown when an Opus decoder error occurs. + */ +public final class OpusDecoderException extends Exception { + + /* package */ OpusDecoderException(String message) { + super(message); + } + +} diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusOutputBuffer.java b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusOutputBuffer.java new file mode 100644 index 0000000000..c4af70794f --- /dev/null +++ b/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusOutputBuffer.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 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.exoplayer.ext.opus; + +import com.google.android.exoplayer.util.extensions.OutputBuffer; + +import java.nio.ByteBuffer; + +/** + * Buffer for {@link OpusDecoder} output. + */ +public final class OpusOutputBuffer extends OutputBuffer { + + private final OpusDecoder owner; + + public ByteBuffer data; + + /* package */ OpusOutputBuffer(OpusDecoder owner) { + this.owner = owner; + } + + /* package */ void init(int size) { + if (data == null || data.capacity() < size) { + data = ByteBuffer.allocateDirect(size); + } + data.position(0); + data.limit(size); + } + + @Override + public void clear() { + super.clear(); + if (data != null) { + data.clear(); + } + } + + @Override + public void release() { + owner.releaseOutputBuffer(this); + } + +} diff --git a/extensions/opus/src/main/jni/Android.mk b/extensions/opus/src/main/jni/Android.mk new file mode 100644 index 0000000000..892715e9c6 --- /dev/null +++ b/extensions/opus/src/main/jni/Android.mk @@ -0,0 +1,33 @@ +# +# Copyright (C) 2014 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. +# + +WORKING_DIR := $(call my-dir) +include $(CLEAR_VARS) + +# build libopus.so +LOCAL_PATH := $(WORKING_DIR) +include libopus.mk + +# build libopusJNI.so +include $(CLEAR_VARS) +LOCAL_PATH := $(WORKING_DIR) +LOCAL_MODULE := libopusJNI +LOCAL_ARM_MODE := arm +LOCAL_CPP_EXTENSION := .cc +LOCAL_SRC_FILES := opus_jni.cc +LOCAL_LDLIBS := -llog -lz -lm +LOCAL_SHARED_LIBRARIES := libopus +include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/opus/src/main/jni/Application.mk b/extensions/opus/src/main/jni/Application.mk new file mode 100644 index 0000000000..7dc417cda1 --- /dev/null +++ b/extensions/opus/src/main/jni/Application.mk @@ -0,0 +1,20 @@ +# +# Copyright (C) 2014 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. +# + +APP_OPTIM := release +APP_STL := gnustl_static +APP_CPPFLAGS := -frtti +APP_PLATFORM := android-9 diff --git a/extensions/opus/src/main/jni/convert_android_asm.sh b/extensions/opus/src/main/jni/convert_android_asm.sh new file mode 100755 index 0000000000..c789c66a1a --- /dev/null +++ b/extensions/opus/src/main/jni/convert_android_asm.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# Copyright (C) 2014 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. +# + +set -e +ASM_CONVERTER="./libopus/celt/arm/arm2gnu.pl" + +if [[ ! -x "${ASM_CONVERTER}" ]]; then + echo "Please make sure you have checked out libopus." + exit +fi + +while read file; do + # This check is required because the ASM conversion script doesn't seem to be + # idempotent. + if [[ ! "${file}" =~ .*_gnu\.s$ ]]; then + gnu_file="${file%.s}_gnu.s" + ${ASM_CONVERTER} "${file}" > "${gnu_file}" + # The ASM conversion script replaces includes with *_gnu.S. So, replace + # occurences of "*-gnu.S" with "*_gnu.s". + perl -pi -e "s/-gnu\.S/_gnu\.s/g" "${gnu_file}" + rm -f "${file}" + fi +done < <(find . -iname '*.s') + +# Generate armopts.s from armopts.s.in +sed \ + -e "s/@OPUS_ARM_MAY_HAVE_EDSP@/1/g" \ + -e "s/@OPUS_ARM_MAY_HAVE_MEDIA@/1/g" \ + -e "s/@OPUS_ARM_MAY_HAVE_NEON@/1/g" \ + libopus/celt/arm/armopts.s.in > libopus/celt/arm/armopts.s.temp +${ASM_CONVERTER} "libopus/celt/arm/armopts.s.temp" > "libopus/celt/arm/armopts_gnu.s" +rm "libopus/celt/arm/armopts.s.temp" +echo "Converted all ASM files and generated armopts.s successfully." diff --git a/extensions/opus/src/main/jni/libopus.mk b/extensions/opus/src/main/jni/libopus.mk new file mode 100644 index 0000000000..2eb5476e66 --- /dev/null +++ b/extensions/opus/src/main/jni/libopus.mk @@ -0,0 +1,50 @@ +# +# Copyright (C) 2014 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. +# + +LOCAL_PATH := $(call my-dir)/libopus + +include $(CLEAR_VARS) + +include $(LOCAL_PATH)/celt_headers.mk +include $(LOCAL_PATH)/celt_sources.mk +include $(LOCAL_PATH)/opus_headers.mk +include $(LOCAL_PATH)/opus_sources.mk +include $(LOCAL_PATH)/silk_headers.mk +include $(LOCAL_PATH)/silk_sources.mk + +LOCAL_MODULE := libopus +LOCAL_ARM_MODE := arm +LOCAL_CFLAGS := -DOPUS_BUILD -DFIXED_POINT -DUSE_ALLOCA -DHAVE_LRINT \ + -DHAVE_LRINTF +LOCAL_C_INCLUDES := $(LOCAL_PATH)/include $(LOCAL_PATH)/src \ + $(LOCAL_PATH)/silk $(LOCAL_PATH)/celt \ + $(LOCAL_PATH)/silk/fixed +LOCAL_SRC_FILES := $(CELT_SOURCES) $(OPUS_SOURCES) $(OPUS_SOURCES_FLOAT) \ + $(SILK_SOURCES) $(SILK_SOURCES_FIXED) + +ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),) +LOCAL_SRC_FILES += $(CELT_SOURCES_ARM) +LOCAL_SRC_FILES += celt/arm/armopts_gnu.s.neon +LOCAL_SRC_FILES += $(subst .s,_gnu.s.neon,$(CELT_SOURCES_ARM_ASM)) +LOCAL_CFLAGS += -DOPUS_ARM_ASM -DOPUS_ARM_INLINE_ASM -DOPUS_ARM_INLINE_EDSP \ + -DOPUS_ARM_INLINE_MEDIA -DOPUS_ARM_INLINE_NEON \ + -DOPUS_ARM_MAY_HAVE_NEON -DOPUS_ARM_MAY_HAVE_MEDIA \ + -DOPUS_ARM_MAY_HAVE_EDSP +endif + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include + +include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/opus/src/main/jni/opus_jni.cc b/extensions/opus/src/main/jni/opus_jni.cc new file mode 100644 index 0000000000..b15da26565 --- /dev/null +++ b/extensions/opus/src/main/jni/opus_jni.cc @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2014 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. + */ + +#include + +#include + +#include + +#include "opus.h" // NOLINT +#include "opus_multistream.h" // NOLINT + +#define LOG_TAG "libopus_native" +#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \ + __VA_ARGS__)) + +#define FUNC(RETURN_TYPE, NAME, ...) \ + extern "C" { \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer_ext_opus_OpusDecoder_ ## NAME \ + (JNIEnv* env, jobject thiz, ##__VA_ARGS__);\ + } \ + JNIEXPORT RETURN_TYPE \ + Java_com_google_android_exoplayer_ext_opus_OpusDecoder_ ## NAME \ + (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + return JNI_VERSION_1_6; +} + +static const int kBytesPerSample = 2; // opus fixed point uses 16 bit samples. +static int channelCount; + +FUNC(jlong, opusInit, jint sampleRate, jint channelCount, jint numStreams, + jint numCoupled, jint gain, jbyteArray jStreamMap) { + int status = OPUS_INVALID_STATE; + ::channelCount = channelCount; + jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0); + uint8_t* streamMap = reinterpret_cast(streamMapBytes); + OpusMSDecoder* decoder = opus_multistream_decoder_create( + sampleRate, channelCount, numStreams, numCoupled, streamMap, &status); + env->ReleaseByteArrayElements(jStreamMap, streamMapBytes, 0); + if (!decoder || status != OPUS_OK) { + LOGE("Failed to create Opus Decoder; status=%s", opus_strerror(status)); + return 0; + } + status = opus_multistream_decoder_ctl(decoder, OPUS_SET_GAIN(gain)); + if (status != OPUS_OK) { + LOGE("Failed to set Opus header gain; status=%s", opus_strerror(status)); + return 0; + } + return reinterpret_cast(decoder); +} + +FUNC(jint, opusDecode, jlong jDecoder, jobject jInputBuffer, jint inputSize, + jobject jOutputBuffer, jint outputSize) { + OpusMSDecoder* decoder = reinterpret_cast(jDecoder); + const uint8_t* inputBuffer = + reinterpret_cast( + env->GetDirectBufferAddress(jInputBuffer)); + int16_t* outputBuffer = reinterpret_cast( + env->GetDirectBufferAddress(jOutputBuffer)); + int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize, + outputBuffer, outputSize, 0); + return (sampleCount < 0) ? sampleCount + : sampleCount * kBytesPerSample * channelCount; +} + +FUNC(jint, opusGetRequiredOutputBufferSize, jobject jInputBuffer, + jint inputSize, jint sampleRate) { + const uint8_t* inputBuffer = reinterpret_cast( + env->GetDirectBufferAddress(jInputBuffer)); + const int32_t sampleCount = + opus_packet_get_nb_samples(inputBuffer, inputSize, sampleRate); + return sampleCount * kBytesPerSample * channelCount; +} + +FUNC(void, opusClose, jlong jDecoder) { + OpusMSDecoder* decoder = reinterpret_cast(jDecoder); + opus_multistream_decoder_destroy(decoder); +} + +FUNC(void, opusReset, jlong jDecoder) { + OpusMSDecoder* decoder = reinterpret_cast(jDecoder); + opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE); +} + +FUNC(jstring, getLibopusVersion) { + return env->NewStringUTF(opus_get_version_string()); +} + +FUNC(jstring, opusGetErrorMessage, jint errorCode) { + return env->NewStringUTF(opus_strerror(errorCode)); +} diff --git a/extensions/opus/src/main/proguard.cfg b/extensions/opus/src/main/proguard.cfg new file mode 100644 index 0000000000..15e910b1e9 --- /dev/null +++ b/extensions/opus/src/main/proguard.cfg @@ -0,0 +1,6 @@ +# Proguard rules specific to the Opus extension. + +# This prevents the names of native methods from being obfuscated. +-keepclasseswithmembernames class * { + native ; +} diff --git a/extensions/opus/src/main/project.properties b/extensions/opus/src/main/project.properties new file mode 100644 index 0000000000..b92a03b7ab --- /dev/null +++ b/extensions/opus/src/main/project.properties @@ -0,0 +1,16 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-23 +android.library=true +android.library.reference.1=../../../../library/src/main diff --git a/extensions/opus/src/main/res/.README.txt b/extensions/opus/src/main/res/.README.txt new file mode 100644 index 0000000000..c27147ce56 --- /dev/null +++ b/extensions/opus/src/main/res/.README.txt @@ -0,0 +1,2 @@ +This file is needed to make sure the res directory is present. +The file is ignored by the Android toolchain because its name starts with a dot. diff --git a/library/src/main/java/com/google/android/exoplayer/text/SubtitleParser.java b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParser.java index c48e6ab671..f46698bc3d 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/SubtitleParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/SubtitleParser.java @@ -46,7 +46,7 @@ public abstract class SubtitleParser extends @Override protected final ParserException decode(SubtitleInputBuffer inputBuffer, - SubtitleOutputBuffer outputBuffer) { + SubtitleOutputBuffer outputBuffer, boolean reset) { try { Subtitle subtitle = decode(inputBuffer.data.array(), inputBuffer.size); outputBuffer.setOutput(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); diff --git a/library/src/main/java/com/google/android/exoplayer/util/extensions/SimpleDecoder.java b/library/src/main/java/com/google/android/exoplayer/util/extensions/SimpleDecoder.java index 502c92c416..f9618f4880 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/extensions/SimpleDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer/util/extensions/SimpleDecoder.java @@ -52,7 +52,7 @@ public abstract class SimpleDecoder