Add on the fly transcoding

This commit is contained in:
Michael Pearson 2025-11-05 12:28:54 +11:00
parent b751228476
commit 21387d5d66
6 changed files with 144 additions and 2 deletions

View File

@ -69,6 +69,7 @@ http {
vod_mode mapped;
vod_max_mapping_response_size 1m;
vod_upstream_location /api;
vod_remote_upstream_location '/transcode';
vod_align_segments_to_key_frames on;
vod_manifest_segment_durations_mode accurate;
vod_ignore_edit_list on;
@ -102,6 +103,12 @@ http {
include auth_location.conf;
include base_path.conf;
location ~ /transcode/(.*) {
include auth_request.conf;
internal;
proxy_pass 'http://frigate_api/vod/transcode?file=$1';
}
location /vod/ {
include auth_request.conf;
aio threads;

View File

@ -34,6 +34,7 @@ from frigate.embeddings import EmbeddingsContext
from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter
from frigate.storage import StorageMaintainer
from frigate.transcode.temp_file_cache import TempFileCache
logger = logging.getLogger(__name__)
@ -55,6 +56,7 @@ class RemoteUserPlugin(Plugin):
def create_fastapi_app(
frigate_config: FrigateConfig,
database: SqliteQueueDatabase,
temp_file_cache: TempFileCache,
embeddings: Optional[EmbeddingsContext],
detected_frames_processor,
storage_maintainer: StorageMaintainer,
@ -134,6 +136,7 @@ def create_fastapi_app(
app.stats_emitter = stats_emitter
app.event_metadata_updater = event_metadata_updater
app.config_publisher = config_publisher
app.temp_file_cache = temp_file_cache
if frigate_config.auth.enabled:
secret = get_jwt_secret()

View File

@ -11,7 +11,7 @@ from datetime import datetime, timedelta, timezone
from functools import reduce
from pathlib import Path as FilePath
from typing import Any, List
from urllib.parse import unquote
from urllib.parse import quote, unquote
import cv2
import numpy as np
@ -815,6 +815,46 @@ async def recording_clip(
)
@router.get("/vod/transcode")
def clip(request: Request, file: str):
config: FrigateConfig = request.app.frigate_config
def transcode(input: str, output: str):
ffmpeg_cmd = [
config.ffmpeg.ffmpeg_path,
"-hide_banner",
"-hwaccel",
"qsv",
"-hwaccel_output_format",
"qsv",
"-i",
input,
"-vf",
"scale_qsv=854:480",
"-c:v",
"h264_qsv",
"-c:a",
"copy",
"-f",
"mp4",
output,
]
with sp.Popen(
ffmpeg_cmd,
stdout=sp.PIPE,
stderr=sp.PIPE,
text=False,
bufsize=0,
) as ffmpeg:
ret = ffmpeg.wait()
if ret != 0:
raise Exception("Failed to transcode!")
cache = request.app.temp_file_cache
transcoded_path = cache.get(file, lambda output: transcode(file, output))
return FileResponse(transcoded_path, media_type="video/mp4")
@router.get(
"/vod/{camera_name}/start/{start_ts}/end/{end_ts}",
dependencies=[Depends(require_camera_access)],
@ -844,7 +884,11 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
recording: Recordings
for recording in recordings:
clip = {"type": "source", "path": recording.path}
clip = {
"type": "source",
"sourceType": "http",
"path": f"/{quote(recording.path, safe='')}",
}
duration = int(recording.duration * 1000)
# adjust start offset if start_ts is after recording.start_time

View File

@ -73,6 +73,7 @@ from frigate.stats.util import stats_init
from frigate.storage import StorageMaintainer
from frigate.timeline import TimelineProcessor
from frigate.track.object_processing import TrackedObjectProcessor
from frigate.transcode.temp_file_cache import TempFileCache
from frigate.util.builtin import empty_and_close_queue
from frigate.util.image import UntrackedSharedMemory
from frigate.util.services import set_file_limit
@ -225,6 +226,9 @@ class FrigateApp:
migrate_db.close()
def init_transcode_cache(self) -> None:
self.transcode_cache = TempFileCache()
def init_go2rtc(self) -> None:
for proc in psutil.process_iter(["pid", "name"]):
if proc.info["name"] == "go2rtc":
@ -530,6 +534,7 @@ class FrigateApp:
self.init_camera_metrics()
self.init_queues()
self.init_database()
self.init_transcode_cache()
self.init_onvif()
self.init_recording_manager()
self.init_review_segment_manager()
@ -561,6 +566,7 @@ class FrigateApp:
create_fastapi_app(
self.config,
self.db,
self.transcode_cache,
self.embeddings,
self.detected_frames_processor,
self.storage_maintainer,
@ -634,6 +640,7 @@ class FrigateApp:
self.stats_emitter.join()
self.frigate_watchdog.join()
self.db.stop()
self.transcode_cache.stop()
# Save embeddings stats to disk
if self.embeddings:

View File

View File

@ -0,0 +1,81 @@
import os
import tempfile
import threading
import time
class TempFileCache:
def __init__(self, ttl_seconds=300, cleanup_interval=1):
self.ttl = ttl_seconds
self.cleanup_interval = cleanup_interval
self.cache = {} # key -> (path, timestamp)
self.pending = set() # keys being generated
self.lock = threading.Condition()
self._stop = False
# Start background cleanup thread
self.thread = threading.Thread(target=self._cleanup_loop, daemon=True)
self.thread.start()
def _remove_file(self, path):
if path and os.path.exists(path):
try:
os.remove(path)
except Exception:
pass
def _cleanup_expired(self):
now = time.time()
expired_keys = [k for k, (_, ts) in self.cache.items() if now - ts > self.ttl]
for key in expired_keys:
path, _ = self.cache.pop(key, (None, None))
self._remove_file(path)
def _cleanup_loop(self):
while not self._stop:
with self.lock:
self._cleanup_expired()
time.sleep(self.cleanup_interval)
def stop(self):
"""Stop the cleanup thread."""
self._stop = True
self.thread.join(timeout=2)
def get(self, key, generator_fn):
with self.lock:
# Return cached file if fresh
if key in self.cache:
path, ts = self.cache[key]
# refresh timestamp
self.cache[key] = (path, time.time())
return path
# If another thread is generating this file, wait
while key in self.pending:
self.lock.wait()
# Mark this key as pending generation
self.pending.add(key)
# Outside lock: generate the file
path = tempfile.mktemp()
try:
generator_fn(path)
except Exception:
self._remove_file(path)
with self.lock:
self.pending.remove(key)
self.lock.notify_all()
raise
# Store file and notify waiters
with self.lock:
self.cache[key] = (path, time.time())
self.pending.remove(key)
self.lock.notify_all()
return path