手機端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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...