| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- import {
- ICameraService,
- MediaStreamConstraints,
- } from './CameraService';
- /**
- * 浏览器环境下的摄像头服务实现
- * 使用标准的 Web API
- */
- export class BrowserCameraService implements ICameraService {
- /**
- * 请求摄像头权限
- * @returns 是否成功获取权限
- */
- async requestPermission(): Promise<boolean> {
- try {
- // 检查是否在安全上下文中
- if (!window.isSecureContext) {
- throw new Error(
- '摄像头访问需要安全上下文(HTTPS)。' +
- '请使用 HTTPS 访问页面,或在 localhost 环境下测试。' +
- `当前访问地址:${window.location.href}`
- );
- }
- // 检查浏览器 API 支持
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
- throw new Error('浏览器不支持摄像头 API,请使用最新版浏览器');
- }
- // 检查是否有摄像头设备
- const devices = await navigator.mediaDevices.enumerateDevices();
- const hasCamera = devices.some(device => device.kind === 'videoinput');
- if (!hasCamera) {
- throw new Error('未检测到摄像头设备,请连接摄像头后重试');
- }
- // 请求权限
- const stream = await navigator.mediaDevices.getUserMedia({ video: true });
- stream.getTracks().forEach(track => track.stop());
- return true;
- } catch (error: any) {
- console.error('摄像头权限请求失败:', error);
- throw error;
- }
- }
- /**
- * 获取媒体流
- * @param constraints 媒体约束条件
- * @returns MediaStream 对象
- */
- async getMediaStream(
- constraints: MediaStreamConstraints = { video: true }
- ): Promise<MediaStream> {
- try {
- // 检查浏览器支持
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
- throw new Error('当前浏览器不支持摄像头访问');
- }
- // 获取媒体流
- const stream = await navigator.mediaDevices.getUserMedia(
- constraints as globalThis.MediaStreamConstraints
- );
- return stream;
- } catch (error: any) {
- // 处理不同的错误类型
- if (error.name === 'NotAllowedError') {
- throw new Error('用户拒绝了摄像头权限');
- } else if (error.name === 'NotFoundError') {
- throw new Error('未找到摄像头设备');
- } else if (error.name === 'NotReadableError') {
- throw new Error('摄像头正被其他应用使用');
- } else {
- throw new Error(`获取摄像头失败: ${error.message}`);
- }
- }
- }
- /**
- * 从媒体流中捕获照片
- * @param stream 媒体流对象
- * @returns base64 格式的图片数据
- */
- async capturePhoto(stream: MediaStream): Promise<string> {
- return new Promise((resolve, reject) => {
- try {
- // 创建 video 元素
- const video = document.createElement('video');
- video.srcObject = stream;
- video.autoplay = true;
- video.playsInline = true; // iOS 需要
- video.onloadedmetadata = () => {
- // 等待视频准备好
- video.play();
- // 创建 canvas
- const canvas = document.createElement('canvas');
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
- // 绘制当前帧
- const ctx = canvas.getContext('2d');
- if (!ctx) {
- reject(new Error('无法获取 Canvas 2D Context'));
- return;
- }
- ctx.drawImage(video, 0, 0);
- // 转换为 base64
- const base64 = canvas.toDataURL('image/jpeg', 0.8);
- // 清理
- video.srcObject = null;
- resolve(base64);
- };
- video.onerror = () => {
- reject(new Error('视频加载失败'));
- };
- } catch (error) {
- reject(error);
- }
- });
- }
- /**
- * 停止媒体流
- * @param stream 要停止的媒体流
- */
- stopStream(stream: MediaStream): void {
- if (stream) {
- stream.getTracks().forEach((track) => {
- track.stop();
- });
- }
- }
- }
|