BrowserCameraService.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import {
  2. ICameraService,
  3. MediaStreamConstraints,
  4. } from './CameraService';
  5. /**
  6. * 浏览器环境下的摄像头服务实现
  7. * 使用标准的 Web API
  8. */
  9. export class BrowserCameraService implements ICameraService {
  10. /**
  11. * 请求摄像头权限
  12. * @returns 是否成功获取权限
  13. */
  14. async requestPermission(): Promise<boolean> {
  15. try {
  16. // 检查是否在安全上下文中
  17. if (!window.isSecureContext) {
  18. throw new Error(
  19. '摄像头访问需要安全上下文(HTTPS)。' +
  20. '请使用 HTTPS 访问页面,或在 localhost 环境下测试。' +
  21. `当前访问地址:${window.location.href}`
  22. );
  23. }
  24. // 检查浏览器 API 支持
  25. if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
  26. throw new Error('浏览器不支持摄像头 API,请使用最新版浏览器');
  27. }
  28. // 检查是否有摄像头设备
  29. const devices = await navigator.mediaDevices.enumerateDevices();
  30. const hasCamera = devices.some(device => device.kind === 'videoinput');
  31. if (!hasCamera) {
  32. throw new Error('未检测到摄像头设备,请连接摄像头后重试');
  33. }
  34. // 请求权限
  35. const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  36. stream.getTracks().forEach(track => track.stop());
  37. return true;
  38. } catch (error: any) {
  39. console.error('摄像头权限请求失败:', error);
  40. throw error;
  41. }
  42. }
  43. /**
  44. * 获取媒体流
  45. * @param constraints 媒体约束条件
  46. * @returns MediaStream 对象
  47. */
  48. async getMediaStream(
  49. constraints: MediaStreamConstraints = { video: true }
  50. ): Promise<MediaStream> {
  51. try {
  52. // 检查浏览器支持
  53. if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
  54. throw new Error('当前浏览器不支持摄像头访问');
  55. }
  56. // 获取媒体流
  57. const stream = await navigator.mediaDevices.getUserMedia(
  58. constraints as globalThis.MediaStreamConstraints
  59. );
  60. return stream;
  61. } catch (error: any) {
  62. // 处理不同的错误类型
  63. if (error.name === 'NotAllowedError') {
  64. throw new Error('用户拒绝了摄像头权限');
  65. } else if (error.name === 'NotFoundError') {
  66. throw new Error('未找到摄像头设备');
  67. } else if (error.name === 'NotReadableError') {
  68. throw new Error('摄像头正被其他应用使用');
  69. } else {
  70. throw new Error(`获取摄像头失败: ${error.message}`);
  71. }
  72. }
  73. }
  74. /**
  75. * 从媒体流中捕获照片
  76. * @param stream 媒体流对象
  77. * @returns base64 格式的图片数据
  78. */
  79. async capturePhoto(stream: MediaStream): Promise<string> {
  80. return new Promise((resolve, reject) => {
  81. try {
  82. // 创建 video 元素
  83. const video = document.createElement('video');
  84. video.srcObject = stream;
  85. video.autoplay = true;
  86. video.playsInline = true; // iOS 需要
  87. video.onloadedmetadata = () => {
  88. // 等待视频准备好
  89. video.play();
  90. // 创建 canvas
  91. const canvas = document.createElement('canvas');
  92. canvas.width = video.videoWidth;
  93. canvas.height = video.videoHeight;
  94. // 绘制当前帧
  95. const ctx = canvas.getContext('2d');
  96. if (!ctx) {
  97. reject(new Error('无法获取 Canvas 2D Context'));
  98. return;
  99. }
  100. ctx.drawImage(video, 0, 0);
  101. // 转换为 base64
  102. const base64 = canvas.toDataURL('image/jpeg', 0.8);
  103. // 清理
  104. video.srcObject = null;
  105. resolve(base64);
  106. };
  107. video.onerror = () => {
  108. reject(new Error('视频加载失败'));
  109. };
  110. } catch (error) {
  111. reject(error);
  112. }
  113. });
  114. }
  115. /**
  116. * 停止媒体流
  117. * @param stream 要停止的媒体流
  118. */
  119. stopStream(stream: MediaStream): void {
  120. if (stream) {
  121. stream.getTracks().forEach((track) => {
  122. track.stop();
  123. });
  124. }
  125. }
  126. }