fix(app-phone): звук при watch (диагностика+подстраховка mirrorByItem) + качка не воскресает после удаления
- onWatch: явные логи в каждой точке отказа (watch result / audio source / уже играет / не скачано / mediaDir пуст), свежий mirrorByItem если files не догрузился, звук стартует с позиции очков при уже идущем видео - resolveAudioSource → resolveAudioSourceDetailed (AudioSourceResult с причиной null) - DownloadManager: поколения generations — delete() инвалидирует старую корутину (не воскрешает качку), ensureActive перед .part/rename, jobs.remove(itemId, job) - Тесты 62/0 (в т.ч. поколения — поймали null != 0L) Инфраструктурный блокер (не код): SeaweedFS на Server3 76.66 в IO-шторме (load ~184, wa 20%, чужой yt-dlp на хосте) — большие GET с s3.binom.pw висят/рвутся с ~14:00; качка на очки FAILED по этой причине.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
package pw.binom.viewmate.glasses
|
||||
|
||||
import android.content.Context
|
||||
import android.os.BatteryManager
|
||||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -14,7 +18,10 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import pw.binom.viewmate.core.net.GlassesWsClient
|
||||
import pw.binom.viewmate.core.protocol.AssistantStateMsg
|
||||
import pw.binom.viewmate.core.protocol.ChatSessionMsg
|
||||
import pw.binom.viewmate.core.protocol.DeleteFiles
|
||||
import pw.binom.viewmate.core.protocol.DownloadFiles
|
||||
import pw.binom.viewmate.core.protocol.GlassesStatus
|
||||
import pw.binom.viewmate.core.protocol.GlassesToHost
|
||||
import pw.binom.viewmate.core.protocol.Hello
|
||||
import pw.binom.viewmate.core.protocol.HostToGlasses
|
||||
import pw.binom.viewmate.core.protocol.MediaCommandMsg
|
||||
@@ -27,12 +34,16 @@ import pw.binom.viewmate.core.protocol.Welcome
|
||||
/**
|
||||
* Обвязка над [GlassesWsClient] для UI очков:
|
||||
* реконнект, Hello при подключении, периодический статус/позиция,
|
||||
* обработка входящих HostToGlasses (PlayVideo/MediaCommand выполняются плеером).
|
||||
* обработка входящих HostToGlasses (PlayVideo/MediaCommand выполняются плеером,
|
||||
* DownloadFiles/DeleteFiles — менеджером скачивания).
|
||||
*/
|
||||
class HostConnection(
|
||||
private val url: String,
|
||||
private val movieController: MovieController,
|
||||
private val downloadManager: GlassesDownloadManager,
|
||||
context: Context,
|
||||
) {
|
||||
private val appContext = context.applicationContext
|
||||
private val client = GlassesWsClient(url)
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@@ -53,7 +64,7 @@ class HostConnection(
|
||||
_connected.value = true
|
||||
_status.value = "подключено: $url"
|
||||
addMessage("соединение установлено")
|
||||
client.send(Hello(appVersion = "0.1.0"))
|
||||
client.send(Hello(appVersion = APP_VERSION))
|
||||
},
|
||||
onDisconnected = {
|
||||
_connected.value = false
|
||||
@@ -64,14 +75,44 @@ class HostConnection(
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
delay(5_000)
|
||||
// статус/позиция — заглушки (реальные батарея/память — позже)
|
||||
client.send(GlassesStatus(batteryPercent = 80, storageUsedGb = 10.5, storageTotalGb = 32.0))
|
||||
client.send(buildStatus())
|
||||
val (positionMs, playing) = withContext(Dispatchers.Main) { movieController.playbackPosition() }
|
||||
client.send(PlaybackPosition(positionMs = positionMs, playing = playing))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Отправка сообщения на телефон (используется менеджером скачивания). */
|
||||
suspend fun sendToHost(msg: GlassesToHost) = client.send(msg)
|
||||
|
||||
private fun buildStatus(): GlassesStatus {
|
||||
val (totalBytes, freeBytes) = diskBytes()
|
||||
val usedBytes = (totalBytes - freeBytes).coerceAtLeast(0L)
|
||||
val media = downloadManager.mediaBytesNow()
|
||||
return GlassesStatus(
|
||||
batteryPercent = batteryPercent(),
|
||||
storageUsedGb = usedBytes / 1_000_000_000.0,
|
||||
storageTotalGb = totalBytes / 1_000_000_000.0,
|
||||
storageTotalBytes = totalBytes,
|
||||
storageFreeBytes = freeBytes,
|
||||
mediaBytes = media,
|
||||
downloadedItemIds = downloadManager.downloadedIds.value.toList(),
|
||||
mediaDir = downloadManager.baseDir.absolutePath,
|
||||
appVersion = APP_VERSION,
|
||||
)
|
||||
}
|
||||
|
||||
private fun batteryPercent(): Int {
|
||||
val bm = appContext.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager
|
||||
val level = bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
|
||||
return if (level != null && level in 0..100) level else 80
|
||||
}
|
||||
|
||||
private fun diskBytes(): Pair<Long, Long> = runCatching {
|
||||
val stat = StatFs(Environment.getDataDirectory().absolutePath)
|
||||
stat.totalBytes to stat.availableBytes
|
||||
}.getOrDefault(0L to 0L)
|
||||
|
||||
private suspend fun handleHostMessage(msg: HostToGlasses) {
|
||||
when (msg) {
|
||||
is Welcome -> {
|
||||
@@ -91,6 +132,16 @@ class HostConnection(
|
||||
movieController.applyMediaCommand(msg.command, msg.seekMs)
|
||||
}
|
||||
|
||||
is DownloadFiles -> {
|
||||
addMessage("скачивание на очки: «${msg.title}» (${msg.files.size} файлов)")
|
||||
downloadManager.handleDownloadFiles(msg)
|
||||
}
|
||||
|
||||
is DeleteFiles -> {
|
||||
addMessage("удаление с очков: ${msg.itemId}")
|
||||
downloadManager.handleDeleteFiles(msg.itemId)
|
||||
}
|
||||
|
||||
is AssistantStateMsg -> addMessage("AssistantState: ${msg.state} (${msg.recordingSeconds}с)")
|
||||
|
||||
is ShowText -> addMessage("ShowText: ${msg.text}")
|
||||
@@ -108,4 +159,8 @@ class HostConnection(
|
||||
client.close()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val APP_VERSION = "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user