import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, } from "@material-ui/core"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { isIOS } from "react-device-detect"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { useRafLoop } from "react-use"; import styled from "styled-components"; import { useCamera } from "../hooks/useCamera"; import { getMediaStream, mediaStreamErrorType } from "../utils/mediaStream"; type Props = { onFrame?: (imageData: ImageData) => void; suppressError?: boolean; }; export const MediaStream = ({ onFrame, suppressError = false }: Props) => { const { t } = useTranslation("qr_reader"); const { preferredCameraId } = useCamera(); const [showUnSupportErrorModal, setShowUnSupportErrorModal] = useState(false); const [showCameraActivationErrorModal, setShowCameraActivationErrorModal] = useState(false); const videoRef = useRef<HTMLVideoElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null); const [loopStop, loopStart] = useRafLoop(() => { const canvasElement = canvasRef.current; const videoElement = videoRef.current; if ( canvasElement && videoElement && videoElement.readyState === videoElement.HAVE_ENOUGH_DATA ) { const canvas = canvasElement.getContext("2d"); if (!canvas) return; canvasElement.height = videoElement.videoHeight; canvasElement.width = videoElement.videoWidth; canvas.drawImage( videoElement, 0, 0, canvasElement.width, canvasElement.height ); const imageData = canvas.getImageData( 0, 0, canvasElement.width, canvasElement.height ); onFrame && onFrame(imageData); } }, false); const initMediaStream = useCallback(async () => { const videoElement = videoRef.current; if (!videoElement) return; try { const stream = await getMediaStream( preferredCameraId === "AUTO" ? undefined : preferredCameraId ); if (!stream) return; videoElement.srcObject = stream; videoElement.play(); loopStart(); } catch (e) { if (e instanceof Error) { switch (e.message) { case mediaStreamErrorType.GET_USER_MEDIA_NOT_FOUND: setShowUnSupportErrorModal(true); break; case mediaStreamErrorType.CAMERA_ACTIVATE_ERROR: if (suppressError) return; setShowCameraActivationErrorModal(true); break; default: console.error(e); } } } }, [loopStart, preferredCameraId, suppressError]); useEffect(() => { const videoElement = videoRef.current; initMediaStream(); return () => { loopStop(); if (videoElement) { const stream = videoElement.srcObject as MediaStream | null; if (!stream) return; const tracks = stream.getTracks(); tracks.forEach((track) => { track.stop(); }); videoElement.srcObject = null; } }; }, [loopStart, loopStop, videoRef, initMediaStream, preferredCameraId]); return ( <> <Video ref={videoRef} playsInline /> <Canvas ref={canvasRef} /> <Dialog open={showUnSupportErrorModal} keepMounted aria-labelledby="unsupported-device-title" aria-describedby="unsupported-device-description" > <DialogTitle id="unsupported-device-title">不支援的裝置</DialogTitle> <DialogContent> <DialogContentText id="unsupported-device-description"> {t("message.doesnt_support_get_user_media")} {isIOS && <>{t("message.sure_latest_ios")}</>} </DialogContentText> </DialogContent> <DialogActions> <Link to="/"> <Button color="primary">{t("button.back_home")}</Button> </Link> </DialogActions> </Dialog> <Dialog open={showCameraActivationErrorModal} keepMounted aria-labelledby="camera-activation-title" aria-describedby="camera-activation-description" > <DialogTitle id="camera-activation-title"> {t("dialog.cannot_open_camera.title")} </DialogTitle> <DialogContent> <DialogContentText id="camera-activation-description"> {t("dialog.cannot_open_camera.content")} </DialogContentText> </DialogContent> <DialogActions> <Link to="/"> <Button color="primary">{t("button.back_home")}</Button> </Link> <Link to="/cameraSetting"> <Button color="primary">{t("button.camera_setting")}</Button> </Link> </DialogActions> </Dialog> </> ); }; const Video = styled.video` /* Make video to at least 100% wide and tall */ min-width: 100%; min-height: 100%; /* Setting width & height to auto prevents the browser from stretching or squishing the video */ width: auto; height: auto; /* Center the video */ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); `; const Canvas = styled.canvas` display: none; `;