9d876de930
- Complete refactoring of old frontend into Next.js App Router workspace - Redesigned sidebar collapsing animation with absolute toggle positioning - Resolved visual canvas bleed transitions between light/dark themes - Added custom dark theme variant for toggle switch buttons - Implemented full localization across Indonesian, English, Spanish, Japanese, and Chinese - Synchronized HTML document themes to apply dark mode styles to portals/overlays
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
|
|
export const usePictureInPicture = () => {
|
|
const [isPipActive, setIsPipActive] = useState(false);
|
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
const video = document.createElement('video');
|
|
video.muted = true;
|
|
video.playsInline = true;
|
|
videoRef.current = video;
|
|
|
|
const handleLeavePip = () => {
|
|
setIsPipActive(false);
|
|
};
|
|
|
|
video.addEventListener('leavepictureinpicture', handleLeavePip);
|
|
|
|
return () => {
|
|
video.removeEventListener('leavepictureinpicture', handleLeavePip);
|
|
if (document.pictureInPictureElement === video) {
|
|
document.exitPictureInPicture().catch(() => {});
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const togglePip = useCallback(async (canvas: HTMLCanvasElement | null) => {
|
|
if (!canvas || !videoRef.current) return;
|
|
|
|
const video = videoRef.current;
|
|
|
|
try {
|
|
if (document.pictureInPictureElement === video) {
|
|
await document.exitPictureInPicture();
|
|
setIsPipActive(false);
|
|
} else {
|
|
// Capture a stream of the Canvas at 30 fps
|
|
const stream = canvas.captureStream
|
|
? canvas.captureStream(30)
|
|
: (canvas as any).mozCaptureStream
|
|
? (canvas as any).mozCaptureStream(30)
|
|
: null;
|
|
|
|
if (!stream) {
|
|
throw new Error("Canvas.captureStream() is not supported on this browser.");
|
|
}
|
|
|
|
video.srcObject = stream;
|
|
|
|
await new Promise<void>((resolve) => {
|
|
video.onloadedmetadata = () => {
|
|
video.play().then(() => resolve());
|
|
};
|
|
});
|
|
|
|
await video.requestPictureInPicture();
|
|
setIsPipActive(true);
|
|
}
|
|
} catch (error) {
|
|
console.error("Picture-in-Picture failed:", error);
|
|
alert("Picture-in-Picture error: " + (error instanceof Error ? error.message : String(error)));
|
|
}
|
|
}, []);
|
|
|
|
const isSupported = typeof window !== 'undefined' && 'pictureInPictureEnabled' in document;
|
|
|
|
return { togglePip, isPipActive, isSupported };
|
|
};
|
|
export default usePictureInPicture;
|