import { IStore } from '../app/types'; import { MEDIA_TYPE } from '../base/media/constants'; import { isLocalTrackMuted } from '../base/tracks/functions.any'; import { handleToggleVideoMuted } from '../toolbox/actions.any'; import { muteLocal } from '../video-menu/actions.any'; import { SET_PIP_ACTIVE } from './actionTypes'; import { cleanupMediaSessionHandlers, enterPiP, setupMediaSessionHandlers, shouldShowPiP } from './functions'; import logger from './logger'; /** * Action to set Picture-in-Picture active state. * * @param {boolean} isPiPActive - Whether PiP is active. * @returns {{ * type: SET_PIP_ACTIVE, * isPiPActive: boolean * }} */ export function setPiPActive(isPiPActive: boolean) { return { type: SET_PIP_ACTIVE, isPiPActive }; } /** * Toggles audio mute from PiP MediaSession controls. * Uses exact same logic as toolbar audio button including GUM pending state. * * @returns {Function} */ export function toggleAudioFromPiP() { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const state = getState(); const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO); // Use the exact same action as toolbar button. dispatch(muteLocal(!audioMuted, MEDIA_TYPE.AUDIO)); }; } /** * Toggles video mute from PiP MediaSession controls. * Uses exact same logic as toolbar video button including GUM pending state. * * @returns {Function} */ export function toggleVideoFromPiP() { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const state = getState(); const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO); // Use the exact same action as toolbar button (showUI=true, ensureTrack=true). dispatch(handleToggleVideoMuted(!videoMuted, true, true)); }; } /** * Action to exit Picture-in-Picture mode. * * @returns {Function} */ export function exitPiP() { return (dispatch: IStore['dispatch']) => { if (document.pictureInPictureElement) { document.exitPictureInPicture() .then(() => { logger.debug('Exited Picture-in-Picture mode'); }) .catch((err: Error) => { logger.error(`Error while exiting PiP: ${err.message}`); }); } dispatch(setPiPActive(false)); cleanupMediaSessionHandlers(); }; } /** * Action to handle window blur or tab switch. * Enters PiP mode if not already active. * * @param {HTMLVideoElement} videoElement - The video element we will use for PiP. * @returns {Function} */ export function handleWindowBlur(videoElement: HTMLVideoElement) { return (_dispatch: IStore['dispatch'], getState: IStore['getState']) => { const state = getState(); const isPiPActive = state['features/pip']?.isPiPActive; if (!isPiPActive) { enterPiP(videoElement); } }; } /** * Action to handle window focus. * Exits PiP mode if currently active (matches old AOT behavior). * * @returns {Function} */ export function handleWindowFocus() { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const state = getState(); const isPiPActive = state['features/pip']?.isPiPActive; if (isPiPActive) { dispatch(exitPiP()); } }; } /** * Action to handle the browser's leavepictureinpicture event. * Updates state and cleans up MediaSession handlers. * * @returns {Function} */ export function handlePiPLeaveEvent() { return (dispatch: IStore['dispatch']) => { logger.log('Left Picture-in-Picture mode'); dispatch(setPiPActive(false)); cleanupMediaSessionHandlers(); APP.API.notifyPictureInPictureLeft(); }; } /** * Action to handle the browser's enterpictureinpicture event. * Updates state and sets up MediaSession handlers. * * @returns {Function} */ export function handlePipEnterEvent() { return (dispatch: IStore['dispatch']) => { logger.log('Entered Picture-in-Picture mode'); dispatch(setPiPActive(true)); setupMediaSessionHandlers(dispatch); APP.API.notifyPictureInPictureEntered(); }; } /** * Shows Picture-in-Picture window. * Called from external API when iframe becomes not visible (IntersectionObserver). * * @returns {Function} */ export function showPiP() { return (_dispatch: IStore['dispatch'], getState: IStore['getState']) => { const state = getState(); const isPiPActive = state['features/pip']?.isPiPActive; if (!shouldShowPiP(state)) { return; } if (!isPiPActive) { const videoElement = document.getElementById('pipVideo') as HTMLVideoElement; if (videoElement) { enterPiP(videoElement); } } }; } /** * Hides Picture-in-Picture window. * Called from external API when iframe becomes visible. * * @returns {Function} */ export function hidePiP() { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const state = getState(); const isPiPActive = state['features/pip']?.isPiPActive; if (isPiPActive) { dispatch(exitPiP()); } }; }