Add support for custom go2rtc URLs.
This commit is contained in:
@@ -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` 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` |
|
||||||
|
|
||||||
#### 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
|
||||||
|
|||||||
@@ -1148,16 +1148,22 @@ export class FrigateCameraManagerEngine
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getGo2RTC = (): CameraEndpoint | null => {
|
const getGo2RTC = (): CameraEndpoint | null => {
|
||||||
return {
|
const stream = cameraConfig.go2rtc?.stream ?? cameraConfig.frigate.camera_name;
|
||||||
endpoint:
|
const url = cameraConfig.go2rtc?.url
|
||||||
`/api/frigate/${cameraConfig.frigate.client_id}` +
|
? cameraConfig.go2rtc.url.replace(/\/+$/, '')
|
||||||
// go2rtc is exposed by the integration under the (slightly
|
: 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
|
// misleading) 'mse' path, even though that path can serve all go2rtc
|
||||||
// modes.
|
// modes.
|
||||||
`/mse/api/ws?src=${
|
`/api/frigate/${cameraConfig.frigate.client_id}/mse/api/ws?src=${stream}`;
|
||||||
cameraConfig.go2rtc?.stream ?? cameraConfig.frigate.camera_name
|
|
||||||
}`,
|
return {
|
||||||
sign: true,
|
endpoint: endpoint,
|
||||||
|
|
||||||
|
// Only sign the endpoint if it's local to HA.
|
||||||
|
sign: endpoint.startsWith('/'),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -774,6 +774,7 @@ 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().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,281 @@ 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('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', () => {
|
||||||
|
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/',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user