diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 0aac601045..6c3ece68a2 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -19,8 +19,8 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,21 +43,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * @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 FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @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. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index eb7206c9cf..5efaf98512 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -38,21 +38,12 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @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 LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @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. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } @Override diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 1850e68229..f31f80f518 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -40,35 +40,26 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @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. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); + public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } /** * @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. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); - } - - /** - * @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. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { - super(eventHandler, eventListener, audioCapabilities, drmSessionManager, - playClearSamplesWithoutKeys); + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, + bufferProcessors); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 298e528246..4547ec7e08 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -29,6 +29,7 @@ import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer { buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, componentListener, allowedVideoJoiningTimeMs, out); buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, out); + componentListener, buildBufferProcessors(), out); buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); @@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers @@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. + * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. May be empty. * @param out An array to which the built renderers should be appended. */ protected void buildAudioRenderers(Context context, Handler mainHandler, DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, - ArrayList out) { + BufferProcessor[] bufferProcessors, ArrayList out) { out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - mainHandler, eventListener, AudioCapabilities.getCapabilities(context))); + mainHandler, eventListener, AudioCapabilities.getCapabilities(context), bufferProcessors)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { @@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { @@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { @@ -787,6 +793,14 @@ public class SimpleExoPlayer implements ExoPlayer { // Do nothing. } + /** + * Builds an array of {@link BufferProcessor}s which will process PCM audio buffers before they + * are output. + */ + protected BufferProcessor[] buildBufferProcessors() { + return new BufferProcessor[0]; + } + // Internal methods. private void removeSurfaceCallbacks() { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 11c388fdab..c0ba7ad3e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -90,6 +90,21 @@ public final class AudioTrack { } + /** + * Thrown when a failure occurs configuring the track. + */ + public static final class ConfigurationException extends Exception { + + public ConfigurationException(Throwable cause) { + super(cause); + } + + public ConfigurationException(String message) { + super(message); + } + + } + /** * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. */ @@ -254,6 +269,7 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; + private final BufferProcessor[] bufferProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; @@ -267,12 +283,12 @@ public final class AudioTrack { private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; - @C.StreamType - private int streamType; @C.Encoding - private int inputEncoding; + private int encoding; @C.Encoding private int outputEncoding; + @C.StreamType + private int streamType; private boolean passthrough; private int pcmFrameSize; private int bufferSize; @@ -303,8 +319,6 @@ public final class AudioTrack { private byte[] preV21OutputBuffer; private int preV21OutputBufferOffset; - private BufferProcessor resampler; - private boolean playing; private int audioSessionId; private boolean tunneling; @@ -312,11 +326,18 @@ public final class AudioTrack { private long lastFeedElapsedRealtimeMs; /** - * @param audioCapabilities The current audio capabilities. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. May be empty. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) { + public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors, + Listener listener) { this.audioCapabilities = audioCapabilities; + this.bufferProcessors = new BufferProcessor[bufferProcessors.length + 1]; + this.bufferProcessors[0] = new ResamplingBufferProcessor(); + System.arraycopy(bufferProcessors, 0, this.bufferProcessors, 1, bufferProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -413,9 +434,23 @@ public final class AudioTrack { * {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size automatically. + * @throws ConfigurationException If an error occurs configuring the track. */ public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) { + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); + @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; + if (!passthrough) { + for (BufferProcessor bufferProcessor : bufferProcessors) { + try { + bufferProcessor.configure(sampleRate, channelCount, encoding); + } catch (BufferProcessor.UnhandledFormatException e) { + throw new ConfigurationException(e); + } + encoding = bufferProcessor.getOutputEncoding(); + } + } + int channelConfig; switch (channelCount) { case 1: @@ -443,7 +478,7 @@ public final class AudioTrack { channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; break; default: - throw new IllegalArgumentException("Unsupported channel count: " + channelCount); + throw new ConfigurationException("Unsupported channel count: " + channelCount); } // Workaround for overly strict channel configuration checks on nVidia Shield. @@ -461,25 +496,13 @@ public final class AudioTrack { } } - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - // Workaround for Nexus Player not reporting support for mono passthrough. // (See [Internal: b/34268671].) if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - @C.Encoding int inputEncoding; - if (passthrough) { - inputEncoding = getEncodingForMimeType(mimeType); - } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT - || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { - inputEncoding = pcmEncoding; - } else { - throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); - } - - if (isInitialized() && this.inputEncoding == inputEncoding && this.sampleRate == sampleRate + if (isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -487,15 +510,12 @@ public final class AudioTrack { reset(); - this.inputEncoding = inputEncoding; + this.encoding = encoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. - outputEncoding = passthrough ? inputEncoding : C.ENCODING_PCM_16BIT; - - resampler = outputEncoding != inputEncoding ? new ResamplingBufferProcessor(inputEncoding) - : null; + outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; @@ -684,8 +704,12 @@ public final class AudioTrack { } inputBuffer = buffer; - outputBuffer = resampler != null ? resampler.handleBuffer(inputBuffer, outputBuffer) - : inputBuffer; + if (!passthrough) { + for (BufferProcessor bufferProcessor : bufferProcessors) { + buffer = bufferProcessor.handleBuffer(buffer); + } + } + outputBuffer = buffer; if (Util.SDK_INT < 21) { int bytesRemaining = outputBuffer.remaining(); if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { @@ -886,6 +910,9 @@ public final class AudioTrack { framesPerEncodedSample = 0; inputBuffer = null; avSyncHeader = null; + for (BufferProcessor bufferProcessor : bufferProcessors) { + bufferProcessor.flush(); + } bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; latencyUs = 0; @@ -919,6 +946,9 @@ public final class AudioTrack { public void release() { reset(); releaseKeepSessionIdAudioTrack(); + for (BufferProcessor bufferProcessor : bufferProcessors) { + bufferProcessor.release(); + } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java index a10e8c05af..4f604f1a5d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -15,23 +15,60 @@ */ package com.google.android.exoplayer2.audio; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; /** - * Interface for processors of buffers, for use with {@link AudioTrack}. + * Interface for processors of audio buffers. */ public interface BufferProcessor { /** - * Processes the data in the specified input buffer in its entirety. Populates {@code output} with - * processed data if is not {@code null} and has sufficient capacity. Otherwise a different buffer - * will be populated and returned. + * Exception thrown when a processor can't be configured for a given input format. + */ + final class UnhandledFormatException extends Exception { + + public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) { + super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " + + encoding); + } + + } + + /** + * Configures this processor to take input buffers with the specified format. + * + * @param sampleRateHz The sample rate of input audio in Hz. + * @param channelCount The number of interleaved channels in input audio. + * @param encoding The encoding of input audio. + * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + */ + void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException; + + /** + * Returns the encoding used in buffers output by this processor. + */ + @C.Encoding + int getOutputEncoding(); + + /** + * Processes the data in the specified input buffer in its entirety. * * @param input A buffer containing the input data to process. - * @param output A buffer into which the output should be written, if its capacity is sufficient. - * @return The processed output. Different to {@code output} if null was passed, or if its - * capacity was insufficient. + * @return A buffer containing the processed output. This may be the same as the input buffer if + * no processing was required. */ - ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output); + ByteBuffer handleBuffer(ByteBuffer input); + + /** + * Clears any state in preparation for receiving a new stream of buffers. + */ + void flush(); + + /** + * Releases any resources associated with this instance. + */ + void release(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f8501c3858..dc7cdf42c8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -121,13 +121,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -219,14 +222,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { boolean passthrough = passthroughMediaFormat != null; String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) : MimeTypes.AUDIO_RAW; MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + try { + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + } catch (AudioTrack.ConfigurationException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index f0ea5e60c7..4495cfdbee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -21,35 +21,47 @@ import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** - * A {@link BufferProcessor} that converts PCM input buffers from a specified input bit depth to - * {@link C#ENCODING_PCM_16BIT} in preparation for writing to an {@link android.media.AudioTrack}. + * A {@link BufferProcessor} that outputs buffers in {@link C#ENCODING_PCM_16BIT}. */ /* package */ final class ResamplingBufferProcessor implements BufferProcessor { @C.PcmEncoding - private final int inputEncoding; + private int encoding; + private ByteBuffer outputBuffer; - /** - * Creates a new buffer processor for resampling input in the specified encoding. - * - * @param inputEncoding The PCM encoding of input buffers. - * @throws IllegalArgumentException Thrown if the input encoding is not PCM or its bit depth is - * not 8, 24 or 32-bits. - */ - public ResamplingBufferProcessor(@C.PcmEncoding int inputEncoding) { - Assertions.checkArgument(inputEncoding == C.ENCODING_PCM_8BIT - || inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); - this.inputEncoding = inputEncoding; + public ResamplingBufferProcessor() { + encoding = C.ENCODING_INVALID; } @Override - public ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output) { - int offset = input.position(); - int limit = input.limit(); - int size = limit - offset; + public void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (encoding == C.ENCODING_PCM_16BIT) { + outputBuffer = null; + } + this.encoding = encoding; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public ByteBuffer handleBuffer(ByteBuffer buffer) { + int position = buffer.position(); + int limit = buffer.limit(); + int size = limit - position; int resampledSize; - switch (inputEncoding) { + switch (encoding) { + case C.ENCODING_PCM_16BIT: + // No processing required. + return buffer; case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; @@ -59,7 +71,6 @@ import java.nio.ByteBuffer; case C.ENCODING_PCM_32BIT: resampledSize = size / 2; break; - case C.ENCODING_PCM_16BIT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -67,34 +78,34 @@ import java.nio.ByteBuffer; throw new IllegalStateException(); } - ByteBuffer resampledBuffer = output; - if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { - resampledBuffer = ByteBuffer.allocateDirect(resampledSize); + if (outputBuffer == null || outputBuffer.capacity() < resampledSize) { + outputBuffer = ByteBuffer.allocateDirect(resampledSize).order(buffer.order()); + } else { + Assertions.checkState(!outputBuffer.hasRemaining()); + outputBuffer.clear(); } - resampledBuffer.position(0); - resampledBuffer.limit(resampledSize); // Samples are little endian. - switch (inputEncoding) { + switch (encoding) { case C.ENCODING_PCM_8BIT: // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. - for (int i = offset; i < limit; i++) { - resampledBuffer.put((byte) 0); - resampledBuffer.put((byte) ((input.get(i) & 0xFF) - 128)); + for (int i = position; i < limit; i++) { + outputBuffer.put((byte) 0); + outputBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); } break; case C.ENCODING_PCM_24BIT: // 24->16 bit resampling. Drop the least significant byte. - for (int i = offset; i < limit; i += 3) { - resampledBuffer.put(input.get(i + 1)); - resampledBuffer.put(input.get(i + 2)); + for (int i = position; i < limit; i += 3) { + outputBuffer.put(buffer.get(i + 1)); + outputBuffer.put(buffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: // 32->16 bit resampling. Drop the two least significant bytes. - for (int i = offset; i < limit; i += 4) { - resampledBuffer.put(input.get(i + 2)); - resampledBuffer.put(input.get(i + 3)); + for (int i = position; i < limit; i += 4) { + outputBuffer.put(buffer.get(i + 2)); + outputBuffer.put(buffer.get(i + 3)); } break; case C.ENCODING_PCM_16BIT: @@ -105,8 +116,18 @@ import java.nio.ByteBuffer; throw new IllegalStateException(); } - resampledBuffer.position(0); - return resampledBuffer; + outputBuffer.flip(); + return outputBuffer; + } + + @Override + public void flush() { + // Do nothing. + } + + @Override + public void release() { + outputBuffer = null; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index d23ee769dd..9e75145626 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -102,10 +102,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @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. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener) { - this(eventHandler, eventListener, null); + AudioRendererEventListener eventListener, BufferProcessor... bufferProcessors) { + this(eventHandler, eventListener, null, null, false, bufferProcessors); } /** @@ -133,13 +135,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO); eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); this.drmSessionManager = drmSessionManager; formatHolder = new FormatHolder(); this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; @@ -193,8 +198,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (AudioTrack.InitializationException | AudioTrack.WriteException - | AudioDecoderException e) { + } catch (AudioDecoderException | AudioTrack.ConfigurationException + | AudioTrack.InitializationException | AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } decoderCounters.ensureUpdated(); @@ -255,7 +260,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, - AudioTrack.InitializationException, AudioTrack.WriteException { + AudioTrack.ConfigurationException, AudioTrack.InitializationException, + AudioTrack.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 9be1c59baf..0330b13eb6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -779,8 +779,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param codec The {@link MediaCodec} instance. * @param outputFormat The new output format. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output format. */ - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { // Do nothing. } @@ -918,7 +920,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Processes a new output format. */ - private void processOutputFormat() { + private void processOutputFormat() throws ExoPlaybackException { MediaFormat format = codec.getOutputFormat(); if (codecNeedsAdaptationWorkaround && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT