194 lines
5.3 KiB
TypeScript
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());
|
|
}
|
|
};
|
|
}
|