Merge "AudioRecord: Allow to share capture history." into sc-dev

This commit is contained in:
Eric Laurent
2021-04-21 17:03:28 +00:00
committed by Android (Google) Code Review
6 changed files with 286 additions and 34 deletions

View File

@@ -23413,11 +23413,14 @@ package android.media {
method public void onError(@NonNull android.media.MediaSync, int, int);
}
public class MediaSyncEvent {
public class MediaSyncEvent implements android.os.Parcelable {
method public static android.media.MediaSyncEvent createEvent(int) throws java.lang.IllegalArgumentException;
method public int describeContents();
method public int getAudioSessionId();
method public int getType();
method public android.media.MediaSyncEvent setAudioSessionId(int) throws java.lang.IllegalArgumentException;
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.MediaSyncEvent> CREATOR;
field public static final int SYNC_EVENT_NONE = 0; // 0x0
field public static final int SYNC_EVENT_PRESENTATION_COMPLETE = 1; // 0x1
}

View File

@@ -5271,11 +5271,15 @@ package android.media {
public class AudioRecord implements android.media.AudioRecordingMonitor android.media.AudioRouting android.media.MicrophoneDirection {
ctor @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public AudioRecord(android.media.AudioAttributes, android.media.AudioFormat, int, int) throws java.lang.IllegalArgumentException;
method public static long getMaxSharedAudioHistoryMillis();
method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.MediaSyncEvent shareAudioHistory(@NonNull String, @IntRange(from=0) long);
}
public static class AudioRecord.Builder {
method public android.media.AudioRecord.Builder setAudioAttributes(@NonNull android.media.AudioAttributes) throws java.lang.IllegalArgumentException;
method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.AudioRecord.Builder setMaxSharedAudioHistoryMillis(long) throws java.lang.IllegalArgumentException;
method public android.media.AudioRecord.Builder setSessionId(int) throws java.lang.IllegalArgumentException;
method @NonNull public android.media.AudioRecord.Builder setSharedAudioEvent(@NonNull android.media.MediaSyncEvent) throws java.lang.IllegalArgumentException;
}
public final class AudioRecordingConfiguration implements android.os.Parcelable {
@@ -5338,6 +5342,10 @@ package android.media {
method public void onPreferredFeaturesChanged(@NonNull java.util.List<java.lang.String>);
}
public class MediaSyncEvent implements android.os.Parcelable {
field public static final int SYNC_EVENT_SHARE_AUDIO_HISTORY = 100; // 0x64
}
public class PlayerProxy {
method public void pause();
method public void setPan(float);

View File

@@ -189,7 +189,8 @@ static jint android_media_AudioRecord_setup(JNIEnv *env, jobject thiz, jobject w
jobject jaa, jintArray jSampleRate, jint channelMask,
jint channelIndexMask, jint audioFormat,
jint buffSizeInBytes, jintArray jSession,
jobject jIdentity, jlong nativeRecordInJavaObj) {
jobject jIdentity, jlong nativeRecordInJavaObj,
jint sharedAudioHistoryMs) {
//ALOGV(">> Entering android_media_AudioRecord_setup");
//ALOGV("sampleRate=%d, audioFormat=%d, channel mask=%x, buffSizeInBytes=%d "
// "nativeRecordInJavaObj=0x%llX",
@@ -288,20 +289,18 @@ static jint android_media_AudioRecord_setup(JNIEnv *env, jobject thiz, jobject w
lpCallbackData->audioRecord_ref = env->NewGlobalRef(weak_this);
lpCallbackData->busy = false;
const status_t status = lpRecorder->set(paa->source,
sampleRateInHertz,
format, // word length, PCM
localChanMask,
frameCount,
recorderCallback,// callback_t
lpCallbackData,// void* user
0, // notificationFrames,
true, // threadCanCallJava
sessionId,
AudioRecord::TRANSFER_DEFAULT,
flags,
-1, -1, // default uid, pid
paa.get());
const status_t status =
lpRecorder->set(paa->source, sampleRateInHertz,
format, // word length, PCM
localChanMask, frameCount,
recorderCallback, // callback_t
lpCallbackData, // void* user
0, // notificationFrames,
true, // threadCanCallJava
sessionId, AudioRecord::TRANSFER_DEFAULT, flags, -1,
-1, // default uid, pid
paa.get(), AUDIO_PORT_HANDLE_NONE, MIC_DIRECTION_UNSPECIFIED,
MIC_FIELD_DIMENSION_DEFAULT, sharedAudioHistoryMs);
if (status != NO_ERROR) {
ALOGE("Error creating AudioRecord instance: initialization check failed with status %d.",
@@ -877,6 +876,23 @@ static void android_media_AudioRecord_setLogSessionId(JNIEnv *env, jobject thiz,
record->setLogSessionId(logSessionId.c_str());
}
static jint android_media_AudioRecord_shareAudioHistory(JNIEnv *env, jobject thiz,
jstring jSharedPackageName,
jlong jSharedStartMs) {
sp<AudioRecord> record = getAudioRecord(env, thiz);
if (record == nullptr) {
jniThrowException(env, "java/lang/IllegalStateException",
"Unable to retrieve AudioRecord pointer for setLogSessionId()");
}
if (jSharedPackageName == nullptr) {
jniThrowException(env, "java/lang/IllegalArgumentException", "package name cannot be null");
}
ScopedUtfChars nSharedPackageName(env, jSharedPackageName);
ALOGV("%s: nSharedPackageName '%s'", __func__, nSharedPackageName.c_str());
return nativeToJavaStatus(record->shareAudioHistory(nSharedPackageName.c_str(),
static_cast<int64_t>(jSharedStartMs)));
}
// ----------------------------------------------------------------------------
static jint android_media_AudioRecord_get_port_id(JNIEnv *env, jobject thiz) {
sp<AudioRecord> lpRecorder = getAudioRecord(env, thiz);
@@ -896,7 +912,7 @@ static const JNINativeMethod gMethods[] = {
{"native_start", "(II)I", (void *)android_media_AudioRecord_start},
{"native_stop", "()V", (void *)android_media_AudioRecord_stop},
{"native_setup",
"(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILandroid/media/permission/Identity;J)I",
"(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILandroid/media/permission/Identity;JI)I",
(void *)android_media_AudioRecord_setup},
{"native_finalize", "()V", (void *)android_media_AudioRecord_finalize},
{"native_release", "()V", (void *)android_media_AudioRecord_release},
@@ -936,6 +952,8 @@ static const JNINativeMethod gMethods[] = {
(void *)android_media_AudioRecord_set_preferred_microphone_field_dimension},
{"native_setLogSessionId", "(Ljava/lang/String;)V",
(void *)android_media_AudioRecord_setLogSessionId},
{"native_shareAudioHistory", "(Ljava/lang/String;J)I",
(void *)android_media_AudioRecord_shareAudioHistory},
};
// field names found in android/media/AudioRecord.java

View File

@@ -21,6 +21,7 @@ import static android.media.permission.PermissionUtil.myIdentity;
import android.annotation.CallbackExecutor;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -358,7 +359,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
@RequiresPermission(android.Manifest.permission.RECORD_AUDIO)
public AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int sessionId) throws IllegalArgumentException {
this(attributes, format, bufferSizeInBytes, sessionId, ActivityThread.currentApplication());
this(attributes, format, bufferSizeInBytes, sessionId,
ActivityThread.currentApplication(), 0 /*maxSharedAudioHistoryMs*/);
}
/**
@@ -383,7 +385,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
* @throws IllegalArgumentException
*/
private AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int sessionId, @Nullable Context context) throws IllegalArgumentException {
int sessionId, @Nullable Context context,
int maxSharedAudioHistoryMs) throws IllegalArgumentException {
mRecordingState = RECORDSTATE_STOPPED;
if (attributes == null) {
@@ -455,12 +458,14 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
int[] sampleRate = new int[] {mSampleRate};
int[] session = new int[1];
session[0] = sessionId;
//TODO: update native initialization when information about hardware init failure
// due to capture device already open is available.
int initResult = native_setup(new WeakReference<AudioRecord>(this),
mAudioAttributes, sampleRate, mChannelMask, mChannelIndexMask,
mAudioFormat, mNativeBufferSizeInBytes,
session, identity, 0 /*nativeRecordInJavaObj*/);
session, identity, 0 /*nativeRecordInJavaObj*/,
maxSharedAudioHistoryMs);
if (initResult != SUCCESS) {
loge("Error code "+initResult+" when initializing native AudioRecord object.");
return; // with mState == STATE_UNINITIALIZED
@@ -522,7 +527,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
0 /*mNativeBufferSizeInBytes*/,
session,
myIdentity(null),
nativeRecordInJavaObj);
nativeRecordInJavaObj,
0);
if (initResult != SUCCESS) {
loge("Error code "+initResult+" when initializing native AudioRecord object.");
return; // with mState == STATE_UNINITIALIZED
@@ -581,7 +587,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
private int mBufferSizeInBytes;
private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT;
private int mMaxSharedAudioHistoryMs = 0;
private static final int PRIVACY_SENSITIVE_DEFAULT = -1;
private static final int PRIVACY_SENSITIVE_DISABLED = 0;
private static final int PRIVACY_SENSITIVE_ENABLED = 1;
@@ -747,7 +753,12 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
if (sessionId < 0) {
throw new IllegalArgumentException("Invalid session ID " + sessionId);
}
mSessionId = sessionId;
// Do not override a session ID previously set with setSharedAudioEvent()
if (mSessionId == AudioManager.AUDIO_SESSION_ID_GENERATE) {
mSessionId = sessionId;
} else {
Log.e(TAG, "setSessionId() called twice or after setSharedAudioEvent()");
}
return this;
}
@@ -771,6 +782,57 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
return record;
}
/**
* @hide
* Specifies the maximum duration in the past of the this AudioRecord's capture buffer
* that can be shared with another app by calling
* {@link AudioRecord#shareAudioHistory(String, long)}.
* @param maxSharedAudioHistoryMillis the maximum duration that will be available
* in milliseconds.
* @return the same Builder instance.
* @throws IllegalArgumentException
*
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
public @NonNull Builder setMaxSharedAudioHistoryMillis(long maxSharedAudioHistoryMillis)
throws IllegalArgumentException {
if (maxSharedAudioHistoryMillis <= 0
|| maxSharedAudioHistoryMillis > MAX_SHARED_AUDIO_HISTORY_MS) {
throw new IllegalArgumentException("Illegal maxSharedAudioHistoryMillis argument");
}
mMaxSharedAudioHistoryMs = (int) maxSharedAudioHistoryMillis;
return this;
}
/**
* @hide
* Indicates that this AudioRecord will use the audio history shared by another app's
* AudioRecord. See {@link AudioRecord#shareAudioHistory(String, long)}.
* The audio session ID set with {@link AudioRecord.Builder#setSessionId(int)} will be
* ignored if this method is used.
* @param event The {@link MediaSyncEvent} provided by the app sharing its audio history
* with this AudioRecord.
* @return the same Builder instance.
* @throws IllegalArgumentException
*/
@SystemApi
public @NonNull Builder setSharedAudioEvent(@NonNull MediaSyncEvent event)
throws IllegalArgumentException {
Objects.requireNonNull(event);
if (event.getType() != MediaSyncEvent.SYNC_EVENT_SHARE_AUDIO_HISTORY) {
throw new IllegalArgumentException(
"Invalid event type " + event.getType());
}
if (event.getAudioSessionId() == AudioSystem.AUDIO_SESSION_ALLOCATE) {
throw new IllegalArgumentException(
"Invalid session ID " + event.getAudioSessionId());
}
// This prevails over a session ID set with setSessionId()
mSessionId = event.getAudioSessionId();
return this;
}
/**
* @return a new {@link AudioRecord} instance successfully initialized with all
* the parameters set on this <code>Builder</code>.
@@ -837,7 +899,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
* mFormat.getBytesPerSample(mFormat.getEncoding());
}
final AudioRecord record = new AudioRecord(
mAttributes, mFormat, mBufferSizeInBytes, mSessionId, mContext);
mAttributes, mFormat, mBufferSizeInBytes, mSessionId, mContext,
mMaxSharedAudioHistoryMs);
if (record.getState() == STATE_UNINITIALIZED) {
// release is not necessary
throw new UnsupportedOperationException("Cannot create AudioRecord");
@@ -1423,7 +1486,6 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
|| (offsetInShorts + sizeInShorts > audioData.length)) {
return ERROR_BAD_VALUE;
}
return native_read_in_short_array(audioData, offsetInShorts, sizeInShorts,
readMode == READ_BLOCKING);
}
@@ -1642,6 +1704,70 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_INPUTS);
}
/**
* Must match the native definition in frameworks/av/service/audioflinger/Audioflinger.h.
*/
private static final long MAX_SHARED_AUDIO_HISTORY_MS = 5000;
/**
* @hide
* returns the maximum duration in milliseconds of the audio history that can be requested
* to be made available to other clients using the same session with
* {@Link Builder#setMaxSharedAudioHistory(long)}.
*/
@SystemApi
public static long getMaxSharedAudioHistoryMillis() {
return MAX_SHARED_AUDIO_HISTORY_MS;
}
/**
* @hide
*
* A privileged app with permission CAPTURE_AUDIO_HOTWORD can share part of its recent
* capture history on a given AudioRecord with the following steps:
* 1) Specify the maximum time in the past that will be available for other apps by calling
* {@link Builder#setMaxSharedAudioHistoryMillis(long)} when creating the AudioRecord.
* 2) Start recording and determine where the other app should start capturing in the past.
* 3) Call this method with the package name of the app the history will be shared with and
* the intended start time for this app's capture relative to this AudioRecord's start time.
* 4) Communicate the {@link MediaSyncEvent} returned by this method to the other app.
* 5) The other app will use the MediaSyncEvent when creating its AudioRecord with
* {@link Builder#setSharedAudioEvent(MediaSyncEvent).
* 6) Only after the other app has started capturing can this app stop capturing and
* release its AudioRecord.
* This method is intended to be called only once: if called multiple times, only the last
* request will be honored.
* The implementation is "best effort": if the specified start time if too far in the past
* compared to the max available history specified, the start time will be adjusted to the
* start of the available history.
* @param sharedPackage the package the history will be shared with
* @param startFromMillis the start time, relative to the initial start time of this
* AudioRecord, at which the other AudioRecord will start.
* @return a {@link MediaSyncEvent} to be communicated to the app this AudioRecord's audio
* history will be shared with.
* @throws IllegalArgumentException
* @throws SecurityException
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
@NonNull public MediaSyncEvent shareAudioHistory(@NonNull String sharedPackage,
@IntRange(from = 0) long startFromMillis) {
Objects.requireNonNull(sharedPackage);
if (startFromMillis < 0) {
throw new IllegalArgumentException("Illegal negative sharedAudioHistoryMs argument");
}
int status = native_shareAudioHistory(sharedPackage, startFromMillis);
if (status == AudioSystem.BAD_VALUE) {
throw new IllegalArgumentException("Illegal sharedAudioHistoryMs argument");
} else if (status == AudioSystem.PERMISSION_DENIED) {
throw new SecurityException("permission CAPTURE_AUDIO_HOTWORD required");
}
MediaSyncEvent event =
MediaSyncEvent.createEvent(MediaSyncEvent.SYNC_EVENT_SHARE_AUDIO_HISTORY);
event.setAudioSessionId(mSessionId);
return event;
}
/*
* Call BEFORE adding a routing callback handler.
*/
@@ -2105,13 +2231,14 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
identity.packageName = opPackageName;
return native_setup(audiorecordThis, attributes, sampleRate, channelMask, channelIndexMask,
audioFormat, buffSizeInBytes, sessionId, identity, nativeRecordInJavaObj);
audioFormat, buffSizeInBytes, sessionId, identity, nativeRecordInJavaObj, 0);
}
private native int native_setup(Object audiorecordThis,
Object /*AudioAttributes*/ attributes,
int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
int buffSizeInBytes, int[] sessionId, Identity identity, long nativeRecordInJavaObj);
int buffSizeInBytes, int[] sessionId, Identity identity, long nativeRecordInJavaObj,
int maxSharedAudioHistoryMs);
// TODO remove: implementation calls directly into implementation of native_release()
private native void native_finalize();
@@ -2170,6 +2297,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
private native void native_setLogSessionId(@Nullable String logSessionId);
private native int native_shareAudioHistory(@NonNull String sharedPackage, long startFromMs);
//---------------------------------------------------------
// Utility methods
//------------------

View File

@@ -1464,6 +1464,10 @@ public class AudioSystem
// usage for AudioRecord.startRecordingSync(), must match AudioSystem::sync_event_t
/** @hide */ public static final int SYNC_EVENT_NONE = 0;
/** @hide */ public static final int SYNC_EVENT_PRESENTATION_COMPLETE = 1;
/** @hide
* Not used by native implementation.
* See {@link AudioRecord.Builder#setSharedAudioEvent(MediaSyncEvent) */
public static final int SYNC_EVENT_SHARE_AUDIO_HISTORY = 100;
/**
* @hide

View File

@@ -16,6 +16,13 @@
package android.media;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Objects;
/**
* The MediaSyncEvent class defines events that can be used to synchronize playback or capture
* actions between different players and recorders.
@@ -24,7 +31,7 @@ package android.media;
* The audio session ID is retrieved from a player (e.g {@link MediaPlayer}, {@link AudioTrack} or
* {@link ToneGenerator}) by use of the getAudioSessionId() method.
*/
public class MediaSyncEvent {
public class MediaSyncEvent implements Parcelable {
/**
* No sync event specified. When used with a synchronized playback or capture method, the
@@ -39,8 +46,16 @@ public class MediaSyncEvent {
* {@link #setAudioSessionId(int)} method.
*/
public static final int SYNC_EVENT_PRESENTATION_COMPLETE =
AudioSystem.SYNC_EVENT_PRESENTATION_COMPLETE;
AudioSystem.SYNC_EVENT_PRESENTATION_COMPLETE;
/**
* @hide
* Used when sharing audio history between AudioRecord instances.
* See {@link AudioRecord.Builder#setSharedAudioEvent(MediaSyncEvent).
*/
@SystemApi
public static final int SYNC_EVENT_SHARE_AUDIO_HISTORY =
AudioSystem.SYNC_EVENT_SHARE_AUDIO_HISTORY;
/**
* Creates a synchronization event of the sepcified type.
@@ -112,11 +127,86 @@ public class MediaSyncEvent {
private static boolean isValidType(int type) {
switch (type) {
case SYNC_EVENT_NONE:
case SYNC_EVENT_PRESENTATION_COMPLETE:
return true;
default:
return false;
case SYNC_EVENT_NONE:
case SYNC_EVENT_PRESENTATION_COMPLETE:
case SYNC_EVENT_SHARE_AUDIO_HISTORY:
return true;
default:
return false;
}
}
// Parcelable implementation
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
Objects.requireNonNull(dest);
dest.writeInt(mType);
dest.writeInt(mAudioSession);
}
private MediaSyncEvent(Parcel in) {
mType = in.readInt();
mAudioSession = in.readInt();
}
public static final @NonNull Parcelable.Creator<MediaSyncEvent> CREATOR =
new Parcelable.Creator<MediaSyncEvent>() {
/**
* Rebuilds an MediaSyncEvent previously stored with writeToParcel().
* @param p Parcel object to read the MediaSyncEvent from
* @return a new MediaSyncEvent created from the data in the parcel
*/
public MediaSyncEvent createFromParcel(Parcel p) {
return new MediaSyncEvent(p);
}
public MediaSyncEvent[] newArray(int size) {
return new MediaSyncEvent[size];
}
};
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MediaSyncEvent that = (MediaSyncEvent) o;
return ((mType == that.mType)
&& (mAudioSession == that.mAudioSession));
}
@Override
public int hashCode() {
return Objects.hash(mType, mAudioSession);
}
@Override
public String toString() {
return new String("MediaSyncEvent:"
+ " type=" + typeToString(mType)
+ " session=" + mAudioSession);
}
/**
* Returns the string representation for the type.
* @param type one of the {@link MediaSyncEvent} type constants
* @hide
*/
public static @NonNull String typeToString(int type) {
switch (type) {
case SYNC_EVENT_NONE:
return "SYNC_EVENT_NONE";
case SYNC_EVENT_PRESENTATION_COMPLETE:
return "SYNC_EVENT_PRESENTATION_COMPLETE";
case SYNC_EVENT_SHARE_AUDIO_HISTORY:
return "SYNC_EVENT_SHARE_AUDIO_HISTORY";
default:
return "unknown event type " + type;
}
}
}