From fe3986db9310ffa3c1ddb26069ec81e7da1c1873 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:11:59 +0000 Subject: [PATCH 1/7] feat: add camera utility with Picture-in-Picture support Co-Authored-By: petar@jam.dev --- pages/utilities/cam.tsx | 381 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 pages/utilities/cam.tsx diff --git a/pages/utilities/cam.tsx b/pages/utilities/cam.tsx new file mode 100644 index 0000000..e786eba --- /dev/null +++ b/pages/utilities/cam.tsx @@ -0,0 +1,381 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import PageHeader from "@/components/PageHeader"; +import Header from "@/components/Header"; +import { CMDK } from "@/components/CMDK"; +import CallToActionGrid from "@/components/CallToActionGrid"; +import Meta from "@/components/Meta"; +import { Card } from "@/components/ds/CardComponent"; +import { Button } from "@/components/ds/ButtonComponent"; +import { cn } from "@/lib/utils"; + +type CameraStatus = "idle" | "requesting" | "active" | "pip" | "error"; + +type ErrorType = + | "permission_denied" + | "not_supported" + | "pip_not_supported" + | "stream_error"; + +interface ErrorInfo { + type: ErrorType; + title: string; + message: string; + instructions: string[]; +} + +const getPermissionDeniedError = (): ErrorInfo => ({ + type: "permission_denied", + title: "Camera Access Denied", + message: + "You've denied camera access. To use this feature, you'll need to enable camera permissions in your browser.", + instructions: [ + "Look for the camera icon in your browser's address bar and click it to change permissions", + "Alternatively, go to your browser's Settings > Privacy & Security > Site Settings > Camera", + "Find this website and change the camera permission to 'Allow'", + "Refresh this page after enabling camera access", + ], +}); + +const getNotSupportedError = (): ErrorInfo => ({ + type: "not_supported", + title: "Camera Not Supported", + message: + "Your browser doesn't support camera access. This feature requires a modern browser with MediaDevices API support.", + instructions: [ + "Try using a modern browser like Chrome, Edge, Safari, or Firefox", + "Make sure your browser is up to date", + "If you're on iOS, use Safari as other browsers have limited camera support", + ], +}); + +const getPipNotSupportedError = (): ErrorInfo => ({ + type: "pip_not_supported", + title: "Picture-in-Picture Not Supported", + message: + "Your browser doesn't support Picture-in-Picture mode. You can still use the camera preview on this page.", + instructions: [ + "Picture-in-Picture works best in Chrome, Edge, and Safari", + "Firefox has limited PIP support for camera streams", + "Try updating your browser to the latest version", + ], +}); + +const getStreamError = (errorMessage?: string): ErrorInfo => ({ + type: "stream_error", + title: "Camera Error", + message: + errorMessage || + "An error occurred while accessing your camera. Please try again.", + instructions: [ + "Make sure no other application is using your camera", + "Check that your camera is properly connected", + "Try refreshing the page and granting camera access again", + ], +}); + +export default function CameraUtility() { + const [status, setStatus] = useState("idle"); + const [errorInfo, setErrorInfo] = useState(null); + const [isPipSupported, setIsPipSupported] = useState(true); + const [isMediaSupported, setIsMediaSupported] = useState(true); + + const videoRef = useRef(null); + const streamRef = useRef(null); + + useEffect(() => { + setIsPipSupported( + typeof document !== "undefined" && + "pictureInPictureEnabled" in document && + document.pictureInPictureEnabled + ); + setIsMediaSupported( + typeof navigator !== "undefined" && + "mediaDevices" in navigator && + "getUserMedia" in navigator.mediaDevices + ); + }, []); + + useEffect(() => { + return () => { + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + } + }; + }, []); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + const handlePipExit = () => { + if (status === "pip") { + setStatus("active"); + } + }; + + video.addEventListener("leavepictureinpicture", handlePipExit); + return () => { + video.removeEventListener("leavepictureinpicture", handlePipExit); + }; + }, [status]); + + const startCamera = useCallback(async () => { + if (!isMediaSupported) { + setErrorInfo(getNotSupportedError()); + setStatus("error"); + return; + } + + setStatus("requesting"); + setErrorInfo(null); + + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: "user", + width: { ideal: 1280 }, + height: { ideal: 720 }, + }, + audio: false, + }); + + streamRef.current = stream; + + if (videoRef.current) { + videoRef.current.srcObject = stream; + await videoRef.current.play(); + } + + setStatus("active"); + } catch (err) { + const error = err as Error; + + if ( + error.name === "NotAllowedError" || + error.name === "PermissionDeniedError" + ) { + setErrorInfo(getPermissionDeniedError()); + } else if ( + error.name === "NotFoundError" || + error.name === "DevicesNotFoundError" + ) { + setErrorInfo( + getStreamError( + "No camera found. Please connect a camera and try again." + ) + ); + } else if ( + error.name === "NotReadableError" || + error.name === "TrackStartError" + ) { + setErrorInfo( + getStreamError( + "Camera is in use by another application. Please close other apps using the camera and try again." + ) + ); + } else { + setErrorInfo(getStreamError(error.message)); + } + + setStatus("error"); + } + }, [isMediaSupported]); + + const enablePip = useCallback(async () => { + if (!videoRef.current) return; + + if (!isPipSupported) { + setErrorInfo(getPipNotSupportedError()); + return; + } + + try { + await videoRef.current.requestPictureInPicture(); + setStatus("pip"); + } catch (err) { + const error = err as Error; + if (error.name === "NotAllowedError") { + setErrorInfo({ + type: "pip_not_supported", + title: "PIP Request Denied", + message: + "Picture-in-Picture was blocked. This usually happens if the request wasn't triggered by a user action.", + instructions: ["Try clicking the button again"], + }); + } + } + }, [isPipSupported]); + + const exitPip = useCallback(async () => { + try { + if (document.pictureInPictureElement) { + await document.exitPictureInPicture(); + } + setStatus("active"); + } catch { + setStatus("active"); + } + }, []); + + const stopCamera = useCallback(async () => { + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + } + + try { + if (document.pictureInPictureElement) { + await document.exitPictureInPicture(); + } + } catch { + // Ignore errors when exiting PIP + } + + if (videoRef.current) { + videoRef.current.srcObject = null; + } + + setStatus("idle"); + setErrorInfo(null); + }, []); + + const retry = useCallback(() => { + setErrorInfo(null); + setStatus("idle"); + }, []); + + return ( +
+ +
+ + +
+ +
+ +
+ + {status === "idle" && ( +
+

+ Click the button below to start your camera and enable + Picture-in-Picture mode. +

+ +
+ )} + + {status === "requesting" && ( +
+
+
+

+ Requesting camera access... +

+
+
+ )} + + {status === "error" && errorInfo && ( +
+
+

+ {errorInfo.title} +

+

{errorInfo.message}

+
+

How to fix this:

+
    + {errorInfo.instructions.map((instruction, index) => ( +
  • {instruction}
  • + ))} +
+
+
+
+ +
+
+ )} + + {(status === "active" || status === "pip") && ( +
+
+
+ +
+ {status === "active" && ( + <> + + + + )} + + {status === "pip" && ( + <> + + + + )} +
+ + {!isPipSupported && status === "active" && ( +

+ Picture-in-Picture is not supported in your browser. Try using + Chrome, Edge, or Safari. +

+ )} +
+ )} + +
+ + +
+ ); +} From ba0c11e937bc4f31fa43a3fa415a34a14cb6243c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:14:40 +0000 Subject: [PATCH 2/7] style: fix Prettier formatting Co-Authored-By: petar@jam.dev --- pages/utilities/cam.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/utilities/cam.tsx b/pages/utilities/cam.tsx index e786eba..69e1331 100644 --- a/pages/utilities/cam.tsx +++ b/pages/utilities/cam.tsx @@ -291,7 +291,9 @@ export default function CameraUtility() {

{errorInfo.title}

-

{errorInfo.message}

+

+ {errorInfo.message} +

How to fix this:

    From b04e7db8c28a6aa70eb236261eefd1618c06a7cc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:21:33 +0000 Subject: [PATCH 3/7] fix: resolve race condition where video stream wasn't displayed The video element was not rendered when startCamera ran (status was 'requesting'), so videoRef.current was null when trying to set srcObject. Added a useEffect that connects the stream to the video element after the element is rendered. Co-Authored-By: petar@jam.dev --- pages/utilities/cam.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pages/utilities/cam.tsx b/pages/utilities/cam.tsx index 69e1331..41f92ef 100644 --- a/pages/utilities/cam.tsx +++ b/pages/utilities/cam.tsx @@ -119,6 +119,20 @@ export default function CameraUtility() { }; }, [status]); + useEffect(() => { + const video = videoRef.current; + const stream = streamRef.current; + + if (video && stream && (status === "active" || status === "pip")) { + if (video.srcObject !== stream) { + video.srcObject = stream; + video.play().catch(() => { + // Ignore play errors - browser may block autoplay + }); + } + } + }, [status]); + const startCamera = useCallback(async () => { if (!isMediaSupported) { setErrorInfo(getNotSupportedError()); From 959f5ffd4d67edf3f7ea4efd84e092845f7c195b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:34:06 +0000 Subject: [PATCH 4/7] fix: mirror video stream using canvas for consistent PIP display - Use Canvas API to create a mirrored video stream - Both page preview and PIP now show the same mirrored view - Remove CSS transform since stream is now mirrored at source - Add proper cleanup for canvas and animation frame resources Co-Authored-By: petar@jam.dev --- pages/utilities/cam.tsx | 96 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/pages/utilities/cam.tsx b/pages/utilities/cam.tsx index 41f92ef..dcba300 100644 --- a/pages/utilities/cam.tsx +++ b/pages/utilities/cam.tsx @@ -80,7 +80,11 @@ export default function CameraUtility() { const [isMediaSupported, setIsMediaSupported] = useState(true); const videoRef = useRef(null); + const sourceVideoRef = useRef(null); + const canvasRef = useRef(null); const streamRef = useRef(null); + const mirroredStreamRef = useRef(null); + const animationFrameRef = useRef(null); useEffect(() => { setIsPipSupported( @@ -97,9 +101,22 @@ export default function CameraUtility() { useEffect(() => { return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); } + if (mirroredStreamRef.current) { + mirroredStreamRef.current.getTracks().forEach((track) => track.stop()); + } + if (sourceVideoRef.current) { + sourceVideoRef.current.srcObject = null; + sourceVideoRef.current = null; + } + if (canvasRef.current) { + canvasRef.current = null; + } }; }, []); @@ -121,11 +138,11 @@ export default function CameraUtility() { useEffect(() => { const video = videoRef.current; - const stream = streamRef.current; + const mirroredStream = mirroredStreamRef.current; - if (video && stream && (status === "active" || status === "pip")) { - if (video.srcObject !== stream) { - video.srcObject = stream; + if (video && mirroredStream && (status === "active" || status === "pip")) { + if (video.srcObject !== mirroredStream) { + video.srcObject = mirroredStream; video.play().catch(() => { // Ignore play errors - browser may block autoplay }); @@ -133,6 +150,53 @@ export default function CameraUtility() { } }, [status]); + const createMirroredStream = useCallback( + (originalStream: MediaStream): MediaStream => { + const videoTrack = originalStream.getVideoTracks()[0]; + const settings = videoTrack.getSettings(); + const width = settings.width || 1280; + const height = settings.height || 720; + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + canvasRef.current = canvas; + + const ctx = canvas.getContext("2d"); + if (!ctx) { + return originalStream; + } + + const sourceVideo = document.createElement("video"); + sourceVideo.srcObject = originalStream; + sourceVideo.muted = true; + sourceVideo.playsInline = true; + sourceVideoRef.current = sourceVideo; + + const drawMirroredFrame = () => { + if (!sourceVideoRef.current || !canvasRef.current) return; + + ctx.save(); + ctx.scale(-1, 1); + ctx.drawImage(sourceVideo, -width, 0, width, height); + ctx.restore(); + + animationFrameRef.current = requestAnimationFrame(drawMirroredFrame); + }; + + sourceVideo.onloadedmetadata = () => { + sourceVideo.play(); + drawMirroredFrame(); + }; + + const mirroredStream = canvas.captureStream(30); + mirroredStreamRef.current = mirroredStream; + + return mirroredStream; + }, + [] + ); + const startCamera = useCallback(async () => { if (!isMediaSupported) { setErrorInfo(getNotSupportedError()); @@ -155,8 +219,10 @@ export default function CameraUtility() { streamRef.current = stream; + const mirroredStream = createMirroredStream(stream); + if (videoRef.current) { - videoRef.current.srcObject = stream; + videoRef.current.srcObject = mirroredStream; await videoRef.current.play(); } @@ -193,7 +259,7 @@ export default function CameraUtility() { setStatus("error"); } - }, [isMediaSupported]); + }, [isMediaSupported, createMirroredStream]); const enablePip = useCallback(async () => { if (!videoRef.current) return; @@ -232,11 +298,28 @@ export default function CameraUtility() { }, []); const stopCamera = useCallback(async () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); streamRef.current = null; } + if (mirroredStreamRef.current) { + mirroredStreamRef.current.getTracks().forEach((track) => track.stop()); + mirroredStreamRef.current = null; + } + + if (sourceVideoRef.current) { + sourceVideoRef.current.srcObject = null; + sourceVideoRef.current = null; + } + + canvasRef.current = null; + try { if (document.pictureInPictureElement) { await document.exitPictureInPicture(); @@ -337,7 +420,6 @@ export default function CameraUtility() { playsInline muted className="w-full aspect-video object-cover" - style={{ transform: "scaleX(-1)" }} /> {status === "pip" && (
    From 96f96914c2ebe8f5b2785fcdbcbc05a3aa13ad86 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:36:49 +0000 Subject: [PATCH 5/7] feat: use Document Picture-in-Picture API for clean PIP window - Use Document PIP API when available (Chrome 116+) - Creates a clean PIP window without scrub timeline or LIVE badge - Falls back to standard PIP API for other browsers - Add proper cleanup for Document PIP window Co-Authored-By: petar@jam.dev --- pages/utilities/cam.tsx | 75 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/pages/utilities/cam.tsx b/pages/utilities/cam.tsx index dcba300..db0f50a 100644 --- a/pages/utilities/cam.tsx +++ b/pages/utilities/cam.tsx @@ -85,6 +85,8 @@ export default function CameraUtility() { const streamRef = useRef(null); const mirroredStreamRef = useRef(null); const animationFrameRef = useRef(null); + const pipWindowRef = useRef(null); + const [isDocPipSupported, setIsDocPipSupported] = useState(false); useEffect(() => { setIsPipSupported( @@ -97,6 +99,9 @@ export default function CameraUtility() { "mediaDevices" in navigator && "getUserMedia" in navigator.mediaDevices ); + setIsDocPipSupported( + typeof window !== "undefined" && "documentPictureInPicture" in window + ); }, []); useEffect(() => { @@ -117,6 +122,10 @@ export default function CameraUtility() { if (canvasRef.current) { canvasRef.current = null; } + if (pipWindowRef.current) { + pipWindowRef.current.close(); + pipWindowRef.current = null; + } }; }, []); @@ -262,16 +271,57 @@ export default function CameraUtility() { }, [isMediaSupported, createMirroredStream]); const enablePip = useCallback(async () => { - if (!videoRef.current) return; + if (!mirroredStreamRef.current) return; - if (!isPipSupported) { + if (!isPipSupported && !isDocPipSupported) { setErrorInfo(getPipNotSupportedError()); return; } try { - await videoRef.current.requestPictureInPicture(); - setStatus("pip"); + if ( + isDocPipSupported && + "documentPictureInPicture" in window && + window.documentPictureInPicture + ) { + const pipWindow = await ( + window.documentPictureInPicture as { + requestWindow: (options?: { + width?: number; + height?: number; + }) => Promise; + } + ).requestWindow({ + width: 320, + height: 180, + }); + + pipWindowRef.current = pipWindow; + + const pipVideo = pipWindow.document.createElement("video"); + pipVideo.srcObject = mirroredStreamRef.current; + pipVideo.autoplay = true; + pipVideo.muted = true; + pipVideo.playsInline = true; + pipVideo.style.cssText = + "width: 100%; height: 100%; object-fit: cover; background: black;"; + + pipWindow.document.body.style.cssText = + "margin: 0; padding: 0; overflow: hidden; background: black;"; + pipWindow.document.body.appendChild(pipVideo); + + pipVideo.play().catch(() => {}); + + pipWindow.addEventListener("pagehide", () => { + pipWindowRef.current = null; + setStatus("active"); + }); + + setStatus("pip"); + } else if (videoRef.current) { + await videoRef.current.requestPictureInPicture(); + setStatus("pip"); + } } catch (err) { const error = err as Error; if (error.name === "NotAllowedError") { @@ -284,10 +334,14 @@ export default function CameraUtility() { }); } } - }, [isPipSupported]); + }, [isPipSupported, isDocPipSupported]); const exitPip = useCallback(async () => { try { + if (pipWindowRef.current) { + pipWindowRef.current.close(); + pipWindowRef.current = null; + } if (document.pictureInPictureElement) { await document.exitPictureInPicture(); } @@ -320,6 +374,11 @@ export default function CameraUtility() { canvasRef.current = null; + if (pipWindowRef.current) { + pipWindowRef.current.close(); + pipWindowRef.current = null; + } + try { if (document.pictureInPictureElement) { await document.exitPictureInPicture(); @@ -437,9 +496,9 @@ export default function CameraUtility() { <>