mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Shared super class for LibflacAudioTrackRenderer and LibopusAudioTrackRenderer.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=120225655
This commit is contained in:
parent
a760c9bfd9
commit
a7d7859478
11 changed files with 458 additions and 759 deletions
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer.ext.flac;
|
||||||
|
|
||||||
import com.google.android.exoplayer.DecoderInputBuffer;
|
import com.google.android.exoplayer.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer.util.extensions.SimpleDecoder;
|
import com.google.android.exoplayer.util.extensions.SimpleDecoder;
|
||||||
|
import com.google.android.exoplayer.util.extensions.SimpleOutputBuffer;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -25,21 +26,23 @@ import java.util.List;
|
||||||
* Flac decoder.
|
* Flac decoder.
|
||||||
*/
|
*/
|
||||||
/* package */ final class FlacDecoder extends
|
/* package */ final class FlacDecoder extends
|
||||||
SimpleDecoder<DecoderInputBuffer, FlacOutputBuffer, FlacDecoderException> {
|
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> {
|
||||||
|
|
||||||
private final int maxOutputBufferSize;
|
private final int maxOutputBufferSize;
|
||||||
private final FlacJni decoder;
|
private final FlacJni decoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Flac decoder.
|
* Creates a Flac decoder.
|
||||||
*
|
*
|
||||||
* @param numInputBuffers The number of input buffers.
|
* @param numInputBuffers The number of input buffers.
|
||||||
* @param numOutputBuffers The number of output 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.
|
* @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder.
|
||||||
*/
|
*/
|
||||||
public FlacDecoder(int numInputBuffers, int numOutputBuffers, List<byte[]> initializationData)
|
public FlacDecoder(int numInputBuffers, int numOutputBuffers, List<byte[]> initializationData)
|
||||||
throws FlacDecoderException {
|
throws FlacDecoderException {
|
||||||
super(new DecoderInputBuffer[numInputBuffers], new FlacOutputBuffer[numOutputBuffers]);
|
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||||
if (initializationData.size() != 1) {
|
if (initializationData.size() != 1) {
|
||||||
throw new FlacDecoderException("Wrong number of initialization data");
|
throw new FlacDecoderException("Wrong number of initialization data");
|
||||||
}
|
}
|
||||||
|
|
@ -63,18 +66,13 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FlacOutputBuffer createOutputBuffer() {
|
public SimpleOutputBuffer createOutputBuffer() {
|
||||||
return new FlacOutputBuffer(this);
|
return new SimpleOutputBuffer(this);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void releaseOutputBuffer(FlacOutputBuffer buffer) {
|
|
||||||
super.releaseOutputBuffer(buffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FlacDecoderException decode(DecoderInputBuffer inputBuffer,
|
public FlacDecoderException decode(DecoderInputBuffer inputBuffer,
|
||||||
FlacOutputBuffer outputBuffer, boolean reset) {
|
SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
decoder.flush();
|
decoder.flush();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.ext.flac;
|
package com.google.android.exoplayer.ext.flac;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.extensions.AudioDecoderException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when an Flac decoder error occurs.
|
* Thrown when an Flac decoder error occurs.
|
||||||
*/
|
*/
|
||||||
public final class FlacDecoderException extends Exception {
|
public final class FlacDecoderException extends AudioDecoderException {
|
||||||
|
|
||||||
/* package */ FlacDecoderException(String message) {
|
/* package */ FlacDecoderException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,9 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.ext.flac;
|
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.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.MimeTypes;
|
||||||
|
import com.google.android.exoplayer.util.extensions.AudioDecoderTrackRenderer;
|
||||||
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
||||||
|
|
@ -34,63 +26,16 @@ import java.util.List;
|
||||||
/**
|
/**
|
||||||
* Decodes and renders audio using the native Flac decoder.
|
* Decodes and renders audio using the native Flac decoder.
|
||||||
*/
|
*/
|
||||||
public final class LibflacAudioTrackRenderer extends TrackRenderer implements MediaClock {
|
public class LibflacAudioTrackRenderer extends AudioDecoderTrackRenderer {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
private static final int NUM_BUFFERS = 16;
|
private static final int NUM_BUFFERS = 16;
|
||||||
|
|
||||||
public final CodecCounters codecCounters = new CodecCounters();
|
/**
|
||||||
|
* Returns whether the underlying libflac library is available.
|
||||||
private final Handler eventHandler;
|
*/
|
||||||
private final EventListener eventListener;
|
public static boolean isLibflacAvailable() {
|
||||||
private final FormatHolder formatHolder;
|
return FlacJni.IS_AVAILABLE;
|
||||||
|
}
|
||||||
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;
|
|
||||||
|
|
||||||
public LibflacAudioTrackRenderer() {
|
public LibflacAudioTrackRenderer() {
|
||||||
this(null, null);
|
this(null, null);
|
||||||
|
|
@ -101,26 +46,10 @@ public final class LibflacAudioTrackRenderer extends TrackRenderer implements Me
|
||||||
* null if delivery of events is not required.
|
* 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 eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
*/
|
*/
|
||||||
public LibflacAudioTrackRenderer(Handler eventHandler, EventListener eventListener) {
|
public LibflacAudioTrackRenderer(Handler eventHandler,
|
||||||
this.eventHandler = eventHandler;
|
AudioDecoderTrackRenderer.EventListener eventListener) {
|
||||||
this.eventListener = eventListener;
|
super(eventHandler, eventListener);
|
||||||
this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
|
||||||
this.audioTrack = new AudioTrack();
|
|
||||||
formatHolder = new FormatHolder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the underlying libflac library is available.
|
|
||||||
*/
|
|
||||||
public static boolean isLibflacAvailable() {
|
|
||||||
return FlacJni.IS_AVAILABLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected MediaClock getMediaClock() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int supportsFormat(Format format) {
|
protected int supportsFormat(Format format) {
|
||||||
return MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)
|
return MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)
|
||||||
|
|
@ -128,252 +57,8 @@ public final class LibflacAudioTrackRenderer extends TrackRenderer implements Me
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
protected FlacDecoder createDecoder(List<byte[]> initializationData) throws FlacDecoderException {
|
||||||
if (outputStreamEnded) {
|
return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, initializationData);
|
||||||
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<byte[]> 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,9 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.ext.opus;
|
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.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.MimeTypes;
|
||||||
|
import com.google.android.exoplayer.util.extensions.AudioDecoderTrackRenderer;
|
||||||
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
||||||
|
|
@ -34,82 +26,11 @@ import java.util.List;
|
||||||
/**
|
/**
|
||||||
* Decodes and renders audio using the native Opus decoder.
|
* Decodes and renders audio using the native Opus decoder.
|
||||||
*/
|
*/
|
||||||
public final class LibopusAudioTrackRenderer extends TrackRenderer implements MediaClock {
|
public final class LibopusAudioTrackRenderer extends AudioDecoderTrackRenderer {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
private static final int NUM_BUFFERS = 16;
|
private static final int NUM_BUFFERS = 16;
|
||||||
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
|
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.
|
* 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;
|
return isLibopusAvailable() ? OpusDecoder.getLibopusVersion() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public LibopusAudioTrackRenderer() {
|
||||||
protected MediaClock getMediaClock() {
|
this(null, null);
|
||||||
return this;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
@Override
|
||||||
|
|
@ -136,256 +65,9 @@ public final class LibopusAudioTrackRenderer extends TrackRenderer implements Me
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
protected OpusDecoder createDecoder(List<byte[]> initializationData) throws OpusDecoderException {
|
||||||
if (outputStreamEnded) {
|
return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
|
||||||
return;
|
initializationData);
|
||||||
}
|
|
||||||
|
|
||||||
// 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<byte[]> 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer.ext.opus;
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.DecoderInputBuffer;
|
import com.google.android.exoplayer.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer.util.extensions.SimpleDecoder;
|
import com.google.android.exoplayer.util.extensions.SimpleDecoder;
|
||||||
|
import com.google.android.exoplayer.util.extensions.SimpleOutputBuffer;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
|
@ -27,7 +28,7 @@ import java.util.List;
|
||||||
* JNI wrapper for the libopus Opus decoder.
|
* JNI wrapper for the libopus Opus decoder.
|
||||||
*/
|
*/
|
||||||
/* package */ final class OpusDecoder extends
|
/* package */ final class OpusDecoder extends
|
||||||
SimpleDecoder<DecoderInputBuffer, OpusOutputBuffer, OpusDecoderException> {
|
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, OpusDecoderException> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the underlying libopus library is available.
|
* Whether the underlying libopus library is available.
|
||||||
|
|
@ -77,7 +78,7 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||||
List<byte[]> initializationData) throws OpusDecoderException {
|
List<byte[]> initializationData) throws OpusDecoderException {
|
||||||
super(new DecoderInputBuffer[numInputBuffers], new OpusOutputBuffer[numOutputBuffers]);
|
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||||
byte[] headerBytes = initializationData.get(0);
|
byte[] headerBytes = initializationData.get(0);
|
||||||
if (headerBytes.length < 19) {
|
if (headerBytes.length < 19) {
|
||||||
throw new OpusDecoderException("Header size is too small.");
|
throw new OpusDecoderException("Header size is too small.");
|
||||||
|
|
@ -139,18 +140,13 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OpusOutputBuffer createOutputBuffer() {
|
public SimpleOutputBuffer createOutputBuffer() {
|
||||||
return new OpusOutputBuffer(this);
|
return new SimpleOutputBuffer(this);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void releaseOutputBuffer(OpusOutputBuffer buffer) {
|
|
||||||
super.releaseOutputBuffer(buffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OpusDecoderException decode(DecoderInputBuffer inputBuffer,
|
public OpusDecoderException decode(DecoderInputBuffer inputBuffer,
|
||||||
OpusOutputBuffer outputBuffer, boolean reset) {
|
SimpleOutputBuffer outputBuffer, boolean reset) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
opusReset(nativeDecoderContext);
|
opusReset(nativeDecoderContext);
|
||||||
// When seeking to 0, skip number of samples as specified in opus header. When seeking to
|
// When seeking to 0, skip number of samples as specified in opus header. When seeking to
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.ext.opus;
|
package com.google.android.exoplayer.ext.opus;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.extensions.AudioDecoderException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when an Opus decoder error occurs.
|
* Thrown when an Opus decoder error occurs.
|
||||||
*/
|
*/
|
||||||
public final class OpusDecoderException extends Exception {
|
public final class OpusDecoderException extends AudioDecoderException {
|
||||||
|
|
||||||
/* package */ OpusDecoderException(String message) {
|
/* package */ OpusDecoderException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -59,13 +59,10 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
||||||
/* package */ void initForRgbFrame(int width, int height) {
|
/* package */ void initForRgbFrame(int width, int height) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
this.yuvPlanes = null;
|
||||||
|
|
||||||
int minimumRgbSize = width * height * 2;
|
int minimumRgbSize = width * height * 2;
|
||||||
if (data == null || data.capacity() < minimumRgbSize) {
|
initData(minimumRgbSize);
|
||||||
data = ByteBuffer.allocateDirect(minimumRgbSize);
|
|
||||||
yuvPlanes = null;
|
|
||||||
}
|
|
||||||
data.position(0);
|
|
||||||
data.limit(minimumRgbSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,13 +73,12 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.colorspace = colorspace;
|
this.colorspace = colorspace;
|
||||||
|
|
||||||
int yLength = yStride * height;
|
int yLength = yStride * height;
|
||||||
int uvLength = uvStride * ((height + 1) / 2);
|
int uvLength = uvStride * ((height + 1) / 2);
|
||||||
int minimumYuvSize = yLength + (uvLength * 2);
|
int minimumYuvSize = yLength + (uvLength * 2);
|
||||||
if (data == null || data.capacity() < minimumYuvSize) {
|
initData(minimumYuvSize);
|
||||||
data = ByteBuffer.allocateDirect(minimumYuvSize);
|
|
||||||
}
|
|
||||||
data.limit(minimumYuvSize);
|
|
||||||
if (yuvPlanes == null) {
|
if (yuvPlanes == null) {
|
||||||
yuvPlanes = new ByteBuffer[3];
|
yuvPlanes = new ByteBuffer[3];
|
||||||
}
|
}
|
||||||
|
|
@ -104,4 +100,12 @@ public final class VpxOutputBuffer extends OutputBuffer {
|
||||||
yuvStrides[2] = uvStride;
|
yuvStrides[2] = uvStride;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initData(int size) {
|
||||||
|
if (data == null || data.capacity() < size) {
|
||||||
|
data = ByteBuffer.allocateDirect(size);
|
||||||
|
}
|
||||||
|
data.position(0);
|
||||||
|
data.limit(size);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<DecoderInputBuffer, ? extends SimpleOutputBuffer,
|
||||||
|
? extends AudioDecoderException> 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<DecoderInputBuffer, ? extends SimpleOutputBuffer,
|
||||||
|
? extends AudioDecoderException> createDecoder(List<byte[]> 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -13,26 +13,24 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.ext.flac;
|
package com.google.android.exoplayer.util.extensions;
|
||||||
|
|
||||||
import com.google.android.exoplayer.util.extensions.OutputBuffer;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
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<?, SimpleOutputBuffer, ?> owner;
|
||||||
|
|
||||||
public ByteBuffer data;
|
public ByteBuffer data;
|
||||||
|
|
||||||
/* package */ FlacOutputBuffer(FlacDecoder owner) {
|
public SimpleOutputBuffer(SimpleDecoder<?, SimpleOutputBuffer, ?> owner) {
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void init(int size) {
|
public void init(int size) {
|
||||||
if (data == null || data.capacity() < size) {
|
if (data == null || data.capacity() < size) {
|
||||||
data = ByteBuffer.allocateDirect(size);
|
data = ByteBuffer.allocateDirect(size);
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue