blob: 771704198e739025abc767b2777f2cca975c4d3d [file] [log] [blame]
/*
* Copyright (C) 2015 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.android.tv.tuner.exoplayer.audio;
import android.media.MediaCodec;
import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import com.google.android.exoplayer.CodecCounters;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaClock;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.android.tv.tuner.exoplayer.ffmpeg.FfmpegDecoderClient;
import com.android.tv.tuner.tvinput.TunerDebug;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
* Decodes and renders DTV audio. Supports MediaCodec based decoding, passthrough playback and
* ffmpeg based software decoding (AC3, MP2).
*/
public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements MediaClock {
public static final int MSG_SET_VOLUME = 10000;
public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1;
public static final int MSG_SET_PLAYBACK_SPEED = MSG_SET_VOLUME + 2;
// ATSC/53 allows sample rate to be only 48Khz.
// One AC3 sample has 1536 frames, and its duration is 32ms.
public static final long AC3_SAMPLE_DURATION_US = 32000;
// TODO: Check whether DVB broadcasting uses sample rate other than 48Khz.
// MPEG-1 audio Layer II and III has 1152 frames per sample.
// 1152 frames duration is 24ms when sample rate is 48Khz.
static final long MP2_SAMPLE_DURATION_US = 24000;
// This is around 150ms, 150ms is big enough not to under-run AudioTrack,
// and 150ms is also small enough to fill the buffer rapidly.
static int BUFFERED_SAMPLES_IN_AUDIOTRACK = 5;
public static final long INITIAL_AUDIO_BUFFERING_TIME_US =
BUFFERED_SAMPLES_IN_AUDIOTRACK * AC3_SAMPLE_DURATION_US;
private static final String TAG = "MpegTsDefaultAudioTrac";
private static final boolean DEBUG = false;
/**
* Interface definition for a callback to be notified of
* {@link com.google.android.exoplayer.audio.AudioTrack} error.
*/
public interface EventListener {
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
void onAudioTrackWriteError(AudioTrack.WriteException e);
}
private static final int DEFAULT_INPUT_BUFFER_SIZE = 16384 * 2;
private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024*1024;
private static final int MONITOR_DURATION_MS = 1000;
private static final int AC3_HEADER_BITRATE_OFFSET = 4;
private static final int MP2_HEADER_BITRATE_OFFSET = 2;
private static final int MP2_HEADER_BITRATE_MASK = 0xfc;
// Keep this as static in order to prevent new framework AudioTrack creation
// while old AudioTrack is being released.
private static final AudioTrackWrapper AUDIO_TRACK = new AudioTrackWrapper();
private static final long KEEP_ALIVE_AFTER_EOS_DURATION_MS = 3000;
// Ignore AudioTrack backward movement if duration of movement is below the threshold.
private static final long BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US = 3000;
// AudioTrack position cannot go ahead beyond this limit.
private static final long CURRENT_POSITION_FROM_PTS_LIMIT_US = 1000000;
// Since MediaCodec processing and AudioTrack playing add delay,
// PTS interpolated time should be delayed reasonably when AudioTrack is not used.
private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000;
private final MediaCodecSelector mSelector;
private final CodecCounters mCodecCounters;
private final SampleSource.SampleSourceReader mSource;
private final MediaFormatHolder mFormatHolder;
private final EventListener mEventListener;
private final Handler mEventHandler;
private final AudioTrackMonitor mMonitor;
private final AudioClock mAudioClock;
private final boolean mAc3Passthrough;
private final boolean mSoftwareDecoderAvailable;
private MediaFormat mFormat;
private SampleHolder mSampleHolder;
private String mDecodingMime;
private boolean mFormatConfigured;
private int mSampleSize;
private final ByteBuffer mOutputBuffer;
private AudioDecoder mAudioDecoder;
private boolean mOutputReady;
private int mTrackIndex;
private boolean mSourceStateReady;
private boolean mInputStreamEnded;
private boolean mOutputStreamEnded;
private long mEndOfStreamMs;
private long mCurrentPositionUs;
private int mPresentationCount;
private long mPresentationTimeUs;
private long mInterpolatedTimeUs;
private long mPreviousPositionUs;
private boolean mIsStopped;
private boolean mEnabled = true;
private boolean mIsMuted;
private ArrayList<Integer> mTracksIndex;
private boolean mUseFrameworkDecoder;
public MpegTsDefaultAudioTrackRenderer(
SampleSource source,
MediaCodecSelector selector,
Handler eventHandler,
EventListener listener,
boolean hasSoftwareAudioDecoder,
boolean usePassthrough) {
mSource = source.register();
mSelector = selector;
mEventHandler = eventHandler;
mEventListener = listener;
mTrackIndex = -1;
mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE);
mFormatHolder = new MediaFormatHolder();
AUDIO_TRACK.restart();
mCodecCounters = new CodecCounters();
mMonitor = new AudioTrackMonitor();
mAudioClock = new AudioClock();
mTracksIndex = new ArrayList<>();
mAc3Passthrough = usePassthrough;
mSoftwareDecoderAvailable = hasSoftwareAudioDecoder && FfmpegDecoderClient.isAvailable();
}
@Override
protected MediaClock getMediaClock() {
return this;
}
private boolean handlesMimeType(String mimeType) {
return mimeType.equals(MimeTypes.AUDIO_AC3)
|| mimeType.equals(MimeTypes.AUDIO_E_AC3)
|| mimeType.equals(MimeTypes.AUDIO_MPEG_L2)
|| MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType);
}
@Override
protected boolean doPrepare(long positionUs) throws ExoPlaybackException {
boolean sourcePrepared = mSource.prepare(positionUs);
if (!sourcePrepared) {
return false;
}
for (int i = 0; i < mSource.getTrackCount(); i++) {
String mimeType = mSource.getFormat(i).mimeType;
if (MimeTypes.isAudio(mimeType) && handlesMimeType(mimeType)) {
if (mTrackIndex < 0) {
mTrackIndex = i;
}
mTracksIndex.add(i);
}
}
// TODO: Check this case. Source does not have the proper mime type.
return true;
}
@Override
protected int getTrackCount() {
return mTracksIndex.size();
}
@Override
protected MediaFormat getFormat(int track) {
Assertions.checkArgument(track >= 0 && track < mTracksIndex.size());
return mSource.getFormat(mTracksIndex.get(track));
}
@Override
protected void onEnabled(int track, long positionUs, boolean joining) {
Assertions.checkArgument(track >= 0 && track < mTracksIndex.size());
mTrackIndex = mTracksIndex.get(track);
mSource.enable(mTrackIndex, positionUs);
seekToInternal(positionUs);
}
@Override
protected void onDisabled() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
AUDIO_TRACK.resetSessionId();
}
clearDecodeState();
mFormat = null;
mSource.disable(mTrackIndex);
}
@Override
protected void onReleased() {
releaseDecoder();
AUDIO_TRACK.release();
mSource.release();
}
@Override
protected boolean isEnded() {
return mOutputStreamEnded && AUDIO_TRACK.isEnded();
}
@Override
protected boolean isReady() {
return AUDIO_TRACK.isReady() || (mFormat != null && (mSourceStateReady || mOutputReady));
}
private void seekToInternal(long positionUs) {
mMonitor.reset(MONITOR_DURATION_MS);
mSourceStateReady = false;
mInputStreamEnded = false;
mOutputStreamEnded = false;
mPresentationTimeUs = positionUs;
mPresentationCount = 0;
mPreviousPositionUs = 0;
mCurrentPositionUs = Long.MIN_VALUE;
mInterpolatedTimeUs = Long.MIN_VALUE;
mAudioClock.setPositionUs(positionUs);
}
@Override
protected void seekTo(long positionUs) {
mSource.seekToUs(positionUs);
AUDIO_TRACK.reset();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// resetSessionId() will create a new framework AudioTrack instead of reusing old one.
AUDIO_TRACK.resetSessionId();
}
seekToInternal(positionUs);
clearDecodeState();
}
@Override
protected void onStarted() {
AUDIO_TRACK.play();
mAudioClock.start();
mIsStopped = false;
}
@Override
protected void onStopped() {
AUDIO_TRACK.pause();
mAudioClock.stop();
mIsStopped = true;
}
@Override
protected void maybeThrowError() throws ExoPlaybackException {
try {
mSource.maybeThrowError();
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
mMonitor.maybeLog();
try {
if (mEndOfStreamMs != 0) {
// Ensure playback stops, after EoS was notified.
// Sometimes MediaCodecTrackRenderer does not fetch EoS timely
// after EoS was notified here long before.
long diff = SystemClock.elapsedRealtime() - mEndOfStreamMs;
if (diff >= KEEP_ALIVE_AFTER_EOS_DURATION_MS && !mIsStopped) {
throw new ExoPlaybackException("Much time has elapsed after EoS");
}
}
boolean continueBuffering = mSource.continueBuffering(mTrackIndex, positionUs);
if (mSourceStateReady != continueBuffering) {
mSourceStateReady = continueBuffering;
if (DEBUG) {
Log.d(TAG, "mSourceStateReady: " + String.valueOf(mSourceStateReady));
}
}
long discontinuity = mSource.readDiscontinuity(mTrackIndex);
if (discontinuity != SampleSource.NO_DISCONTINUITY) {
AUDIO_TRACK.handleDiscontinuity();
mPresentationTimeUs = discontinuity;
mPresentationCount = 0;
clearDecodeState();
return;
}
if (mFormat == null) {
readFormat();
return;
}
if (mAudioDecoder != null) {
mAudioDecoder.maybeInitDecoder(mFormat);
}
// Process only one sample at a time for doSomeWork() when using FFmpeg decoder.
if (processOutput()) {
if (!mOutputReady) {
while (feedInputBuffer()) {
if (mOutputReady) break;
}
}
}
mCodecCounters.ensureUpdated();
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
private void ensureAudioTrackInitialized() {
if (!AUDIO_TRACK.isInitialized()) {
try {
if (DEBUG) {
Log.d(TAG, "AudioTrack initialized");
}
AUDIO_TRACK.initialize();
} catch (AudioTrack.InitializationException e) {
Log.e(TAG, "Error on AudioTrack initialization", e);
notifyAudioTrackInitializationError(e);
// Do not throw exception here but just disabling audioTrack to keep playing
// video without audio.
AUDIO_TRACK.setStatus(false);
}
if (getState() == TrackRenderer.STATE_STARTED) {
if (DEBUG) {
Log.d(TAG, "AudioTrack played");
}
AUDIO_TRACK.play();
}
}
}
private void clearDecodeState() {
mOutputReady = false;
if (mAudioDecoder != null) {
mAudioDecoder.resetDecoderState(mDecodingMime);
}
AUDIO_TRACK.reset();
}
private void releaseDecoder() {
if (mAudioDecoder != null) {
mAudioDecoder.release();
}
}
private void readFormat() throws IOException, ExoPlaybackException {
int result = mSource.readData(mTrackIndex, mCurrentPositionUs,
mFormatHolder, mSampleHolder);
if (result == SampleSource.FORMAT_READ) {
onInputFormatChanged(mFormatHolder);
}
}
private MediaFormat convertMediaFormatToRaw(MediaFormat format) {
return MediaFormat.createAudioFormat(
format.trackId,
MimeTypes.AUDIO_RAW,
format.bitrate,
format.maxInputSize,
format.durationUs,
format.channelCount,
format.sampleRate,
format.initializationData,
format.language);
}
private void onInputFormatChanged(MediaFormatHolder formatHolder)
throws ExoPlaybackException {
String mimeType = formatHolder.format.mimeType;
mUseFrameworkDecoder = MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType);
if (mUseFrameworkDecoder) {
mAudioDecoder = new MediaCodecAudioDecoder(mSelector);
mFormat = formatHolder.format;
mAudioDecoder.maybeInitDecoder(mFormat);
mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
} else if (mSoftwareDecoderAvailable
&& (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mimeType)
|| MimeTypes.AUDIO_AC3.equalsIgnoreCase(mimeType) && !mAc3Passthrough)) {
releaseDecoder();
mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE);
mAudioDecoder = FfmpegDecoderClient.getInstance();
mDecodingMime = mimeType;
mFormat = convertMediaFormatToRaw(formatHolder.format);
} else {
mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE);
mFormat = formatHolder.format;
releaseDecoder();
}
mFormatConfigured = true;
mMonitor.setEncoding(mimeType);
if (DEBUG && !mUseFrameworkDecoder) {
Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString());
}
clearDecodeState();
if (!mUseFrameworkDecoder) {
AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), 0);
}
}
private void onSampleSizeChanged(int sampleSize) {
if (DEBUG) {
Log.d(TAG, "Sample size was changed to : " + sampleSize);
}
clearDecodeState();
int audioBufferSize = sampleSize * BUFFERED_SAMPLES_IN_AUDIOTRACK;
mSampleSize = sampleSize;
AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), audioBufferSize);
}
private void onOutputFormatChanged(android.media.MediaFormat format) {
if (DEBUG) {
Log.d(TAG, "AudioTrack was configured to FORMAT: " + format.toString());
}
AUDIO_TRACK.reconfigure(format, 0);
}
private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
if (mInputStreamEnded) {
return false;
}
if (mUseFrameworkDecoder) {
boolean indexChanged =
((MediaCodecAudioDecoder) mAudioDecoder).getInputIndex()
== MediaCodecAudioDecoder.INDEX_INVALID;
if (indexChanged) {
mSampleHolder.data = mAudioDecoder.getInputBuffer();
if (mSampleHolder.data != null) {
mSampleHolder.clearData();
} else {
return false;
}
}
} else {
mSampleHolder.data.clear();
mSampleHolder.size = 0;
}
int result =
mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, mSampleHolder);
switch (result) {
case SampleSource.NOTHING_READ: {
return false;
}
case SampleSource.FORMAT_READ: {
Log.i(TAG, "Format was read again");
onInputFormatChanged(mFormatHolder);
return true;
}
case SampleSource.END_OF_STREAM: {
Log.i(TAG, "End of stream from SampleSource");
mInputStreamEnded = true;
return false;
}
default: {
if (mSampleHolder.size != mSampleSize
&& mFormatConfigured
&& !mUseFrameworkDecoder) {
onSampleSizeChanged(mSampleHolder.size);
}
mSampleHolder.data.flip();
if (!mUseFrameworkDecoder) {
if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) {
mMonitor.addPts(
mSampleHolder.timeUs,
mOutputBuffer.position(),
mSampleHolder.data.get(MP2_HEADER_BITRATE_OFFSET)
& MP2_HEADER_BITRATE_MASK);
} else {
mMonitor.addPts(
mSampleHolder.timeUs,
mOutputBuffer.position(),
mSampleHolder.data.get(AC3_HEADER_BITRATE_OFFSET) & 0xff);
}
}
if (mAudioDecoder != null) {
mAudioDecoder.decode(mSampleHolder);
if (mUseFrameworkDecoder) {
int outputIndex =
((MediaCodecAudioDecoder) mAudioDecoder).getOutputIndex();
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
onOutputFormatChanged(mAudioDecoder.getOutputFormat());
return true;
} else if (outputIndex < 0) {
return true;
}
if (((MediaCodecAudioDecoder) mAudioDecoder).maybeDecodeOnlyIndex()) {
AUDIO_TRACK.handleDiscontinuity();
return true;
}
}
ByteBuffer outputBuffer = mAudioDecoder.getDecodedSample();
long presentationTimeUs = mAudioDecoder.getDecodedTimeUs();
decodeDone(outputBuffer, presentationTimeUs);
} else {
decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
}
return true;
}
}
}
private boolean processOutput() throws ExoPlaybackException {
if (mOutputStreamEnded) {
return false;
}
if (!mOutputReady) {
if (mInputStreamEnded) {
mOutputStreamEnded = true;
mEndOfStreamMs = SystemClock.elapsedRealtime();
return false;
}
return true;
}
ensureAudioTrackInitialized();
int handleBufferResult;
try {
// To reduce discontinuity, interpolate presentation time.
if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) {
mInterpolatedTimeUs = mPresentationTimeUs
+ mPresentationCount * MP2_SAMPLE_DURATION_US;
} else if (!mUseFrameworkDecoder) {
mInterpolatedTimeUs = mPresentationTimeUs
+ mPresentationCount * AC3_SAMPLE_DURATION_US;
} else {
mInterpolatedTimeUs = mPresentationTimeUs;
}
handleBufferResult =
AUDIO_TRACK.handleBuffer(
mOutputBuffer, 0, mOutputBuffer.limit(), mInterpolatedTimeUs);
} catch (AudioTrack.WriteException e) {
notifyAudioTrackWriteError(e);
throw new ExoPlaybackException(e);
}
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
Log.i(TAG, "Play discontinuity happened");
mCurrentPositionUs = Long.MIN_VALUE;
}
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
mCodecCounters.renderedOutputBufferCount++;
mOutputReady = false;
if (mUseFrameworkDecoder) {
((MediaCodecAudioDecoder) mAudioDecoder).releaseOutputBuffer();
}
return true;
}
return false;
}
@Override
protected long getDurationUs() {
return mSource.getFormat(mTrackIndex).durationUs;
}
@Override
protected long getBufferedPositionUs() {
long pos = mSource.getBufferedPositionUs();
return pos == UNKNOWN_TIME_US || pos == END_OF_TRACK_US
? pos : Math.max(pos, getPositionUs());
}
@Override
public long getPositionUs() {
if (!AUDIO_TRACK.isInitialized()) {
return mAudioClock.getPositionUs();
} else if (!AUDIO_TRACK.isEnabled()) {
if (mInterpolatedTimeUs > 0 && !mUseFrameworkDecoder) {
return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US;
}
return mPresentationTimeUs;
}
long audioTrackCurrentPositionUs = AUDIO_TRACK.getCurrentPositionUs(isEnded());
if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) {
mPreviousPositionUs = 0L;
if (DEBUG) {
long oldPositionUs = Math.max(mCurrentPositionUs, 0);
long currentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs);
Log.d(TAG, "Audio position is not set, diff in us: "
+ String.valueOf(currentPositionUs - oldPositionUs));
}
mCurrentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs);
} else {
if (mPreviousPositionUs
> audioTrackCurrentPositionUs + BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US) {
Log.e(TAG, "audio_position BACK JUMP: "
+ (mPreviousPositionUs - audioTrackCurrentPositionUs));
mCurrentPositionUs = audioTrackCurrentPositionUs;
} else {
mCurrentPositionUs = Math.max(mCurrentPositionUs, audioTrackCurrentPositionUs);
}
mPreviousPositionUs = audioTrackCurrentPositionUs;
}
long upperBound = mPresentationTimeUs + CURRENT_POSITION_FROM_PTS_LIMIT_US;
if (mCurrentPositionUs > upperBound) {
mCurrentPositionUs = upperBound;
}
return mCurrentPositionUs;
}
private void decodeDone(ByteBuffer outputBuffer, long presentationTimeUs) {
if (outputBuffer == null || mOutputBuffer == null) {
return;
}
if (presentationTimeUs < 0) {
Log.e(TAG, "decodeDone - invalid presentationTimeUs");
return;
}
if (TunerDebug.ENABLED) {
TunerDebug.setAudioPtsUs(presentationTimeUs);
}
mOutputBuffer.clear();
Assertions.checkState(mOutputBuffer.remaining() >= outputBuffer.limit());
mOutputBuffer.put(outputBuffer);
if (presentationTimeUs == mPresentationTimeUs) {
mPresentationCount++;
} else {
mPresentationCount = 0;
mPresentationTimeUs = presentationTimeUs;
}
mOutputBuffer.flip();
mOutputReady = true;
}
private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) {
if (mEventHandler == null || mEventListener == null) {
return;
}
mEventHandler.post(new Runnable() {
@Override
public void run() {
mEventListener.onAudioTrackInitializationError(e);
}
});
}
private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) {
if (mEventHandler == null || mEventListener == null) {
return;
}
mEventHandler.post(new Runnable() {
@Override
public void run() {
mEventListener.onAudioTrackWriteError(e);
}
});
}
@Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
case MSG_SET_VOLUME:
float volume = (Float) message;
// Workaround: we cannot mute the audio track by setting the volume to 0, we need to
// disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track
// whenever volume is being set might cause side effects, therefore we only handle
// "explicit mute operations", i.e., only after certain non-zero volume has been
// set, the subsequent volume setting operations will be consider as mute/un-mute
// operations and thus enable/disable the audio track.
if (mIsMuted && volume > 0) {
mIsMuted = false;
if (mEnabled) {
setStatus(true);
}
} else if (!mIsMuted && volume == 0) {
mIsMuted = true;
if (mEnabled) {
setStatus(false);
}
}
AUDIO_TRACK.setVolume(volume);
break;
case MSG_SET_AUDIO_TRACK:
mEnabled = (Integer) message == 1;
setStatus(mEnabled);
break;
case MSG_SET_PLAYBACK_SPEED:
mAudioClock.setPlaybackSpeed((Float) message);
break;
default:
super.handleMessage(messageType, message);
}
}
private void setStatus(boolean enabled) {
if (enabled == AUDIO_TRACK.isEnabled()) {
return;
}
if (!enabled) {
// mAudioClock can be different from getPositionUs. In order to sync them,
// we set mAudioClock.
mAudioClock.setPositionUs(getPositionUs());
}
AUDIO_TRACK.setStatus(enabled);
if (enabled) {
// When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to
// the current position. If not, AUDIO_TRACK has the obsolete data.
seekTo(mAudioClock.getPositionUs());
}
}
}