Recorder: drena stderr di ffmpeg + messaggi di errore leggibili
Il vecchio worker non drenava lo stderr di ffmpeg: dopo ~64KB il
pipe si bloccava e quando ffmpeg falliva subito (es. permesso
microfono negato su macOS) si vedeva solo 'ffmpeg exit -6' senza
contesto.
- Thread dedicato _drain_stderr() che legge stderr in continuo e
mantiene le ultime 50 righe in un buffer protetto
- _extract_friendly_error() riconosce i casi tipici e produce
messaggi italiani:
* exit -6 su macOS -> 'permesso Microfono mancante'
* Input/Output error -> 'dispositivo non disponibile'
* 'no such device' -> 'premere Aggiorna'
- Pre-flight check: aspetta che ffmpeg stampi 'Press [q]' (segnale
di device aperto) prima di emettere 'started'. Se ffmpeg muore
prima, l'errore viene segnalato senza falso 'recording in corso'
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
5041d3e14c
commit
9f37b7eb94
1 file changed
+86
-21
+86
-21
@@ -29,6 +29,8 @@ _current_proc: Optional[subprocess.Popen] = None
|
|||||||
_proc_lock = threading.Lock()
|
_proc_lock = threading.Lock()
|
||||||
_stop_event = threading.Event()
|
_stop_event = threading.Event()
|
||||||
_recording_thread: Optional[threading.Thread] = None
|
_recording_thread: Optional[threading.Thread] = None
|
||||||
|
_stderr_buf: list = []
|
||||||
|
_stderr_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def is_recording() -> bool:
|
def is_recording() -> bool:
|
||||||
@@ -216,6 +218,24 @@ def start_recording(
|
|||||||
cmd = _build_record_cmd(ffmpeg, device_id, str(out), bitrate)
|
cmd = _build_record_cmd(ffmpeg, device_id, str(out), bitrate)
|
||||||
|
|
||||||
_stop_event.clear()
|
_stop_event.clear()
|
||||||
|
with _stderr_lock:
|
||||||
|
_stderr_buf.clear()
|
||||||
|
|
||||||
|
def _drain_stderr(proc):
|
||||||
|
"""Drena lo stderr di ffmpeg in modo continuo per evitare blocchi
|
||||||
|
del pipe e per salvare le ultime righe ai fini diagnostici."""
|
||||||
|
try:
|
||||||
|
for raw in iter(proc.stderr.readline, b""):
|
||||||
|
if not raw:
|
||||||
|
break
|
||||||
|
line = raw.decode("utf-8", errors="ignore").rstrip()
|
||||||
|
with _stderr_lock:
|
||||||
|
_stderr_buf.append(line)
|
||||||
|
# Mantieni solo le ultime 50 righe per non sforare la RAM
|
||||||
|
if len(_stderr_buf) > 50:
|
||||||
|
del _stderr_buf[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _worker():
|
def _worker():
|
||||||
global _current_proc
|
global _current_proc
|
||||||
@@ -224,16 +244,41 @@ def start_recording(
|
|||||||
_current_proc = subprocess.Popen(
|
_current_proc = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=False, # bytes su stdin per "q"
|
text=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
drain_t = threading.Thread(target=_drain_stderr,
|
||||||
|
args=(_current_proc,), daemon=True)
|
||||||
|
drain_t.start()
|
||||||
|
|
||||||
|
# Aspetta brevemente che ffmpeg apra il device. Se fallisce nei
|
||||||
|
# primi ~1.5s, segnala errore prima di emettere 'started'.
|
||||||
|
start = time.monotonic()
|
||||||
|
opened = False
|
||||||
|
for _ in range(15):
|
||||||
|
if _current_proc.poll() is not None:
|
||||||
|
break
|
||||||
|
# ffmpeg stampa "Press [q] to stop" quando il device e' aperto
|
||||||
|
with _stderr_lock:
|
||||||
|
if any("Press [q]" in l for l in _stderr_buf):
|
||||||
|
opened = True
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
rc_early = _current_proc.poll()
|
||||||
|
if not opened and rc_early is not None:
|
||||||
|
# ffmpeg e' uscito prima di iniziare a registrare
|
||||||
|
with _proc_lock:
|
||||||
|
_current_proc = None
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback("error", {"error": _extract_friendly_error(rc_early)})
|
||||||
|
return
|
||||||
|
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback("started", {"output_path": str(out)})
|
progress_callback("started", {"output_path": str(out)})
|
||||||
|
|
||||||
start = time.monotonic()
|
|
||||||
# Pubblica un tick al secondo
|
|
||||||
while True:
|
while True:
|
||||||
if _stop_event.is_set():
|
if _stop_event.is_set():
|
||||||
break
|
break
|
||||||
@@ -271,12 +316,7 @@ def start_recording(
|
|||||||
if rc == 0 or out.exists():
|
if rc == 0 or out.exists():
|
||||||
progress_callback("stopped", {"output_path": str(out), "seconds": seconds})
|
progress_callback("stopped", {"output_path": str(out), "seconds": seconds})
|
||||||
else:
|
else:
|
||||||
err = ""
|
progress_callback("error", {"error": _extract_friendly_error(rc)})
|
||||||
try:
|
|
||||||
err = (_get_last_stderr() or "").strip().splitlines()[-1]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
progress_callback("error", {"error": err or f"ffmpeg exit {rc}"})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
with _proc_lock:
|
with _proc_lock:
|
||||||
_current_proc = None
|
_current_proc = None
|
||||||
@@ -289,17 +329,42 @@ def start_recording(
|
|||||||
return {"ok": True, "output_path": str(out)}
|
return {"ok": True, "output_path": str(out)}
|
||||||
|
|
||||||
|
|
||||||
def _get_last_stderr() -> str:
|
def _extract_friendly_error(rc: int) -> str:
|
||||||
"""Tenta di leggere lo stderr residuo del processo corrente."""
|
"""Trasforma rc + stderr di ffmpeg in un messaggio leggibile.
|
||||||
with _proc_lock:
|
Riconosce i casi tipici: permessi microfono, device occupato,
|
||||||
proc = _current_proc
|
device non disponibile, ..."""
|
||||||
if proc is None or proc.stderr is None:
|
with _stderr_lock:
|
||||||
return ""
|
lines = list(_stderr_buf)
|
||||||
try:
|
|
||||||
data = proc.stderr.read() or b""
|
text = "\n".join(lines).lower()
|
||||||
return data.decode("utf-8", errors="ignore")
|
|
||||||
except Exception:
|
if "abort" in text and rc == -6 and sys.platform == "darwin":
|
||||||
return ""
|
return (
|
||||||
|
"ffmpeg abortito (errore -6): probabilmente l'app non ha il "
|
||||||
|
"permesso 'Microfono' di macOS. Vai su Preferenze di Sistema "
|
||||||
|
"→ Privacy e sicurezza → Microfono e abilita Terminal/Python. "
|
||||||
|
"Poi riavvia l'app."
|
||||||
|
)
|
||||||
|
|
||||||
|
if any(s in text for s in ("input/output error", "errno 22", "device not configured")):
|
||||||
|
return "Dispositivo non disponibile (riavvia il servizio audio o ricontrolla la selezione)."
|
||||||
|
|
||||||
|
if "permission" in text or "not authorized" in text or "not permitted" in text:
|
||||||
|
return "Permesso microfono negato da macOS. Abilita Python/MusicDownload in Privacy → Microfono."
|
||||||
|
|
||||||
|
if "no such device" in text or "no such audio device" in text:
|
||||||
|
return "Dispositivo non trovato. Premi '↻ Aggiorna' e riseleziona."
|
||||||
|
|
||||||
|
# Restituisci l'ultima riga utile dello stderr (esclude le righe di banner/progress)
|
||||||
|
for line in reversed(lines):
|
||||||
|
l = line.strip()
|
||||||
|
if not l:
|
||||||
|
continue
|
||||||
|
if l.startswith("[avfoundation") or l.startswith("[dshow") or "@ 0x" in l:
|
||||||
|
return l
|
||||||
|
if "error" in l.lower() or "fail" in l.lower() or "denied" in l.lower():
|
||||||
|
return l
|
||||||
|
return f"ffmpeg exit {rc}"
|
||||||
|
|
||||||
|
|
||||||
def stop_recording() -> dict:
|
def stop_recording() -> dict:
|
||||||
|
|||||||
Reference in new issue
Block a user