手機端 H5 實現自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實現,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,並將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ...
手機端 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>
最終效果展示:
作者:yuzhihui出處:http://www.cnblogs.com/yuzhihui/ 聲明:歡迎任何形式的轉載,但請務必註明出處!!!