mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-21 11:51:53 +03:00
236 lines
8.2 KiB
TypeScript
236 lines
8.2 KiB
TypeScript
|
|
/**
|
||
|
|
* go2rtc streams settings page tests -- MEDIUM tier.
|
||
|
|
*
|
||
|
|
* Regression coverage for the compat-mode (ffmpeg:) URL editor: unknown
|
||
|
|
* fragments like #timeout=10 must remain visible and editable when the
|
||
|
|
* stream is using compatibility mode.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { test, expect } from "../../fixtures/frigate-test";
|
||
|
|
import type { Page } from "@playwright/test";
|
||
|
|
|
||
|
|
const STREAM_NAME = "dome_sub";
|
||
|
|
const FFMPEG_URL_WITH_TIMEOUT =
|
||
|
|
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#timeout=10";
|
||
|
|
|
||
|
|
async function installRawPathsRoute(page: Page, streamUrl: string) {
|
||
|
|
let lastSavedConfig: unknown = null;
|
||
|
|
await page.route("**/api/config/raw_paths", (route) =>
|
||
|
|
route.fulfill({
|
||
|
|
json: {
|
||
|
|
cameras: {},
|
||
|
|
go2rtc: { streams: { [STREAM_NAME]: [streamUrl] } },
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
await page.route("**/api/config/set", async (route) => {
|
||
|
|
lastSavedConfig = route.request().postDataJSON();
|
||
|
|
await route.fulfill({ json: { success: true, require_restart: false } });
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
capturedConfig: () => lastSavedConfig,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function expandStream(page: Page, streamName: string) {
|
||
|
|
// Each StreamCard renders the stream name as an h4 next to a rename
|
||
|
|
// button, with the chevron toggle as the last button in the header row.
|
||
|
|
// Scope to the header row (h4's grandparent) and click that last button.
|
||
|
|
const headerRow = page
|
||
|
|
.locator(`h4:text-is("${streamName}")`)
|
||
|
|
.locator("xpath=../..");
|
||
|
|
await headerRow.getByRole("button").last().click();
|
||
|
|
}
|
||
|
|
|
||
|
|
test.describe("go2rtc streams settings — ffmpeg compat mode @medium", () => {
|
||
|
|
test("preserves unknown fragments like #timeout= in the URL input", async ({
|
||
|
|
frigateApp,
|
||
|
|
}) => {
|
||
|
|
await installRawPathsRoute(frigateApp.page, FFMPEG_URL_WITH_TIMEOUT);
|
||
|
|
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
frigateApp.page.getByRole("heading", { name: STREAM_NAME }),
|
||
|
|
).toBeVisible();
|
||
|
|
|
||
|
|
await expandStream(frigateApp.page, STREAM_NAME);
|
||
|
|
|
||
|
|
const urlInput = frigateApp.page.getByPlaceholder(
|
||
|
|
"e.g., rtsp://user:pass@192.168.1.100/stream",
|
||
|
|
);
|
||
|
|
await expect(urlInput).toBeVisible();
|
||
|
|
|
||
|
|
// Focus the input so credential masking is bypassed and the raw value
|
||
|
|
// is rendered — this matches how a user would inspect the URL before
|
||
|
|
// editing it.
|
||
|
|
await urlInput.focus();
|
||
|
|
await expect(urlInput).toHaveValue(
|
||
|
|
"rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("lets the user add an extra fragment in compat mode", async ({
|
||
|
|
frigateApp,
|
||
|
|
}) => {
|
||
|
|
const capture = await installRawPathsRoute(
|
||
|
|
frigateApp.page,
|
||
|
|
FFMPEG_URL_WITH_TIMEOUT,
|
||
|
|
);
|
||
|
|
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
|
||
|
|
await expandStream(frigateApp.page, STREAM_NAME);
|
||
|
|
|
||
|
|
const urlInput = frigateApp.page.getByPlaceholder(
|
||
|
|
"e.g., rtsp://user:pass@192.168.1.100/stream",
|
||
|
|
);
|
||
|
|
await urlInput.focus();
|
||
|
|
await urlInput.fill(
|
||
|
|
"rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10#backchannel=0",
|
||
|
|
);
|
||
|
|
await urlInput.blur();
|
||
|
|
|
||
|
|
// Reopen and re-focus to assert the new value round-tripped through
|
||
|
|
// parseFfmpegBaseAndExtras + buildFfmpegUrl back into the displayed text.
|
||
|
|
await urlInput.focus();
|
||
|
|
await expect(urlInput).toHaveValue(
|
||
|
|
"rtsp://user:pass@192.168.0.20:554/Stream1#timeout=10#backchannel=0",
|
||
|
|
);
|
||
|
|
|
||
|
|
// Save and verify the persisted URL includes both extras after the
|
||
|
|
// recognized video/audio directives.
|
||
|
|
await frigateApp.page.getByRole("button", { name: "Save" }).click();
|
||
|
|
await expect
|
||
|
|
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
|
||
|
|
.toMatchObject({
|
||
|
|
config_data: {
|
||
|
|
go2rtc: {
|
||
|
|
streams: {
|
||
|
|
[STREAM_NAME]: [
|
||
|
|
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#timeout=10#backchannel=0",
|
||
|
|
],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("preserves repeatable #audio= fallback chain and lets the user add another codec", async ({
|
||
|
|
frigateApp,
|
||
|
|
}) => {
|
||
|
|
const capture = await installRawPathsRoute(
|
||
|
|
frigateApp.page,
|
||
|
|
// Idiomatic go2rtc fallback: copy if source has the codec, else transcode
|
||
|
|
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus",
|
||
|
|
);
|
||
|
|
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
|
||
|
|
await expandStream(frigateApp.page, STREAM_NAME);
|
||
|
|
|
||
|
|
// Two pre-populated audio rows — one per #audio= fragment.
|
||
|
|
const audioLabel = frigateApp.page.locator(`label:text-is("Audio")`);
|
||
|
|
const audioRowsContainer = audioLabel.locator("xpath=../..");
|
||
|
|
await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(2);
|
||
|
|
await expect(audioRowsContainer.getByRole("combobox").first()).toHaveText(
|
||
|
|
"Copy",
|
||
|
|
);
|
||
|
|
await expect(audioRowsContainer.getByRole("combobox").nth(1)).toHaveText(
|
||
|
|
"Transcode to Opus",
|
||
|
|
);
|
||
|
|
|
||
|
|
// Add a third audio codec via the LuPlus next to the "Audio" label.
|
||
|
|
await audioRowsContainer
|
||
|
|
.getByRole("button", { name: "Add audio codec" })
|
||
|
|
.click();
|
||
|
|
await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(3);
|
||
|
|
|
||
|
|
// Change the newly-added entry to AAC.
|
||
|
|
await audioRowsContainer.getByRole("combobox").nth(2).click();
|
||
|
|
await frigateApp.page
|
||
|
|
.getByRole("option", { name: "Transcode to AAC" })
|
||
|
|
.click();
|
||
|
|
|
||
|
|
await frigateApp.page.getByRole("button", { name: "Save" }).click();
|
||
|
|
await expect
|
||
|
|
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
|
||
|
|
.toMatchObject({
|
||
|
|
config_data: {
|
||
|
|
go2rtc: {
|
||
|
|
streams: {
|
||
|
|
[STREAM_NAME]: [
|
||
|
|
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus#audio=aac",
|
||
|
|
],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("LuX is only shown on fallback rows and removes only that codec", async ({
|
||
|
|
frigateApp,
|
||
|
|
}) => {
|
||
|
|
const capture = await installRawPathsRoute(
|
||
|
|
frigateApp.page,
|
||
|
|
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy#audio=opus",
|
||
|
|
);
|
||
|
|
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
|
||
|
|
await expandStream(frigateApp.page, STREAM_NAME);
|
||
|
|
|
||
|
|
const audioLabel = frigateApp.page.locator(`label:text-is("Audio")`);
|
||
|
|
const audioRowsContainer = audioLabel.locator("xpath=../..");
|
||
|
|
const removeButtons = audioRowsContainer.getByRole("button", {
|
||
|
|
name: "Remove codec",
|
||
|
|
});
|
||
|
|
// Primary (audio=copy) row is permanent and has no X; only the audio=opus
|
||
|
|
// fallback exposes a remove button.
|
||
|
|
await expect(removeButtons).toHaveCount(1);
|
||
|
|
|
||
|
|
await removeButtons.first().click();
|
||
|
|
await expect(audioRowsContainer.getByRole("combobox")).toHaveCount(1);
|
||
|
|
await expect(audioRowsContainer.getByRole("combobox")).toHaveText("Copy");
|
||
|
|
|
||
|
|
await frigateApp.page.getByRole("button", { name: "Save" }).click();
|
||
|
|
await expect
|
||
|
|
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
|
||
|
|
.toMatchObject({
|
||
|
|
config_data: {
|
||
|
|
go2rtc: {
|
||
|
|
streams: {
|
||
|
|
[STREAM_NAME]: [
|
||
|
|
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy",
|
||
|
|
],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("picking Exclude on the primary row drops the #video= fragment entirely", async ({
|
||
|
|
frigateApp,
|
||
|
|
}) => {
|
||
|
|
const capture = await installRawPathsRoute(
|
||
|
|
frigateApp.page,
|
||
|
|
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#video=copy#audio=copy",
|
||
|
|
);
|
||
|
|
await frigateApp.goto("/settings?page=systemGo2rtcStreams");
|
||
|
|
await expandStream(frigateApp.page, STREAM_NAME);
|
||
|
|
|
||
|
|
const videoLabel = frigateApp.page.locator(`label:text-is("Video")`);
|
||
|
|
const videoRowsContainer = videoLabel.locator("xpath=../..");
|
||
|
|
await videoRowsContainer.getByRole("combobox").first().click();
|
||
|
|
await frigateApp.page.getByRole("option", { name: "Exclude" }).click();
|
||
|
|
|
||
|
|
await frigateApp.page.getByRole("button", { name: "Save" }).click();
|
||
|
|
await expect
|
||
|
|
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
|
||
|
|
.toMatchObject({
|
||
|
|
config_data: {
|
||
|
|
go2rtc: {
|
||
|
|
streams: {
|
||
|
|
[STREAM_NAME]: [
|
||
|
|
"ffmpeg:rtsp://user:pass@192.168.0.20:554/Stream1#audio=copy",
|
||
|
|
],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|