fix(audio,#2): переиспользование одного MediaProjection — устранён слом повторного ON_PHONE
- MainActivity: requestTick собирается в repeatOnLifecycle(RESUMED) + флаг AudioBridge.activityResumed - requestProjection(): не дёргает startActivity(MainActivity), пока Activity RESUMED (гонка с grant) - ensureAudioCaptureLoop(): запрос проекции один раз, повтор не чаще 20 c (было 2.5 c) - AudioBridge/GlassesAudioCapture: проекция одна на процесс и не гасится, переиспользуется - ShizukuProvisioner: PROJECT_MEDIA per-UID (на Android 12 package-level = no-op) - отладочные логи phone-side audio; BLOCKERS.md B-3 закрыт (E2E проверен)
This commit is contained in:
@@ -34,7 +34,11 @@ class AudioPlaybackManager(private val context: Context) {
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
fun start(glassesHost: String) {
|
||||
if (track != null) return // уже запущено
|
||||
if (track != null) {
|
||||
Log.w(TAG, "start: уже running — пропускаю (host=$glassesHost)")
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "start: host=$glassesHost")
|
||||
|
||||
val sampleRate = SAMPLE_RATE
|
||||
val channelMask = AudioFormat.CHANNEL_OUT_STEREO
|
||||
@@ -73,6 +77,7 @@ class AudioPlaybackManager(private val context: Context) {
|
||||
|
||||
// Подключаемся к очкам по тому же ws://host:port/vm-audio
|
||||
val wsUrl = "ws://$glassesHost:${GLASSES_PORT}/vm-audio"
|
||||
Log.i(TAG, "start: подключаюсь к $wsUrl")
|
||||
transport = Transports.wifiBytes(wsUrl, autoReconnect = true)
|
||||
|
||||
streamJob = scope.launch {
|
||||
@@ -82,25 +87,31 @@ class AudioPlaybackManager(private val context: Context) {
|
||||
transport!!.connect(
|
||||
onMessage = { pcm ->
|
||||
val t = track ?: return@connect
|
||||
t.write(pcm, 0, pcm.size)
|
||||
if (frames == 0 || frames % 40 == 0) {
|
||||
Log.i(TAG, "playing: frame#=$frames size=${pcm.size} bytes=$totalBytes rms=${rms16(pcm).toInt()}")
|
||||
}
|
||||
val written = t.write(pcm, 0, pcm.size)
|
||||
if (written < 0) {
|
||||
Log.w(TAG, "playing: AudioTrack.write вернул $written (size=${pcm.size})")
|
||||
}
|
||||
totalBytes += pcm.size
|
||||
frames++
|
||||
if (frames % 40 == 0) {
|
||||
Log.i(TAG, "playing: bytes=$totalBytes rms=${rms16(pcm).toInt()}")
|
||||
}
|
||||
},
|
||||
onConnected = { Log.i(TAG, "audio connected to $wsUrl") },
|
||||
onDisconnected = { Log.w(TAG, "audio disconnected") },
|
||||
onDisconnected = { Log.w(TAG, "audio disconnected from $wsUrl") },
|
||||
)
|
||||
} catch (t: kotlinx.coroutines.CancellationException) {
|
||||
Log.i(TAG, "start: stream job cancelled (stop)")
|
||||
// Штатная остановка (stop()/смена режима) — не ошибка.
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "audio transport failed", t)
|
||||
}
|
||||
Log.i(TAG, "start: stream job завершился frames=$frames bytes=$totalBytes")
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Log.i(TAG, "stop: track=${track != null} transport=${transport != null}")
|
||||
streamJob?.cancel(); streamJob = null
|
||||
transport?.close(); transport = null
|
||||
track?.stop(); track?.release(); track = null
|
||||
|
||||
+23
-3
@@ -103,6 +103,11 @@ class ShizukuProvisioner(
|
||||
/**
|
||||
* Разовые разрешения на очках. Идемпотентно, один раз за сессию. Не блокирует ensure():
|
||||
* `adb.ensureConnected()` переиспользует соединение, поднятое бутстрапом Shizuku.
|
||||
*
|
||||
* `PROJECT_MEDIA` ставится и для пакета, и для UID — иначе на свежих прошивках
|
||||
* `MediaProjectionManager.createScreenCaptureIntent()` возвращает `RESULT_CANCELED`,
|
||||
* даже если `cmd appops get <pkg> PROJECT_MEDIA` показывает `allow`. Проверено на
|
||||
* RayNeo X3 Pro / Android 12 (см. BLOCKERS.md, B-3 follow-up).
|
||||
*/
|
||||
private fun maybeApplyGrants() {
|
||||
if (grantsAppliedInSession) return
|
||||
@@ -110,7 +115,19 @@ class ShizukuProvisioner(
|
||||
val serial = adb.ensureConnected() ?: return@launch
|
||||
Log.i(TAG, "применяю системные гранты на очках ($serial)")
|
||||
val grant = adb.shell("pm grant $GLASSES_PACKAGE android.permission.RECORD_AUDIO", timeoutMs = 15_000)
|
||||
val appop = adb.shell("cmd appops set $GLASSES_PACKAGE PROJECT_MEDIA allow", timeoutMs = 15_000)
|
||||
val appopPkg = adb.shell("cmd appops set $GLASSES_PACKAGE PROJECT_MEDIA allow", timeoutMs = 15_000)
|
||||
// PROJECT_MEDIA на Android 12+ привязан к UID, не к пакету: даже если per-package
|
||||
// стоит allow, диалог проекции сразу RESULT_CANCELED пока per-UID = ignore.
|
||||
val uidOut = adb.shell("cmd package list packages -U $GLASSES_PACKAGE", timeoutMs = 15_000).getOrNull()
|
||||
val uid = uidOut?.lineSequence()
|
||||
?.mapNotNull { Regex("""uid:(\d+)""").find(it)?.groupValues?.get(1)?.toIntOrNull() }
|
||||
?.firstOrNull()
|
||||
val appopUid = if (uid != null) {
|
||||
adb.shell("cmd appops set --uid $uid PROJECT_MEDIA allow", timeoutMs = 15_000)
|
||||
} else {
|
||||
Log.w(TAG, "не удалось узнать UID $GLASSES_PACKAGE — PROJECT_MEDIA per-UID не выставлен")
|
||||
Result.failure(IllegalStateException("uid not found"))
|
||||
}
|
||||
// #4.1: назначаем наш VmNoopImeService дефолтным IME навсегда (см. [VmNoopImeService]).
|
||||
// Подавляет системный IME, который гость (Zona) поднимает при фокусе в поле ввода.
|
||||
val ime = adb.shell(
|
||||
@@ -120,9 +137,12 @@ class ShizukuProvisioner(
|
||||
)
|
||||
Log.i(
|
||||
TAG,
|
||||
"гранты: RECORD_AUDIO=${grant.isSuccess}, PROJECT_MEDIA=${appop.isSuccess}, IME=${ime.isSuccess}",
|
||||
"гранты: RECORD_AUDIO=${grant.isSuccess}, PROJECT_MEDIA[pkg]=${appopPkg.isSuccess}" +
|
||||
"[uid=$uid]=${appopUid.isSuccess}, IME=${ime.isSuccess}",
|
||||
)
|
||||
if (grant.isSuccess && appop.isSuccess && ime.isSuccess) grantsAppliedInSession = true
|
||||
if (grant.isSuccess && appopPkg.isSuccess && appopUid.isSuccess && ime.isSuccess) {
|
||||
grantsAppliedInSession = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@ class VmPhoneController(
|
||||
* Шлёт SetAudioMode очкам, и на телефоне запускает/останавливает AudioPlaybackManager.
|
||||
*/
|
||||
suspend fun setAudioMode(mode: pw.binom.rayneovm.shared.AudioMode) {
|
||||
android.util.Log.i("VmPhoneController", "setAudioMode: prev=${_audioMode.value} → $mode state=${_state.value}")
|
||||
_audioMode.value = mode
|
||||
send(SetAudioMode(mode))
|
||||
when (mode) {
|
||||
@@ -249,6 +250,7 @@ class VmPhoneController(
|
||||
pw.binom.rayneovm.shared.AudioMode.ON_PHONE -> {
|
||||
val host = (_state.value as? ConnectionState.Connected)?.host
|
||||
if (host != null) {
|
||||
android.util.Log.i("VmPhoneController", "setAudioMode: ON_PHONE → audioManager.start($host)")
|
||||
audioManager.start(host)
|
||||
} else {
|
||||
// Не подключены — команда уйдёт при следующем подключении не сразу,
|
||||
@@ -257,6 +259,7 @@ class VmPhoneController(
|
||||
}
|
||||
}
|
||||
}
|
||||
android.util.Log.i("VmPhoneController", "setAudioMode done: mode=$mode audioManager=${audioManager}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
Reference in New Issue
Block a user