记一个拍照的web页面demo

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <title>摄像头拍照示例</title>
    <style>
        :root {
            --primary-color: #4CAF50;
            --secondary-color: #2196F3;
            --shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }

        body {
            font-family: system-ui, -apple-system, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }

        h2 {
            color: #333;
            margin: 1.5rem 0;
        }

        .container {
            position: relative;
            width: min(90vw, 1280px);
            aspect-ratio: 16/9;
            margin: 1rem 0;
            background: #000;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: var(--shadow);
        }

        #video,
        #canvas {
            width: 100%;
            height: 100%;
            object-fit: cover;
            image-rendering: crisp-edges;
            transform: scaleX(-1); /* 镜像翻转 */
        }

        .button-group {
            display: flex;
            gap: 1rem;
            margin: 1.5rem 0;
        }

        button {
            padding: 0.8rem 1.6rem;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 1rem;
            font-weight: 500;
            transition:
                transform 0.2s ease,
                opacity 0.2s ease;
            box-shadow: var(--shadow);
        }

        button:active {
            transform: scale(0.96);
        }

        #capture { background: var(--primary-color); color: white; }
        #download { background: var(--secondary-color); color: white; }

        #status {
            color: #666;
            font-size: 0.9em;
            margin-top: 0.5rem;
        }

        #errorMsg {
            color: #d32f2f;
            padding: 0.8rem;
            background: #ffebee;
            border-radius: 4px;
            margin: 1rem 0;
            display: none;
            max-width: 80%;
            text-align: center;
        }

        @media (max-width: 480px) {
            .button-group {
                flex-direction: column;
                width: 100%;
            }

            button {
                width: 100%;
            }
        }
    </style>
</head>
<body>
    <h2>摄像头拍照演示</h2>
    <div class="container">
        <video id="video" autoplay playsinline></video>
        <canvas id="canvas" style="display: none;"></canvas>
    </div>

    <div class="button-group">
        <button id="capture">拍摄照片</button>
        <button id="download" disabled>下载照片</button>
    </div>

    <div id="status">正在初始化摄像头...</div>
    <div id="errorMsg"></div>

    <script>
        (() => {
            // 配置常量
            const CONFIG = {
                resolutions: [
                    { width: 1920, height: 1080 },
                    { width: 1280, height: 720 },
                    { width: 640, height: 480 }
                ],
                jpegQuality: 0.92,
                facingMode: 'environment'
            };

            // DOM 元素
            const el = {
                video: document.getElementById('video'),
                canvas: document.getElementById('canvas'),
                capture: document.getElementById('capture'),
                download: document.getElementById('download'),
                status: document.getElementById('status'),
                errorMsg: document.getElementById('errorMsg')
            };

            let mediaStream = null;

            // 摄像头初始化
            const initCamera = async () => {
                try {
                    const stream = await getOptimalStream();
                    handleStreamSuccess(stream);
                } catch (error) {
                    handleStreamError(error);
                }
            };

            const getOptimalStream = async () => {
                for (const res of CONFIG.resolutions) {
                    try {
                        return await navigator.mediaDevices.getUserMedia({
                            video: {
                                facingMode: CONFIG.facingMode,
                                width: { ideal: res.width, max: res.width },
                                height: { ideal: res.height, max: res.height }
                            }
                        });
                    } catch (error) {
                        if (res === CONFIG.resolutions.at(-1)) throw error;
                    }
                }
            };

            const handleStreamSuccess = (stream) => {
                mediaStream = stream;
                el.video.srcObject = stream;
                el.video.onloadedmetadata = () => {
                    el.video.play();
                    updateResolutionStatus();
                    el.capture.disabled = false;
                };
            };

            const handleStreamError = (error) => {
                console.error('摄像头错误:', error);
                showError(`摄像头访问失败: ${errorToString(error)}`);
                el.status.style.display = 'none';
                el.capture.disabled = true;
            };

            // 工具函数
            const errorToString = (error) => {
                const errors = {
                    NotAllowedError: '用户拒绝了权限请求',
                    NotFoundError: '未找到视频设备',
                    NotReadableError: '设备正被其他程序使用',
                    OverconstrainedError: '无法满足分辨率要求'
                };
                return errors[error.name] || error.message;
            };

            const updateResolutionStatus = () => {
                el.status.textContent = `当前分辨率: ${el.video.videoWidth}x${el.video.videoHeight}`;
            };

            const showError = (message) => {
                el.errorMsg.textContent = message;
                el.errorMsg.style.display = 'block';
                setTimeout(() => el.errorMsg.style.display = 'none', 5000);
            };

            // 事件处理
            el.capture.addEventListener('click', () => {
                const ctx = el.canvas.getContext('2d');
                [el.canvas.width, el.canvas.height] = [el.video.videoWidth, el.video.videoHeight];

                ctx.save();
                ctx.scale(-1, 1); // 镜像处理
                ctx.drawImage(el.video, -el.canvas.width, 0, el.canvas.width, el.canvas.height);
                ctx.restore();

                el.video.style.display = 'none';
                el.canvas.style.display = 'block';
                el.download.disabled = false;
            });

            el.download.addEventListener('click', () => {
                const link = document.createElement('a');
                link.download = `photo_${Date.now()}.jpg`;
                link.href = el.canvas.toDataURL('image/jpeg', CONFIG.jpegQuality);
                link.click();
            });

            // 清理资源
            window.addEventListener('pagehide', () => {
                mediaStream?.getTracks().forEach(track => track.stop());
            });

            // 初始化
            initCamera();
        })();
    </script>
</body>
</html>
posted @ 2025-03-17 14:38  厚礼蝎  阅读(55)  评论(0)    收藏  举报