diff --git a/record_live.py b/record_live.py index 6c267f4..9013526 100644 --- a/record_live.py +++ b/record_live.py @@ -106,7 +106,35 @@ def mint_master(entry_id): "Referer": "https://www.bristolrovers.co.uk/"}) data = json.loads(urllib.request.urlopen(req, timeout=20) .read().decode("utf-8", "replace")) - return (data.get("media") or {}).get("hls") or None + hls = (data.get("media") or {}).get("hls") or None + if hls: + log_renditions(hls) + return hls + + +def log_renditions(master_url): + """Record what the master offers. We have never seen one carrying both + video and audio, so log it every time until we know how that looks.""" + import urllib.request + try: + req = urllib.request.Request(master_url, headers={"User-Agent": UA, + "Referer": "https://www.bristolrovers.co.uk/"}) + body = urllib.request.urlopen(req, timeout=15).read().decode("utf-8", "replace") + except Exception as exc: + log(f" (could not read master for rendition list: {exc})") + return + PREFIX = "#EXT-X-STREAM-INF:" + streams = [ln[len(PREFIX):] for ln in body.splitlines() + if ln.startswith(PREFIX)] + log(f" master offers {len(streams)} rendition(s):") + for st in streams: + bw = re.search(r"BANDWIDTH=(\d+)", st) + res = re.search(r"RESOLUTION=([\dx]+)", st) + cod = re.search(r'CODECS="([^"]+)"', st) + codecs = cod.group(1) if cod else "?" + kind = "audio-only" if ("avc" not in codecs and "hvc" not in codecs) else "video" + log(f" {kind:11s} {int(bw.group(1))//1000 if bw else '?':>6} kbps " + f"{res.group(1) if res else '-':>10} {codecs}") def find_manifest(timeout_s=90): @@ -175,7 +203,7 @@ def find_manifest(timeout_s=90): return m.group(1) if m else None -def record(url, duration_min): +def record(url, duration_min, prefer='best'): stamp = datetime.now().strftime("%Y%m%d-%H%M%S") out = os.path.join(RECORDINGS, f"brfc-{stamp}.ts") os.makedirs(RECORDINGS, exist_ok=True) @@ -197,8 +225,14 @@ def record(url, duration_min): "-reconnect_delay_max 30", "--downloader-args", f"ffmpeg_o:-t {duration_min * 60}", "-o", out, - url, ] + if prefer == "audio": + # Pin audio when the entitlement is audio-only: if a video rendition + # ever appears in the master, taking it would 403 on the segments. + cmd += ["-f", "bestaudio/best"] + elif prefer == "video": + cmd += ["-f", "bestvideo*+bestaudio/best"] + cmd.append(url) log(f"recording -> {os.path.basename(out)} (cap {duration_min} min)") log(f" yt-dlp log -> {os.path.basename(logfile)}") with open(logfile, "w", encoding="utf-8") as fh: @@ -213,6 +247,9 @@ def main(): ap.add_argument("--duration", type=int, default=150, help="recording cap in minutes") ap.add_argument("--poll", type=int, default=60, help="seconds between attempts") + ap.add_argument("--prefer", choices=("best", "audio", "video"), default="best", + help="which rendition to take when the master offers both; " + "'best' is yt-dlp's default and prefers video") args = ap.parse_args() if args.at: @@ -244,7 +281,7 @@ def main(): time.sleep(args.poll) continue log(" minted fresh master playlist") - proc = record(url, args.duration) + proc = record(url, args.duration, args.prefer) log(f" downloader pid {proc.pid}; waiting for it to finish") t0 = time.time() proc.wait()