feat: Add PIP (Picture-in-Picture) support (#2391)

- Closes: #1657
This commit is contained in:
Dermot Duffy
2026-03-05 20:19:20 -08:00
committed by GitHub
parent ecc8ecfd7e
commit c398562d40
32 changed files with 735 additions and 14 deletions
@@ -114,4 +114,10 @@ describe('ImageMediaPlayerController', () => {
expect(controller.getFullscreenElement()).toBeNull();
});
});
it('should return null for getPIPElement', () => {
const controller = new ImageMediaPlayerController(createLitElement(), () => null);
expect(controller.getPIPElement()).toBeNull();
});
});
@@ -249,4 +249,14 @@ describe('JSMPEGMediaPlayerController', () => {
expect(controller.getFullscreenElement()).toBeNull();
});
});
it('should return null for getPIPElement', () => {
const controller = new JSMPEGMediaPlayerController(
createLitElement(),
() => mock<JSMpeg.VideoElement>(),
() => null,
);
expect(controller.getPIPElement()).toBeNull();
});
});
@@ -189,4 +189,14 @@ describe('UpdatingImageMediaPlayerController', () => {
expect(controller.getFullscreenElement()).toBeNull();
});
});
it('should return null for getPIPElement', () => {
const controller = new UpdatingImageMediaPlayerController(
createLitElement(),
() => null,
() => mock<CachedValueController<string>>(),
);
expect(controller.getPIPElement()).toBeNull();
});
});
@@ -270,4 +270,20 @@ describe('VideoMediaPlayerController', () => {
expect(controller.getFullscreenElement()).toBeNull();
});
});
describe('should get PIP element', () => {
it('should return video element when available', () => {
const video = mock<HTMLVideoElement>();
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
expect(controller.getPIPElement()).toBe(video);
});
it('should return null without video', () => {
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
expect(controller.getPIPElement()).toBeNull();
});
});
});
@@ -8,6 +8,7 @@ import { FoldersManager } from '../../src/card-controller/folders/manager.js';
import { FolderQuery } from '../../src/card-controller/folders/types';
import { FullscreenManager } from '../../src/card-controller/fullscreen/fullscreen-manager.js';
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager.js';
import { PIPManager } from '../../src/card-controller/pip-manager.js';
import { MicrophoneManager } from '../../src/card-controller/microphone-manager.js';
import { ViewManager } from '../../src/card-controller/view/view-manager.js';
import {
@@ -1484,6 +1485,75 @@ describe('MenuButtonController', () => {
});
});
describe('should have pip button', () => {
it('when not in PIP mode', () => {
const pipManager = mock<PIPManager>();
vi.mocked(pipManager.isInPIP).mockReturnValue(false);
vi.mocked(pipManager.isAvailable).mockReturnValue(true);
const buttons = calculateButtons(controller, { pipManager });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:picture-in-picture-bottom-right',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Picture in Picture',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'pip',
},
style: {},
});
});
it('when in PIP mode', () => {
const pipManager = mock<PIPManager>();
vi.mocked(pipManager.isInPIP).mockReturnValue(true);
vi.mocked(pipManager.isAvailable).mockReturnValue(true);
const buttons = calculateButtons(controller, { pipManager });
expect(buttons).toContainEqual({
alignment: 'matching',
state_color: true,
permanent: false,
icon: 'mdi:picture-in-picture-bottom-right-outline',
enabled: false,
priority: 50,
type: 'custom:advanced-camera-card-menu-icon',
title: 'Picture in Picture',
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'pip',
},
style: { color: 'var(--advanced-camera-card-menu-button-active-color)' },
});
});
it('when not supported', () => {
const pipManager = mock<PIPManager>();
vi.mocked(pipManager.isAvailable).mockReturnValue(false);
const buttons = calculateButtons(controller, { pipManager });
expect(buttons).not.toContainEqual(
expect.objectContaining({ title: 'Picture in Picture' }),
);
});
it('when no pipManager provided', () => {
const buttons = calculateButtons(controller, {});
expect(buttons).not.toContainEqual(
expect.objectContaining({ title: 'Picture in Picture' }),
);
});
});
describe('should have expand button', () => {
it('when not expanded', () => {
const buttons = calculateButtons(controller, { inExpandedMode: false });