diff --git a/README.md b/README.md
index ec39d229..1f499bf2 100644
--- a/README.md
+++ b/README.md
@@ -177,8 +177,8 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
|Engine / Live Provider|`ha`|`image`|`jsmpeg`|`go2rtc`|`webrtc-card`|
| - | - | - | - | - | - |
|`frigate`| :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
-|`generic`| :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: |
-|`motioneye`| :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: |
+|`generic`| :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :white_check_mark: |
+|`motioneye`| :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: |
@@ -250,7 +250,7 @@ cameras:
| - | - | - | - |
| `modes` | `[webrtc, mse, mp4, mjpeg]` | :white_check_mark: | An ordered array of `go2rtc` modes to use. Valid values are `webrtc`, `mse`, `mp4` or `mjpeg` values. |
| `stream` | Determined by camera engine (e.g. `frigate` camera name). | :white_check_mark: | A valid `go2rtc` stream name. |
-| `url` | Determined by camera engine (e.g. the `frigate` engine will automatically generate a URL for the go2rtc backend that runs in the Frigate container). | :white_check_mark: | The root `go2rtc` backend URL the card should stream the video from. This will only need to be set in advanced cases -- for the vast majority of people the default will work. Example: `http://my-custom-go2rtc:1984` |
+| `url` | Determined by camera engine (e.g. the `frigate` engine will automatically generate a URL for the go2rtc backend that runs in the Frigate container). | :white_check_mark: | The root `go2rtc` URL the card should stream the video from. This is only needed for advanced usecases. Example: `http://my-custom-go2rtc:1984` |
#### Live Provider: Camera WebRTC Card configuration
diff --git a/src/camera-manager/engine-factory.ts b/src/camera-manager/engine-factory.ts
index 87b0e471..99dca886 100644
--- a/src/camera-manager/engine-factory.ts
+++ b/src/camera-manager/engine-factory.ts
@@ -10,7 +10,7 @@ import { MemoryRequestCache, RecordingSegmentsCache, RequestCache } from './cach
import { CameraManagerEngine } from './engine';
import { CameraInitializationError } from './error';
import { Engine } from './types';
-import { getCameraEntityFromConfig } from './util';
+import { getCameraEntityFromConfig } from './utils';
export class CameraManagerEngineFactory {
protected _entityRegistryManager: EntityRegistryManager;
diff --git a/src/camera-manager/frigate/engine-frigate.ts b/src/camera-manager/frigate/engine-frigate.ts
index 79f9625a..adea7998 100644
--- a/src/camera-manager/frigate/engine-frigate.ts
+++ b/src/camera-manager/frigate/engine-frigate.ts
@@ -62,7 +62,7 @@ import {
RecordingSegmentsQuery,
RecordingSegmentsQueryResultsMap,
} from '../types';
-import { getCameraEntityFromConfig } from '../util';
+import { getCameraEntityFromConfig, getDefaultGo2RTCEndpoint } from '../utils.js';
import frigateLogo from './assets/frigate-logo-dark.svg';
import { FrigateViewMediaFactory } from './media';
import { FrigateViewMediaClassifier } from './media-classifier';
@@ -1147,26 +1147,6 @@ export class FrigateCameraManagerEngine
};
};
- const getGo2RTC = (): CameraEndpoint | null => {
- const stream = cameraConfig.go2rtc?.stream ?? cameraConfig.frigate.camera_name;
- const url = cameraConfig.go2rtc?.url
- ? cameraConfig.go2rtc.url.replace(/\/+$/, '')
- : null;
- const endpoint = url
- ? `${url}/api/ws?src=${stream}`
- : // go2rtc is exposed by the integration under the (slightly
- // misleading) 'mse' path, even though that path can serve all go2rtc
- // modes.
- `/api/frigate/${cameraConfig.frigate.client_id}/mse/api/ws?src=${stream}`;
-
- return {
- endpoint: endpoint,
-
- // Only sign the endpoint if it's local to HA.
- sign: endpoint.startsWith('/'),
- };
- };
-
const getJSMPEG = (): CameraEndpoint | null => {
return {
endpoint:
@@ -1189,11 +1169,20 @@ export class FrigateCameraManagerEngine
};
const ui = getUIEndpoint();
- const go2rtc = getGo2RTC();
+ const go2rtc = getDefaultGo2RTCEndpoint(cameraConfig, {
+ url:
+ cameraConfig.go2rtc?.url ??
+ // go2rtc is exposed by the Frigate integration under the (slightly
+ // misleading) 'mse' path, even though that path can serve all go2rtc
+ // modes.
+ `/api/frigate/${cameraConfig.frigate.client_id}/mse`,
+ stream: cameraConfig.go2rtc?.stream ?? cameraConfig.frigate.camera_name,
+ });
const jsmpeg = getJSMPEG();
const webrtcCard = getWebRTCCard();
return {
+ ...super.getCameraEndpoints(cameraConfig, context),
...(ui && { ui: ui }),
...(go2rtc && { go2rtc: go2rtc }),
...(jsmpeg && { jsmpeg: jsmpeg }),
diff --git a/src/camera-manager/generic/engine-generic.ts b/src/camera-manager/generic/engine-generic.ts
index dff7966c..c7d38e78 100644
--- a/src/camera-manager/generic/engine-generic.ts
+++ b/src/camera-manager/generic/engine-generic.ts
@@ -31,6 +31,7 @@ import {
RecordingSegmentsQuery,
RecordingSegmentsQueryResultsMap,
} from '../types';
+import { getDefaultGo2RTCEndpoint } from '../utils.js';
export class GenericCameraManagerEngine implements CameraManagerEngine {
public getEngineType(): Engine {
@@ -165,10 +166,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
getEntityTitle(hass, cameraConfig.webrtc_card?.entity) ??
cameraConfig.id ??
'',
- icon:
- cameraConfig?.icon ??
- getEntityIcon(hass, cameraConfig.camera_entity) ??
- 'mdi:video',
+ icon: cameraConfig?.icon ?? getEntityIcon(hass, cameraConfig.camera_entity),
};
}
@@ -191,9 +189,14 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
}
public getCameraEndpoints(
- _cameraConfig: CameraConfig,
+ cameraConfig: CameraConfig,
_context?: CameraEndpointsContext,
): CameraEndpoints | null {
- return null;
+ const go2rtc = getDefaultGo2RTCEndpoint(cameraConfig);
+ return go2rtc
+ ? {
+ go2rtc: go2rtc,
+ }
+ : null;
}
}
diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts
index 3a6f3719..86df2f06 100644
--- a/src/camera-manager/manager.ts
+++ b/src/camera-manager/manager.ts
@@ -3,12 +3,12 @@ import add from 'date-fns/add';
import cloneDeep from 'lodash-es/cloneDeep';
import merge from 'lodash-es/merge.js';
import sum from 'lodash-es/sum';
+import { CardCameraAPI } from '../card-controller/types.js';
import { CameraConfig, CamerasConfig } from '../config/types.js';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
import { localize } from '../localize/localize.js';
import { allPromises, arrayify, setify } from '../utils/basic.js';
import { getCameraID } from '../utils/camera.js';
-import { CardCameraAPI } from '../card-controller/types.js';
import { log } from '../utils/debug.js';
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
import { ViewMedia } from '../view/media.js';
@@ -51,7 +51,7 @@ import {
RecordingSegmentsQueryResultsMap,
ResultsMap,
} from './types.js';
-import { sortMedia } from './util.js';
+import { sortMedia } from './utils.js';
class QueryClassifier {
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
diff --git a/src/camera-manager/motioneye/engine-motioneye.ts b/src/camera-manager/motioneye/engine-motioneye.ts
index fbb2e5f8..a220a948 100644
--- a/src/camera-manager/motioneye/engine-motioneye.ts
+++ b/src/camera-manager/motioneye/engine-motioneye.ts
@@ -384,8 +384,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
public getCameraEndpoints(
cameraConfig: CameraConfig,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _context?: CameraEndpointsContext,
+ context?: CameraEndpointsContext,
): CameraEndpoints | null {
const getUIEndpoint = (): CameraEndpoint | null => {
return cameraConfig.motioneye?.url
@@ -394,9 +393,9 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
}
: null;
};
-
const ui = getUIEndpoint();
return {
+ ...super.getCameraEndpoints(cameraConfig, context),
...(ui && { ui: ui }),
};
}
diff --git a/src/camera-manager/util.ts b/src/camera-manager/utils.ts
similarity index 76%
rename from src/camera-manager/util.ts
rename to src/camera-manager/utils.ts
index d8a5effc..dfa33d8b 100644
--- a/src/camera-manager/util.ts
+++ b/src/camera-manager/utils.ts
@@ -8,6 +8,7 @@ import uniqBy from 'lodash-es/uniqBy';
import { CameraConfig } from '../config/types';
import { ViewMedia } from '../view/media';
import { DateRange } from './range';
+import { CameraEndpoint } from './types';
export const convertRangeToCacheFriendlyTimes = (
range: DateRange,
@@ -58,3 +59,26 @@ export const sortMedia = (mediaArray: ViewMedia[]): ViewMedia[] => {
export const getCameraEntityFromConfig = (cameraConfig: CameraConfig): string | null => {
return cameraConfig.camera_entity ?? cameraConfig.webrtc_card?.entity ?? null;
};
+
+export const getDefaultGo2RTCEndpoint = (
+ cameraConfig: CameraConfig,
+ options?: {
+ url?: string;
+ stream?: string;
+ },
+): CameraEndpoint | null => {
+ const url = options?.url ?? cameraConfig.go2rtc?.url;
+ const stream = options?.stream ?? cameraConfig.go2rtc?.stream;
+
+ if (!url || !stream) {
+ return null;
+ }
+ const endpoint = `${url}/api/ws?src=${stream}`;
+
+ return {
+ endpoint: endpoint,
+
+ // Only sign the endpoint if it's local to HA.
+ sign: endpoint.startsWith('/'),
+ };
+};
diff --git a/src/components-lib/timeline-source.ts b/src/components-lib/timeline-source.ts
index 7e9d10c9..3aac95b4 100644
--- a/src/components-lib/timeline-source.ts
+++ b/src/components-lib/timeline-source.ts
@@ -9,10 +9,10 @@ import {
compressRanges,
} from '../camera-manager/range';
import { EventQuery, RecordingQuery, RecordingSegment } from '../camera-manager/types';
-import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
+import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera-manager/utils';
import { ClipsOrSnapshotsOrAll } from '../types';
-import { ViewMedia } from '../view/media';
import { ModifyInterface, errorToConsole } from '../utils/basic.js';
+import { ViewMedia } from '../view/media';
// Allow timeline freshness to be at least this number of seconds out of date
// (caching times in the data-engine may increase the effective delay).
diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts
index d9c0a72e..b0d62eb8 100644
--- a/src/components/timeline-core.ts
+++ b/src/components/timeline-core.ts
@@ -29,7 +29,7 @@ import {
import { CameraManager } from '../camera-manager/manager';
import { rangesOverlap } from '../camera-manager/range';
import { MediaQuery } from '../camera-manager/types';
-import { convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
+import { convertRangeToCacheFriendlyTimes } from '../camera-manager/utils';
import {
FrigateCardTimelineItem,
TimelineDataSource,
diff --git a/src/config/types.ts b/src/config/types.ts
index 6b61520c..5618ebb6 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -774,7 +774,11 @@ const microphoneConfigSchema = z
export type MicrophoneConfig = z.infer;
const go2rtcConfigSchema = z.object({
- url: z.string().optional(),
+ url: z
+ .string()
+ .transform((input) => input.replace(/\/+$/, ''))
+ .optional(),
+ host: z.string().optional(),
modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(),
stream: z.string().optional(),
});
diff --git a/tests/camera-manager/frigate/engine-frigate.test.ts b/tests/camera-manager/frigate/engine-frigate.test.ts
index 3bf4e54c..81f7ebd1 100644
--- a/tests/camera-manager/frigate/engine-frigate.test.ts
+++ b/tests/camera-manager/frigate/engine-frigate.test.ts
@@ -191,25 +191,6 @@ describe('getCameraEndpoints', () => {
}),
);
});
-
- it('when remote with superfluous slashes', () => {
- const endpoints = createEngine().getCameraEndpoints(
- createFrigateCameraConfig({
- go2rtc: {
- url: 'https://my.custom.go2rtc///',
- },
- }),
- );
-
- expect(endpoints).toEqual(
- expect.objectContaining({
- go2rtc: {
- endpoint: 'https://my.custom.go2rtc/api/ws?src=camera-1',
- sign: false,
- },
- }),
- );
- });
});
it('should not set webrtc_card endpoint without camera name', () => {
diff --git a/tests/camera-manager/generic/engine-generic.test.ts b/tests/camera-manager/generic/engine-generic.test.ts
new file mode 100644
index 00000000..b6fe7cf5
--- /dev/null
+++ b/tests/camera-manager/generic/engine-generic.test.ts
@@ -0,0 +1,294 @@
+import { describe, expect, it } from 'vitest';
+import { mock } from 'vitest-mock-extended';
+import { GenericCameraManagerEngine } from '../../../src/camera-manager/generic/engine-generic';
+import { Engine, QueryResultsType, QueryType } from '../../../src/camera-manager/types';
+import { CameraConfig, RawFrigateCardConfig } from '../../../src/config/types';
+import { EntityRegistryManager } from '../../../src/utils/ha/entity-registry';
+import {
+ TestViewMedia,
+ createCameraConfig,
+ createHASS,
+ createStateEntity,
+} from '../../test-utils';
+
+const createEngine = (): GenericCameraManagerEngine => {
+ return new GenericCameraManagerEngine();
+};
+
+const createGenericCameraConfig = (config?: RawFrigateCardConfig): CameraConfig => {
+ return createCameraConfig(config);
+};
+
+describe('GenericCameraManagerEngine', () => {
+ it('should get engine type', () => {
+ expect(createEngine().getEngineType()).toBe(Engine.Generic);
+ });
+
+ it('should initialize camera', async () => {
+ const config = createGenericCameraConfig();
+ expect(
+ await createEngine().initializeCamera(
+ createHASS(),
+ mock(),
+ config,
+ ),
+ ).toEqual(config);
+ });
+
+ it('should generate default event query', () => {
+ expect(
+ createEngine().generateDefaultEventQuery(
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ new Set(['camera-1']),
+ {},
+ ),
+ ).toBeNull();
+ });
+
+ it('should generate default recording query', () => {
+ expect(
+ createEngine().generateDefaultRecordingQuery(
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ new Set(['camera-1']),
+ {},
+ ),
+ ).toBeNull();
+ });
+
+ it('should generate default recording segments query', () => {
+ expect(
+ createEngine().generateDefaultRecordingSegmentsQuery(
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ new Set(['camera-1']),
+ {},
+ ),
+ ).toBeNull();
+ });
+
+ it('should get events', async () => {
+ expect(
+ await createEngine().getEvents(
+ createHASS(),
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ { type: QueryType.Event, cameraIDs: new Set(['camera-1']) },
+ ),
+ ).toBeNull();
+ });
+
+ it('should get recordings', async () => {
+ expect(
+ await createEngine().getRecordings(
+ createHASS(),
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ { type: QueryType.Recording, cameraIDs: new Set(['camera-1']) },
+ ),
+ ).toBeNull();
+ });
+
+ it('should get recording segments', async () => {
+ expect(
+ await createEngine().getRecordingSegments(
+ createHASS(),
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ {
+ type: QueryType.RecordingSegments,
+ cameraIDs: new Set(['camera-1']),
+ start: new Date(),
+ end: new Date(),
+ },
+ ),
+ ).toBeNull();
+ });
+
+ it('should generate media from events', async () => {
+ expect(
+ createEngine().generateMediaFromEvents(
+ createHASS(),
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ {
+ type: QueryType.Event,
+ cameraIDs: new Set(['camera-1']),
+ },
+ {
+ type: QueryResultsType.Event,
+ engine: Engine.Generic,
+ },
+ ),
+ ).toBeNull();
+ });
+
+ it('should generate media from recordings', async () => {
+ expect(
+ createEngine().generateMediaFromRecordings(
+ createHASS(),
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ {
+ type: QueryType.Recording,
+ cameraIDs: new Set(['camera-1']),
+ start: new Date(),
+ end: new Date(),
+ },
+ {
+ type: QueryResultsType.Recording,
+ engine: Engine.Generic,
+ },
+ ),
+ ).toBeNull();
+ });
+
+ it('should get media download path', async () => {
+ expect(
+ await createEngine().getMediaDownloadPath(
+ createHASS(),
+ createGenericCameraConfig(),
+ new TestViewMedia(),
+ ),
+ ).toBeNull();
+ });
+
+ it('should favorite media', async () => {
+ expect(
+ await createEngine().favoriteMedia(
+ createHASS(),
+ createGenericCameraConfig(),
+ new TestViewMedia(),
+ true,
+ ),
+ ).toBeUndefined();
+ });
+
+ it('should get query result max age', () => {
+ expect(
+ createEngine().getQueryResultMaxAge({
+ type: QueryType.Event,
+ cameraIDs: new Set(['camera-1']),
+ }),
+ ).toBeNull();
+ });
+
+ it('should get media seek time', async () => {
+ expect(
+ await createEngine().getMediaSeekTime(
+ createHASS(),
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ new TestViewMedia(),
+ new Date(),
+ ),
+ ).toBeNull();
+ });
+
+ it('should get media metadata', async () => {
+ expect(
+ await createEngine().getMediaMetadata(
+ createHASS(),
+ new Map([['camera-1', createGenericCameraConfig()]]),
+ { type: QueryType.MediaMetadata, cameraIDs: new Set(['camera-1']) },
+ ),
+ ).toBeNull();
+ });
+
+ describe('should get camera metadata', () => {
+ it('with empty config', async () => {
+ expect(
+ createEngine().getCameraMetadata(createHASS(), createGenericCameraConfig()),
+ ).toEqual({
+ icon: 'mdi:bookmark',
+ title: '',
+ });
+ });
+
+ it('with configured title', async () => {
+ expect(
+ createEngine().getCameraMetadata(
+ createHASS(),
+ createGenericCameraConfig({
+ title: 'My Camera',
+ }),
+ ),
+ ).toEqual({
+ icon: 'mdi:bookmark',
+ title: 'My Camera',
+ });
+ });
+
+ describe('with entity title', () => {
+ it('camera_entity', async () => {
+ expect(
+ createEngine().getCameraMetadata(
+ createHASS({
+ 'camera.test': createStateEntity({
+ attributes: { friendly_name: 'My Entity Camera' },
+ }),
+ }),
+ createGenericCameraConfig({
+ camera_entity: 'camera.test',
+ }),
+ ),
+ ).toEqual({
+ icon: 'mdi:bookmark',
+ title: 'My Entity Camera',
+ });
+ });
+
+ it('webrtc_card.entity', async () => {
+ expect(
+ createEngine().getCameraMetadata(
+ createHASS({
+ 'camera.test': createStateEntity({
+ attributes: { friendly_name: 'My Entity Camera' },
+ }),
+ }),
+ createGenericCameraConfig({
+ webrtc_card: {
+ entity: 'camera.test',
+ },
+ }),
+ ),
+ ).toEqual({
+ icon: 'mdi:bookmark',
+ title: 'My Entity Camera',
+ });
+ });
+ });
+ });
+
+ it('should get camera capabilities metadata', async () => {
+ expect(createEngine().getCameraCapabilities(createGenericCameraConfig())).toEqual({
+ canFavoriteEvents: false,
+ canFavoriteRecordings: false,
+ canSeek: false,
+ supportsClips: false,
+ supportsRecordings: false,
+ supportsSnapshots: false,
+ supportsTimeline: false,
+ });
+ });
+
+ it('should get media capabilities', () => {
+ expect(createEngine().getMediaCapabilities(new TestViewMedia())).toBeNull();
+ });
+
+ describe('should get camera endpoints', () => {
+ it('default', () => {
+ expect(createEngine().getCameraEndpoints(createGenericCameraConfig())).toBeNull();
+ });
+
+ it('for go2rtc', () => {
+ expect(
+ createEngine().getCameraEndpoints(
+ createGenericCameraConfig({
+ go2rtc: {
+ stream: 'stream',
+ url: '/local/path',
+ },
+ }),
+ ),
+ ).toEqual({
+ go2rtc: {
+ endpoint: '/local/path/api/ws?src=stream',
+ sign: true,
+ },
+ });
+ });
+ });
+});
diff --git a/tests/camera-manager/utils.test.ts b/tests/camera-manager/utils.test.ts
index a4efec66..e5a58d0b 100644
--- a/tests/camera-manager/utils.test.ts
+++ b/tests/camera-manager/utils.test.ts
@@ -3,10 +3,11 @@ import {
capEndDate,
convertRangeToCacheFriendlyTimes,
getCameraEntityFromConfig,
+ getDefaultGo2RTCEndpoint,
sortMedia,
-} from '../../src/camera-manager/util.js';
+} from '../../src/camera-manager/utils.js';
import { CameraConfig, cameraConfigSchema } from '../../src/config/types.js';
-import { TestViewMedia } from '../test-utils.js';
+import { TestViewMedia, createCameraConfig } from '../test-utils.js';
describe('convertRangeToCacheFriendlyTimes', () => {
it('should return cache friendly within hour range', () => {
@@ -134,3 +135,41 @@ describe('getCameraEntityFromConfig', () => {
expect(getCameraEntityFromConfig(createCameraConfig({}))).toBeNull();
});
});
+
+describe('getDefaultGo2RTCEndpoint', () => {
+ it('with local configuration', () => {
+ expect(
+ getDefaultGo2RTCEndpoint(
+ createCameraConfig({
+ go2rtc: {
+ stream: 'stream',
+ url: '/local/path',
+ },
+ }),
+ ),
+ ).toEqual({
+ endpoint: '/local/path/api/ws?src=stream',
+ sign: true,
+ });
+ });
+
+ it('with remote configuration', () => {
+ expect(
+ getDefaultGo2RTCEndpoint(
+ createCameraConfig({
+ go2rtc: {
+ stream: 'stream',
+ url: 'https://my-custom-go2rtc',
+ },
+ }),
+ ),
+ ).toEqual({
+ endpoint: 'https://my-custom-go2rtc/api/ws?src=stream',
+ sign: false,
+ });
+ });
+
+ it('without configuration', () => {
+ expect(getDefaultGo2RTCEndpoint(createCameraConfig())).toBeNull();
+ });
+});
diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts
index 4f49e905..6793101b 100644
--- a/tests/config/types.test.ts
+++ b/tests/config/types.test.ts
@@ -435,3 +435,17 @@ it('should not require title controls to specify all options', () => {
}),
).toBeTruthy();
});
+
+it('should strip trailing slashes from go2rtc url', () => {
+ const config = createConfig({
+ cameras: [
+ {
+ go2rtc: {
+ url: 'https://my-custom-go2rtc//',
+ },
+ },
+ ],
+ });
+ expect(config).toBeTruthy();
+ expect(config.cameras[0].go2rtc.url).toBe('https://my-custom-go2rtc');
+});