手機端H5 實現自定義拍照界面

来源:https://www.cnblogs.com/yuzhihui/archive/2023/01/11/17044075.html
-Advertisement-
Play Games

手機端 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/ 聲明:歡迎任何形式的轉載,但請務必註明出處!!!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在使用MybatisPlus分頁功能時發現:前端查詢第一頁是沒問題的,但是向後查詢的時候數據始終是第一頁的 查詢第一頁的時候發現沒有任何問題 往後查詢,比如查詢第二頁時數據和第一頁一樣 開始以為是代碼問題,把sql單獨拿出來執行發現還是會有重覆數據 上網查詢發現是因為ORDER BY排序的欄位有重覆 ...
  • 分享概要 一、業務場景 二、架構演進 三、架構設計 四、穩定性 五、效率 一、業務場景 在開始講解之前,我先為大家介紹一下B站的業務場景。B站的業務大體上可以分為以下幾類: 1、點播類業務 點播類業務就是大家經常看的視頻以及稿件之類相關的業務,這類數據使用場景的特點有: 數據一致性要求較高 耗時敏感 ...
  • 前言 最近閱讀 Catcher、BugSnag、Rollbar 三個 Flutter 異常監控開源框架,文章鏈接如下: Flutter 異常監控 - 壹 | 從 Zone 說起 Flutter 異常監控 - 貳 | 框架 Catcher 原理分析 Flutter 異常監控 - 叄 | 從 bugsn ...
  • 一、推送成功收不到消息,推送返回:{"message":"success","requestID":"1523868*****2842718","resultcode":0} 排查步驟: 1、網路不穩定,切換穩定網路進行測試; 2、檢查手機是否為EMUI8.0.0系統,如果是早期的EMUI8.0,則 ...
  • Vue.js是一個漸進式的JavaScript框架,它使用了響應式系統來維護應用程式的狀態。響應式系統是Vue.js的核心部分,它使得應用程式能夠自動地更新視圖,當數據發生變化時。 ...
  • Vue04 12.Vue2 腳手架模塊化開發 目前開發模式的問題: 開發效率低 不夠規範 維護和升級,可讀性比較差 12.1基本介紹 官網地址 什麼是Vue Cli腳手架 12.2環境配置,搭建項目 VUE安裝教程+VScode配置 搭建Vue腳手架工程,需要用到NPM(node package m ...
  • Flexbox 是 CSS3 中的一種佈局模式。它允許元素在一個容器中自動排列,可以使用靈活的方式創建複雜的佈局。Flex 佈局有很多優點,例如,它很容易實現響應式設計,並且可以很容易地對齊和排列元素。 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 在當下前後端分離的主流環境下,前端部分的優化變得越來越重要。為了提升前端的性能和用戶體驗,我覺得可能需要從三個維度採集數據進行分析。 前端埋點。通過埋點收集和統計網頁的UV/PV、設備型號、瀏覽器等數據進行分析,比如可以有針對性對使 ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...