Chapter tweaks (#23440)
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled

* Add camera metadata and fix preview chapters

* Add config option for chapters
This commit is contained in:
Nicolas Mowen 2026-06-09 09:07:42 -06:00 committed by GitHub
parent 28e3e1ec74
commit 06e3d0ac5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 28 additions and 12 deletions

View File

@ -31,7 +31,6 @@ from frigate.api.defs.tags import Tags
from frigate.const import CLIPS_DIR, EXPORT_DIR from frigate.const import CLIPS_DIR, EXPORT_DIR
from frigate.models import Export, Previews, Recordings from frigate.models import Export, Previews, Recordings
from frigate.record.export import ( from frigate.record.export import (
ChaptersEnum,
PlaybackFactorEnum, PlaybackFactorEnum,
PlaybackSourceEnum, PlaybackSourceEnum,
RecordingExporter, RecordingExporter,
@ -94,6 +93,14 @@ def export_recording(
friendly_name = body.name friendly_name = body.name
existing_image = sanitize_filepath(body.image_path) if body.image_path else None existing_image = sanitize_filepath(body.image_path) if body.image_path else None
# a chapters value in the request body overrides the camera's export config
camera_config = request.app.frigate_config.cameras[camera_name]
chapters = (
body.chapters
if body.chapters is not None
else camera_config.record.export.chapters
)
# Ensure that existing_image is a valid path # Ensure that existing_image is a valid path
if existing_image and not existing_image.startswith(CLIPS_DIR): if existing_image and not existing_image.startswith(CLIPS_DIR):
return JSONResponse( return JSONResponse(
@ -162,7 +169,7 @@ def export_recording(
if playback_source in PlaybackSourceEnum.__members__.values() if playback_source in PlaybackSourceEnum.__members__.values()
else PlaybackSourceEnum.recordings else PlaybackSourceEnum.recordings
), ),
chapters=ChaptersEnum(body.chapters) if body.chapters else None, chapters=chapters,
) )
exporter.start() exporter.start()
return JSONResponse( return JSONResponse(

View File

@ -9,6 +9,7 @@ from frigate.review.types import SeverityEnum
from ..base import FrigateBaseModel from ..base import FrigateBaseModel
__all__ = [ __all__ = [
"ChaptersEnum",
"RecordConfig", "RecordConfig",
"RecordExportConfig", "RecordExportConfig",
"RecordPreviewConfig", "RecordPreviewConfig",
@ -66,10 +67,19 @@ class RecordPreviewConfig(FrigateBaseModel):
) )
class ChaptersEnum(str, Enum):
none = "none"
recording_segments = "recording_segments"
class RecordExportConfig(FrigateBaseModel): class RecordExportConfig(FrigateBaseModel):
timelapse_args: str = Field( timelapse_args: str = Field(
default=DEFAULT_TIME_LAPSE_FFMPEG_ARGS, title="Timelapse Args" default=DEFAULT_TIME_LAPSE_FFMPEG_ARGS, title="Timelapse Args"
) )
chapters: ChaptersEnum = Field(
default=ChaptersEnum.none,
title="Chapter metadata to embed in exported recordings",
)
class RecordConfig(FrigateBaseModel): class RecordConfig(FrigateBaseModel):

View File

@ -16,6 +16,7 @@ import pytz
from peewee import DoesNotExist from peewee import DoesNotExist
from frigate.config import FfmpegConfig, FrigateConfig from frigate.config import FfmpegConfig, FrigateConfig
from frigate.config.camera.record import ChaptersEnum
from frigate.const import ( from frigate.const import (
CACHE_DIR, CACHE_DIR,
CLIPS_DIR, CLIPS_DIR,
@ -51,14 +52,6 @@ class PlaybackSourceEnum(str, Enum):
preview = "preview" preview = "preview"
class ChaptersEnum(str, Enum):
# One chapter per recording segment, titled with the segment's
# wallclock start time in strict ISO 8601 form. Lets viewers map
# output playback time back to wallclock without reading a timestamp
# overlay via OCR.
recording_segments = "recording_segments"
class RecordingExporter(threading.Thread): class RecordingExporter(threading.Thread):
"""Exports a specific set of recordings for a camera to storage as a single file.""" """Exports a specific set of recordings for a camera to storage as a single file."""
@ -358,6 +351,8 @@ class RecordingExporter(threading.Thread):
f"title={title}", f"title={title}",
"-metadata", "-metadata",
f"creation_time={creation_time}", f"creation_time={creation_time}",
"-metadata",
f"comment=Camera: {self.camera}",
] ]
) )
@ -435,7 +430,7 @@ class RecordingExporter(threading.Thread):
if self.playback_factor == PlaybackFactorEnum.realtime: if self.playback_factor == PlaybackFactorEnum.realtime:
ffmpeg_cmd = ( ffmpeg_cmd = (
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}" f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart"
).split(" ") ).split(" ")
elif self.playback_factor == PlaybackFactorEnum.timelapse_25x: elif self.playback_factor == PlaybackFactorEnum.timelapse_25x:
ffmpeg_cmd = ( ffmpeg_cmd = (
@ -443,7 +438,7 @@ class RecordingExporter(threading.Thread):
self.config.ffmpeg.ffmpeg_path, self.config.ffmpeg.ffmpeg_path,
self.config.ffmpeg.hwaccel_args, self.config.ffmpeg.hwaccel_args,
f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}", f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}",
f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart {video_path}", f"{self.config.cameras[self.camera].record.export.timelapse_args} -movflags +faststart",
EncodeTypeEnum.timelapse, EncodeTypeEnum.timelapse,
) )
).split(" ") ).split(" ")
@ -459,9 +454,13 @@ class RecordingExporter(threading.Thread):
f"title={title}", f"title={title}",
"-metadata", "-metadata",
f"creation_time={creation_time}", f"creation_time={creation_time}",
"-metadata",
f"comment=Camera: {self.camera}",
] ]
) )
ffmpeg_cmd.append(video_path)
return ffmpeg_cmd, playlist_lines return ffmpeg_cmd, playlist_lines
def run(self) -> None: def run(self) -> None: