mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 17:47:37 +03:00
Add on the fly transcoding
This commit is contained in:
parent
b751228476
commit
21387d5d66
@ -69,6 +69,7 @@ http {
|
|||||||
vod_mode mapped;
|
vod_mode mapped;
|
||||||
vod_max_mapping_response_size 1m;
|
vod_max_mapping_response_size 1m;
|
||||||
vod_upstream_location /api;
|
vod_upstream_location /api;
|
||||||
|
vod_remote_upstream_location '/transcode';
|
||||||
vod_align_segments_to_key_frames on;
|
vod_align_segments_to_key_frames on;
|
||||||
vod_manifest_segment_durations_mode accurate;
|
vod_manifest_segment_durations_mode accurate;
|
||||||
vod_ignore_edit_list on;
|
vod_ignore_edit_list on;
|
||||||
@ -102,6 +103,12 @@ http {
|
|||||||
include auth_location.conf;
|
include auth_location.conf;
|
||||||
include base_path.conf;
|
include base_path.conf;
|
||||||
|
|
||||||
|
location ~ /transcode/(.*) {
|
||||||
|
include auth_request.conf;
|
||||||
|
internal;
|
||||||
|
proxy_pass 'http://frigate_api/vod/transcode?file=$1';
|
||||||
|
}
|
||||||
|
|
||||||
location /vod/ {
|
location /vod/ {
|
||||||
include auth_request.conf;
|
include auth_request.conf;
|
||||||
aio threads;
|
aio threads;
|
||||||
|
|||||||
@ -34,6 +34,7 @@ from frigate.embeddings import EmbeddingsContext
|
|||||||
from frigate.ptz.onvif import OnvifController
|
from frigate.ptz.onvif import OnvifController
|
||||||
from frigate.stats.emitter import StatsEmitter
|
from frigate.stats.emitter import StatsEmitter
|
||||||
from frigate.storage import StorageMaintainer
|
from frigate.storage import StorageMaintainer
|
||||||
|
from frigate.transcode.temp_file_cache import TempFileCache
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -55,6 +56,7 @@ class RemoteUserPlugin(Plugin):
|
|||||||
def create_fastapi_app(
|
def create_fastapi_app(
|
||||||
frigate_config: FrigateConfig,
|
frigate_config: FrigateConfig,
|
||||||
database: SqliteQueueDatabase,
|
database: SqliteQueueDatabase,
|
||||||
|
temp_file_cache: TempFileCache,
|
||||||
embeddings: Optional[EmbeddingsContext],
|
embeddings: Optional[EmbeddingsContext],
|
||||||
detected_frames_processor,
|
detected_frames_processor,
|
||||||
storage_maintainer: StorageMaintainer,
|
storage_maintainer: StorageMaintainer,
|
||||||
@ -134,6 +136,7 @@ def create_fastapi_app(
|
|||||||
app.stats_emitter = stats_emitter
|
app.stats_emitter = stats_emitter
|
||||||
app.event_metadata_updater = event_metadata_updater
|
app.event_metadata_updater = event_metadata_updater
|
||||||
app.config_publisher = config_publisher
|
app.config_publisher = config_publisher
|
||||||
|
app.temp_file_cache = temp_file_cache
|
||||||
|
|
||||||
if frigate_config.auth.enabled:
|
if frigate_config.auth.enabled:
|
||||||
secret = get_jwt_secret()
|
secret = get_jwt_secret()
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pathlib import Path as FilePath
|
from pathlib import Path as FilePath
|
||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
from urllib.parse import unquote
|
from urllib.parse import quote, unquote
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
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(
|
@router.get(
|
||||||
"/vod/{camera_name}/start/{start_ts}/end/{end_ts}",
|
"/vod/{camera_name}/start/{start_ts}/end/{end_ts}",
|
||||||
dependencies=[Depends(require_camera_access)],
|
dependencies=[Depends(require_camera_access)],
|
||||||
@ -844,7 +884,11 @@ async def vod_ts(camera_name: str, start_ts: float, end_ts: float):
|
|||||||
|
|
||||||
recording: Recordings
|
recording: Recordings
|
||||||
for recording in 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)
|
duration = int(recording.duration * 1000)
|
||||||
|
|
||||||
# adjust start offset if start_ts is after recording.start_time
|
# adjust start offset if start_ts is after recording.start_time
|
||||||
|
|||||||
@ -73,6 +73,7 @@ from frigate.stats.util import stats_init
|
|||||||
from frigate.storage import StorageMaintainer
|
from frigate.storage import StorageMaintainer
|
||||||
from frigate.timeline import TimelineProcessor
|
from frigate.timeline import TimelineProcessor
|
||||||
from frigate.track.object_processing import TrackedObjectProcessor
|
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.builtin import empty_and_close_queue
|
||||||
from frigate.util.image import UntrackedSharedMemory
|
from frigate.util.image import UntrackedSharedMemory
|
||||||
from frigate.util.services import set_file_limit
|
from frigate.util.services import set_file_limit
|
||||||
@ -225,6 +226,9 @@ class FrigateApp:
|
|||||||
|
|
||||||
migrate_db.close()
|
migrate_db.close()
|
||||||
|
|
||||||
|
def init_transcode_cache(self) -> None:
|
||||||
|
self.transcode_cache = TempFileCache()
|
||||||
|
|
||||||
def init_go2rtc(self) -> None:
|
def init_go2rtc(self) -> None:
|
||||||
for proc in psutil.process_iter(["pid", "name"]):
|
for proc in psutil.process_iter(["pid", "name"]):
|
||||||
if proc.info["name"] == "go2rtc":
|
if proc.info["name"] == "go2rtc":
|
||||||
@ -530,6 +534,7 @@ class FrigateApp:
|
|||||||
self.init_camera_metrics()
|
self.init_camera_metrics()
|
||||||
self.init_queues()
|
self.init_queues()
|
||||||
self.init_database()
|
self.init_database()
|
||||||
|
self.init_transcode_cache()
|
||||||
self.init_onvif()
|
self.init_onvif()
|
||||||
self.init_recording_manager()
|
self.init_recording_manager()
|
||||||
self.init_review_segment_manager()
|
self.init_review_segment_manager()
|
||||||
@ -561,6 +566,7 @@ class FrigateApp:
|
|||||||
create_fastapi_app(
|
create_fastapi_app(
|
||||||
self.config,
|
self.config,
|
||||||
self.db,
|
self.db,
|
||||||
|
self.transcode_cache,
|
||||||
self.embeddings,
|
self.embeddings,
|
||||||
self.detected_frames_processor,
|
self.detected_frames_processor,
|
||||||
self.storage_maintainer,
|
self.storage_maintainer,
|
||||||
@ -634,6 +640,7 @@ class FrigateApp:
|
|||||||
self.stats_emitter.join()
|
self.stats_emitter.join()
|
||||||
self.frigate_watchdog.join()
|
self.frigate_watchdog.join()
|
||||||
self.db.stop()
|
self.db.stop()
|
||||||
|
self.transcode_cache.stop()
|
||||||
|
|
||||||
# Save embeddings stats to disk
|
# Save embeddings stats to disk
|
||||||
if self.embeddings:
|
if self.embeddings:
|
||||||
|
|||||||
0
frigate/transcode/__init__.py
Normal file
0
frigate/transcode/__init__.py
Normal file
81
frigate/transcode/temp_file_cache.py
Normal file
81
frigate/transcode/temp_file_cache.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user