こんにちは、ファンと友達!
今日、scrcpy の新しいバージョンを使用したところ、scrcpy の新しいバージョンがオーディオをサポートしていることがわかりました。これには非常に興味があると言わざるを得ません。ここでは、関連する研究結果を紹介します。
より多くのフレームワークの乾物知識の実践的な教育
Log.i("qq群",“422901085”);
1. scrcpyで音声を取得する方法
システム サウンドを取得するには、通常、次のインターフェイスを通じてシステム サウンドを取得する必要があることは誰もが知っています。このクラスは、内部録音と呼ばれるものです: builder.setAudioSource(
MediaRecorder.AudioSource.REMOTE_SUBMIX);
しかし、この REMOTE_SUBMIX には許可が必要です。リスクの高い動作に属します。サードパーティのアプリに携帯電話のシステムサウンドを自由に録音させたいので、
scrcpy でサウンドを取得できます。実際、これは Android システムの権限の脆弱性に関連しています (おそらく、問題ではないかもしれません)。抜け穴、これは scrcpy で使われているだけです。これができることを今知りました)
2. scrcpy 音声取得原理の分析
scrcpy コード分析の結果、実際の実装原理はこのような
builder.setAudioSource(MediaRecorder. AudioSource.REMOTE_SUBMIX); 録音方法:
private static AudioRecord createAudioRecord() {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
// This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize);
return builder.build();
}
しかし、この呼び出しは実際には非常に単純です。鍵となるのは権限です。なぜこの scrcpy が REMOTE_SUBMIX を取得できるのかということです。
<!-- Allows an application to capture audio output.
Use the {@code CAPTURE_MEDIA_OUTPUT} permission if only the {@code USAGE_UNKNOWN}),
{@code USAGE_MEDIA}) or {@code USAGE_GAME}) usages are intended to be captured.
<p>Not for use by third-party applications.</p> -->
<permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT"
android:protectionLevel="signature|privileged|role" />
ここでプラットフォーム署名と特権が必要であることは明らかですが、
scrcpy にはこれらがありません
。では、どうすれば通常に呼び出すことができますか?
ここで関連する権限を確認します
@Override
@PermissionCheckerManager.PermissionResult
public int checkPermission(@NonNull String permission,
@NonNull AttributionSourceState attributionSourceState, @Nullable String message,
boolean forDataDelivery, boolean startDataDelivery, boolean fromDatasource,
int attributedOp) {
Objects.requireNonNull(permission);
Objects.requireNonNull(attributionSourceState);
final AttributionSource attributionSource = new AttributionSource(
attributionSourceState);
android.util.Log.i("lsm","checkPermission permission " + permission + " attributionSource = " + attributionSource);
final int result = checkPermission(mContext, mPermissionManagerServiceInternal,
permission, attributionSource, message, forDataDelivery, startDataDelivery,
fromDatasource, attributedOp);
// Finish any started op if some step in the attribution chain failed.
if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED
&& result != PermissionChecker.PERMISSION_SOFT_DENIED) {
if (attributedOp == AppOpsManager.OP_NONE) {
finishDataDelivery(AppOpsManager.permissionToOpCode(permission),
attributionSource.asState(), fromDatasource);
} else {
finishDataDelivery(attributedOp, attributionSource.asState(), fromDatasource);
}
}
return result;
}
印刷を追加すると次のようになります。
03-07 18:09:07.367 2240 2597 I lsm : checkPermission permission android.permission.CAPTURE_AUDIO_OUTPUT attributionSource = AttributionSource {
uid = 2000, packageName = com.android.shell, attributionTag = null, token = android.os.BinderProxy@584a956, next = null }
ここで検出された uid 2000 は、scrcpy のユーザー自体がシェルであるため、誰でも理解できることがわかりますが、packageName = com.android.shell はどこから来たのでしょうか?
ここで、このパッケージ名であっても、CAPTURE_AUDIO_OUTPUT を取得するにはどうすればよいかを確認する必要があります。
1. ここでまず、packageName = com.android.shell である最初の理由を解決します。
では、実際にはこれに依存しています
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
FakeContext コードは次のとおりです。
public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
private static final FakeContext INSTANCE = new FakeContext();
public static FakeContext get() {
return INSTANCE;
}
private FakeContext() {
super(null);
}
@Override
public String getPackageName() {
return PACKAGE_NAME;
}
@Override
public String getOpPackageName() {
return PACKAGE_NAME;
}
@TargetApi(Build.VERSION_CODES.S)
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
builder.setPackageName(PACKAGE_NAME);
return builder.build();
}
}
したがって、呼び出しが行われたときに Context が渡されたことが一般に知られています。
もちろん疑問も同時に出てきますので、通らなくても大丈夫ですか?Android 12 がパスされないのですが、なぜそれが可能なのでしょうか。実際、ここで
builder.setContext(FakeContext.get()) がブロックされていても、実際には packageName = com.android.shell を持つのが正常であることを確認しました。 ,
したがって、この Context 設定は単に最も重要ではないようです。設定する他の場所があるはずです。検索した結果、ここでも設定されていることがわかりました。Context が null の場合:
private AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int sessionId, @Nullable Context context,
int maxSharedAudioHistoryMs) throws IllegalArgumentException {
//省略
AttributionSource attributionSource = (context != null)
? context.getAttributionSource() : AttributionSource.myAttributionSource();
//省略
}
public static @NonNull AttributionSource myAttributionSource() {
//省略
try {
if (uid == Process.SHELL_UID) {
android.util.Log.i("lsm","uid Process.SHELL_UID packageName = "+ AppGlobals.getPackageManager().getPackagesForUid(uid)[0]);
}
//这里最为关键,或根据uid从AppGlobals.getPackageManager().getPackagesForUid查询的第一个作为packageName
return new AttributionSource.Builder(uid)
.setPackageName(AppGlobals.getPackageManager().getPackagesForUid(uid)[0])
.build();
} catch (Exception ignored) {
}
throw new IllegalStateException("Failed to resolve AttributionSource");
}
次のように、対応する印刷を結合します。
03-07 18:09:07.356 4700 4730 I lsm : uid Process.SHELL_UID packageName = com.android.shell
2. なぜ com.android.shell は許可を取得できるのでしょうか?
com.android.shell は実際には Shell.apk です。もちろん、Shell.apk はプラットフォームの署名に属しているため、システムによって許可されると言えます。もちろん、マニフェストには対応する許可ステートメントが存在する必要があります. はは、実際はこんな感じです 真実は次のとおりです。
そうすれば、scrcpy はオーディオ全体をサポートし、それを復号化できます。