251 lines
11 KiB
Java
251 lines
11 KiB
Java
/*
|
|
* Copyright (C) 2019 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 android.media;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.media.AudioAttributes.AttributeUsage;
|
|
import android.media.audiopolicy.AudioMix;
|
|
import android.media.audiopolicy.AudioMixingRule;
|
|
import android.media.audiopolicy.AudioMixingRule.AudioMixMatchCriterion;
|
|
import android.media.projection.MediaProjection;
|
|
import android.os.RemoteException;
|
|
|
|
import com.android.internal.util.Preconditions;
|
|
|
|
import java.util.function.ToIntFunction;
|
|
|
|
/**
|
|
* Configuration for capturing audio played by other apps.
|
|
*
|
|
* When capturing audio signals played by other apps (and yours),
|
|
* you will only capture a mix of the audio signals played by players
|
|
* (such as AudioTrack or MediaPlayer) which present the following characteristics:
|
|
* <ul>
|
|
* <li> the usage value MUST be {@link AudioAttributes#USAGE_UNKNOWN} or
|
|
* {@link AudioAttributes#USAGE_GAME}
|
|
* or {@link AudioAttributes#USAGE_MEDIA}. All other usages CAN NOT be captured. </li>
|
|
* <li> AND the capture policy set by their app (with {@link AudioManager#setAllowedCapturePolicy})
|
|
* or on each player (with {@link AudioAttributes.Builder#setAllowedCapturePolicy}) is
|
|
* {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL}, whichever is the most strict. </li>
|
|
* <li> AND their app attribute allowAudioPlaybackCapture in their manifest
|
|
* MUST either be: <ul>
|
|
* <li> set to "true" </li>
|
|
* <li> not set, and their {@code targetSdkVersion} MUST be equal to or greater than
|
|
* {@link android.os.Build.VERSION_CODES#Q}.
|
|
* Ie. Apps that do not target at least Android Q must explicitly opt-in to be captured
|
|
* by a MediaProjection. </li></ul>
|
|
* <li> AND their apps MUST be in the same user profile as your app
|
|
* (eg work profile cannot capture user profile apps and vice-versa). </li>
|
|
* </ul>
|
|
*
|
|
* <p>An example for creating a capture configuration for capturing all media playback:
|
|
*
|
|
* <pre>
|
|
* MediaProjection mediaProjection;
|
|
* // Retrieve a audio capable projection from the MediaProjectionManager
|
|
* AudioPlaybackCaptureConfiguration config =
|
|
* new AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
|
|
* .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
|
* .build();
|
|
* AudioRecord record = new AudioRecord.Builder()
|
|
* .setAudioPlaybackCaptureConfig(config)
|
|
* .build();
|
|
* </pre>
|
|
*
|
|
* @see Builder
|
|
* @see android.media.projection.MediaProjectionManager#getMediaProjection(int, Intent)
|
|
* @see AudioRecord.Builder#setAudioPlaybackCaptureConfig(AudioPlaybackCaptureConfiguration)
|
|
*/
|
|
public final class AudioPlaybackCaptureConfiguration {
|
|
|
|
private final AudioMixingRule mAudioMixingRule;
|
|
private final MediaProjection mProjection;
|
|
|
|
private AudioPlaybackCaptureConfiguration(AudioMixingRule audioMixingRule,
|
|
MediaProjection projection) {
|
|
mAudioMixingRule = audioMixingRule;
|
|
mProjection = projection;
|
|
}
|
|
|
|
/**
|
|
* @return the {@code MediaProjection} used to build this object.
|
|
* @see Builder#Builder(MediaProjection)
|
|
*/
|
|
public @NonNull MediaProjection getMediaProjection() {
|
|
return mProjection;
|
|
}
|
|
|
|
/** @return the usages passed to {@link Builder#addMatchingUsage(int)}. */
|
|
@AttributeUsage
|
|
public @NonNull int[] getMatchingUsages() {
|
|
return getIntPredicates(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE,
|
|
criterion -> criterion.getAudioAttributes().getUsage());
|
|
}
|
|
|
|
/** @return the UIDs passed to {@link Builder#addMatchingUid(int)}. */
|
|
public @NonNull int[] getMatchingUids() {
|
|
return getIntPredicates(AudioMixingRule.RULE_MATCH_UID,
|
|
criterion -> criterion.getIntProp());
|
|
}
|
|
|
|
/** @return the usages passed to {@link Builder#excludeUsage(int)}. */
|
|
@AttributeUsage
|
|
public @NonNull int[] getExcludeUsages() {
|
|
return getIntPredicates(AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE,
|
|
criterion -> criterion.getAudioAttributes().getUsage());
|
|
}
|
|
|
|
/** @return the UIDs passed to {@link Builder#excludeUid(int)}. */
|
|
public @NonNull int[] getExcludeUids() {
|
|
return getIntPredicates(AudioMixingRule.RULE_EXCLUDE_UID,
|
|
criterion -> criterion.getIntProp());
|
|
}
|
|
|
|
private int[] getIntPredicates(int rule,
|
|
ToIntFunction<AudioMixMatchCriterion> getPredicate) {
|
|
return mAudioMixingRule.getCriteria().stream()
|
|
.filter(criterion -> criterion.getRule() == rule)
|
|
.mapToInt(getPredicate)
|
|
.toArray();
|
|
}
|
|
|
|
/**
|
|
* Returns a mix that routes audio back into the app while still playing it from the speakers.
|
|
*
|
|
* @param audioFormat The format in which to capture the audio.
|
|
*/
|
|
@NonNull AudioMix createAudioMix(@NonNull AudioFormat audioFormat) {
|
|
return new AudioMix.Builder(mAudioMixingRule)
|
|
.setFormat(audioFormat)
|
|
.setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK | AudioMix.ROUTE_FLAG_RENDER)
|
|
.build();
|
|
}
|
|
|
|
/** Builder for creating {@link AudioPlaybackCaptureConfiguration} instances. */
|
|
public static final class Builder {
|
|
|
|
private static final int MATCH_TYPE_UNSPECIFIED = 0;
|
|
private static final int MATCH_TYPE_INCLUSIVE = 1;
|
|
private static final int MATCH_TYPE_EXCLUSIVE = 2;
|
|
|
|
private static final String ERROR_MESSAGE_MISMATCHED_RULES =
|
|
"Inclusive and exclusive usage rules cannot be combined";
|
|
private static final String ERROR_MESSAGE_START_ACTIVITY_FAILED =
|
|
"startActivityForResult failed";
|
|
private static final String ERROR_MESSAGE_NON_AUDIO_PROJECTION =
|
|
"MediaProjection can not project audio";
|
|
|
|
private final AudioMixingRule.Builder mAudioMixingRuleBuilder;
|
|
private final MediaProjection mProjection;
|
|
private int mUsageMatchType = MATCH_TYPE_UNSPECIFIED;
|
|
private int mUidMatchType = MATCH_TYPE_UNSPECIFIED;
|
|
|
|
/** @param projection A MediaProjection that supports audio projection. */
|
|
public Builder(@NonNull MediaProjection projection) {
|
|
Preconditions.checkNotNull(projection);
|
|
try {
|
|
Preconditions.checkArgument(projection.getProjection().canProjectAudio(),
|
|
ERROR_MESSAGE_NON_AUDIO_PROJECTION);
|
|
} catch (RemoteException e) {
|
|
throw e.rethrowFromSystemServer();
|
|
}
|
|
mProjection = projection;
|
|
mAudioMixingRuleBuilder = new AudioMixingRule.Builder();
|
|
}
|
|
|
|
/**
|
|
* Only capture audio output with the given {@link AudioAttributes}.
|
|
*
|
|
* <p>If called multiple times, will capture audio output that matches any of the given
|
|
* attributes.
|
|
*
|
|
* @throws IllegalStateException if called in conjunction with
|
|
* {@link #excludeUsage(int)}.
|
|
*/
|
|
public @NonNull Builder addMatchingUsage(@AttributeUsage int usage) {
|
|
Preconditions.checkState(
|
|
mUsageMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
|
|
mAudioMixingRuleBuilder.addRule(new AudioAttributes.Builder().setUsage(usage).build(),
|
|
AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
|
|
mUsageMatchType = MATCH_TYPE_INCLUSIVE;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Only capture audio output by app with the matching {@code uid}.
|
|
*
|
|
* <p>If called multiple times, will capture audio output by apps whose uid is any of the
|
|
* given uids.
|
|
*
|
|
* @throws IllegalStateException if called in conjunction with {@link #excludeUid(int)}.
|
|
*/
|
|
public @NonNull Builder addMatchingUid(int uid) {
|
|
Preconditions.checkState(
|
|
mUidMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
|
|
mAudioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
|
|
mUidMatchType = MATCH_TYPE_INCLUSIVE;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Only capture audio output that does not match the given {@link AudioAttributes}.
|
|
*
|
|
* <p>If called multiple times, will capture audio output that does not match any of the
|
|
* given attributes.
|
|
*
|
|
* @throws IllegalStateException if called in conjunction with
|
|
* {@link #addMatchingUsage(int)}.
|
|
*/
|
|
public @NonNull Builder excludeUsage(@AttributeUsage int usage) {
|
|
Preconditions.checkState(
|
|
mUsageMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
|
|
mAudioMixingRuleBuilder.excludeRule(new AudioAttributes.Builder()
|
|
.setUsage(usage)
|
|
.build(),
|
|
AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
|
|
mUsageMatchType = MATCH_TYPE_EXCLUSIVE;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Only capture audio output by apps that do not have the matching {@code uid}.
|
|
*
|
|
* <p>If called multiple times, will capture audio output by apps whose uid is not any of
|
|
* the given uids.
|
|
*
|
|
* @throws IllegalStateException if called in conjunction with {@link #addMatchingUid(int)}.
|
|
*/
|
|
public @NonNull Builder excludeUid(int uid) {
|
|
Preconditions.checkState(
|
|
mUidMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
|
|
mAudioMixingRuleBuilder.excludeMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
|
|
mUidMatchType = MATCH_TYPE_EXCLUSIVE;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Builds the configuration instance.
|
|
*
|
|
* @throws UnsupportedOperationException if the parameters set are incompatible.
|
|
*/
|
|
public @NonNull AudioPlaybackCaptureConfiguration build() {
|
|
return new AudioPlaybackCaptureConfiguration(mAudioMixingRuleBuilder.build(),
|
|
mProjection);
|
|
}
|
|
}
|
|
}
|