mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-16 05:28:15 +00:00
Addresses multiple issues when enabling/disabling PiP dynamically: 1. External API: Replace dead config-overwrite event handler with interception in executeCommand. This properly manages the intersection observer and PiP state when pip config changes via overwriteConfig. 2. PiPVideoElement: Fix ref access pattern - access videoRef.current inside useEffects instead of capturing at render time. The captured value was null on first render, causing blur/focus listeners to never be set up when component mounted into a stable conference. 3. useCanvasAvatar: Return streamRef object instead of refs.current.stream so consumers can access .current inside their effects. The stream is created in an effect and wasn't available at render time. 4. Add on-mount focus check with loadedmetadata wait to handle PiP enable while app is in background, ensuring video source is ready before attempting to enter PiP mode.
184 lines
5.5 KiB
TypeScript
184 lines
5.5 KiB
TypeScript
import React, { useEffect, useRef } from 'react';
|
|
|
|
import IconUserSVG from '../base/icons/svg/user.svg?raw';
|
|
import { IParticipant } from '../base/participants/types';
|
|
import { TILE_ASPECT_RATIO } from '../filmstrip/constants';
|
|
|
|
import { renderAvatarOnCanvas } from './functions';
|
|
import logger from './logger';
|
|
|
|
/**
|
|
* Canvas dimensions for PiP avatar rendering.
|
|
*/
|
|
const CANVAS_WIDTH = 640;
|
|
const CANVAS_HEIGHT = Math.floor(CANVAS_WIDTH / TILE_ASPECT_RATIO);
|
|
|
|
/**
|
|
* Frame rate 0 means capture on-demand when canvas changes.
|
|
* We manually request frames after drawing to ensure capture.
|
|
*/
|
|
const CANVAS_FRAME_RATE = 0;
|
|
|
|
/**
|
|
* Options for the useCanvasAvatar hook.
|
|
*/
|
|
interface IUseCanvasAvatarOptions {
|
|
backgroundColor: string;
|
|
customAvatarBackgrounds: string[];
|
|
displayName: string;
|
|
displayNameColor: string;
|
|
fontFamily: string;
|
|
initialsColor: string;
|
|
participant: IParticipant | undefined;
|
|
}
|
|
|
|
/**
|
|
* Result returned by the useCanvasAvatar hook.
|
|
* Returns a ref object so consumers can access .current inside effects
|
|
* (the stream is created in an effect and won't be available at render time).
|
|
*/
|
|
interface IUseCanvasAvatarResult {
|
|
canvasStreamRef: React.MutableRefObject<MediaStream | null>;
|
|
}
|
|
|
|
/**
|
|
* Internal refs managed by the hook.
|
|
*/
|
|
interface ICanvasRefs {
|
|
canvas: HTMLCanvasElement | null;
|
|
defaultIcon: HTMLImageElement | null;
|
|
}
|
|
|
|
/**
|
|
* Loads and prepares the default user icon SVG as an Image element.
|
|
*
|
|
* @returns {HTMLImageElement} The prepared image element.
|
|
*/
|
|
function createDefaultIconImage(): HTMLImageElement {
|
|
let svgText = IconUserSVG;
|
|
|
|
if (!svgText.includes('fill=')) {
|
|
svgText = svgText.replace('<svg', '<svg fill="#FFFFFF"');
|
|
}
|
|
|
|
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svgText)
|
|
.replace(/'/g, '%27')
|
|
.replace(/"/g, '%22')}`;
|
|
|
|
const img = new Image();
|
|
|
|
img.src = dataUrl;
|
|
|
|
return img;
|
|
}
|
|
|
|
/**
|
|
* Custom hook that manages canvas-based avatar rendering for Picture-in-Picture.
|
|
* Creates and maintains a canvas element with a MediaStream that can be used
|
|
* as a video source when the participant's video is unavailable.
|
|
*
|
|
* @param {IUseCanvasAvatarOptions} options - The hook options.
|
|
* @returns {IUseCanvasAvatarResult} The canvas stream for use as video source.
|
|
*/
|
|
export function useCanvasAvatar(options: IUseCanvasAvatarOptions): IUseCanvasAvatarResult {
|
|
const {
|
|
participant,
|
|
displayName,
|
|
customAvatarBackgrounds,
|
|
backgroundColor,
|
|
fontFamily,
|
|
initialsColor,
|
|
displayNameColor
|
|
} = options;
|
|
|
|
const refs = useRef<ICanvasRefs>({
|
|
canvas: null,
|
|
defaultIcon: null
|
|
});
|
|
|
|
// Separate ref for the stream to return to consumers.
|
|
// This allows consumers to access .current inside their effects.
|
|
//
|
|
// NOTE: If we ever need to recreate the stream (e.g., different canvas size),
|
|
// consumers' effects won't automatically re-run since refs don't trigger re-renders.
|
|
// To fix this, we could return an additional state flag like `streamReady` that
|
|
// changes when the stream is set, and consumers would add it to their effect deps.
|
|
const streamRef = useRef<MediaStream | null>(null);
|
|
|
|
/**
|
|
* Initialize canvas, stream, and default icon on mount.
|
|
*/
|
|
useEffect(() => {
|
|
// Create canvas.
|
|
const canvas = document.createElement('canvas');
|
|
|
|
canvas.width = CANVAS_WIDTH;
|
|
canvas.height = CANVAS_HEIGHT;
|
|
refs.current.canvas = canvas;
|
|
|
|
// Create stream from canvas.
|
|
streamRef.current = canvas.captureStream(CANVAS_FRAME_RATE);
|
|
|
|
// Load default icon.
|
|
refs.current.defaultIcon = createDefaultIconImage();
|
|
|
|
logger.log('Canvas avatar initialized');
|
|
|
|
// Cleanup on unmount.
|
|
return () => {
|
|
if (streamRef.current) {
|
|
streamRef.current.getTracks().forEach(track => track.stop());
|
|
streamRef.current = null;
|
|
}
|
|
refs.current.canvas = null;
|
|
refs.current.defaultIcon = null;
|
|
logger.log('Canvas avatar cleaned up');
|
|
};
|
|
}, []);
|
|
|
|
/**
|
|
* Re-render avatar when participant or display name changes.
|
|
*/
|
|
useEffect(() => {
|
|
const { canvas, defaultIcon } = refs.current;
|
|
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
if (!ctx) {
|
|
logger.error('Failed to get canvas 2D context');
|
|
|
|
return;
|
|
}
|
|
|
|
renderAvatarOnCanvas(
|
|
canvas,
|
|
ctx,
|
|
participant,
|
|
displayName,
|
|
customAvatarBackgrounds,
|
|
defaultIcon,
|
|
backgroundColor,
|
|
fontFamily,
|
|
initialsColor,
|
|
displayNameColor
|
|
).then(() => {
|
|
// Request a frame capture after drawing.
|
|
// For captureStream(0), we need to manually trigger frame capture.
|
|
const track = streamRef.current?.getVideoTracks()[0] as MediaStreamTrack & { requestFrame?: () => void; };
|
|
|
|
if (track?.requestFrame) {
|
|
track.requestFrame();
|
|
logger.log('Canvas frame requested after render');
|
|
}
|
|
}).catch((error: Error) => logger.error('Error rendering avatar on canvas:', error));
|
|
}, [ participant?.loadableAvatarUrl, participant?.name, displayName, customAvatarBackgrounds, backgroundColor, fontFamily, initialsColor, displayNameColor ]);
|
|
|
|
return {
|
|
canvasStreamRef: streamRef
|
|
};
|
|
}
|