mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
Implements Picture-in-Picture functionality for the Electron wrapper to maintain video engagement when users are not actively focused on the conference window. This feature addresses the need to keep users visually connected to the conference even when multitasking. Key features: - Automatic PiP mode activation and deactivation based on user interaction - Displays large video participant's stream or renders their avatar on canvas when video unavailable - Provides audio/video mute controls via MediaSession API directly in PiP window - Adds API events (_pip-requested) for Electron wrapper integration Implementation includes new pip feature module with Redux architecture, canvas-based avatar rendering with custom backgrounds support, and integration with existing mute/unmute logic. Depends on jitsi-meet-electron-sdk#479 for proper user gesture handling in Electron.
62 lines
2.0 KiB
TypeScript
62 lines
2.0 KiB
TypeScript
import { IReduxState } from '../app/types';
|
|
import { MEDIA_TYPE } from '../base/media/constants';
|
|
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
|
import { isLocalTrackMuted } from '../base/tracks/functions.any';
|
|
import { getElectronGlobalNS } from '../base/util/helpers';
|
|
|
|
import { requestPictureInPicture, shouldShowPiP, updateMediaSessionState } from './functions';
|
|
|
|
/**
|
|
* Listens to audio and video mute state changes when PiP is active
|
|
* and updates the MediaSession API to reflect the current state in PiP controls.
|
|
*/
|
|
StateListenerRegistry.register(
|
|
/* selector */ (state: IReduxState) => {
|
|
// Skip if PiP is disabled or shouldn't be shown (e.g., on prejoin without showOnPrejoin).
|
|
if (!shouldShowPiP(state)) {
|
|
return null;
|
|
}
|
|
|
|
const isPiPActive = state['features/pip']?.isPiPActive;
|
|
|
|
if (!isPiPActive) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
audioMuted: isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO),
|
|
videoMuted: isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO)
|
|
};
|
|
},
|
|
/* listener */ (muteState: { audioMuted: boolean; videoMuted: boolean; } | null) => {
|
|
if (muteState === null) {
|
|
return;
|
|
}
|
|
|
|
updateMediaSessionState({
|
|
cameraActive: !muteState.videoMuted,
|
|
microphoneActive: !muteState.audioMuted
|
|
});
|
|
},
|
|
{
|
|
deepEquals: true
|
|
}
|
|
);
|
|
|
|
StateListenerRegistry.register(
|
|
/* selector */ shouldShowPiP,
|
|
/* listener */ (_shouldShowPiP: boolean) => {
|
|
const electronNS = getElectronGlobalNS();
|
|
|
|
if (_shouldShowPiP) {
|
|
// Expose requestPictureInPicture for Electron main process.
|
|
if (!electronNS.requestPictureInPicture) {
|
|
electronNS.requestPictureInPicture = requestPictureInPicture;
|
|
}
|
|
} else if (typeof electronNS.requestPictureInPicture === 'function') {
|
|
delete electronNS.requestPictureInPicture;
|
|
}
|
|
}
|
|
);
|
|
|