手机端H5 实现自定义拍照界面

  • 手机端H5 实现自定义拍照界面已关闭评论
  • 126 次浏览
  • A+
所属分类:Web前端
摘要

手机端 H5 实现自定义拍照界面也可以使用 MediaDevices API 和 <video> 标签来实现,和在桌面端做法基本一致。

手机端 H5 实现自定义拍照界面也可以使用 MediaDevices API 和 <video> 标签来实现,和在桌面端做法基本一致。

首先,使用 MediaDevices.getUserMedia() 方法获取摄像头媒体流,并将其传递给 <video> 标签进行渲染。

接着,使用 HTML 的 <canvas> 标签来截取当前摄像头的画面,通过 <canvas> 上的 getContext('2d') 方法来绘制。

最后,使用 canvas.toDataURL() 方法将图像转换为 base64 格式,可以通过将其保存到本地或发送到服务器来存储照片。

但是需要注意的是,在手机端,调用摄像头需要在 HTTPS 或 localhost 下访问,还需要用户事先进行授权。

且在手机端可能会有些浏览器对于getUserMedia有所限制,需要额外兼容性处理。且手机端的实现需要考虑屏幕的方向,在绘制截图时需要根据不同的屏幕方向调整画布尺寸。

在手机端,为了让用户能够在页面中手动切换摄像头,需要检测手机端设备是否有多个摄像头,在有多个摄像头时,提供给用户切换摄像头的选项。

此外,需要进行一些兼容性处理,以便在不同浏览器和手机设备上正常工作。同时,需要考虑手机端的交互体验,例如提供给用户切换摄像头和调整照片尺寸的选项。

对于一些高级功能,例如人脸检测和识别,美颜,以及其他高级图像处理功能可以使用第三方库,如openCV.js,tracking.js, face-api.js等来实现。

还可以使用框架,如 React Native, Ionic, PhoneGap 等更加轻松地在移动端实现相关功能。

总之,通过使用 MediaDevices API 和 <video> 标签在手机端实现自定义拍照界面是可行的,但是需要注意的点比桌面端多一些。虽然在手机端实现自定义拍照界面有一定的挑战,但是通过使用 MediaDevices API 和相关第三方库,还有经验丰富的前端工程师在这个问题上是有解决方案的。

一、实现示例框架代码

<video id="camera" width="640" height="480" autoplay></video> <button id="invoking" onclick="invokingCamera">invoking Camera</button> <button id="snapshot" onclick="takeSnapshot">Take snapshot</button>

 使用 MediaDevices.getUserMedia() 方法获取摄像头媒体流,并将其传递给 <video> 标签进行渲染

// 调用摄像头 function invokingCamera() {     // 注意本例需要在HTTPS协议网站中运行,新版本Chrome中getUserMedia接口在http下不再支持。      // 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象     if (navigator.mediaDevices === undefined) {         navigator.mediaDevices = {};     }      // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia      // 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。     if (navigator.mediaDevices.getUserMedia === undefined) {         navigator.mediaDevices.getUserMedia = function (constraints) {             // 首先,如果有getUserMedia的话,就获得它             const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||                 navigator.mozGetUserMedia;              // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口             if (!getUserMedia) {                 return Promise.reject(new Error(                     'getUserMedia is not implemented in this browser'));             }              // 否则,为老的navigator.getUserMedia方法包裹一个Promise             return new Promise(function (resolve, reject) {                 getUserMedia.call(navigator, constraints, resolve, reject);             });         }     }      // 手机可视区域宽度(请通过相关API获取真实宽度)     const windowWidth = 375;     // 手机可视区域高度(请通过相关API获取真实高度)     const windowHeight = 700;      const constraints = {         audio: false,         video: {             // 前置摄像头             facingMode: 'user',             // 该属性相当于手机端的高             width: Math.max(windowWidth, windowHeight) - 120,   // 减去 120 用于在页面底部放置拍照等功能按钮             // 该属性相当于手机端的宽             height: Math.min(windowWidth, windowHeight),         }     };      navigator.mediaDevices.getUserMedia(constraints)         .then(function (stream) {             const video = document.querySelector('camera');             // 旧的浏览器可能没有srcObject             if ("srcObject" in video) {                 video.srcObject = stream;             } else {                 // 防止在新的浏览器里使用它,应为它已经不再支持了                 video.src = window.URL.createObjectURL(stream);             }             video.onloadedmetadata = function (e) {                 video.play();             };         })         .catch(function (err) {             console.log(err.name + ": " + err.message);          }); }

 使用 HTML 的 <canvas> 标签来截取当前摄像头的画面,通过 <canvas> 上的 getContext('2d') 方法来绘制

