102 lines
3.3 KiB
Python
102 lines
3.3 KiB
Python
|
|
"""水上视频:从 FishAction 输入目录或显式路径发布 H.264 MP4 到 MEDIA_ROOT。"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import shutil
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
from loguru import logger
|
|||
|
|
|
|||
|
|
from app.services.action_watch import iter_mp4
|
|||
|
|
from app.services.measure import transcode_src_to_h264_dst
|
|||
|
|
from app.settings import Settings
|
|||
|
|
|
|||
|
|
_publish_lock = asyncio.Lock()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _public_media_url(settings: Settings, basename: str) -> str:
|
|||
|
|
base = settings.public_base_url.rstrip("/")
|
|||
|
|
return f"{base}/media/{basename}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _safe_water_media_basename(raw: str) -> str:
|
|||
|
|
n = (raw or "").strip()
|
|||
|
|
if not n:
|
|||
|
|
return "biomass_water_surface.mp4"
|
|||
|
|
return Path(n).name or "biomass_water_surface.mp4"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def resolve_water_video_source(settings: Settings) -> Path | None:
|
|||
|
|
"""优先 BIOMASS_WATER_VIDEO_SOURCE;否则取 ACTION_WATCH_DIR 中 mtime 最新的 .mp4。"""
|
|||
|
|
cfg = settings.biomass_water_video_source
|
|||
|
|
if cfg is not None:
|
|||
|
|
if cfg.is_file():
|
|||
|
|
return cfg
|
|||
|
|
logger.warning(
|
|||
|
|
"[water-video] BIOMASS_WATER_VIDEO_SOURCE is not a file: {}",
|
|||
|
|
cfg,
|
|||
|
|
)
|
|||
|
|
return None
|
|||
|
|
aw = settings.action_watch_dir
|
|||
|
|
if aw is None or not aw.is_dir():
|
|||
|
|
return None
|
|||
|
|
mp4s = iter_mp4(aw, settings.action_watch_recursive)
|
|||
|
|
if not mp4s:
|
|||
|
|
return None
|
|||
|
|
try:
|
|||
|
|
return max(mp4s, key=lambda p: p.stat().st_mtime)
|
|||
|
|
except OSError as e:
|
|||
|
|
logger.warning("[water-video] could not pick latest mp4: {}", e)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def get_water_video_public_url(settings: Settings) -> str:
|
|||
|
|
"""转码并发布到 MEDIA_ROOT 后返回绝对 URL;无可用源且无已发布文件时返回空串。"""
|
|||
|
|
settings.media_root.mkdir(parents=True, exist_ok=True)
|
|||
|
|
basename = _safe_water_media_basename(settings.biomass_water_video_media_name)
|
|||
|
|
dst = settings.media_root / basename
|
|||
|
|
|
|||
|
|
src = resolve_water_video_source(settings)
|
|||
|
|
if src is None:
|
|||
|
|
if dst.is_file():
|
|||
|
|
return _public_media_url(settings, dst.name)
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
async with _publish_lock:
|
|||
|
|
need_publish = True
|
|||
|
|
if dst.is_file():
|
|||
|
|
try:
|
|||
|
|
if dst.stat().st_mtime >= src.stat().st_mtime:
|
|||
|
|
need_publish = False
|
|||
|
|
except OSError:
|
|||
|
|
pass
|
|||
|
|
if not need_publish:
|
|||
|
|
return _public_media_url(settings, dst.name)
|
|||
|
|
|
|||
|
|
tmp = dst.with_name(dst.stem + "_tmp.mp4")
|
|||
|
|
tmp.unlink(missing_ok=True)
|
|||
|
|
try:
|
|||
|
|
ok = await asyncio.to_thread(transcode_src_to_h264_dst, src, tmp)
|
|||
|
|
if ok and tmp.is_file() and tmp.stat().st_size > 0:
|
|||
|
|
tmp.replace(dst)
|
|||
|
|
logger.info("[water-video] published H.264: {} -> {}", src.name, dst.name)
|
|||
|
|
else:
|
|||
|
|
tmp.unlink(missing_ok=True)
|
|||
|
|
await asyncio.to_thread(shutil.copy2, src, dst)
|
|||
|
|
logger.warning(
|
|||
|
|
"[water-video] transcode failed, copied raw: {} -> {}",
|
|||
|
|
src.name,
|
|||
|
|
dst.name,
|
|||
|
|
)
|
|||
|
|
except Exception:
|
|||
|
|
logger.exception("[water-video] publish failed")
|
|||
|
|
tmp.unlink(missing_ok=True)
|
|||
|
|
if dst.is_file():
|
|||
|
|
return _public_media_url(settings, dst.name)
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
if dst.is_file():
|
|||
|
|
return _public_media_url(settings, dst.name)
|
|||
|
|
return ""
|