Merge pull request #1291 from dermotduffy/custom-go2rtc
Add support for go2rtc to non-Frigate cameras
This commit is contained in:
@@ -177,8 +177,8 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
|
|||||||
|Engine / Live Provider|`ha`|`image`|`jsmpeg`|`go2rtc`|`webrtc-card`|
|
|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: |
|
|`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: |
|
|`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: | :heavy_multiplication_x: | :heavy_multiplication_x: |
|
|`motioneye`| :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: |
|
||||||
|
|
||||||
<a name="camera-frigate-configuration"></a>
|
<a name="camera-frigate-configuration"></a>
|
||||||
|
|
||||||
@@ -249,7 +249,8 @@ cameras:
|
|||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `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. |
|
| `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` | Determind by camera engine (e.g. `frigate` camera name). | :white_check_mark: | A valid `go2rtc` stream name. |
|
| `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` 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
|
#### Live Provider: Camera WebRTC Card configuration
|
||||||
|
|
||||||
@@ -1745,6 +1746,7 @@ cameras:
|
|||||||
- mp4
|
- mp4
|
||||||
- mjpeg
|
- mjpeg
|
||||||
stream: sitting_room
|
stream: sitting_room
|
||||||
|
url: 'https://my.custom.go2rtc.backend'
|
||||||
- camera_entity: camera.sitting_room_webrtc_card
|
- camera_entity: camera.sitting_room_webrtc_card
|
||||||
live_provider: webrtc_card
|
live_provider: webrtc_card
|
||||||
webrtc_card:
|
webrtc_card:
|
||||||
@@ -1816,6 +1818,7 @@ cameras_global:
|
|||||||
- mp4
|
- mp4
|
||||||
- mjpeg
|
- mjpeg
|
||||||
stream: sitting_room
|
stream: sitting_room
|
||||||
|
url: 'https://my.custom.go2rtc.backend'
|
||||||
webrtc_card:
|
webrtc_card:
|
||||||
# Arbitrary WebRTC Card options, see https://github.com/AlexxIT/WebRTC#configuration .
|
# Arbitrary WebRTC Card options, see https://github.com/AlexxIT/WebRTC#configuration .
|
||||||
entity: camera.sitting_room_rtsp
|
entity: camera.sitting_room_rtsp
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { MemoryRequestCache, RecordingSegmentsCache, RequestCache } from './cach
|
|||||||
import { CameraManagerEngine } from './engine';
|
import { CameraManagerEngine } from './engine';
|
||||||
import { CameraInitializationError } from './error';
|
import { CameraInitializationError } from './error';
|
||||||
import { Engine } from './types';
|
import { Engine } from './types';
|
||||||
import { getCameraEntityFromConfig } from './util';
|
import { getCameraEntityFromConfig } from './utils';
|
||||||
|
|
||||||
export class CameraManagerEngineFactory {
|
export class CameraManagerEngineFactory {
|
||||||
protected _entityRegistryManager: EntityRegistryManager;
|
protected _entityRegistryManager: EntityRegistryManager;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ import {
|
|||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getCameraEntityFromConfig } from '../util';
|
import { getCameraEntityFromConfig, getDefaultGo2RTCEndpoint } from '../utils.js';
|
||||||
import frigateLogo from './assets/frigate-logo-dark.svg';
|
import frigateLogo from './assets/frigate-logo-dark.svg';
|
||||||
import { FrigateViewMediaFactory } from './media';
|
import { FrigateViewMediaFactory } from './media';
|
||||||
import { FrigateViewMediaClassifier } from './media-classifier';
|
import { FrigateViewMediaClassifier } from './media-classifier';
|
||||||
@@ -1147,20 +1147,6 @@ export class FrigateCameraManagerEngine
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGo2RTC = (): CameraEndpoint | null => {
|
|
||||||
return {
|
|
||||||
endpoint:
|
|
||||||
`/api/frigate/${cameraConfig.frigate.client_id}` +
|
|
||||||
// go2rtc is exposed by the integration under the (slightly
|
|
||||||
// misleading) 'mse' path, even though that path can serve all go2rtc
|
|
||||||
// modes.
|
|
||||||
`/mse/api/ws?src=${
|
|
||||||
cameraConfig.go2rtc?.stream ?? cameraConfig.frigate.camera_name
|
|
||||||
}`,
|
|
||||||
sign: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getJSMPEG = (): CameraEndpoint | null => {
|
const getJSMPEG = (): CameraEndpoint | null => {
|
||||||
return {
|
return {
|
||||||
endpoint:
|
endpoint:
|
||||||
@@ -1183,11 +1169,20 @@ export class FrigateCameraManagerEngine
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ui = getUIEndpoint();
|
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 jsmpeg = getJSMPEG();
|
||||||
const webrtcCard = getWebRTCCard();
|
const webrtcCard = getWebRTCCard();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...super.getCameraEndpoints(cameraConfig, context),
|
||||||
...(ui && { ui: ui }),
|
...(ui && { ui: ui }),
|
||||||
...(go2rtc && { go2rtc: go2rtc }),
|
...(go2rtc && { go2rtc: go2rtc }),
|
||||||
...(jsmpeg && { jsmpeg: jsmpeg }),
|
...(jsmpeg && { jsmpeg: jsmpeg }),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { getDefaultGo2RTCEndpoint } from '../utils.js';
|
||||||
|
|
||||||
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||||
public getEngineType(): Engine {
|
public getEngineType(): Engine {
|
||||||
@@ -165,10 +166,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
getEntityTitle(hass, cameraConfig.webrtc_card?.entity) ??
|
getEntityTitle(hass, cameraConfig.webrtc_card?.entity) ??
|
||||||
cameraConfig.id ??
|
cameraConfig.id ??
|
||||||
'',
|
'',
|
||||||
icon:
|
icon: cameraConfig?.icon ?? getEntityIcon(hass, cameraConfig.camera_entity),
|
||||||
cameraConfig?.icon ??
|
|
||||||
getEntityIcon(hass, cameraConfig.camera_entity) ??
|
|
||||||
'mdi:video',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,9 +189,14 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCameraEndpoints(
|
public getCameraEndpoints(
|
||||||
_cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
_context?: CameraEndpointsContext,
|
_context?: CameraEndpointsContext,
|
||||||
): CameraEndpoints | null {
|
): CameraEndpoints | null {
|
||||||
return null;
|
const go2rtc = getDefaultGo2RTCEndpoint(cameraConfig);
|
||||||
|
return go2rtc
|
||||||
|
? {
|
||||||
|
go2rtc: go2rtc,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import add from 'date-fns/add';
|
|||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
import merge from 'lodash-es/merge.js';
|
import merge from 'lodash-es/merge.js';
|
||||||
import sum from 'lodash-es/sum';
|
import sum from 'lodash-es/sum';
|
||||||
|
import { CardCameraAPI } from '../card-controller/types.js';
|
||||||
import { CameraConfig, CamerasConfig } from '../config/types.js';
|
import { CameraConfig, CamerasConfig } from '../config/types.js';
|
||||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
||||||
import { getCameraID } from '../utils/camera.js';
|
import { getCameraID } from '../utils/camera.js';
|
||||||
import { CardCameraAPI } from '../card-controller/types.js';
|
|
||||||
import { log } from '../utils/debug.js';
|
import { log } from '../utils/debug.js';
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||||
import { ViewMedia } from '../view/media.js';
|
import { ViewMedia } from '../view/media.js';
|
||||||
@@ -51,7 +51,7 @@ import {
|
|||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
ResultsMap,
|
ResultsMap,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { sortMedia } from './util.js';
|
import { sortMedia } from './utils.js';
|
||||||
|
|
||||||
class QueryClassifier {
|
class QueryClassifier {
|
||||||
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
||||||
|
|||||||
@@ -384,8 +384,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
|
|
||||||
public getCameraEndpoints(
|
public getCameraEndpoints(
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
context?: CameraEndpointsContext,
|
||||||
_context?: CameraEndpointsContext,
|
|
||||||
): CameraEndpoints | null {
|
): CameraEndpoints | null {
|
||||||
const getUIEndpoint = (): CameraEndpoint | null => {
|
const getUIEndpoint = (): CameraEndpoint | null => {
|
||||||
return cameraConfig.motioneye?.url
|
return cameraConfig.motioneye?.url
|
||||||
@@ -394,9 +393,9 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ui = getUIEndpoint();
|
const ui = getUIEndpoint();
|
||||||
return {
|
return {
|
||||||
|
...super.getCameraEndpoints(cameraConfig, context),
|
||||||
...(ui && { ui: ui }),
|
...(ui && { ui: ui }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import uniqBy from 'lodash-es/uniqBy';
|
|||||||
import { CameraConfig } from '../config/types';
|
import { CameraConfig } from '../config/types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import { DateRange } from './range';
|
import { DateRange } from './range';
|
||||||
|
import { CameraEndpoint } from './types';
|
||||||
|
|
||||||
export const convertRangeToCacheFriendlyTimes = (
|
export const convertRangeToCacheFriendlyTimes = (
|
||||||
range: DateRange,
|
range: DateRange,
|
||||||
@@ -58,3 +59,26 @@ export const sortMedia = (mediaArray: ViewMedia[]): ViewMedia[] => {
|
|||||||
export const getCameraEntityFromConfig = (cameraConfig: CameraConfig): string | null => {
|
export const getCameraEntityFromConfig = (cameraConfig: CameraConfig): string | null => {
|
||||||
return cameraConfig.camera_entity ?? cameraConfig.webrtc_card?.entity ?? 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('/'),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
compressRanges,
|
compressRanges,
|
||||||
} from '../camera-manager/range';
|
} from '../camera-manager/range';
|
||||||
import { EventQuery, RecordingQuery, RecordingSegment } from '../camera-manager/types';
|
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 { ClipsOrSnapshotsOrAll } from '../types';
|
||||||
import { ViewMedia } from '../view/media';
|
|
||||||
import { ModifyInterface, errorToConsole } from '../utils/basic.js';
|
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
|
// 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).
|
// (caching times in the data-engine may increase the effective delay).
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
import { CameraConfig, MicrophoneConfig } from '../../config/types.js';
|
import { CameraConfig, MicrophoneConfig } from '../../config/types.js';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import liveMSEStyle from '../../scss/live-go2rtc.scss';
|
import liveGo2RTCStyle from '../../scss/live-go2rtc.scss';
|
||||||
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
||||||
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint';
|
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint';
|
||||||
import { setControlsOnVideo } from '../../utils/media.js';
|
import { setControlsOnVideo } from '../../utils/media.js';
|
||||||
@@ -167,7 +167,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(liveMSEStyle);
|
return unsafeCSS(liveGo2RTCStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { rangesOverlap } from '../camera-manager/range';
|
import { rangesOverlap } from '../camera-manager/range';
|
||||||
import { MediaQuery } from '../camera-manager/types';
|
import { MediaQuery } from '../camera-manager/types';
|
||||||
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/utils';
|
||||||
import {
|
import {
|
||||||
FrigateCardTimelineItem,
|
FrigateCardTimelineItem,
|
||||||
TimelineDataSource,
|
TimelineDataSource,
|
||||||
|
|||||||
@@ -774,6 +774,11 @@ const microphoneConfigSchema = z
|
|||||||
export type MicrophoneConfig = z.infer<typeof microphoneConfigSchema>;
|
export type MicrophoneConfig = z.infer<typeof microphoneConfigSchema>;
|
||||||
|
|
||||||
const go2rtcConfigSchema = z.object({
|
const go2rtcConfigSchema = z.object({
|
||||||
|
url: z
|
||||||
|
.string()
|
||||||
|
.transform((input) => input.replace(/\/+$/, ''))
|
||||||
|
.optional(),
|
||||||
|
host: z.string().optional(),
|
||||||
modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(),
|
modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(),
|
||||||
stream: z.string().optional(),
|
stream: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterEach, describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { RecordingSegmentsCache, RequestCache } from '../../../src/camera-manager/cache';
|
import { RecordingSegmentsCache, RequestCache } from '../../../src/camera-manager/cache';
|
||||||
import { FrigateCameraManagerEngine } from '../../../src/camera-manager/frigate/engine-frigate';
|
import { FrigateCameraManagerEngine } from '../../../src/camera-manager/frigate/engine-frigate';
|
||||||
import {
|
import {
|
||||||
@@ -6,9 +6,13 @@ import {
|
|||||||
FrigateRecordingViewMedia,
|
FrigateRecordingViewMedia,
|
||||||
} from '../../../src/camera-manager/frigate/media';
|
} from '../../../src/camera-manager/frigate/media';
|
||||||
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
||||||
import { CameraConfig, RawFrigateCardConfig } from '../../../src/config/types';
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
FrigateCardView,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
} from '../../../src/config/types';
|
||||||
import { ViewMedia } from '../../../src/view/media';
|
import { ViewMedia } from '../../../src/view/media';
|
||||||
import { createCameraConfig, createHASS } from '../../test-utils';
|
import { TestViewMedia, createCameraConfig, createHASS } from '../../test-utils';
|
||||||
|
|
||||||
const createEngine = (): FrigateCameraManagerEngine => {
|
const createEngine = (): FrigateCameraManagerEngine => {
|
||||||
return new FrigateCameraManagerEngine(
|
return new FrigateCameraManagerEngine(
|
||||||
@@ -80,8 +84,6 @@ const createFrigateCameraConfig = (config?: RawFrigateCardConfig): CameraConfig
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('getMediaDownloadPath', () => {
|
describe('getMediaDownloadPath', () => {
|
||||||
afterEach(() => {});
|
|
||||||
|
|
||||||
it('should get event with clip download path', async () => {
|
it('should get event with clip download path', async () => {
|
||||||
const endpoint = await createEngine().getMediaDownloadPath(
|
const endpoint = await createEngine().getMediaDownloadPath(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
@@ -131,3 +133,262 @@ describe('getMediaDownloadPath', () => {
|
|||||||
expect(endpoint).toBeNull();
|
expect(endpoint).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getCameraEndpoints', () => {
|
||||||
|
it('should get basic endpoints', () => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(createFrigateCameraConfig());
|
||||||
|
|
||||||
|
expect(endpoints).toEqual({
|
||||||
|
go2rtc: {
|
||||||
|
endpoint: '/api/frigate/frigate/mse/api/ws?src=camera-1',
|
||||||
|
sign: true,
|
||||||
|
},
|
||||||
|
jsmpeg: {
|
||||||
|
endpoint: '/api/frigate/frigate/jsmpeg/camera-1',
|
||||||
|
sign: true,
|
||||||
|
},
|
||||||
|
webrtcCard: {
|
||||||
|
endpoint: 'camera-1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get overridden go2rtc url', () => {
|
||||||
|
it('when local HA path', () => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createFrigateCameraConfig({
|
||||||
|
go2rtc: {
|
||||||
|
url: '/local/path',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
go2rtc: {
|
||||||
|
endpoint: '/local/path/api/ws?src=camera-1',
|
||||||
|
sign: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when remote', () => {
|
||||||
|
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', () => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(createCameraConfig());
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
webrtcCard: expect.anything(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should include UI endpoint', () => {
|
||||||
|
it('with basic url', () => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createCameraConfig({
|
||||||
|
frigate: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ui: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with camera name', () => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createCameraConfig({
|
||||||
|
frigate: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
camera_name: 'my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ui: {
|
||||||
|
url: 'http://my.frigate/cameras/my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with event media type', () => {
|
||||||
|
it.each([['clip' as const], ['snapshot' as const]])(
|
||||||
|
'%s',
|
||||||
|
(mediaType: 'clip' | 'snapshot') => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createCameraConfig({
|
||||||
|
frigate: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
camera_name: 'my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
media: new TestViewMedia({ mediaType: mediaType }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ui: {
|
||||||
|
url: 'http://my.frigate/events?camera=my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with recording media type', () => {
|
||||||
|
it('with start time', () => {
|
||||||
|
const startTime = new Date('2023-10-07T16:42:00');
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createCameraConfig({
|
||||||
|
frigate: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
camera_name: 'my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
media: new TestViewMedia({ mediaType: 'recording', startTime: startTime }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ui: {
|
||||||
|
url: 'http://my.frigate/recording/my-camera/2023-10-07/16',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without start time', () => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createCameraConfig({
|
||||||
|
frigate: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
camera_name: 'my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
media: new TestViewMedia({ mediaType: 'recording' }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ui: {
|
||||||
|
url: 'http://my.frigate/recording/my-camera/',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with view', () => {
|
||||||
|
it('live', () => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createCameraConfig({
|
||||||
|
frigate: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
camera_name: 'my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
view: 'live',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ui: {
|
||||||
|
url: 'http://my.frigate/cameras/my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['clip' as const],
|
||||||
|
['clips' as const],
|
||||||
|
['snapshot' as const],
|
||||||
|
['snapshots' as const],
|
||||||
|
])('%s', (viewName: FrigateCardView) => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createCameraConfig({
|
||||||
|
frigate: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
camera_name: 'my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
view: viewName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ui: {
|
||||||
|
url: 'http://my.frigate/events?camera=my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([['recording' as const], ['recordings' as const]])(
|
||||||
|
'%s',
|
||||||
|
(viewName: FrigateCardView) => {
|
||||||
|
const endpoints = createEngine().getCameraEndpoints(
|
||||||
|
createCameraConfig({
|
||||||
|
frigate: {
|
||||||
|
url: 'http://my.frigate',
|
||||||
|
camera_name: 'my-camera',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
view: viewName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(endpoints).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ui: {
|
||||||
|
url: 'http://my.frigate/recording/my-camera/',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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<EntityRegistryManager>(),
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,10 +3,11 @@ import {
|
|||||||
capEndDate,
|
capEndDate,
|
||||||
convertRangeToCacheFriendlyTimes,
|
convertRangeToCacheFriendlyTimes,
|
||||||
getCameraEntityFromConfig,
|
getCameraEntityFromConfig,
|
||||||
|
getDefaultGo2RTCEndpoint,
|
||||||
sortMedia,
|
sortMedia,
|
||||||
} from '../../src/camera-manager/util.js';
|
} from '../../src/camera-manager/utils.js';
|
||||||
import { CameraConfig, cameraConfigSchema } from '../../src/config/types.js';
|
import { CameraConfig, cameraConfigSchema } from '../../src/config/types.js';
|
||||||
import { TestViewMedia } from '../test-utils.js';
|
import { TestViewMedia, createCameraConfig } from '../test-utils.js';
|
||||||
|
|
||||||
describe('convertRangeToCacheFriendlyTimes', () => {
|
describe('convertRangeToCacheFriendlyTimes', () => {
|
||||||
it('should return cache friendly within hour range', () => {
|
it('should return cache friendly within hour range', () => {
|
||||||
@@ -134,3 +135,41 @@ describe('getCameraEntityFromConfig', () => {
|
|||||||
expect(getCameraEntityFromConfig(createCameraConfig({}))).toBeNull();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -435,3 +435,17 @@ it('should not require title controls to specify all options', () => {
|
|||||||
}),
|
}),
|
||||||
).toBeTruthy();
|
).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');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user