diff --git a/src/camera-manager/browse-media/media.ts b/src/camera-manager/browse-media/media.ts index 5deb4842..b3109074 100644 --- a/src/camera-manager/browse-media/media.ts +++ b/src/camera-manager/browse-media/media.ts @@ -57,7 +57,7 @@ class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia { return this._browseMedia.thumbnail; } public getWhat(): string[] | null { - return null; + return this._browseMedia._metadata?.what ?? null; } public getScore(): number | null { return null; diff --git a/src/camera-manager/browse-media/types.ts b/src/camera-manager/browse-media/types.ts index 09226f98..f50282fa 100644 --- a/src/camera-manager/browse-media/types.ts +++ b/src/camera-manager/browse-media/types.ts @@ -2,4 +2,5 @@ export interface BrowseMediaMetadata { cameraID: string; startDate: Date; endDate: Date; + what?: string[]; } diff --git a/src/camera-manager/reolink/engine-reolink.ts b/src/camera-manager/reolink/engine-reolink.ts index 90aa2567..0cbec88f 100644 --- a/src/camera-manager/reolink/engine-reolink.ts +++ b/src/camera-manager/reolink/engine-reolink.ts @@ -68,20 +68,22 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine { return null; } - // Title of the form "21:47:03 0:00:44" + // Titles may be of the form: + // - "21:47:03 0:00:44" + // - "21:47:03 0:00:44 Person" (https://github.com/dermotduffy/advanced-camera-card/issues/1870) + // - "21:47:03 0:00:44 Vehicle Person" (https://github.com/dermotduffy/advanced-camera-card/issues/1870) const parts = media.title.split(/ +/); - if (parts.length !== 2) { - return null; - } const startDate = parse(parts[0], 'HH:mm:ss', parent._metadata.startDate); if (!isValidDate(startDate)) { return null; } - const durationMatch = parts[1].match( - /(?\d+):(?\d+):(?\d+)/, - ); + const durationMatch = + parts.length > 1 + ? parts[1].match(/(?\d+):(?\d+):(?\d+)/) + : null; + const duration = durationMatch?.groups ? { hours: Number(durationMatch.groups.hours), @@ -90,10 +92,19 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine { } : null; + const what = + parts.length > 2 + ? parts + .splice(2) + .map((l) => l.toLowerCase()) + .sort() + : null; + return { cameraID: cameraID, startDate: startDate, endDate: duration ? add(startDate, duration) : startDate, + ...(what && { what: what }), }; } diff --git a/src/const.ts b/src/const.ts index feddd406..d00ce437 100644 --- a/src/const.ts +++ b/src/const.ts @@ -382,7 +382,7 @@ export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.b export const CONF_PROFILES = 'profiles' as const; -export const CONF_REMOTE_CONTROL = 'remote_control' as const; +const CONF_REMOTE_CONTROL = 'remote_control' as const; export const CONF_REMOTE_CONTROL_ENTITIES_CAMERA = `${CONF_REMOTE_CONTROL}.entities.camera` as const; diff --git a/tests/camera-manager/reolink/engine-reolink.test.ts b/tests/camera-manager/reolink/engine-reolink.test.ts index 1fe4c507..a4099502 100644 --- a/tests/camera-manager/reolink/engine-reolink.test.ts +++ b/tests/camera-manager/reolink/engine-reolink.test.ts @@ -143,6 +143,28 @@ const TEST_FILES: BrowseMedia = { can_expand: false, thumbnail: null, }, + { + title: '21:35:05 0:00:41 Zebra Person Vehicle', + media_class: 'video', + media_content_type: 'video', + media_content_id: + 'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4', + children_media_class: null, + can_play: true, + can_expand: false, + thumbnail: null, + }, + { + title: '21:37:00', + media_class: 'video', + media_content_type: 'video', + media_content_id: + 'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4', + children_media_class: null, + can_play: true, + can_expand: false, + thumbnail: null, + }, { title: '22:04:49 0:00:35', media_class: 'video', @@ -363,6 +385,39 @@ describe('ReolinkCameraManagerEngine', () => { }, { browseMedia: [ + { + _metadata: { + cameraID: 'office', + startDate: new Date('2024-11-04T21:37:00.000Z'), + endDate: new Date('2024-11-04T21:37:00.000Z'), + }, + can_expand: false, + can_play: true, + children_media_class: null, + media_class: 'video', + media_content_id: + 'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4', + media_content_type: 'video', + thumbnail: null, + title: '21:37:00', + }, + { + _metadata: { + cameraID: 'office', + endDate: new Date('2024-11-04T21:35:46.000Z'), + startDate: new Date('2024-11-04T21:35:05.000Z'), + what: ['person', 'vehicle', 'zebra'], + }, + can_expand: false, + can_play: true, + children_media_class: null, + media_class: 'video', + media_content_id: + 'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4', + media_content_type: 'video', + thumbnail: null, + title: '21:35:05 0:00:41 Zebra Person Vehicle', + }, { _metadata: { cameraID: 'office', @@ -720,8 +775,8 @@ describe('ReolinkCameraManagerEngine', () => { thumbnail: null, children: [ { - // Title not matching format. - title: '21:23:53 0:00:34 _____MALFORMED_____', + // Title has invalid title (should be timestamp). + title: 'AA:BB:CC', media_class: 'video', media_content_type: 'video', media_content_id: diff --git a/tests/card-controller/fullscreen/webkit/index.test.ts b/tests/card-controller/fullscreen/webkit/index.test.ts index 15d52d15..08809c93 100644 --- a/tests/card-controller/fullscreen/webkit/index.test.ts +++ b/tests/card-controller/fullscreen/webkit/index.test.ts @@ -1,6 +1,5 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; -import { CardController } from '../../../../src/card-controller/controller'; import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit'; import { ConditionStateManager } from '../../../../src/conditions/state-manager'; import { @@ -14,21 +13,19 @@ const createWebkitVideoElement = (): HTMLVideoElement & return document.createElement('video'); }; -const setElement = (api: CardController, element: HTMLElement): void => { +const createPlayer = (element: HTMLElement): AdvancedCameraCardMediaPlayer => { const player = mock(); player.getFullscreenElement.mockReturnValue(element); - - vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( - createMediaLoadedInfo({ - player: player, - }), - ); + return player; }; // @vitest-environment jsdom describe('WebkitFullScreenProvider', () => { - afterEach(() => { - vi.restoreAllMocks(); + beforeAll(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { vi.useRealTimers(); }); @@ -60,7 +57,12 @@ describe('WebkitFullScreenProvider', () => { const element = createWebkitVideoElement(); element.webkitDisplayingFullscreen = fullscreen; - setElement(api, element); + const player = createPlayer(element); + vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( + createMediaLoadedInfo({ + player: player, + }), + ); expect(provider.isInFullscreen()).toBe(fullscreen); }); @@ -74,7 +76,12 @@ describe('WebkitFullScreenProvider', () => { const element = createWebkitVideoElement(); element.webkitSupportsFullscreen = supported; - setElement(api, element); + const player = createPlayer(element); + vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( + createMediaLoadedInfo({ + player: player, + }), + ); expect(provider.isSupported()).toBe(supported); }); @@ -89,7 +96,12 @@ describe('WebkitFullScreenProvider', () => { element.webkitEnterFullscreen = vi.fn(); element.webkitSupportsFullscreen = true; - setElement(api, element); + const player = createPlayer(element); + vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( + createMediaLoadedInfo({ + player: player, + }), + ); provider.setFullscreen(true); @@ -104,7 +116,12 @@ describe('WebkitFullScreenProvider', () => { element.webkitExitFullscreen = vi.fn(); element.webkitSupportsFullscreen = true; - setElement(api, element); + const player = createPlayer(element); + vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( + createMediaLoadedInfo({ + player: player, + }), + ); provider.setFullscreen(false); @@ -120,7 +137,12 @@ describe('WebkitFullScreenProvider', () => { element.webkitExitFullscreen = vi.fn(); element.webkitSupportsFullscreen = false; - setElement(api, element); + const player = createPlayer(element); + vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( + createMediaLoadedInfo({ + player: player, + }), + ); provider.setFullscreen(true); provider.setFullscreen(false); @@ -138,7 +160,12 @@ describe('WebkitFullScreenProvider', () => { element.webkitEnterFullscreen = vi.fn(); element.webkitExitFullscreen = vi.fn(); - setElement(api, element); + const player = createPlayer(element); + vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( + createMediaLoadedInfo({ + player: player, + }), + ); provider.setFullscreen(true); provider.setFullscreen(false); @@ -204,8 +231,6 @@ describe('WebkitFullScreenProvider', () => { }); it('should play the video after fullscreen ends', () => { - vi.useFakeTimers(); - const handler = vi.fn(); const api = createCardAPI(); const stateManager = new ConditionStateManager(); @@ -218,10 +243,13 @@ describe('WebkitFullScreenProvider', () => { const element = createWebkitVideoElement(); element.play = vi.fn(); - setElement(api, element); + const player = createPlayer(element); + vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue( + createMediaLoadedInfo({ + player: player, + }), + ); - const player = mock(); - player.getFullscreenElement.mockReturnValue(element); const mediaLoadedInfo = createMediaLoadedInfo({ player }); stateManager.setState({ mediaLoadedInfo });