function takeSnapshot() {     const canvas = document.createElement('canvas');     const ctx = canvas.getContext('2d');     const video = document.querySelector('video');     canvas.width = Math.min(video.videoWidth, video.videoHeight);     canvas.height = Math.max(video.videoWidth, video.videoHeight);     ctx.drawImage(video, 0, 0, canvas.width, canvas.height);      // ****** 镜像处理 ******     function getPixel(imageData, row, column) {         const uint8ClampedArray = imageData.data;         const width = imageData.width;         const height = imageData.height;         const pixel = [];         for (let i = 0; i < 4; i++) {             pixel.push(uint8ClampedArray[row * width * 4 + column * 4 + i]);         }         return pixel;     }      function setPixel(imageData, row, column, pixel) {         const uint8ClampedArray = imageData.data;         const width = imageData.width;         const height = imageData.height;         for (let i = 0; i < 4; i++) {             uint8ClampedArray[row * width * 4 + column * 4 + i] = pixel[i];         }     }      const mirrorImageData = ctx.createImageData(canvas.width, canvas.height);     const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);     for (let h = 0; h < canvas.height; h++) {         for (let w = 0; w < canvas.width; w++) {             const pixel = getPixel(imageData, h, canvas.width - w - 1);             setPixel(mirrorImageData, h, w, pixel);         }     }     ctx.putImageData(mirrorImageData, 0, 0);     // ****** 镜像处理 ******      const base64 = canvas.toDataURL('image/jpeg'); }

最后,使用 canvas.toDataURL() 方法将图像转换为 base64 格式,可以通过将其保存到本地或发送到服务器来存储照片

二、具体实现代码(基于uni-app)

布局代码:

<template>     <view class="" style="width: 100vw;height: 100vh;background-color: #000;">         <video style="width: 100vw;height:calc(100vh - 240rpx);" object-fit="fill"></video>         <view class=""               style="width: 100vw;height: 100vh;background-color: transparent;opacity: 1;position: absolute;top: 0; left: 0;z-index: 1;">             <view class="flex justify-center align-center"                   style="width: 100vw;height: 100vh;position: absolute;top: 0; left: 0;z-index: 2;">                 <image :src="qjkImgSrc" mode="widthFix" style="width: 600rpx;margin-top: -200rpx;"></image>             </view>             <view class="flex justify-center align-center"                   style="width: 100vw;height: 100vh;position: absolute;top: 0; left: 0;z-index: 3;">                 <image :src="qjtxkImgSrc" mode="widthFix" style="width: 500rpx;margin-top: -200rpx;"></image>             </view>              <view class="flex justify-center"                   style="width: 100vw;height: 100vh;position: absolute;top: 0; left: 0;z-index: 5;">                 <view class=""                       style="position: absolute;bottom: 88rpx;left: 68rpx; color: #fff;font-weight: bold;background: #fff;border-radius: 16rpx;">                     <uni-icons type="close" :size="32" @click="handlePhotographCloseClick">                     </uni-icons>                 </view>                 <view class="outer-ring" style="position: absolute;bottom: 40rpx;" @click="handlePhotographClick">                     <view class="middle-ring">                         <view class="inner-ring"></view>                     </view>                 </view>                 <view class=""                       style="position: absolute;bottom: 88rpx;right: 68rpx; color: #fff;font-weight: bold;background: #fff;border-radius: 16rpx;">                     <uni-icons type="folder-add" :size="32" @click="handleAddPhotographClick">                     </uni-icons>                 </view>             </view>         </view>     </view> </template>

 JavaScript 代码:

 export default {     data() {         return {             imageUrl: "",             // 媒体流,用于关闭摄像头             mediaStreamTrack: null,         };     },     onLoad() {         this.invokingCamera();     },     onUnload() {         this.handlePhotographCloseClick();     },     methods: {         invokingCamera() {             const self = this;             // 注意本例需要在HTTPS协议网站中运行,新版本Chrome中getUserMedia接口在http下不再支持。              // 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象             if (navigator.mediaDevices === undefined) {                 navigator.mediaDevices = {};             }              // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia              // 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。             if (navigator.mediaDevices.getUserMedia === undefined) {                 navigator.mediaDevices.getUserMedia = function (constraints) {                     // 首先,如果有getUserMedia的话,就获得它                     const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||                         navigator.mozGetUserMedia;                      // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口                     if (!getUserMedia) {                         return Promise.reject(new Error(                             'getUserMedia is not implemented in this browser'));                     }                      // 否则,为老的navigator.getUserMedia方法包裹一个Promise                     return new Promise(function (resolve, reject) {                         getUserMedia.call(navigator, constraints, resolve, reject);                     });                 }             }              uni.getSystemInfo({                 success: function (res) {                     const constraints = {                         audio: false,                         video: {                             // 前置摄像头                             facingMode: 'user',                             // 手机端相当于高                             width: Math.max(res.windowWidth, res.windowHeight) - 120,                             // 手机端相当于宽                             height: Math.min(res.windowWidth, res.windowHeight),                         }                     };                      navigator.mediaDevices.getUserMedia(constraints)                         .then(function (stream) {                             self.mediaStreamTrack = stream;                              const video = document.querySelector('video');                             // 旧的浏览器可能没有srcObject                             if ("srcObject" in video) {                                 video.srcObject = stream;                             } else {                                 // 防止在新的浏览器里使用它,应为它已经不再支持了                                 video.src = window.URL.createObjectURL(stream);                             }                             video.onloadedmetadata = function (e) {                                 video.play();                             };                         })                         .catch(function (err) {                             console.log(err.name + ": " + err.message);                         });                 }             });         },         handlePhotographCloseClick() {             if (this.mediaStreamTrack) {                 // 关闭摄像头                 this.mediaStreamTrack.getTracks().forEach(function (track) {                     track.stop();                 });                 this.mediaStreamTrack = null;             }         },         handlePhotographClick() {             const self = this;             const canvas = document.createElement('canvas');             const ctx = canvas.getContext('2d');             const video = document.querySelector('video');             canvas.width = Math.min(video.videoWidth, video.videoHeight);             canvas.height = Math.max(video.videoWidth, video.videoHeight);             ctx.drawImage(video, 0, 0, canvas.width, canvas.height);              // ****** 镜像处理 ******             function getPixel(imageData, row, column) {                 const uint8ClampedArray = imageData.data;                 const width = imageData.width;                 const height = imageData.height;                 const pixel = [];                 for (let i = 0; i < 4; i++) {                     pixel.push(uint8ClampedArray[row * width * 4 + column * 4 + i]);                 }                 return pixel;             }              function setPixel(imageData, row, column, pixel) {                 const uint8ClampedArray = imageData.data;                 const width = imageData.width;                 const height = imageData.height;                 for (let i = 0; i < 4; i++) {                     uint8ClampedArray[row * width * 4 + column * 4 + i] = pixel[i];                 }             }              const mirrorImageData = ctx.createImageData(canvas.width, canvas.height);             const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);             for (let h = 0; h < canvas.height; h++) {                 for (let w = 0; w < canvas.width; w++) {                     const pixel = getPixel(imageData, h, canvas.width - w - 1);                     setPixel(mirrorImageData, h, w, pixel);                 }             }             ctx.putImageData(mirrorImageData, 0, 0);             // ****** 镜像处理 ******              self.$nextTick(() => {                 const base64 = canvas.toDataURL('image/jpeg');                 self.imageUrl = base64;                 self.handlePhotographCloseClick();             });         },         handleAddPhotographClick() {             this.uploadImage();         },         uploadImage: function () {             const self = this;             uni.chooseImage({                 count: 1,                 sizeType: ['compressed'],                 success: function (res) {                     self.handlePhotographCloseClick();                     const file = res.tempFiles[0];                      const reader = new FileReader();                     reader.readAsDataURL(file);                     reader.onload = function (e) {                         self.imageUrl = e.target.result;                     }                 }             });         },     } };

样式代码:

<style scoped> 	video { 		transform: rotateY(180deg); 		-webkit-transform: rotateY(180deg); 		/* Safari 和 Chrome */ 		-moz-transform: rotateY(180deg); 	}  	/deep/ .uni-video-bar { 		display: none; 	}  	/deep/ .uni-video-cover { 		display: none; 	}  	.outer-ring { 		width: 160rpx; 		height: 160rpx; 		border-radius: 50%; 		/* background-color: #40ff2e; */ 		background-color: #fff; 		display: flex; 		justify-content: center; 		align-items: center; 	}  	.middle-ring { 		width: 150rpx; 		height: 150rpx; 		border-radius: 50%; 		background-color: #000000; 		/* background-color: #fff; */ 		/* position: absolute; 		top: 10rpx; 		left: 10rpx; */ 		display: flex; 		justify-content: center; 		align-items: center; 	}  	.inner-ring { 		width: 140rpx; 		height: 140rpx; 		border-radius: 50%; 		/* background-color: #009966; */ 		background-color: #fff; 		/* position: relative; 		top: 10rpx; 		left: 10rpx; */ 	} </style>

最终效果展示:

手机端H5 实现自定义拍照界面