mirror of
https://github.com/samsonjs/media.git
synced 2026-03-28 09:55:48 +00:00
456 lines
18 KiB
Java
456 lines
18 KiB
Java
/*
|
|
* 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.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.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 {
|
|
|
|
/**
|
|
* Interface definition for a callback to be notified of {@link MediaCodecAudioTrackRenderer}
|
|
* events.
|
|
*/
|
|
public interface EventListener extends MediaCodecTrackRenderer.EventListener {
|
|
|
|
/**
|
|
* Invoked when an {@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 an {@link AudioTrack} underrun occurs.
|
|
*
|
|
* @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes.
|
|
* @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is
|
|
* configured for PCM output. -1 if it is configured for passthrough output, as the buffered
|
|
* media can have a variable bitrate so the duration may be unknown.
|
|
* @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data.
|
|
*/
|
|
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
|
|
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* 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 android.media.PlaybackParams}, which will be used to configure the
|
|
* underlying {@link android.media.AudioTrack}. The message object should not be modified by the
|
|
* caller after it has been passed
|
|
*/
|
|
public static final int MSG_SET_PLAYBACK_PARAMS = 2;
|
|
|
|
private final EventListener eventListener;
|
|
private final AudioTrack audioTrack;
|
|
|
|
private boolean passthroughEnabled;
|
|
private android.media.MediaFormat passthroughMediaFormat;
|
|
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,
|
|
EventListener 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, EventListener 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, EventListener eventListener, AudioCapabilities audioCapabilities,
|
|
int streamType) {
|
|
super(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
|
|
eventListener);
|
|
this.eventListener = eventListener;
|
|
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
|
this.audioTrack = new AudioTrack(audioCapabilities, streamType);
|
|
}
|
|
|
|
@Override
|
|
protected int supportsFormat(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat)
|
|
throws DecoderQueryException {
|
|
String mimeType = mediaFormat.mimeType;
|
|
if (!MimeTypes.isAudio(mimeType)) {
|
|
return TrackRenderer.FORMAT_UNSUPPORTED_TYPE;
|
|
}
|
|
if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderName() != null) {
|
|
return TrackRenderer.FORMAT_HANDLED;
|
|
}
|
|
// TODO[REFACTOR]: Propagate requiresSecureDecoder to this point. Note that we need to check
|
|
// that the drm session can make use of a secure decoder, as well as that a secure decoder
|
|
// exists.
|
|
DecoderInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mediaFormat.mimeType, false);
|
|
if (decoderInfo == null) {
|
|
return TrackRenderer.FORMAT_UNSUPPORTED_TYPE;
|
|
}
|
|
if (Util.SDK_INT >= 21) {
|
|
// Note: We assume support in the case that the sampleRate or channelCount is unknown.
|
|
if (mediaFormat.sampleRate != MediaFormat.NO_VALUE
|
|
&& !decoderInfo.isAudioSampleRateSupportedV21(mediaFormat.sampleRate)) {
|
|
return TrackRenderer.FORMAT_EXCEEDS_CAPABILITIES;
|
|
}
|
|
if (mediaFormat.channelCount != MediaFormat.NO_VALUE
|
|
&& !decoderInfo.isAudioChannelCountSupportedV21(mediaFormat.channelCount)) {
|
|
return TrackRenderer.FORMAT_EXCEEDS_CAPABILITIES;
|
|
}
|
|
}
|
|
return TrackRenderer.FORMAT_HANDLED;
|
|
}
|
|
|
|
@Override
|
|
protected DecoderInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, MediaFormat format,
|
|
boolean requiresSecureDecoder) throws DecoderQueryException {
|
|
if (allowPassthrough(format.mimeType)) {
|
|
String passthroughDecoderName = mediaCodecSelector.getPassthroughDecoderName();
|
|
if (passthroughDecoderName != null) {
|
|
passthroughEnabled = true;
|
|
return new DecoderInfo(passthroughDecoderName);
|
|
}
|
|
}
|
|
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, MediaFormat 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(android.media.MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
|
|
codec.configure(passthroughMediaFormat, null, crypto, 0);
|
|
passthroughMediaFormat.setString(android.media.MediaFormat.KEY_MIME, format.mimeType);
|
|
} else {
|
|
codec.configure(format.getFrameworkMediaFormatV16(), null, crypto, 0);
|
|
passthroughMediaFormat = null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected MediaClock getMediaClock() {
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
protected void onOutputFormatChanged(android.media.MediaFormat outputFormat) {
|
|
boolean passthrough = passthroughMediaFormat != null;
|
|
audioTrack.configure(passthrough ? passthroughMediaFormat : outputFormat, passthrough);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <p>
|
|
* 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 onStarted() {
|
|
super.onStarted();
|
|
audioTrack.play();
|
|
}
|
|
|
|
@Override
|
|
protected void onStopped() {
|
|
audioTrack.pause();
|
|
super.onStopped();
|
|
}
|
|
|
|
@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 void onDisabled() throws ExoPlaybackException {
|
|
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
|
try {
|
|
audioTrack.release();
|
|
} finally {
|
|
super.onDisabled();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onReset(long positionUs) throws ExoPlaybackException {
|
|
super.onReset(positionUs);
|
|
audioTrack.reset();
|
|
currentPositionUs = positionUs;
|
|
allowPositionDiscontinuity = true;
|
|
}
|
|
|
|
@Override
|
|
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
|
|
ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
|
|
throws ExoPlaybackException {
|
|
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) {
|
|
notifyAudioTrackInitializationError(e);
|
|
throw new ExoPlaybackException(e);
|
|
}
|
|
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.UNKNOWN_TIME_US ? -1 : bufferSizeUs / 1000;
|
|
notifyAudioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs, elapsedSinceLastFeedMs);
|
|
}
|
|
}
|
|
|
|
int handleBufferResult;
|
|
try {
|
|
handleBufferResult = audioTrack.handleBuffer(
|
|
buffer, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs);
|
|
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
|
} catch (AudioTrack.WriteException e) {
|
|
notifyAudioTrackWriteError(e);
|
|
throw new ExoPlaybackException(e);
|
|
}
|
|
|
|
// 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 MSG_SET_VOLUME:
|
|
audioTrack.setVolume((Float) message);
|
|
break;
|
|
case MSG_SET_PLAYBACK_PARAMS:
|
|
audioTrack.setPlaybackParams((PlaybackParams) message);
|
|
break;
|
|
default:
|
|
super.handleMessage(messageType, message);
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 notifyAudioTrackUnderrun(final int bufferSize, final long bufferSizeMs,
|
|
final long elapsedSinceLastFeedMs) {
|
|
if (eventHandler != null && eventListener != null) {
|
|
eventHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
eventListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|