141 lines
4.4 KiB
Python
141 lines
4.4 KiB
Python
"""声呐视频:由显式文件或专用目录中的最新 MP4 发布 H.264 到 MEDIA_ROOT。
|
||
|
||
与水上视频使用相同的转码路径(``transcode_src_to_h264_dst`` / ffmpeg)。
|
||
不切片:整段源转成一个固定文件,每次 GET 返回同一 ``video_path`` URL。
|
||
"""
|
||
|
||
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()
|
||
|
||
DEFAULT_CLIENT_ID = "default"
|
||
|
||
# 源路径 + mtime,用于跳过重复转码
|
||
_last_src_key: tuple[str, float] | None = None
|
||
_cached_public_url: str = ""
|
||
|
||
|
||
def _public_media_url(settings: Settings, basename: str) -> str:
|
||
base = settings.public_base_url.rstrip("/")
|
||
return f"{base}/media/{basename}"
|
||
|
||
|
||
def _safe_sonar_media_basename(raw: str) -> str:
|
||
n = (raw or "").strip()
|
||
if not n:
|
||
return "biomass_sonar.mp4"
|
||
return Path(n).name or "biomass_sonar.mp4"
|
||
|
||
|
||
def resolve_sonar_video_source(settings: Settings) -> Path | None:
|
||
"""优先 BIOMASS_SONAR_VIDEO_SOURCE;否则在 BIOMASS_SONAR_VIDEO_DIR 中取 mtime 最新的 .mp4。"""
|
||
cfg = settings.biomass_sonar_video_source
|
||
if cfg is not None:
|
||
if cfg.is_file():
|
||
return cfg
|
||
logger.warning(
|
||
"[sonar-video] BIOMASS_SONAR_VIDEO_SOURCE is not a file: {}",
|
||
cfg,
|
||
)
|
||
return None
|
||
d = settings.biomass_sonar_video_dir
|
||
if d is None or not d.is_dir():
|
||
return None
|
||
mp4s = iter_mp4(d, settings.biomass_sonar_video_recursive)
|
||
if not mp4s:
|
||
return None
|
||
try:
|
||
return max(mp4s, key=lambda p: p.stat().st_mtime)
|
||
except OSError as e:
|
||
logger.warning("[sonar-video] could not pick latest mp4: {}", e)
|
||
return None
|
||
|
||
|
||
async def _publish_video(
|
||
src: Path,
|
||
dst: Path,
|
||
settings: Settings,
|
||
) -> str:
|
||
"""与 water_video._publish_video 相同:ffmpeg H.264,失败则回退复制。"""
|
||
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("[sonar-video] published H.264: {} -> {}", src.name, dst.name)
|
||
else:
|
||
tmp.unlink(missing_ok=True)
|
||
await asyncio.to_thread(shutil.copy2, src, dst)
|
||
logger.warning(
|
||
"[sonar-video] transcode failed, copied raw: {} -> {}",
|
||
src.name,
|
||
dst.name,
|
||
)
|
||
return _public_media_url(settings, dst.name)
|
||
except Exception:
|
||
logger.exception("[sonar-video] publish failed")
|
||
tmp.unlink(missing_ok=True)
|
||
if dst.is_file():
|
||
return _public_media_url(settings, dst.name)
|
||
return ""
|
||
|
||
|
||
async def get_sonar_video_public_url(
|
||
settings: Settings,
|
||
_client_id: str = DEFAULT_CLIENT_ID,
|
||
) -> str:
|
||
"""转码并发布到 MEDIA_ROOT 后返回绝对 URL;无可用源且无已发布文件时返回空串。
|
||
|
||
始终对应同一发布文件(BIOMASS_SONAR_VIDEO_MEDIA_NAME),所有客户端每次请求返回同一 URL。
|
||
"""
|
||
settings.media_root.mkdir(parents=True, exist_ok=True)
|
||
|
||
global _last_src_key, _cached_public_url
|
||
|
||
async with _publish_lock:
|
||
basename = _safe_sonar_media_basename(settings.biomass_sonar_video_media_name)
|
||
dst = settings.media_root / basename
|
||
src = resolve_sonar_video_source(settings)
|
||
|
||
if src is None:
|
||
_last_src_key = None
|
||
_cached_public_url = ""
|
||
if dst.is_file():
|
||
return _public_media_url(settings, dst.name)
|
||
return ""
|
||
|
||
try:
|
||
key = (str(src.resolve()), src.stat().st_mtime)
|
||
except OSError:
|
||
if dst.is_file():
|
||
return _public_media_url(settings, dst.name)
|
||
return ""
|
||
|
||
if key == _last_src_key and _cached_public_url:
|
||
return _cached_public_url
|
||
|
||
url = await _publish_video(src, dst, settings)
|
||
if url:
|
||
_last_src_key = key
|
||
_cached_public_url = url
|
||
return url
|
||
|
||
if dst.is_file():
|
||
u = _public_media_url(settings, dst.name)
|
||
_last_src_key = key
|
||
_cached_public_url = u
|
||
return u
|
||
return ""
|