This commit is contained in:
Josh Hawkins
2026-09-30 11:22:15 -05:00
parent c4fdbdf541
commit 84d8c82a8c
6 changed files with 76 additions and 3 deletions
+13
View File
@@ -202,6 +202,19 @@ class TestLiveTranscodeConfig(unittest.TestCase):
self.assertEqual(config.cameras["front"].live.transcode.source, "front_sub") self.assertEqual(config.cameras["front"].live.transcode.source, "front_sub")
def test_startup_and_validation_agree_on_inherited_source(self):
config = camera_config({"transcode": {"enabled": True}})
config["live"] = {"streams": {"Sub": "front_sub"}}
self.assertEqual(
raw_transcode_streams(config),
generated_transcode_streams(FrigateConfig(**config)),
)
self.assertEqual(
raw_transcode_streams(config)["front_transcode_360p"],
transcode_stream_source("front_sub", 360, 250),
)
def test_placed_entries_keep_position_and_label(self): def test_placed_entries_keep_position_and_label(self):
streams = self.streams( streams = self.streams(
{ {
+9 -1
View File
@@ -81,6 +81,7 @@ def raw_transcode_streams(config: dict[str, Any]) -> dict[str, str]:
resolves defaults the same way the validator does. resolves defaults the same way the validator does.
""" """
streams: dict[str, str] = {} streams: dict[str, str] = {}
global_streams = (config.get("live") or {}).get("streams") or {}
for camera, camera_config in (config.get("cameras") or {}).items(): for camera, camera_config in (config.get("cameras") or {}).items():
live = (camera_config or {}).get("live") or {} live = (camera_config or {}).get("live") or {}
@@ -89,8 +90,15 @@ def raw_transcode_streams(config: dict[str, Any]) -> dict[str, str]:
if not transcode.get("enabled"): if not transcode.get("enabled"):
continue continue
# cameras inherit global live streams after their own, like deep_merge
camera_streams = live.get("streams") or {}
live_streams = camera_streams | {
label: name
for label, name in global_streams.items()
if label not in camera_streams
}
source = transcode.get("source") or default_transcode_source( source = transcode.get("source") or default_transcode_source(
camera, live.get("streams") or {camera: camera} camera, live_streams or {camera: camera}
) )
if source is None: if source is None:
@@ -150,6 +150,37 @@ test.describe("live stream order and transcoded streams @medium", () => {
).toEqual(["Main", "Sub"]); ).toEqual(["Main", "Sub"]);
}); });
test("save all keeps a reorder made on another page", async ({
frigateApp,
}) => {
const capture = await installRoutes(frigateApp.page);
await frigateApp.goto(SETTINGS_URL);
await expect
.poll(() => streamNames(frigateApp.page))
.toEqual(["Sub", "Main"]);
await dragRow(frigateApp.page, 1, 0);
await expect
.poll(() => streamNames(frigateApp.page))
.toEqual(["Main", "Sub"]);
await frigateApp.page.getByText("Snapshots", { exact: true }).click();
await frigateApp.page.getByRole("button", { name: "Save All" }).click();
await expect.poll(() => capture.saved()).not.toBeNull();
const saved = capture.saved() as {
replace_paths: string[];
config_data: {
cameras: { front_door: { live: { streams: Record<string, string> } } };
};
};
expect(saved.replace_paths).toEqual(["cameras.front_door.live.streams"]);
expect(
Object.keys(saved.config_data.cameras.front_door.live.streams),
).toEqual(["Main", "Sub"]);
});
test("enabling transcoding saves the shown source", async ({ test("enabling transcoding saves the shown source", async ({
frigateApp, frigateApp,
}) => { }) => {
@@ -484,10 +484,13 @@ export function LiveStreamsField(props: FieldProps) {
); );
// Auto order commits after measuring, so edits made meanwhile must survive // Auto order commits after measuring, so edits made meanwhile must survive
// and a camera switched to meanwhile must not receive the result
const dataRef = useRef(data); const dataRef = useRef(data);
const cameraRef = useRef(camera);
useEffect(() => { useEffect(() => {
dataRef.current = data; dataRef.current = data;
}, [data]); cameraRef.current = camera;
}, [data, camera]);
const [measurement, setMeasurement] = useState<{ const [measurement, setMeasurement] = useState<{
data: LiveStreamsData; data: LiveStreamsData;
@@ -527,6 +530,11 @@ export function LiveStreamsField(props: FieldProps) {
}), }),
); );
if (cameraRef.current !== camera) {
setMeasuring(false);
return;
}
const next = Object.fromEntries(measured); const next = Object.fromEntries(measured);
const latest = dataRef.current; const latest = dataRef.current;
setMeasurement({ data: latest, transcode, rates: next }); setMeasurement({ data: latest, transcode, rates: next });
+3
View File
@@ -875,6 +875,9 @@ export default function Settings() {
requires_restart: payload.needsRestart ? 1 : 0, requires_restart: payload.needsRestart ? 1 : 0,
update_topic: payload.updateTopic, update_topic: payload.updateTopic,
config_data: configData, config_data: configData,
...(payload.replacePaths?.length
? { replace_paths: payload.replacePaths }
: {}),
}); });
if (payload.needsRestart) { if (payload.needsRestart) {
+11 -1
View File
@@ -556,6 +556,7 @@ export interface SectionSavePayload {
updateTopic: string | undefined; updateTopic: string | undefined;
needsRestart: boolean; needsRestart: boolean;
pendingDataKey: string; pendingDataKey: string;
replacePaths?: string[];
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -765,7 +766,13 @@ export function prepareSectionSavePayload(opts: {
); );
// Build overrides // Build overrides
const overrides = buildOverrides(pendingData, rawData, effectiveDefaults); const orderedMaps = sectionConfig.orderedMaps ?? [];
const overrides = applyOrderedMaps(
buildOverrides(pendingData, rawData, effectiveDefaults),
pendingData,
rawData,
orderedMaps,
);
const sanitizedOverrides = sanitizeOverridesForSection( const sanitizedOverrides = sanitizeOverridesForSection(
schemaSection, schemaSection,
level, level,
@@ -815,6 +822,9 @@ export function prepareSectionSavePayload(opts: {
updateTopic, updateTopic,
needsRestart, needsRestart,
pendingDataKey, pendingDataKey,
replacePaths: changedOrderedMapPaths(pendingData, rawData, orderedMaps).map(
(path) => `${basePath}.${path}`,
),
}; };
} }