117 lines
3.3 KiB
Python
117 lines
3.3 KiB
Python
from __future__ import annotations
|
||
|
||
"""YouTube 单槽推流协调:与 youtube2 / douyin_youtube_ffplay 语义对齐。"""
|
||
|
||
import threading
|
||
import time
|
||
from typing import Any
|
||
|
||
from web2_youtube_relay_heartbeat import clear_relay_heartbeat, read_relay_heartbeat, write_relay_heartbeat
|
||
|
||
_slot_lock = threading.Lock()
|
||
_slot_owner: str | None = None
|
||
_CLAIM_GRACE_SECONDS = 15.0
|
||
|
||
|
||
def _heartbeat_claim_fresh(hb: dict[str, Any]) -> bool:
|
||
if not hb:
|
||
return False
|
||
state = str(hb.get("state") or "").strip().lower()
|
||
if state not in {"claiming", "starting", "streaming", "source_failover"}:
|
||
return False
|
||
updated = float(hb.get("updated_at") or 0)
|
||
return updated > 0 and (time.time() - updated) <= _CLAIM_GRACE_SECONDS + 90.0
|
||
|
||
|
||
def prune_orphan_relay_recording(
|
||
recording: set[str],
|
||
recording_time_list: dict[str, Any],
|
||
*,
|
||
pm2: str,
|
||
base_dir: str,
|
||
) -> None:
|
||
"""仅清理孤儿占槽:recording 非空且无有效 relay heartbeat。"""
|
||
if not recording:
|
||
return
|
||
hb = read_relay_heartbeat(pm2, base_dir=base_dir)
|
||
if _heartbeat_claim_fresh(hb):
|
||
return
|
||
global _slot_owner
|
||
with _slot_lock:
|
||
recording.clear()
|
||
recording_time_list.clear()
|
||
_slot_owner = None
|
||
clear_relay_heartbeat(pm2, base_dir=base_dir)
|
||
|
||
|
||
def relay_slot_blocks(record_name: str, recording: set[str]) -> bool:
|
||
return any(name for name in recording if name and name != record_name)
|
||
|
||
|
||
def claim_relay_slot(
|
||
record_name: str,
|
||
*,
|
||
pm2: str,
|
||
base_dir: str,
|
||
anchor_name: str = "",
|
||
) -> bool:
|
||
global _slot_owner
|
||
with _slot_lock:
|
||
if _slot_owner is not None and _slot_owner != record_name:
|
||
return False
|
||
_slot_owner = record_name
|
||
write_relay_heartbeat(
|
||
pm2,
|
||
{
|
||
"state": "claiming",
|
||
"record_name": record_name,
|
||
"anchor_name": anchor_name,
|
||
"claimed_at": time.time(),
|
||
},
|
||
base_dir=base_dir,
|
||
)
|
||
return True
|
||
|
||
|
||
def release_relay_slot(record_name: str, *, pm2: str = "", base_dir: str = "") -> None:
|
||
global _slot_owner
|
||
with _slot_lock:
|
||
if _slot_owner == record_name:
|
||
_slot_owner = None
|
||
if pm2 and base_dir:
|
||
hb = read_relay_heartbeat(pm2, base_dir=base_dir)
|
||
if str(hb.get("record_name") or "") == record_name:
|
||
clear_relay_heartbeat(pm2, base_dir=base_dir)
|
||
|
||
|
||
def _flv_is_h265(url: str) -> bool:
|
||
lower = (url or "").lower()
|
||
if "codec=h265" in lower or "codec=hevc" in lower:
|
||
return True
|
||
try:
|
||
from src import utils
|
||
|
||
codec = utils.get_query_params(url, "codec")
|
||
if codec and str(codec[0]).lower() in {"h265", "hevc"}:
|
||
return True
|
||
except Exception:
|
||
pass
|
||
return False
|
||
|
||
|
||
def youtube_stream_fallback_urls(port_info: dict[str, Any], primary: str) -> list[str]:
|
||
primary_text = (primary or "").strip()
|
||
out: list[str] = []
|
||
seen: set[str] = set()
|
||
for key in ("flv_url", "m3u8_url"):
|
||
url = str(port_info.get(key) or "").strip()
|
||
if not url or url == primary_text or url in seen:
|
||
continue
|
||
if not url.startswith("http"):
|
||
continue
|
||
if key == "flv_url" and _flv_is_h265(url):
|
||
continue
|
||
seen.add(url)
|
||
out.append(url)
|
||
return out
|