jeffsi-meet/react/features/pip/actions.ts

194 lines
5.3 KiB
TypeScript

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());
}
};
}