mirror of
https://github.com/samsonjs/media.git
synced 2026-04-08 11:45:51 +00:00
Add tunneling functionality to AudioTrack
Although the underlying platform AudioTrack is capable of writing the AV sync header from M onward, I've opted not to use the functionality since it appears to allocate an unnecessary (and large) number of ByteBuffers. We can swap over from O if this is addressed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142138988
This commit is contained in:
parent
2c3ce7fee3
commit
5bb1d5dc99
1 changed files with 124 additions and 24 deletions
|
|
@ -17,7 +17,9 @@ package com.google.android.exoplayer2.audio;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTimestamp;
|
||||
import android.media.PlaybackParams;
|
||||
import android.os.ConditionVariable;
|
||||
|
|
@ -30,26 +32,28 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
|||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles
|
||||
* playback position smoothing, non-blocking writes and reconfiguration.
|
||||
* <p>
|
||||
* Before starting playback, specify the input format by calling
|
||||
* {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)}, optionally
|
||||
* specifying an audio session.
|
||||
* {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)} or
|
||||
* {@link #initializeV21(int, boolean)}, optionally specifying an audio session and whether the
|
||||
* track is to be used with tunneling video playback.
|
||||
* <p>
|
||||
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
|
||||
* when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
|
||||
* <p>
|
||||
* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If
|
||||
* {@link #isInitialized()} returns {@code false} after the call, it is necessary to call
|
||||
* {@link #initialize(int)} before writing more data.
|
||||
* {@link #initialize(int)} or {@link #initializeV21(int, boolean)} before writing more data.
|
||||
* <p>
|
||||
* The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and
|
||||
* released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input
|
||||
* format is unchanged). It is safe to call {@link #initialize(int)} after calling {@link #reset()}
|
||||
* without reconfiguration.
|
||||
* format is unchanged). It is safe to call {@link #initialize(int)} or
|
||||
* {@link #initializeV21(int, boolean)} after calling {@link #reset()} without reconfiguration.
|
||||
* <p>
|
||||
* Call {@link #release()} when the instance is no longer required.
|
||||
*/
|
||||
|
|
@ -144,9 +148,11 @@ public final class AudioTrack {
|
|||
public static final int RESULT_BUFFER_CONSUMED = 2;
|
||||
|
||||
/**
|
||||
* Represents an unset {@link android.media.AudioTrack} session identifier.
|
||||
* Represents an unset {@link android.media.AudioTrack} session identifier. Equal to
|
||||
* {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
|
||||
*/
|
||||
public static final int SESSION_ID_NOT_SET = 0;
|
||||
@SuppressWarnings("InlinedApi")
|
||||
public static final int SESSION_ID_NOT_SET = AudioManager.AUDIO_SESSION_ID_GENERATE;
|
||||
|
||||
/**
|
||||
* Returned by {@link #getCurrentPositionUs} when the position is not set.
|
||||
|
|
@ -273,6 +279,10 @@ public final class AudioTrack {
|
|||
private int bufferSize;
|
||||
private long bufferSizeUs;
|
||||
|
||||
private boolean useHwAvSync;
|
||||
private ByteBuffer avSyncHeader;
|
||||
private int bytesUntilNextAvSync;
|
||||
|
||||
private int nextPlayheadOffsetIndex;
|
||||
private int playheadOffsetCount;
|
||||
private long smoothedPlayheadOffsetUs;
|
||||
|
|
@ -341,8 +351,8 @@ public final class AudioTrack {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns whether the audio track has been successfully initialized via {@link #initialize} and
|
||||
* not yet {@link #reset}.
|
||||
* Returns whether the audio track has been successfully initialized via {@link #initialize} or
|
||||
* {@link #initializeV21(int, boolean)}, and has not yet been {@link #reset}.
|
||||
*/
|
||||
public boolean isInitialized() {
|
||||
return audioTrack != null;
|
||||
|
|
@ -498,11 +508,26 @@ public final class AudioTrack {
|
|||
/**
|
||||
* Initializes the audio track for writing new buffers using {@link #handleBuffer}.
|
||||
*
|
||||
* @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to
|
||||
* create a new one.
|
||||
* @return The new (or re-used) session identifier.
|
||||
* @param sessionId Audio track session identifier, or {@link #SESSION_ID_NOT_SET} to create one.
|
||||
* @return The audio track session identifier.
|
||||
*/
|
||||
public int initialize(int sessionId) throws InitializationException {
|
||||
return initializeInternal(sessionId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the audio track for writing new buffers using {@link #handleBuffer}.
|
||||
*
|
||||
* @param sessionId Audio track session identifier, or {@link #SESSION_ID_NOT_SET} to create one.
|
||||
* @param tunneling Whether the audio track is to be used with tunneling video playback.
|
||||
* @return The audio track session identifier.
|
||||
*/
|
||||
public int initializeV21(int sessionId, boolean tunneling) throws InitializationException {
|
||||
Assertions.checkState(Util.SDK_INT >= 21);
|
||||
return initializeInternal(sessionId, tunneling);
|
||||
}
|
||||
|
||||
private int initializeInternal(int sessionId, boolean tunneling) throws InitializationException {
|
||||
// If we're asynchronously releasing a previous audio track then we block until it has been
|
||||
// released. This guarantees that we cannot end up in a state where we have multiple audio
|
||||
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
|
||||
|
|
@ -510,7 +535,11 @@ public final class AudioTrack {
|
|||
// initialization of the audio track to fail.
|
||||
releasingConditionVariable.block();
|
||||
|
||||
if (sessionId == SESSION_ID_NOT_SET) {
|
||||
useHwAvSync = tunneling;
|
||||
if (useHwAvSync) {
|
||||
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
|
||||
bufferSize, sessionId);
|
||||
} else if (sessionId == SESSION_ID_NOT_SET) {
|
||||
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
|
||||
targetEncoding, bufferSize, MODE_STREAM);
|
||||
} else {
|
||||
|
|
@ -692,7 +721,9 @@ public final class AudioTrack {
|
|||
buffer.position(buffer.position() + bytesWritten);
|
||||
}
|
||||
} else {
|
||||
bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
|
||||
bytesWritten = useHwAvSync
|
||||
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
|
||||
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
|
||||
}
|
||||
|
||||
if (bytesWritten < 0) {
|
||||
|
|
@ -718,6 +749,7 @@ public final class AudioTrack {
|
|||
public void handleEndOfStream() {
|
||||
if (isInitialized()) {
|
||||
audioTrackUtil.handleEndOfStream(getSubmittedFrames());
|
||||
bytesUntilNextAvSync = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -743,19 +775,27 @@ public final class AudioTrack {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the stream type for audio track. If the stream type has changed, {@link #isInitialized()}
|
||||
* will return {@code false} and the caller must re-{@link #initialize(int)} the audio track
|
||||
* before writing more data. The caller must not reuse the audio session identifier when
|
||||
* re-initializing with a new stream type.
|
||||
* Sets the stream type for audio track. If the stream type has changed and if the audio track
|
||||
* is not configured for use with video tunneling, then the audio track is reset and the caller
|
||||
* must re-initialize the audio track before writing more data. The caller must not reuse the
|
||||
* audio session identifier when re-initializing with a new stream type.
|
||||
* <p>
|
||||
* If the audio track is configured for use with video tunneling then the stream type is ignored
|
||||
* and the audio track is not reset. The passed stream type will be used if the audio track is
|
||||
* later re-configured into non-tunneled mode.
|
||||
*
|
||||
* @param streamType The {@link C.StreamType} to use for audio output.
|
||||
* @return Whether the stream type changed.
|
||||
* @return Whether the audio track was reset as a result of this call.
|
||||
*/
|
||||
public boolean setStreamType(@C.StreamType int streamType) {
|
||||
if (this.streamType == streamType) {
|
||||
return false;
|
||||
}
|
||||
this.streamType = streamType;
|
||||
if (useHwAvSync) {
|
||||
// The stream type is ignored in tunneling mode, so no need to reset.
|
||||
return false;
|
||||
}
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -795,9 +835,9 @@ public final class AudioTrack {
|
|||
/**
|
||||
* Releases the underlying audio track asynchronously.
|
||||
* <p>
|
||||
* Calling {@link #initialize(int)} will block until the audio track has been released, so it is
|
||||
* safe to initialize immediately after a reset. The audio session may remain active until
|
||||
* {@link #release()} is called.
|
||||
* Calling {@link #initialize(int)} or {@link #initializeV21(int, boolean)} will block until the
|
||||
* audio track has been released, so it is safe to initialize immediately after a reset. The audio
|
||||
* session may remain active until {@link #release()} is called.
|
||||
*/
|
||||
public void reset() {
|
||||
if (isInitialized()) {
|
||||
|
|
@ -805,6 +845,7 @@ public final class AudioTrack {
|
|||
submittedEncodedFrames = 0;
|
||||
framesPerEncodedSample = 0;
|
||||
currentSourceBuffer = null;
|
||||
avSyncHeader = null;
|
||||
startMediaTimeState = START_NOT_SET;
|
||||
latencyUs = 0;
|
||||
resetSyncParams();
|
||||
|
|
@ -1020,6 +1061,26 @@ public final class AudioTrack {
|
|||
&& audioTrack.getPlaybackHeadPosition() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate,
|
||||
int channelConfig, int encoding, int bufferSize, int sessionId) {
|
||||
AudioAttributes attributesBuilder = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
|
||||
.setFlags(AudioAttributes.FLAG_HW_AV_SYNC)
|
||||
.build();
|
||||
AudioFormat format = new AudioFormat.Builder()
|
||||
.setChannelMask(channelConfig)
|
||||
.setEncoding(encoding)
|
||||
.setSampleRate(sampleRate)
|
||||
.build();
|
||||
return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM,
|
||||
sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided buffer into 16-bit PCM.
|
||||
*
|
||||
|
|
@ -1125,11 +1186,50 @@ public final class AudioTrack {
|
|||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private static int writeNonBlockingV21(
|
||||
android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) {
|
||||
private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer,
|
||||
int size) {
|
||||
return audioTrack.write(buffer, size, WRITE_NON_BLOCKING);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack,
|
||||
ByteBuffer buffer, int size, long presentationTimeUs) {
|
||||
// TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed.
|
||||
// if (Util.SDK_INT >= 23) {
|
||||
// // The underlying platform AudioTrack writes AV sync headers directly.
|
||||
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
|
||||
// }
|
||||
if (avSyncHeader == null) {
|
||||
avSyncHeader = ByteBuffer.allocate(16);
|
||||
avSyncHeader.order(ByteOrder.BIG_ENDIAN);
|
||||
avSyncHeader.putInt(0x55550001);
|
||||
}
|
||||
if (bytesUntilNextAvSync == 0) {
|
||||
avSyncHeader.putInt(4, size);
|
||||
avSyncHeader.putLong(8, presentationTimeUs * 1000);
|
||||
avSyncHeader.position(0);
|
||||
bytesUntilNextAvSync = size;
|
||||
}
|
||||
int avSyncHeaderBytesRemaining = avSyncHeader.remaining();
|
||||
if (avSyncHeaderBytesRemaining > 0) {
|
||||
int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING);
|
||||
if (result < 0) {
|
||||
bytesUntilNextAvSync = 0;
|
||||
return result;
|
||||
}
|
||||
if (result < avSyncHeaderBytesRemaining) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
int result = writeNonBlockingV21(audioTrack, buffer, size);
|
||||
if (result < 0) {
|
||||
bytesUntilNextAvSync = 0;
|
||||
return result;
|
||||
}
|
||||
bytesUntilNextAvSync -= result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) {
|
||||
audioTrack.setVolume(volume);
|
||||
|
|
|
|||
Loading…
Reference in a new issue