/* * 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; import com.google.android.exoplayer.AudioTrackRendererEventListener.EventDispatcher; import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.AudioManager; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; import android.media.PlaybackParams; import android.media.audiofx.Virtualizer; import android.os.Handler; import android.os.SystemClock; import java.nio.ByteBuffer; /** * Decodes and renders audio using {@link MediaCodec} and {@link android.media.AudioTrack}. */ @TargetApi(16) public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implements MediaClock { private final EventDispatcher eventDispatcher; private final AudioTrack audioTrack; private boolean passthroughEnabled; private android.media.MediaFormat passthroughMediaFormat; private int pcmEncoding; private int audioSessionId; private long currentPositionUs; private boolean allowPositionDiscontinuity; private boolean audioTrackHasData; private long lastFeedElapsedRealtimeMs; /** * @param mediaCodecSelector A decoder selector. */ public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector) { this(mediaCodecSelector, null, true); } /** * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. * For example a media file may start with a short clear region so as to allow playback to * 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. */ public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); } /** * @param mediaCodecSelector A decoder selector. * @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 MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector, Handler eventHandler, AudioTrackRendererEventListener eventListener) { this(mediaCodecSelector, null, true, eventHandler, eventListener); } /** * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. * For example a media file may start with a short clear region so as to allow playback to * 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 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 MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, AudioTrackRendererEventListener eventListener) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener, null, AudioManager.STREAM_MUSIC); } /** * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. * For example a media file may start with a short clear region so as to allow playback to * 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 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 streamType The type of audio stream for the {@link AudioTrack}. */ public MediaCodecAudioTrackRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, AudioTrackRendererEventListener eventListener, AudioCapabilities audioCapabilities, int streamType) { super(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioTrack = new AudioTrack(audioCapabilities, streamType); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @Override public int getTrackType() { return C.TRACK_TYPE_AUDIO; } @Override protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { return FORMAT_UNSUPPORTED_TYPE; } if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED; } // TODO[REFACTOR]: If requiresSecureDecryption then we should probably also check that the // drmSession is able to make use of a secure decoder. DecoderInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, format.requiresSecureDecryption); if (decoderInfo == null) { return FORMAT_UNSUPPORTED_SUBTYPE; } // Note: We assume support for unknown sampleRate and channelCount. boolean decoderCapable = Util.SDK_INT < 21 || ((format.sampleRate == Format.NO_VALUE || decoderInfo.isAudioSampleRateSupportedV21(format.sampleRate)) && (format.channelCount == Format.NO_VALUE || decoderInfo.isAudioChannelCountSupportedV21(format.channelCount))); int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; return ADAPTIVE_NOT_SEAMLESS | formatSupport; } @Override protected DecoderInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) throws DecoderQueryException { if (allowPassthrough(format.sampleMimeType)) { DecoderInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); if (passthroughDecoderInfo != null) { passthroughEnabled = true; return passthroughDecoderInfo; } } passthroughEnabled = false; return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder); } /** * Returns whether encoded audio passthrough should be used for playing back the input format. * This implementation returns true if the {@link AudioTrack}'s audio capabilities indicate that * passthrough is supported. * * @param mimeType The type of input media. * @return True if passthrough playback should be used. False otherwise. */ protected boolean allowPassthrough(String mimeType) { return audioTrack.isPassthroughSupported(mimeType); } @Override protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) { if (passthroughEnabled) { // Override the MIME type used to configure the codec if we are using a passthrough decoder. passthroughMediaFormat = format.getFrameworkMediaFormatV16(); passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW); codec.configure(passthroughMediaFormat, null, crypto, 0); passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); } else { codec.configure(format.getFrameworkMediaFormatV16(), null, crypto, 0); passthroughMediaFormat = null; } } @Override protected MediaClock getMediaClock() { return this; } @Override protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); } @Override protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { super.onInputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat); // If the input format is anything other than PCM then we assume that the audio decoder will // output 16-bit PCM. pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding : C.ENCODING_PCM_16BIT; } @Override protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { boolean passthrough = passthroughMediaFormat != null; String mimeType = passthrough ? passthroughMediaFormat.getString(android.media.MediaFormat.KEY_MIME) : MimeTypes.AUDIO_RAW; android.media.MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(android.media.MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(android.media.MediaFormat.KEY_SAMPLE_RATE); audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding); } /** * Invoked when the audio session id becomes known. Once the id is known it will not change * (and hence this method will not be invoked again) unless the renderer is disabled and then * subsequently re-enabled. *

* The default implementation is a no-op. One reason for overriding this method would be to * instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For * this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()} * (if not before). * * @param audioSessionId The audio session id. */ protected void onAudioSessionId(int audioSessionId) { // Do nothing. } @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { codecCounters.reset(); eventDispatcher.enabled(codecCounters); super.onEnabled(joining); } @Override protected void onReset(long positionUs) throws ExoPlaybackException { super.onReset(positionUs); audioTrack.reset(); currentPositionUs = positionUs; allowPositionDiscontinuity = true; } @Override protected void onStarted() { super.onStarted(); audioTrack.play(); } @Override protected void onStopped() { audioTrack.pause(); super.onStopped(); } @Override protected void onDisabled() { eventDispatcher.disabled(); audioSessionId = AudioTrack.SESSION_ID_NOT_SET; try { audioTrack.release(); } finally { super.onDisabled(); } } @Override protected boolean isEnded() { return super.isEnded() && !audioTrack.hasPendingData(); } @Override protected boolean isReady() { return audioTrack.hasPendingData() || super.isReady(); } @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 boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException { if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // Discard output buffers from the passthrough (raw) decoder containing codec specific data. codec.releaseOutputBuffer(bufferIndex, false); return true; } if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); codecCounters.skippedOutputBufferCount++; audioTrack.handleDiscontinuity(); return true; } if (!audioTrack.isInitialized()) { // Initialize the AudioTrack now. try { if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) { audioTrack.initialize(audioSessionId); } else { audioSessionId = audioTrack.initialize(); onAudioSessionId(audioSessionId); } audioTrackHasData = false; } catch (AudioTrack.InitializationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } if (getState() == TrackRenderer.STATE_STARTED) { audioTrack.play(); } } else { // Check for AudioTrack underrun. boolean audioTrackHadData = audioTrackHasData; audioTrackHasData = audioTrack.hasPendingData(); if (audioTrackHadData && !audioTrackHasData && getState() == TrackRenderer.STATE_STARTED) { long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; long bufferSizeUs = audioTrack.getBufferSizeUs(); long bufferSizeMs = bufferSizeUs == C.UNSET_TIME_US ? -1 : bufferSizeUs / 1000; eventDispatcher.audioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs, elapsedSinceLastFeedMs); } } int handleBufferResult; try { handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs); lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); } catch (AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } // If we are out of sync, allow currentPositionUs to jump backwards. if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { handleAudioTrackDiscontinuity(); allowPositionDiscontinuity = true; } // Release the buffer if it was consumed. if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { codec.releaseOutputBuffer(bufferIndex, false); codecCounters.renderedOutputBufferCount++; return true; } return false; } @Override protected void onOutputStreamEnded() { audioTrack.handleEndOfStream(); } protected void handleAudioTrackDiscontinuity() { // Do nothing } @Override public void handleMessage(int messageType, Object message) throws ExoPlaybackException { switch (messageType) { case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; case C.MSG_SET_PLAYBACK_PARAMS: audioTrack.setPlaybackParams((PlaybackParams) message); break; default: super.handleMessage(messageType, message); break; } } }