From 980d38ff7743c3f15a9674844d1f9e855b547650 Mon Sep 17 00:00:00 2001 From: Jean-Michel Trivi Date: Mon, 8 Jan 2018 15:43:35 -0800 Subject: [PATCH] AudioTrack: support for offloaded playback Surface new callbacks from native AudioTrack to Java through the existing callback method and post to the Java instance. New AudioManager method to query whether offload is supported. Allow using non-PCM format on a track built for offload (new option in AudioTrack.Builder) Numerous "todo" are added for the unhide of the API additions and will be in a separate patch. Bug: 63934228 Test: same as MediaPlayer2 tests Change-Id: I98a81ffffc95c7596a39ee634dcb89bb854f3178 --- core/jni/android_media_AudioFormat.h | 30 +++++ core/jni/android_media_AudioSystem.cpp | 19 +++ core/jni/android_media_AudioTrack.cpp | 72 +++++----- media/java/android/media/AudioFormat.java | 53 ++++++-- media/java/android/media/AudioManager.java | 15 +++ media/java/android/media/AudioSystem.java | 9 ++ media/java/android/media/AudioTrack.java | 145 ++++++++++++++++++++- 7 files changed, 292 insertions(+), 51 deletions(-) diff --git a/core/jni/android_media_AudioFormat.h b/core/jni/android_media_AudioFormat.h index 092aaf62bbc0a..51cefb9accb8e 100644 --- a/core/jni/android_media_AudioFormat.h +++ b/core/jni/android_media_AudioFormat.h @@ -33,6 +33,8 @@ #define ENCODING_AAC_HE_V2 12 #define ENCODING_IEC61937 13 #define ENCODING_DOLBY_TRUEHD 14 +#define ENCODING_AAC_ELD 15 +#define ENCODING_AAC_XHE 16 #define ENCODING_INVALID 0 #define ENCODING_DEFAULT 1 @@ -71,6 +73,10 @@ static inline audio_format_t audioFormatToNative(int audioFormat) return AUDIO_FORMAT_DOLBY_TRUEHD; case ENCODING_IEC61937: return AUDIO_FORMAT_IEC61937; + case ENCODING_AAC_ELD: + return AUDIO_FORMAT_AAC_ELD; + case ENCODING_AAC_XHE: + return AUDIO_FORMAT_AAC; // FIXME temporary value, needs addition of xHE-AAC case ENCODING_DEFAULT: return AUDIO_FORMAT_DEFAULT; default: @@ -114,6 +120,11 @@ static inline int audioFormatFromNative(audio_format_t nativeFormat) return ENCODING_IEC61937; case AUDIO_FORMAT_DOLBY_TRUEHD: return ENCODING_DOLBY_TRUEHD; + case AUDIO_FORMAT_AAC_ELD: + return ENCODING_AAC_ELD; + // FIXME needs addition of AUDIO_FORMAT_AAC_XHE + //case AUDIO_FORMAT_AAC_XHE: + // return ENCODING_AAC_XHE; case AUDIO_FORMAT_DEFAULT: return ENCODING_DEFAULT; default: @@ -121,6 +132,25 @@ static inline int audioFormatFromNative(audio_format_t nativeFormat) } } +// This function converts Java channel masks to a native channel mask. +// validity should be checked with audio_is_output_channel(). +static inline audio_channel_mask_t nativeChannelMaskFromJavaChannelMasks( + jint channelPositionMask, jint channelIndexMask) +{ + // 0 is the java android.media.AudioFormat.CHANNEL_INVALID value + if (channelIndexMask != 0) { // channel index mask takes priority + // To convert to a native channel mask, the Java channel index mask + // requires adding the index representation. + return audio_channel_mask_from_representation_and_bits( + AUDIO_CHANNEL_REPRESENTATION_INDEX, + channelIndexMask); + } + // To convert to a native channel mask, the Java channel position mask + // requires a shift by 2 to skip the two deprecated channel + // configurations "default" and "mono". + return (audio_channel_mask_t)((uint32_t)channelPositionMask >> 2); +} + static inline audio_channel_mask_t outChannelMaskToNative(int channelMask) { switch (channelMask) { diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 7ec68edddf52e..2be947158fc5a 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -1770,6 +1770,24 @@ android_media_AudioSystem_getStreamVolumeDB(JNIEnv *env, jobject thiz, (audio_devices_t)device); } +static jboolean +android_media_AudioSystem_isOffloadSupported(JNIEnv *env, jobject thiz, + jint encoding, jint sampleRate, jint channelMask, jint channelIndexMask) +{ + audio_offload_info_t format = AUDIO_INFO_INITIALIZER; + format.format = (audio_format_t) audioFormatToNative(encoding); + format.sample_rate = (uint32_t) sampleRate; + format.channel_mask = nativeChannelMaskFromJavaChannelMasks(channelMask, channelIndexMask); + format.stream_type = AUDIO_STREAM_MUSIC; + format.has_video = false; + format.is_streaming = false; + // offload duration unknown at this point: + // client side code cannot access "audio.offload.min.duration.secs" property to make a query + // agnostic of duration, so using acceptable estimate of 2mn + format.duration_us = 120 * 1000000; + return AudioSystem::isOffloadSupported(format); +} + // ---------------------------------------------------------------------------- static const JNINativeMethod gMethods[] = { @@ -1823,6 +1841,7 @@ static const JNINativeMethod gMethods[] = { (void *)android_media_AudioSystem_registerRecordingCallback}, {"systemReady", "()I", (void *)android_media_AudioSystem_systemReady}, {"getStreamVolumeDB", "(III)F", (void *)android_media_AudioSystem_getStreamVolumeDB}, + {"native_is_offload_supported", "(IIII)Z", (void *)android_media_AudioSystem_isOffloadSupported}, }; diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp index 556ac27dfe9ed..11011b1d7c164 100644 --- a/core/jni/android_media_AudioTrack.cpp +++ b/core/jni/android_media_AudioTrack.cpp @@ -73,6 +73,7 @@ struct audiotrack_callback_cookie { jobject audioTrack_ref; bool busy; Condition cond; + bool isOffload; }; // keep these values in sync with AudioTrack.java @@ -90,6 +91,7 @@ class AudioTrackJniStorage { AudioTrackJniStorage() { mCallbackData.audioTrack_class = 0; mCallbackData.audioTrack_ref = 0; + mCallbackData.isOffload = false; } ~AudioTrackJniStorage() { @@ -132,27 +134,34 @@ static void audioCallback(int event, void* user, void *info) { } switch (event) { - case AudioTrack::EVENT_MARKER: { - JNIEnv *env = AndroidRuntime::getJNIEnv(); - if (user != NULL && env != NULL) { - env->CallStaticVoidMethod( - callbackInfo->audioTrack_class, - javaAudioTrackFields.postNativeEventInJava, - callbackInfo->audioTrack_ref, event, 0,0, NULL); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); + // Offload only events + case AudioTrack::EVENT_STREAM_END: + case AudioTrack::EVENT_MORE_DATA: + // a.k.a. tear down + case AudioTrack::EVENT_NEW_IAUDIOTRACK: + if (callbackInfo->isOffload) { + JNIEnv *env = AndroidRuntime::getJNIEnv(); + if (user != NULL && env != NULL) { + env->CallStaticVoidMethod( + callbackInfo->audioTrack_class, + javaAudioTrackFields.postNativeEventInJava, + callbackInfo->audioTrack_ref, event, 0,0, NULL); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } } - } } break; + // PCM and offload events + case AudioTrack::EVENT_MARKER: case AudioTrack::EVENT_NEW_POS: { JNIEnv *env = AndroidRuntime::getJNIEnv(); if (user != NULL && env != NULL) { env->CallStaticVoidMethod( - callbackInfo->audioTrack_class, - javaAudioTrackFields.postNativeEventInJava, - callbackInfo->audioTrack_ref, event, 0,0, NULL); + callbackInfo->audioTrack_class, + javaAudioTrackFields.postNativeEventInJava, + callbackInfo->audioTrack_ref, event, 0,0, NULL); if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); @@ -198,30 +207,12 @@ sp android_media_AudioTrack_getAudioTrack(JNIEnv* env, jobject audio return getAudioTrack(env, audioTrackObj); } -// This function converts Java channel masks to a native channel mask. -// validity should be checked with audio_is_output_channel(). -static inline audio_channel_mask_t nativeChannelMaskFromJavaChannelMasks( - jint channelPositionMask, jint channelIndexMask) -{ - if (channelIndexMask != 0) { // channel index mask takes priority - // To convert to a native channel mask, the Java channel index mask - // requires adding the index representation. - return audio_channel_mask_from_representation_and_bits( - AUDIO_CHANNEL_REPRESENTATION_INDEX, - channelIndexMask); - } - // To convert to a native channel mask, the Java channel position mask - // requires a shift by 2 to skip the two deprecated channel - // configurations "default" and "mono". - return (audio_channel_mask_t)(channelPositionMask >> 2); -} - // ---------------------------------------------------------------------------- static jint android_media_AudioTrack_setup(JNIEnv *env, jobject thiz, jobject weak_this, jobject jaa, jintArray jSampleRate, jint channelPositionMask, jint channelIndexMask, jint audioFormat, jint buffSizeInBytes, jint memoryMode, jintArray jSession, - jlong nativeAudioTrack) { + jlong nativeAudioTrack, jboolean offload) { ALOGV("sampleRates=%p, channel mask=%x, index mask=%x, audioFormat(Java)=%d, buffSize=%d" "nativeAudioTrack=0x%" PRIX64, @@ -322,8 +313,19 @@ android_media_AudioTrack_setup(JNIEnv *env, jobject thiz, jobject weak_this, job lpJniStorage->mCallbackData.audioTrack_class = (jclass)env->NewGlobalRef(clazz); // we use a weak reference so the AudioTrack object can be garbage collected. lpJniStorage->mCallbackData.audioTrack_ref = env->NewGlobalRef(weak_this); + lpJniStorage->mCallbackData.isOffload = offload; lpJniStorage->mCallbackData.busy = false; + audio_offload_info_t offloadInfo; + if (offload) { + offloadInfo = AUDIO_INFO_INITIALIZER; + offloadInfo.format = format; + offloadInfo.sample_rate = sampleRateInHertz; + offloadInfo.channel_mask = nativeChannelMask; + offloadInfo.has_video = false; + offloadInfo.stream_type = AUDIO_STREAM_MUSIC; //required for offload + } + // initialize the native AudioTrack object status_t status = NO_ERROR; switch (memoryMode) { @@ -342,7 +344,7 @@ android_media_AudioTrack_setup(JNIEnv *env, jobject thiz, jobject weak_this, job true,// thread can call Java sessionId,// audio session ID AudioTrack::TRANSFER_SYNC, - NULL, // default offloadInfo + offload ? &offloadInfo : NULL, -1, -1, // default uid, pid values paa); break; @@ -1234,7 +1236,7 @@ static const JNINativeMethod gMethods[] = { {"native_stop", "()V", (void *)android_media_AudioTrack_stop}, {"native_pause", "()V", (void *)android_media_AudioTrack_pause}, {"native_flush", "()V", (void *)android_media_AudioTrack_flush}, - {"native_setup", "(Ljava/lang/Object;Ljava/lang/Object;[IIIIII[IJ)I", + {"native_setup", "(Ljava/lang/Object;Ljava/lang/Object;[IIIIII[IJZ)I", (void *)android_media_AudioTrack_setup}, {"native_finalize", "()V", (void *)android_media_AudioTrack_finalize}, {"native_release", "()V", (void *)android_media_AudioTrack_release}, diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index 93fc3da545504..46fe89a74ce57 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -240,20 +240,25 @@ public final class AudioFormat implements Parcelable { public static final int ENCODING_DTS_HD = 8; /** Audio data format: MP3 compressed * @hide + * TODO unhide and add to @Encoding (intentional white space * */ public static final int ENCODING_MP3 = 9; /** Audio data format: AAC LC compressed * @hide + * TODO unhide and add to @Encoding (intentional white space * */ public static final int ENCODING_AAC_LC = 10; /** Audio data format: AAC HE V1 compressed * @hide + * TODO unhide and add to @Encoding (intentional white space * */ public static final int ENCODING_AAC_HE_V1 = 11; /** Audio data format: AAC HE V2 compressed * @hide + * TODO unhide and add to @Encoding (intentional white space * */ public static final int ENCODING_AAC_HE_V2 = 12; + /** Audio data format: compressed audio wrapped in PCM for HDMI * or S/PDIF passthrough. * IEC61937 uses a stereo stream of 16-bit samples as the wrapper. @@ -266,6 +271,16 @@ public final class AudioFormat implements Parcelable { /** Audio data format: DOLBY TRUEHD compressed **/ public static final int ENCODING_DOLBY_TRUEHD = 14; + /** Audio data format: AAC ELD compressed + * @hide + * TODO unhide and add to @Encoding (intentional white space + * */ + public static final int ENCODING_AAC_ELD = 15; + /** Audio data format: AAC xHE compressed + * @hide + * TODO unhide and add to @Encoding (intentional white space + * */ + public static final int ENCODING_AAC_XHE = 16; /** @hide */ public static String toLogFriendlyEncoding(int enc) { @@ -298,6 +313,10 @@ public final class AudioFormat implements Parcelable { return "ENCODING_IEC61937"; case ENCODING_DOLBY_TRUEHD: return "ENCODING_DOLBY_TRUEHD"; + case ENCODING_AAC_ELD: + return "ENCODING_AAC_ELD"; + case ENCODING_AAC_XHE: + return "ENCODING_AAC_XHE"; default : return "invalid encoding " + enc; } @@ -514,6 +533,8 @@ public final class AudioFormat implements Parcelable { case ENCODING_AAC_HE_V1: case ENCODING_AAC_HE_V2: case ENCODING_IEC61937: + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: return true; default: return false; @@ -532,6 +553,13 @@ public final class AudioFormat implements Parcelable { case ENCODING_DTS: case ENCODING_DTS_HD: case ENCODING_IEC61937: + //TODO not true yet (intended white space + case ENCODING_MP3: + case ENCODING_AAC_LC: + case ENCODING_AAC_HE_V1: + case ENCODING_AAC_HE_V2: + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: return true; default: return false; @@ -556,6 +584,8 @@ public final class AudioFormat implements Parcelable { case ENCODING_AAC_HE_V1: case ENCODING_AAC_HE_V2: case ENCODING_IEC61937: // wrapped in PCM but compressed + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: return false; case ENCODING_INVALID: default: @@ -581,6 +611,8 @@ public final class AudioFormat implements Parcelable { case ENCODING_AAC_LC: case ENCODING_AAC_HE_V1: case ENCODING_AAC_HE_V2: + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: return false; case ENCODING_INVALID: default: @@ -794,14 +826,7 @@ public final class AudioFormat implements Parcelable { /** * Sets the data encoding format. - * @param encoding one of {@link AudioFormat#ENCODING_DEFAULT}, - * {@link AudioFormat#ENCODING_PCM_8BIT}, - * {@link AudioFormat#ENCODING_PCM_16BIT}, - * {@link AudioFormat#ENCODING_PCM_FLOAT}, - * {@link AudioFormat#ENCODING_AC3}, - * {@link AudioFormat#ENCODING_E_AC3}. - * {@link AudioFormat#ENCODING_DTS}, - * {@link AudioFormat#ENCODING_DTS_HD}. + * @param encoding the specified encoding or default. * @return the same Builder instance. * @throws java.lang.IllegalArgumentException */ @@ -818,6 +843,12 @@ public final class AudioFormat implements Parcelable { case ENCODING_DTS: case ENCODING_DTS_HD: case ENCODING_IEC61937: + case ENCODING_MP3: + case ENCODING_AAC_LC: + case ENCODING_AAC_HE_V1: + case ENCODING_AAC_HE_V2: + case ENCODING_AAC_ELD: + case ENCODING_AAC_XHE: mEncoding = encoding; break; case ENCODING_INVALID: @@ -1016,7 +1047,7 @@ public final class AudioFormat implements Parcelable { } /** @hide */ - @IntDef({ + @IntDef(flag = false, prefix = "ENCODING", value = { ENCODING_DEFAULT, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, @@ -1025,8 +1056,8 @@ public final class AudioFormat implements Parcelable { ENCODING_E_AC3, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_IEC61937 - }) + ENCODING_IEC61937 } + ) @Retention(RetentionPolicy.SOURCE) public @interface Encoding {} diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 913b5e8411128..3b77aa1e1bd45 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -1328,6 +1328,21 @@ public class AudioManager { } } + //==================================================================== + // Offload query + /** + * @hide + * TODO unhide (intentional white space to attract attention: + * Returns whether offloaded playback of an audio format is supported on the device. + * Offloaded playback is where the decoding of an audio stream is not competing with other + * software resources. In general, it is supported by dedicated hardware, such as audio DSPs. + * @param format the audio format (codec, sample rate, channels) being checked. + * @return true if the given audio format can be offloaded. + */ + public static boolean isOffloadedPlaybackSupported(@NonNull AudioFormat format) { + return AudioSystem.isOffloadSupported(format); + } + //==================================================================== // Bluetooth SCO control /** diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index e56944dff7826..2d048990fe4f9 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -16,6 +16,7 @@ package android.media; +import android.annotation.NonNull; import android.content.Context; import android.content.pm.PackageManager; import android.media.audiopolicy.AudioMix; @@ -818,6 +819,14 @@ public class AudioSystem public static native float getStreamVolumeDB(int stream, int index, int device); + static boolean isOffloadSupported(@NonNull AudioFormat format) { + return native_is_offload_supported(format.getEncoding(), format.getSampleRate(), + format.getChannelMask(), format.getChannelIndexMask()); + } + + private static native boolean native_is_offload_supported(int encoding, int sampleRate, + int channelMask, int channelIndexMask); + // Items shared with audio service /** diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index e535fdf53d018..6add381fc8855 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -24,7 +24,9 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.NioUtils; import java.util.Collection; +import java.util.concurrent.Executor; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -185,6 +187,22 @@ public class AudioTrack extends PlayerBase * Event id denotes when previously set update period has elapsed during playback. */ private static final int NATIVE_EVENT_NEW_POS = 4; + /** + * Callback for more data + * TODO only for offload + */ + private static final int NATIVE_EVENT_MORE_DATA = 0; + /** + * IAudioTrack tear down for offloaded tracks + * TODO: when received, java AudioTrack must be released + */ + private static final int NATIVE_EVENT_NEW_IAUDIOTRACK = 6; + /** + * Event id denotes when all the buffers queued in AF and HW are played + * back (after stop is called) for an offloaded track. + * TODO: not just for offload + */ + private static final int NATIVE_EVENT_STREAM_END = 7; private final static String TAG = "android.media.AudioTrack"; @@ -540,6 +558,12 @@ public class AudioTrack extends PlayerBase public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId) throws IllegalArgumentException { + this(attributes, format, bufferSizeInBytes, mode, sessionId, false /*offload*/); + } + + private AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, + int mode, int sessionId, boolean offload) + throws IllegalArgumentException { super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK); // mState already == STATE_UNINITIALIZED @@ -601,7 +625,8 @@ public class AudioTrack extends PlayerBase // native initialization int initResult = native_setup(new WeakReference(this), mAttributes, sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat, - mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/); + mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/, + offload); if (initResult != SUCCESS) { loge("Error code "+initResult+" when initializing AudioTrack."); return; // with mState == STATE_UNINITIALIZED @@ -681,7 +706,8 @@ public class AudioTrack extends PlayerBase 0 /*mNativeBufferSizeInBytes - NA*/, 0 /*mDataLoadMode - NA*/, session, - nativeTrackInJavaObj); + nativeTrackInJavaObj, + false /*offload*/); if (initResult != SUCCESS) { loge("Error code "+initResult+" when initializing AudioTrack."); return; // with mState == STATE_UNINITIALIZED @@ -730,6 +756,7 @@ public class AudioTrack extends PlayerBase *
If the session ID is not specified with {@link #setSessionId(int)}, a new one will * be generated. */ + // TODO add that offload is false by default (intended white space public static class Builder { private AudioAttributes mAttributes; private AudioFormat mFormat; @@ -737,6 +764,7 @@ public class AudioTrack extends PlayerBase private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE; private int mMode = MODE_STREAM; private int mPerformanceMode = PERFORMANCE_MODE_NONE; + private boolean mOffload = false; /** * Constructs a new Builder with the default values as described above. @@ -866,6 +894,24 @@ public class AudioTrack extends PlayerBase return this; } + /** + * @hide + * TODO unhide (intentional whitespace + * TODO should offload require POWER_SAVING? + * Sets whether this track will play through the offloaded audio path. + * When set to true, at build time, the audio format will be checked against + * {@link AudioManager#isOffloadedPlaybackSupported(AudioFormat)} to verify the audio format + * used by this track is supported on the device's offload path (if any). + *
Offload is only supported for media audio data, and therefore require that + * the usage be {@link AudioAttributes#USAGE_MEDIA}. + * @param offload true to require the offload path for playback. + * @return the same Builder instance. + */ + public @NonNull Builder setOffloadedPlayback(boolean offload) { + mOffload = offload; + return this; + } + /** * Builds an {@link AudioTrack} instance initialized with all the parameters set * on this Builder. @@ -909,6 +955,19 @@ public class AudioTrack extends PlayerBase .setEncoding(AudioFormat.ENCODING_DEFAULT) .build(); } + + //TODO tie offload to PERFORMANCE_MODE_POWER_SAVING? + if (mOffload) { + if (mAttributes.getUsage() != AudioAttributes.USAGE_MEDIA) { + throw new UnsupportedOperationException( + "Cannot create AudioTrack, offload requires USAGE_MEDIA"); + } + if (!AudioManager.isOffloadedPlaybackSupported(mFormat)) { + throw new UnsupportedOperationException( + "Cannot create AudioTrack, offload format not supported"); + } + } + try { // If the buffer size is not specified in streaming mode, // use a single frame for the buffer size and let the @@ -918,7 +977,7 @@ public class AudioTrack extends PlayerBase * mFormat.getBytesPerSample(mFormat.getEncoding()); } final AudioTrack track = new AudioTrack( - mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId); + mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId, mOffload); if (track.getState() == STATE_UNINITIALIZED) { // release is not necessary throw new UnsupportedOperationException("Cannot create AudioTrack"); @@ -2882,6 +2941,55 @@ public class AudioTrack extends PlayerBase void onPeriodicNotification(AudioTrack track); } + /** + * @hide + * TODO unhide (intentional white space to attract attention: + * Abstract class to receive event notification about the stream playback. + */ + public abstract static class StreamEventCallback { + // TODO rename if supported for non offload tracks + public void onTearDown(AudioTrack track) { } + public void onStreamPresentationEnd(AudioTrack track) { } + public void onStreamDataRequest(AudioTrack track) { } + } + + private Executor mStreamEventExec; + private StreamEventCallback mStreamEventCb; + private final Object mStreamEventCbLock = new Object(); + + /** + * @hide + * TODO unhide (intentional white space to attract attention: + * Registers a callback for notification of stream events. + * @param executor {@link Executor} to handle the callbacks + * @param eventCallback the callback to receive the stream events + */ + public void setStreamEventCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull StreamEventCallback eventCallback) { + if (eventCallback == null) { + throw new IllegalArgumentException("Illegal null StreamEventCallback"); + } + if (executor == null) { + throw new IllegalArgumentException("Illegal null Executor for the StreamEventCallback"); + } + synchronized (mStreamEventCbLock) { + mStreamEventExec = executor; + mStreamEventCb = eventCallback; + } + } + + /** + * @hide + * Unregisters the callback for notification of stream events, previously set + * by {@link #setStreamEventCallback(StreamEventCallback, Executor)}. + */ + public void removeStreamEventCallback() { + synchronized (mStreamEventCbLock) { + mStreamEventExec = null; + mStreamEventCb = null; + } + } + //--------------------------------------------------------- // Inner classes //-------------------- @@ -2965,7 +3073,7 @@ public class AudioTrack extends PlayerBase private static void postEventFromNative(Object audiotrack_ref, int what, int arg1, int arg2, Object obj) { //logd("Event posted from the native side: event="+ what + " args="+ arg1+" "+arg2); - AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get(); + final AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get(); if (track == null) { return; } @@ -2974,6 +3082,32 @@ public class AudioTrack extends PlayerBase track.broadcastRoutingChange(); return; } + + if (what == NATIVE_EVENT_MORE_DATA || what == NATIVE_EVENT_NEW_IAUDIOTRACK + || what == NATIVE_EVENT_STREAM_END) { + final Executor exec; + final StreamEventCallback cb; + synchronized (track.mStreamEventCbLock) { + exec = track.mStreamEventExec; + cb = track.mStreamEventCb; + } + if ((exec == null) || (cb == null)) { + return; + } + switch (what) { + case NATIVE_EVENT_MORE_DATA: + exec.execute(() -> cb.onStreamDataRequest(track)); + return; + case NATIVE_EVENT_NEW_IAUDIOTRACK: + // TODO also release track as it's not longer usable + exec.execute(() -> cb.onTearDown(track)); + return; + case NATIVE_EVENT_STREAM_END: + exec.execute(() -> cb.onStreamPresentationEnd(track)); + return; + } + } + NativePositionEventHandlerDelegate delegate = track.mEventHandlerDelegate; if (delegate != null) { Handler handler = delegate.getHandler(); @@ -2995,7 +3129,8 @@ public class AudioTrack extends PlayerBase private native final int native_setup(Object /*WeakReference*/ audiotrack_this, Object /*AudioAttributes*/ attributes, int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, - int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack); + int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack, + boolean offload); private native final void native_finalize();