From 96360a08d580fc9b0ecc03169dee1ad5ba2c12f8 Mon Sep 17 00:00:00 2001 From: Faberto Date: Sun, 22 Mar 2026 17:24:30 +0100 Subject: [PATCH 1/3] Add on-demand H.265 to H.264 transcoded HLS playback When a browser lacks native HEVC/H.265 support, the frontend now requests a server-side transcoded HLS stream instead of the original VOD path. FFmpeg consumes Frigate's existing HLS endpoint and outputs H.264 MPEG-TS segments to /tmp/stream/transcode/, which nginx already serves via the /stream/ location. - New API endpoint: GET /api/transcode/{camera}/start/{ts}/end/{ts} - Spawns FFmpeg to transcode existing H.265 HLS to H.264 HLS - Deduplicates sessions by camera + time range - Limits concurrent transcodes (default 3) - Waits for first segment before returning playlist URL - Cleanup: stale transcode sessions removed every 60s (10min TTL) - Frontend: isHevcSupported() detects browser codec support via MSE and native video element, transparently falls back to transcoded playback in DynamicVideoPlayer and TrackingDetails - Zero changes to existing HLS/VOD path or clip download endpoint Co-Authored-By: Claude Opus 4.6 (1M context) --- frigate/api/fastapi_app.py | 2 + frigate/api/transcode.py | 279 ++++++++++++++++++ frigate/record/cleanup.py | 2 + .../overlay/detail/TrackingDetails.tsx | 21 +- .../player/dynamic/DynamicVideoPlayer.tsx | 14 +- web/src/hooks/use-transcoded-playback.ts | 65 ++++ web/src/utils/codecUtil.ts | 39 +++ 7 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 frigate/api/transcode.py create mode 100644 web/src/hooks/use-transcoded-playback.ts create mode 100644 web/src/utils/codecUtil.ts diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index f201ab713..4daf991c8 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -27,6 +27,7 @@ from frigate.api import ( preview, record, review, + transcode, ) from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default from frigate.comms.dispatcher import Dispatcher @@ -142,6 +143,7 @@ def create_fastapi_app( app.include_router(media.router) app.include_router(motion_search.router) app.include_router(record.router) + app.include_router(transcode.router) app.include_router(debug_replay.router) # App Properties app.frigate_config = frigate_config diff --git a/frigate/api/transcode.py b/frigate/api/transcode.py new file mode 100644 index 000000000..71c246f4d --- /dev/null +++ b/frigate/api/transcode.py @@ -0,0 +1,279 @@ +"""On-demand H.265 to H.264 transcoded HLS endpoint.""" + +import asyncio +import logging +import os +import subprocess as sp +import time +from hashlib import md5 +from pathlib import Path + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import JSONResponse + +from frigate.api.auth import require_camera_access, require_role +from frigate.api.defs.tags import Tags +from frigate.config import FrigateConfig +from frigate.const import PROCESS_PRIORITY_LOW + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=[Tags.media]) + +TRANSCODE_DIR = "/tmp/stream/transcode" +MAX_CONCURRENT_TRANSCODES = 3 +TRANSCODE_TTL_SECONDS = 600 +SEGMENT_WAIT_TIMEOUT = 30 + +# Active transcode sessions: session_id -> subprocess.Popen +_active_sessions: dict[str, sp.Popen] = {} + + +def _lower_priority(): + os.nice(PROCESS_PRIORITY_LOW) + + +def _session_id(camera: str, start_ts: float, end_ts: float) -> str: + """Deterministic session ID for deduplication.""" + raw = f"{camera}_{start_ts}_{end_ts}" + return md5(raw.encode()).hexdigest()[:12] + + +def _session_dir(session_id: str) -> str: + return os.path.join(TRANSCODE_DIR, session_id) + + +def _session_playlist(session_id: str) -> str: + return os.path.join(_session_dir(session_id), "master.m3u8") + + +def _is_session_alive(session_id: str) -> bool: + """Check if a transcode session FFmpeg process is still running.""" + proc = _active_sessions.get(session_id) + if proc is None: + return False + return proc.poll() is None + + +def _get_internal_port(config: FrigateConfig) -> int: + """Get the internal API port, handling string ip:port format.""" + internal_port = config.networking.listen.internal + if isinstance(internal_port, str): + internal_port = int(internal_port.split(":")[-1]) + return int(internal_port) + + +def _build_ffmpeg_cmd( + config: FrigateConfig, + camera_name: str, + start_ts: float, + end_ts: float, + session_id: str, +) -> list[str]: + """Build FFmpeg command that consumes existing HLS and outputs transcoded HLS.""" + internal_port = _get_internal_port(config) + input_url = ( + f"http://127.0.0.1:{internal_port}" + f"/vod/{camera_name}/start/{start_ts}/end/{end_ts}/index.m3u8" + ) + + output_dir = _session_dir(session_id) + os.makedirs(output_dir, exist_ok=True) + + output_playlist = os.path.join(output_dir, "master.m3u8") + segment_pattern = os.path.join(output_dir, "segment-%d.ts") + + return [ + config.ffmpeg.ffmpeg_path, + "-hide_banner", + "-loglevel", + "warning", + "-y", + "-protocol_whitelist", + "pipe,file,http,tcp", + "-i", + input_url, + # Encode to H.264 (software — universally available) + "-c:v", + "libx264", + "-preset", + "fast", + "-crf", + "23", + "-profile:v", + "high", + "-level:v", + "4.1", + "-pix_fmt", + "yuv420p", + "-c:a", + "aac", + "-b:a", + "128k", + # Output as HLS with MPEG-TS segments + "-f", + "hls", + "-hls_time", + "6", + "-hls_list_size", + "0", + "-hls_segment_type", + "mpegts", + "-hls_flags", + "independent_segments+append_list", + "-hls_segment_filename", + segment_pattern, + output_playlist, + ] + + +async def _wait_for_playlist(session_id: str) -> bool: + """Wait until FFmpeg has written the playlist and at least one segment.""" + playlist = _session_playlist(session_id) + deadline = time.monotonic() + SEGMENT_WAIT_TIMEOUT + + while time.monotonic() < deadline: + if os.path.exists(playlist) and os.path.getsize(playlist) > 0: + session_dir = _session_dir(session_id) + segments = list(Path(session_dir).glob("segment-*.ts")) + if segments: + return True + + if not _is_session_alive(session_id): + proc = _active_sessions.get(session_id) + if proc: + stderr = proc.stderr.read() if proc.stderr else b"" + logger.error( + "Transcode FFmpeg exited with code %d: %s", + proc.returncode, + stderr.decode(errors="replace"), + ) + return False + + await asyncio.sleep(0.5) + + return False + + +def cleanup_session(session_id: str) -> None: + """Stop FFmpeg and remove temp files for a session.""" + proc = _active_sessions.pop(session_id, None) + if proc and proc.poll() is None: + proc.terminate() + try: + proc.communicate(timeout=10) + except sp.TimeoutExpired: + proc.kill() + proc.communicate() + + session_dir = _session_dir(session_id) + if os.path.exists(session_dir): + for f in Path(session_dir).iterdir(): + f.unlink(missing_ok=True) + try: + Path(session_dir).rmdir() + except OSError: + pass + + +def cleanup_stale_transcode_sessions() -> None: + """Remove transcode sessions older than TTL. Called from cleanup loop.""" + if not os.path.exists(TRANSCODE_DIR): + return + + now = time.time() + for session_dir in Path(TRANSCODE_DIR).iterdir(): + if not session_dir.is_dir(): + continue + + age = now - session_dir.stat().st_mtime + if age > TRANSCODE_TTL_SECONDS: + session_id = session_dir.name + logger.debug("Cleaning up stale transcode session %s", session_id) + cleanup_session(session_id) + + +@router.get( + "/transcode/{camera_name}/start/{start_ts}/end/{end_ts}", + dependencies=[Depends(require_camera_access)], + description="Start a transcoded HLS session that converts H.265 recordings to H.264 for browser compatibility.", +) +async def transcoded_vod( + request: Request, + camera_name: str, + start_ts: float, + end_ts: float, +): + """Start or reuse a transcoded HLS session for the given time range.""" + config: FrigateConfig = request.app.frigate_config + session_id = _session_id(camera_name, start_ts, end_ts) + + # Reuse existing session if still alive + if _is_session_alive(session_id): + playlist = _session_playlist(session_id) + if os.path.exists(playlist): + return JSONResponse( + content={ + "success": True, + "playlist": f"/stream/transcode/{session_id}/master.m3u8", + } + ) + + # Check concurrent transcode limit + active_count = sum(1 for p in _active_sessions.values() if p.poll() is None) + if active_count >= MAX_CONCURRENT_TRANSCODES: + return JSONResponse( + content={ + "success": False, + "message": "Too many concurrent transcode sessions", + }, + status_code=429, + ) + + ffmpeg_cmd = _build_ffmpeg_cmd(config, camera_name, start_ts, end_ts, session_id) + + logger.info( + "Starting transcode session %s for %s (%s -> %s)", + session_id, + camera_name, + start_ts, + end_ts, + ) + + proc = sp.Popen( + ffmpeg_cmd, + stdout=sp.DEVNULL, + stderr=sp.PIPE, + stdin=sp.DEVNULL, + start_new_session=True, + preexec_fn=_lower_priority, + ) + _active_sessions[session_id] = proc + + ready = await _wait_for_playlist(session_id) + if not ready: + cleanup_session(session_id) + return JSONResponse( + content={ + "success": False, + "message": "Transcode timed out waiting for first segment", + }, + status_code=504, + ) + + return JSONResponse( + content={ + "success": True, + "playlist": f"/stream/transcode/{session_id}/master.m3u8", + } + ) + + +@router.delete( + "/transcode/{session_id}", + dependencies=[Depends(require_role(["admin"]))], +) +async def stop_transcode(session_id: str): + """Stop and clean up a transcode session.""" + cleanup_session(session_id) + return JSONResponse(content={"success": True}) diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py index 15a0ba7e8..303506be0 100644 --- a/frigate/record/cleanup.py +++ b/frigate/record/cleanup.py @@ -10,6 +10,7 @@ from pathlib import Path from playhouse.sqlite_ext import SqliteExtDatabase +from frigate.api.transcode import cleanup_stale_transcode_sessions from frigate.config import CameraConfig, FrigateConfig, RetainModeEnum from frigate.const import CACHE_DIR, CLIPS_DIR, MAX_WAL_SIZE, RECORD_DIR from frigate.models import Previews, Recordings, ReviewSegment, UserReviewStatus @@ -375,6 +376,7 @@ class RecordingCleanup(threading.Thread): break self.clean_tmp_previews() + cleanup_stale_transcode_sessions() if counter == 0: self.clean_tmp_clips() diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index 351370ad8..10adf3bac 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -41,6 +41,7 @@ import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator import ObjectTrackOverlay from "../ObjectTrackOverlay"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { VideoResolutionType } from "@/types/live"; +import { useTranscodedPlayback } from "@/hooks/use-transcoded-playback"; type TrackingDetailsProps = { className?: string; @@ -513,7 +514,14 @@ export function TrackingDetails({ setBlueLineHeightPx(bluePx); }, [eventSequence, timelineSize.width, timelineSize.height, effectiveTime]); - const videoSource = useMemo(() => { + const { resolvePlaylistUrl } = useTranscodedPlayback(baseUrl); + + const [videoSource, setVideoSource] = useState({ + playlist: "", + startPosition: 0, + }); + + useEffect(() => { // event.start_time and event.end_time are in DETECT stream time // Convert to record stream time, then create video clip with padding. // Use sourceOffsetRef (stable per event) so the HLS player doesn't @@ -524,12 +532,13 @@ export function TrackingDetails({ (event.end_time ?? Date.now() / 1000) + sourceOffset / 1000; const startTime = eventStartRec - REVIEW_PADDING; const endTime = eventEndRec + REVIEW_PADDING; - const playlist = `${baseUrl}vod/clip/${event.camera}/start/${startTime}/end/${endTime}/index.m3u8`; + const vodUrl = `${baseUrl}vod/clip/${event.camera}/start/${startTime}/end/${endTime}/index.m3u8`; - return { - playlist, - startPosition: 0, - }; + resolvePlaylistUrl(vodUrl, event.camera, startTime, endTime).then( + (playlist: string) => { + setVideoSource({ playlist, startPosition: 0 }); + }, + ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [event]); diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index c8d95090d..ae5e6514b 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -26,6 +26,7 @@ import { calculateSeekPosition, } from "@/utils/videoUtil"; import { isFirefox } from "react-device-detect"; +import { useTranscodedPlayback } from "@/hooks/use-transcoded-playback"; /** * Dynamically switches between video playback and scrubbing preview player. @@ -71,6 +72,7 @@ export default function DynamicVideoPlayer({ const { t } = useTranslation(["components/player"]); const apiHost = useApiHost(); const { data: config } = useSWR("config"); + const { resolvePlaylistUrl } = useTranscodedPlayback(apiHost); // for detail stream context in History const { @@ -219,9 +221,15 @@ export default function DynamicVideoPlayer({ ); } - setSource({ - playlist: `${apiHost}vod/${camera}/start/${recordingParams.after}/end/${recordingParams.before}/master.m3u8`, - startPosition, + const vodUrl = `${apiHost}vod/${camera}/start/${recordingParams.after}/end/${recordingParams.before}/master.m3u8`; + + resolvePlaylistUrl( + vodUrl, + camera, + recordingParams.after, + recordingParams.before, + ).then((playlist: string) => { + setSource({ playlist, startPosition }); }); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/src/hooks/use-transcoded-playback.ts b/web/src/hooks/use-transcoded-playback.ts new file mode 100644 index 000000000..82aafafed --- /dev/null +++ b/web/src/hooks/use-transcoded-playback.ts @@ -0,0 +1,65 @@ +import { useCallback, useEffect, useRef } from "react"; +import { isHevcSupported } from "@/utils/codecUtil"; +import axios from "axios"; + +interface TranscodeResponse { + success: boolean; + playlist?: string; + message?: string; +} + +/** + * Resolves an HLS playlist URL, requesting server-side H.265->H.264 + * transcoding when the browser does not support HEVC natively. + * + * When HEVC is supported, returns the original VOD URL unchanged. + */ +export function useTranscodedPlayback(apiHost: string) { + const activeSessionRef = useRef(null); + + useEffect(() => { + return () => { + if (activeSessionRef.current) { + axios + .delete(`${apiHost}api/transcode/${activeSessionRef.current}`) + .catch(() => {}); + } + }; + }, [apiHost]); + + const resolvePlaylistUrl = useCallback( + async (vodUrl: string, camera: string, startTs: number, endTs: number) => { + if (isHevcSupported()) { + return vodUrl; + } + + // Clean up previous session + if (activeSessionRef.current) { + axios + .delete(`${apiHost}api/transcode/${activeSessionRef.current}`) + .catch(() => {}); + activeSessionRef.current = null; + } + + try { + const resp = await axios.get( + `${apiHost}api/transcode/${camera}/start/${startTs}/end/${endTs}`, + ); + + if (resp.data.success && resp.data.playlist) { + const sessionId = resp.data.playlist.split("/").at(-2) ?? null; + activeSessionRef.current = sessionId; + return `${apiHost}${resp.data.playlist.replace(/^\//, "")}`; + } + } catch (err) { + console.warn("Transcode request failed, falling back to native HLS", err); + } + + // Fallback to original URL + return vodUrl; + }, + [apiHost], + ); + + return { resolvePlaylistUrl }; +} diff --git a/web/src/utils/codecUtil.ts b/web/src/utils/codecUtil.ts new file mode 100644 index 000000000..729da8592 --- /dev/null +++ b/web/src/utils/codecUtil.ts @@ -0,0 +1,39 @@ +/** + * Browser HEVC/H.265 codec support detection. + */ + +let hevcResult: boolean | null = null; + +/** + * Detect whether the browser supports HEVC/H.265 playback + * via Media Source Extensions or native video element. + * Result is cached after the first call. + */ +export function isHevcSupported(): boolean { + if (hevcResult !== null) return hevcResult; + + const codecs = [ + 'video/mp4; codecs="hvc1.1.6.L153.B0"', + 'video/mp4; codecs="hev1.1.6.L153.B0"', + ]; + + if (typeof MediaSource !== "undefined") { + for (const codec of codecs) { + if (MediaSource.isTypeSupported(codec)) { + hevcResult = true; + return true; + } + } + } + + const video = document.createElement("video"); + for (const codec of codecs) { + if (video.canPlayType(codec) === "probably") { + hevcResult = true; + return true; + } + } + + hevcResult = false; + return false; +} From eeb17ae2b84c53987a4535dea03663e9f8113047 Mon Sep 17 00:00:00 2001 From: Faberto Date: Sun, 22 Mar 2026 19:05:03 +0100 Subject: [PATCH 2/3] faster transcode and debug --- frigate/api/transcode.py | 7 +++++-- web/src/utils/codecUtil.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frigate/api/transcode.py b/frigate/api/transcode.py index 71c246f4d..a469a9c5e 100644 --- a/frigate/api/transcode.py +++ b/frigate/api/transcode.py @@ -93,13 +93,16 @@ def _build_ffmpeg_cmd( "pipe,file,http,tcp", "-i", input_url, + # Scale down to 720p max, preserve aspect ratio + "-vf", + "scale=-2:'min(720,ih)'", # Encode to H.264 (software — universally available) "-c:v", "libx264", "-preset", - "fast", + "ultrafast", "-crf", - "23", + "26", "-profile:v", "high", "-level:v", diff --git a/web/src/utils/codecUtil.ts b/web/src/utils/codecUtil.ts index 729da8592..43299f772 100644 --- a/web/src/utils/codecUtil.ts +++ b/web/src/utils/codecUtil.ts @@ -10,6 +10,7 @@ let hevcResult: boolean | null = null; * Result is cached after the first call. */ export function isHevcSupported(): boolean { + if (localStorage.getItem("forceTranscode") === "true") return false; if (hevcResult !== null) return hevcResult; const codecs = [ From f67297ed584a3cc72ee79ffdd96e440eccd60e8c Mon Sep 17 00:00:00 2001 From: Faberto Date: Sun, 22 Mar 2026 19:32:05 +0100 Subject: [PATCH 3/3] use hardware encode presets --- frigate/api/transcode.py | 70 +++++++++++++++------------------------ frigate/ffmpeg_presets.py | 34 +++++++++++++++++++ 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/frigate/api/transcode.py b/frigate/api/transcode.py index a469a9c5e..0cf972445 100644 --- a/frigate/api/transcode.py +++ b/frigate/api/transcode.py @@ -15,6 +15,10 @@ from frigate.api.auth import require_camera_access, require_role from frigate.api.defs.tags import Tags from frigate.config import FrigateConfig from frigate.const import PROCESS_PRIORITY_LOW +from frigate.ffmpeg_presets import ( + EncodeTypeEnum, + parse_preset_hardware_acceleration_encode, +) logger = logging.getLogger(__name__) @@ -83,51 +87,29 @@ def _build_ffmpeg_cmd( output_playlist = os.path.join(output_dir, "master.m3u8") segment_pattern = os.path.join(output_dir, "segment-%d.ts") - return [ + hwaccel_args = config.ffmpeg.hwaccel_args + + input_args = ( + f"-loglevel warning -y -protocol_whitelist pipe,file,http,tcp -i {input_url}" + ) + output_args = ( + f"-c:a aac -b:a 128k" + f" -f hls -hls_time 6 -hls_list_size 0" + f" -hls_segment_type mpegts" + f" -hls_flags independent_segments+append_list" + f" -hls_segment_filename {segment_pattern}" + f" {output_playlist}" + ) + + cmd_str = parse_preset_hardware_acceleration_encode( config.ffmpeg.ffmpeg_path, - "-hide_banner", - "-loglevel", - "warning", - "-y", - "-protocol_whitelist", - "pipe,file,http,tcp", - "-i", - input_url, - # Scale down to 720p max, preserve aspect ratio - "-vf", - "scale=-2:'min(720,ih)'", - # Encode to H.264 (software — universally available) - "-c:v", - "libx264", - "-preset", - "ultrafast", - "-crf", - "26", - "-profile:v", - "high", - "-level:v", - "4.1", - "-pix_fmt", - "yuv420p", - "-c:a", - "aac", - "-b:a", - "128k", - # Output as HLS with MPEG-TS segments - "-f", - "hls", - "-hls_time", - "6", - "-hls_list_size", - "0", - "-hls_segment_type", - "mpegts", - "-hls_flags", - "independent_segments+append_list", - "-hls_segment_filename", - segment_pattern, - output_playlist, - ] + hwaccel_args, + input_args, + output_args, + EncodeTypeEnum.transcode, + ) + + return cmd_str.split(" ") async def _wait_for_playlist(session_id: str) -> bool: diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 0652ec645..a6f86a49f 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -208,6 +208,37 @@ PRESETS_HW_ACCEL_ENCODE_PREVIEW = { "default": "{0} -hide_banner {1} -c:v libx264 -profile:v baseline -preset:v ultrafast {2}", } +# Presets for on-demand H.265 to H.264 transcode playback +PRESETS_HW_ACCEL_ENCODE_TRANSCODE = { + # Based on birdseye presets with 720p scaling added + "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m -vf scale=-2:'min(720,ih)' {2}", + "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v h264_v4l2m2m -vf scale=-2:'min(720,ih)' {2}", + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -bf 0 -profile:v high -level:v 4.1 -vf format=vaapi|nv12,hwupload,scale_vaapi=w=-2:h=720 {2}", + "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 -vf scale=-2:'min(720,ih)' {2}", + "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 -vf scale=-2:'min(720,ih)' {2}", + FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {1} -c:v h264_nvenc -profile:v high -preset:v p2 -tune:v ll -vf scale_cuda=w=-2:h=720 {2}", + "preset-jetson-h264": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high -vf scale=-2:'min(720,ih)' {2}", + "preset-jetson-h265": "{0} -hide_banner {1} -c:v h264_nvmpi -profile high -vf scale=-2:'min(720,ih)' {2}", + FFMPEG_HWACCEL_RKMPP: "{0} -hide_banner {1} -c:v h264_rkmpp -profile:v high -vf scale=-2:'min(720,ih)' {2}", + FFMPEG_HWACCEL_AMF: "{0} -hide_banner {1} -c:v h264_amf -profile:v high -vf scale=-2:'min(720,ih)' {2}", + "default": "{0} -hide_banner {1} -c:v libx264 -preset:v ultrafast -crf 26 -profile:v high -level:v 4.1 -pix_fmt yuv420p -vf scale=-2:'min(720,ih)' {2}", +} +PRESETS_HW_ACCEL_ENCODE_TRANSCODE["preset-nvidia-h264"] = ( + PRESETS_HW_ACCEL_ENCODE_TRANSCODE[FFMPEG_HWACCEL_NVIDIA] +) +PRESETS_HW_ACCEL_ENCODE_TRANSCODE["preset-nvidia-h265"] = ( + PRESETS_HW_ACCEL_ENCODE_TRANSCODE[FFMPEG_HWACCEL_NVIDIA] +) +PRESETS_HW_ACCEL_ENCODE_TRANSCODE[f"{FFMPEG_HWACCEL_RKMPP}-no-dump_extra"] = ( + PRESETS_HW_ACCEL_ENCODE_TRANSCODE[FFMPEG_HWACCEL_RKMPP] +) +PRESETS_HW_ACCEL_ENCODE_TRANSCODE["preset-rk-h264"] = PRESETS_HW_ACCEL_ENCODE_TRANSCODE[ + FFMPEG_HWACCEL_RKMPP +] +PRESETS_HW_ACCEL_ENCODE_TRANSCODE["preset-rk-h265"] = PRESETS_HW_ACCEL_ENCODE_TRANSCODE[ + FFMPEG_HWACCEL_RKMPP +] + def parse_preset_hardware_acceleration_decode( arg: Any, @@ -251,6 +282,7 @@ class EncodeTypeEnum(str, Enum): birdseye = "birdseye" preview = "preview" timelapse = "timelapse" + transcode = "transcode" def parse_preset_hardware_acceleration_encode( @@ -267,6 +299,8 @@ def parse_preset_hardware_acceleration_encode( arg_map = PRESETS_HW_ACCEL_ENCODE_PREVIEW elif type == EncodeTypeEnum.timelapse: arg_map = PRESETS_HW_ACCEL_ENCODE_TIMELAPSE + elif type == EncodeTypeEnum.transcode: + arg_map = PRESETS_HW_ACCEL_ENCODE_TRANSCODE if not isinstance(arg, str): return arg_map["default"].format(ffmpeg_path, input, output)