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 2f4e14fe7f..d7468e5a8f 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer.ext.flac; import com.google.android.exoplayer.DecoderInputBuffer; import com.google.android.exoplayer.util.extensions.SimpleDecoder; +import com.google.android.exoplayer.util.extensions.SimpleOutputBuffer; import java.nio.ByteBuffer; import java.util.List; @@ -25,21 +26,23 @@ import java.util.List; * Flac decoder. */ /* package */ final class FlacDecoder extends - SimpleDecoder { + SimpleDecoder { private final int maxOutputBufferSize; private final FlacJni decoder; + /** * Creates a Flac decoder. * * @param numInputBuffers The number of input buffers. * @param numOutputBuffers The number of output buffers. - * @param initializationData Codec-specific initialization data. + * @param initializationData Codec-specific initialization data. It should contain only one entry + * which is the flac file header. * @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder. */ public FlacDecoder(int numInputBuffers, int numOutputBuffers, List initializationData) throws FlacDecoderException { - super(new DecoderInputBuffer[numInputBuffers], new FlacOutputBuffer[numOutputBuffers]); + super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); if (initializationData.size() != 1) { throw new FlacDecoderException("Wrong number of initialization data"); } @@ -63,18 +66,13 @@ import java.util.List; } @Override - public FlacOutputBuffer createOutputBuffer() { - return new FlacOutputBuffer(this); - } - - @Override - protected void releaseOutputBuffer(FlacOutputBuffer buffer) { - super.releaseOutputBuffer(buffer); + public SimpleOutputBuffer createOutputBuffer() { + return new SimpleOutputBuffer(this); } @Override public FlacDecoderException decode(DecoderInputBuffer inputBuffer, - FlacOutputBuffer outputBuffer, boolean reset) { + SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { decoder.flush(); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoderException.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoderException.java index 8aeb564bea..331ad07879 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoderException.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacDecoderException.java @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer.ext.flac; +import com.google.android.exoplayer.util.extensions.AudioDecoderException; + /** * Thrown when an Flac decoder error occurs. */ -public final class FlacDecoderException extends Exception { +public final class FlacDecoderException extends AudioDecoderException { /* package */ FlacDecoderException(String message) { super(message); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/LibflacAudioTrackRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/LibflacAudioTrackRenderer.java index f0f6d6d143..8da0a30bc3 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/LibflacAudioTrackRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/LibflacAudioTrackRenderer.java @@ -15,17 +15,9 @@ */ package com.google.android.exoplayer.ext.flac; -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.TrackRenderer; -import com.google.android.exoplayer.TrackStream; -import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.extensions.AudioDecoderTrackRenderer; import android.os.Handler; @@ -34,63 +26,16 @@ import java.util.List; /** * Decodes and renders audio using the native Flac decoder. */ -public final class LibflacAudioTrackRenderer extends TrackRenderer implements MediaClock { - - /** - * Interface definition for a callback to be notified of {@link LibflacAudioTrackRenderer} 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(FlacDecoderException 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; +public class LibflacAudioTrackRenderer extends AudioDecoderTrackRenderer { private static final int NUM_BUFFERS = 16; - public final CodecCounters codecCounters = new CodecCounters(); - - private final Handler eventHandler; - private final EventListener eventListener; - private final FormatHolder formatHolder; - - private Format format; - private FlacDecoder decoder; - private DecoderInputBuffer inputBuffer; - private FlacOutputBuffer outputBuffer; - - private long currentPositionUs; - private boolean allowPositionDiscontinuity; - private boolean inputStreamEnded; - private boolean outputStreamEnded; - - private final AudioTrack audioTrack; - private int audioSessionId; + /** + * Returns whether the underlying libflac library is available. + */ + public static boolean isLibflacAvailable() { + return FlacJni.IS_AVAILABLE; + } public LibflacAudioTrackRenderer() { this(null, null); @@ -101,26 +46,10 @@ public final class LibflacAudioTrackRenderer extends TrackRenderer implements Me * 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 LibflacAudioTrackRenderer(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; - this.audioTrack = new AudioTrack(); - formatHolder = new FormatHolder(); + public LibflacAudioTrackRenderer(Handler eventHandler, + AudioDecoderTrackRenderer.EventListener eventListener) { + super(eventHandler, eventListener); } - - /** - * Returns whether the underlying libflac library is available. - */ - public static boolean isLibflacAvailable() { - return FlacJni.IS_AVAILABLE; - } - - @Override - protected MediaClock getMediaClock() { - return this; - } - @Override protected int supportsFormat(Format format) { return MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType) @@ -128,252 +57,8 @@ public final class LibflacAudioTrackRenderer extends TrackRenderer implements Me } @Override - protected void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputStreamEnded) { - return; - } - - // 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 flac, the format can contain only one entry in initializationData which is the flac - // file header. - List initializationData = format.initializationData; - if (initializationData.size() < 1) { - throw ExoPlaybackException.createForRenderer( - new IllegalStateException("Missing initialization data"), getIndex()); - } - try { - decoder = new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, initializationData); - } catch (FlacDecoderException 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 (FlacDecoderException e) { - notifyDecoderError(e); - throw ExoPlaybackException.createForRenderer(e, getIndex()); - } - codecCounters.ensureUpdated(); - } - - private void renderBuffer() throws FlacDecoderException, 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 FlacDecoderException { - 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 && (isSourceReady() || 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; - 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 FlacDecoderException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDecoderError(e); - } - }); - } + protected FlacDecoder createDecoder(List initializationData) throws FlacDecoderException { + return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, initializationData); } } 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 index a652fd7390..02df69da1e 100644 --- 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 @@ -15,17 +15,9 @@ */ 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.TrackRenderer; -import com.google.android.exoplayer.TrackStream; -import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.extensions.AudioDecoderTrackRenderer; import android.os.Handler; @@ -34,82 +26,11 @@ import java.util.List; /** * Decodes and renders audio using the native Opus decoder. */ -public final class LibopusAudioTrackRenderer extends TrackRenderer 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; +public final class LibopusAudioTrackRenderer extends AudioDecoderTrackRenderer { 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 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. */ @@ -124,9 +45,17 @@ public final class LibopusAudioTrackRenderer extends TrackRenderer implements Me return isLibopusAvailable() ? OpusDecoder.getLibopusVersion() : null; } - @Override - protected MediaClock getMediaClock() { - return this; + 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) { + super(eventHandler, eventListener); } @Override @@ -136,256 +65,9 @@ public final class LibopusAudioTrackRenderer extends TrackRenderer implements Me } @Override - protected void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputStreamEnded) { - return; - } - - // 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 && (isSourceReady() || 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; - 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); - } - }); - } + protected OpusDecoder createDecoder(List initializationData) throws OpusDecoderException { + return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + initializationData); } } 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 index e987d3299f..8493985b5c 100644 --- 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 @@ -18,6 +18,7 @@ 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 com.google.android.exoplayer.util.extensions.SimpleOutputBuffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -27,7 +28,7 @@ import java.util.List; * JNI wrapper for the libopus Opus decoder. */ /* package */ final class OpusDecoder extends - SimpleDecoder { + SimpleDecoder { /** * Whether the underlying libopus library is available. @@ -77,7 +78,7 @@ import java.util.List; */ public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, List initializationData) throws OpusDecoderException { - super(new DecoderInputBuffer[numInputBuffers], new OpusOutputBuffer[numOutputBuffers]); + super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); byte[] headerBytes = initializationData.get(0); if (headerBytes.length < 19) { throw new OpusDecoderException("Header size is too small."); @@ -139,18 +140,13 @@ import java.util.List; } @Override - public OpusOutputBuffer createOutputBuffer() { - return new OpusOutputBuffer(this); - } - - @Override - protected void releaseOutputBuffer(OpusOutputBuffer buffer) { - super.releaseOutputBuffer(buffer); + public SimpleOutputBuffer createOutputBuffer() { + return new SimpleOutputBuffer(this); } @Override public OpusDecoderException decode(DecoderInputBuffer inputBuffer, - OpusOutputBuffer outputBuffer, boolean reset) { + SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { opusReset(nativeDecoderContext); // When seeking to 0, skip number of samples as specified in opus header. When seeking to 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 index a3ff1b0688..375f1b6f2f 100644 --- 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 @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer.ext.opus; +import com.google.android.exoplayer.util.extensions.AudioDecoderException; + /** * Thrown when an Opus decoder error occurs. */ -public final class OpusDecoderException extends Exception { +public final class OpusDecoderException extends AudioDecoderException { /* 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 deleted file mode 100644 index c4af70794f..0000000000 --- a/extensions/opus/src/main/java/com/google/android/exoplayer/ext/opus/OpusOutputBuffer.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java index 709e504139..dd2f9e5f9d 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer/ext/vp9/VpxOutputBuffer.java @@ -59,13 +59,10 @@ public final class VpxOutputBuffer extends OutputBuffer { /* package */ void initForRgbFrame(int width, int height) { this.width = width; this.height = height; + this.yuvPlanes = null; + int minimumRgbSize = width * height * 2; - if (data == null || data.capacity() < minimumRgbSize) { - data = ByteBuffer.allocateDirect(minimumRgbSize); - yuvPlanes = null; - } - data.position(0); - data.limit(minimumRgbSize); + initData(minimumRgbSize); } /** @@ -76,13 +73,12 @@ public final class VpxOutputBuffer extends OutputBuffer { this.width = width; this.height = height; this.colorspace = colorspace; + int yLength = yStride * height; int uvLength = uvStride * ((height + 1) / 2); int minimumYuvSize = yLength + (uvLength * 2); - if (data == null || data.capacity() < minimumYuvSize) { - data = ByteBuffer.allocateDirect(minimumYuvSize); - } - data.limit(minimumYuvSize); + initData(minimumYuvSize); + if (yuvPlanes == null) { yuvPlanes = new ByteBuffer[3]; } @@ -104,4 +100,12 @@ public final class VpxOutputBuffer extends OutputBuffer { yuvStrides[2] = uvStride; } + private void initData(int size) { + if (data == null || data.capacity() < size) { + data = ByteBuffer.allocateDirect(size); + } + data.position(0); + data.limit(size); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderException.java b/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderException.java new file mode 100644 index 0000000000..3661d9debd --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderException.java @@ -0,0 +1,27 @@ +/* + * 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 com.google.android.exoplayer.util.extensions; + +/** + * Thrown when a decoder error occurs. + */ +public class AudioDecoderException extends Exception { + + public AudioDecoderException(String detailMessage) { + super(detailMessage); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderTrackRenderer.java new file mode 100644 index 0000000000..420182ada8 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/extensions/AudioDecoderTrackRenderer.java @@ -0,0 +1,361 @@ +/* + * 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 com.google.android.exoplayer.util.extensions; + +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.TrackRenderer; +import com.google.android.exoplayer.TrackStream; +import com.google.android.exoplayer.audio.AudioTrack; + +import android.os.Handler; + +import java.util.List; + +/** + * Decodes and renders audio using a {@link SimpleDecoder}. + */ +public abstract class AudioDecoderTrackRenderer extends TrackRenderer implements MediaClock { + + /** + * Interface definition for a callback to be notified of {@link AudioDecoderTrackRenderer} 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(AudioDecoderException 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; + + public final CodecCounters codecCounters = new CodecCounters(); + + private final Handler eventHandler; + private final EventListener eventListener; + private final FormatHolder formatHolder; + + private Format format; + private SimpleDecoder decoder; + private DecoderInputBuffer inputBuffer; + private SimpleOutputBuffer outputBuffer; + + private long currentPositionUs; + private boolean allowPositionDiscontinuity; + private boolean inputStreamEnded; + private boolean outputStreamEnded; + + private final AudioTrack audioTrack; + private int audioSessionId; + + public AudioDecoderTrackRenderer() { + 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 AudioDecoderTrackRenderer(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + this.audioTrack = new AudioTrack(); + formatHolder = new FormatHolder(); + } + + @Override + protected MediaClock getMediaClock() { + return this; + } + + @Override + protected void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (outputStreamEnded) { + return; + } + + // 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) { + try { + decoder = createDecoder(format.initializationData); + } catch (AudioDecoderException 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 (AudioDecoderException e) { + notifyDecoderError(e); + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + codecCounters.ensureUpdated(); + } + + protected abstract SimpleDecoder createDecoder(List initializationData) + throws AudioDecoderException; + + private void renderBuffer() throws AudioDecoderException, 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 AudioDecoderException { + 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 && (isSourceReady() || 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; + 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 AudioDecoderException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDecoderError(e); + } + }); + } + } + +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacOutputBuffer.java b/library/src/main/java/com/google/android/exoplayer/util/extensions/SimpleOutputBuffer.java similarity index 75% rename from extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacOutputBuffer.java rename to library/src/main/java/com/google/android/exoplayer/util/extensions/SimpleOutputBuffer.java index 663beba73e..22840f90bf 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer/ext/flac/FlacOutputBuffer.java +++ b/library/src/main/java/com/google/android/exoplayer/util/extensions/SimpleOutputBuffer.java @@ -13,26 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.ext.flac; - -import com.google.android.exoplayer.util.extensions.OutputBuffer; +package com.google.android.exoplayer.util.extensions; import java.nio.ByteBuffer; /** - * Buffer for {@link FlacDecoder} output. + * Buffer for {@link SimpleDecoder} output. */ -public final class FlacOutputBuffer extends OutputBuffer { +public class SimpleOutputBuffer extends OutputBuffer { - private final FlacDecoder owner; + private final SimpleDecoder owner; public ByteBuffer data; - /* package */ FlacOutputBuffer(FlacDecoder owner) { + public SimpleOutputBuffer(SimpleDecoder owner) { this.owner = owner; } - /* package */ void init(int size) { + public void init(int size) { if (data == null || data.capacity() < size) { data = ByteBuffer.allocateDirect(size); }