"""HLS transcoding via ffmpeg. Background-runs and updates DB.""" import asyncio import logging import shutil from pathlib import Path from typing import Optional logger = logging.getLogger("kino.transcode") async def transcode_to_hls( source: Path, out_dir: Path, on_status, ): """ Run ffmpeg to produce HLS playlist + segments. `on_status(status, error=None)` is awaited at start/end with status one of 'running'|'done'|'failed'. Uses stream-copy where possible (fast, no re-encode). """ if not source.is_file(): await on_status("failed", error=f"Source missing: {source}") return out_dir.mkdir(parents=True, exist_ok=True) playlist = out_dir / "playlist.m3u8" cmd = [ "ffmpeg", "-y", "-i", str(source), "-c:v", "copy", "-c:a", "copy", "-bsf:v", "h264_mp4toannexb", "-f", "hls", "-hls_time", "6", "-hls_list_size", "0", "-hls_playlist_type", "vod", "-hls_segment_filename", str(out_dir / "seg_%04d.ts"), str(playlist), ] await on_status("running") try: proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) _stdout, stderr = await proc.communicate() if proc.returncode != 0: err = stderr.decode("utf-8", errors="ignore")[-500:] logger.error(f"ffmpeg failed: {err}") await on_status("failed", error=err[:200]) # cleanup partial files shutil.rmtree(out_dir, ignore_errors=True) return await on_status("done") except FileNotFoundError: await on_status("failed", error="ffmpeg not installed") except Exception as e: logger.exception("transcode crashed") await on_status("failed", error=str(e)) shutil.rmtree(out_dir, ignore_errors=True) def srt_to_vtt(srt_text: str) -> str: """Convert SRT subtitles to WebVTT format (simple, no styling).""" lines = srt_text.replace("\r\n", "\n").split("\n") out = ["WEBVTT", ""] for line in lines: # Replace SRT timestamp commas with VTT dots if "-->" in line: out.append(line.replace(",", ".")) else: out.append(line) return "\n".join(out)