From a67c5ea9af5954d95530ed73b7bd472a2480d373 Mon Sep 17 00:00:00 2001 From: julio Date: Tue, 6 Oct 2026 03:10:39 -0300 Subject: [PATCH] fix(android): transmissao de video nao trava quando o servidor reinicia Se o servidor caia no meio da transmissao, o app tentava reabrir a camera ainda presa pelo sensor de movimento; o getUserMedia ficava esperando para sempre e a fila de pedidos do painel travava (video "desligado" ate reiniciar o app). Agora abrir a camera tem prazo de 8 s, cada pedido do painel tem prazo de 12 s e uma camera encerrada de repente desfaz a sessao para a proxima mensagem reabrir. Validado no S10: servidor reiniciado com video ativo volta sozinho em ~8 s. APK 0.8.2. Co-Authored-By: Claude Opus 5.5 --- android/app/AndroidManifest.xml | 4 ++-- android/app/assets/transmissao.html | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/android/app/AndroidManifest.xml b/android/app/AndroidManifest.xml index 16bc51b..2488da6 100644 --- a/android/app/AndroidManifest.xml +++ b/android/app/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="10" + android:versionName="0.8.2"> diff --git a/android/app/assets/transmissao.html b/android/app/assets/transmissao.html index aa33e1f..f355d8d 100644 --- a/android/app/assets/transmissao.html +++ b/android/app/assets/transmissao.html @@ -12,8 +12,13 @@ let config = {}, signature = '', movementEnabled = true, aviso = '', capacidades let allowed = true; let lastMotionFrame = -1; function chave(c) { return JSON.stringify([c.cameraLado || 'user', c.videoResolucao || 'auto', c.videoFPS || 0]); } +/* Nada pode esperar para sempre: se a câmera estiver presa (ex.: servidor caiu no meio e o + sensor de movimento ainda a segura), desiste e a próxima mensagem do painel tenta de novo. */ +function comPrazo(promessa, ms, oque) { + return Promise.race([promessa, new Promise((_, rej) => setTimeout(() => rej(new Error(oque + ' demorou mais de ' + ms / 1000 + ' s')), ms))]); +} async function capturar(c) { - const media = await navigator.mediaDevices.getUserMedia({audio:false,video:{facingMode:{ideal:c.cameraLado || 'user'},width:{ideal:640},height:{ideal:360},frameRate:{ideal:30}}}); + const media = await comPrazo(navigator.mediaDevices.getUserMedia({audio:false,video:{facingMode:{ideal:c.cameraLado || 'user'},width:{ideal:640},height:{ideal:360},frameRate:{ideal:30}}}), 8000, 'Abrir a câmera'); const track = media.getVideoTracks()[0]; const facing = track.getSettings().facingMode; if (facing && facing !== (c.cameraLado || 'user')) { @@ -74,6 +79,12 @@ async function iniciar(id) { const media = await capturar(config); signature = chave(config); if (!wanted || captureGeneration !== generation) { media.getTracks().forEach(t => t.stop()); return; } stream = media; video.srcObject = stream; await video.play(); + // câmera fechada de repente (outro uso, erro do sistema): desfaz tudo; o próximo pacote reabre + stream.getVideoTracks()[0].onended = () => { + if (captureGeneration !== generation) return; + console.log('RTC câmera encerrada; recomeçando no próximo pedido'); + parar(); sid = null; + }; pc = new RTCPeerConnection({iceServers:[]}); sender = pc.addTrack(stream.getVideoTracks()[0], stream); const mine = pc; @@ -133,11 +144,11 @@ async function receber(packet) { } window.rtcReceber = packet => { allowed = true; - tasks = tasks.then(() => allowed ? receber(packet) : undefined).catch(e => { + tasks = tasks.then(() => allowed ? comPrazo(receber(packet), 12000, 'Pedido de vídeo') : undefined).catch(e => { console.log('RTC erro=' + e.name + ': ' + e.message); sinal({tipo:'erro',erro:e.name + ': ' + e.message}); estado({tipo:'webrtc',estado:'erro',erro:e.message}); - parar(); + parar(); sid = null; }); }; window.rtcParar = () => { allowed = false; parar(); sid = null; };