Electron 應用中實現調用外接攝像頭並拍照上傳

背景

基於Electron實現的 pc 端智能驗機應用,近期迭代了一個新的功能,需求是通過電腦外接攝像頭對手機屏幕進行拍照,拍照後需將照片上傳至服務端進行屏幕信息比對,確定被檢測屏幕是否爲原廠屏。

需求分析

根據上面的需求,分析大概要以下幾個步驟。

  1. 先實現將攝像頭的畫面實時展示在頁面視頻採集區域中;

  2. 將攝像頭中的視頻畫面採集一幀成圖片並回顯;

  3. 將生成的圖片上傳至 CDN 拿到圖片鏈接;

  4. 將圖片鏈接上傳到後端接口做處理;

確定了需要以上四個步驟後,接下來一步一步實現。

實現

視頻採集

由於 Electron 內置了 Chromium 瀏覽器,該瀏覽器對各項前端標準都支持得非常好,所以基於 Electron 開發應用不會遇到瀏覽器兼容性問題。幾乎可以在 Electron 中使用所有 HTML5CSS3ES6 標準中定義的 API

所以基於WebRTC提供的API即可獲取到攝像頭的視頻流。

MediaDevices.getUserMedia()

代碼如下:

methods: {
    getUserMedia() {
        /* 可同時開啓video(攝像頭)和audio(麥克風) 這裏只請求攝像頭,所以只設置video爲true */
        navigator.mediaDevices.getUserMedia({ video: true })
            .then(function(stream) {
              /* 使用這個 stream 傳遞到成功回調中 */
              this.success(stream)
            })
            .catch(function(err) {
              /* 處理 error 信息 */
              this.error(error)
            });
    }
}

MediaDevices.getUserMedia() 會提示用戶給予使用媒體輸入的許可,媒體輸入會產生一個MediaStream,裏面包含了請求的媒體類型的軌道。此流可以包含一個視頻軌道(來自硬件或者虛擬視頻源,比如相機、視頻採集設備和屏幕共享服務等等)、一個音頻軌道(同樣來自硬件或虛擬音頻源,比如麥克風、A/D 轉換器等等),也可能是其它軌道類型。

它返回一個 Promise 對象,成功後會resolve回調一個 MediaStream 對象。若找不到滿足請求參數的媒體類型,promisereject回調一個NotFoundError

現在已經成功獲取到視頻流,接下來就是將視頻流回顯到頁面。這裏使用 video 標籤完成,代碼如下:

<template>
    <div class="video-page">
        <div class="video-content">
            <video ref="video" class="video-item"></video>
        </div>
    </div>
</template>

export default {
   methods: {
       getUserMedia() {
            /* 可同時開啓video(攝像頭)和audio(麥克風) 這裏只請求攝像頭,所以只設置video爲true */
            navigator.mediaDevices.getUserMedia({ video: true })
                .then(function(stream) {
                  /* 使用這個 stream 傳遞到成功回調中 */
                  this.success(stream)
                })
                .catch(function(err) {
                  /* 處理 error 信息 */
                  this.error(error)
                });
        },
       success(stream) {
           console.log('成功', stream);
           /* 將stream 分配給video標籤 */
           this.$refs.video.srcObject = stream;
           this.$refs.video.play();
        }
    }
}

這時,攝像頭中的畫面就可以顯示在頁面 video 標籤內,如下圖。

爲了用戶體驗,在進入頁面之前添加了判斷攝像頭是否已經接入並可用的邏輯,避免用戶的攝像頭未接入或者啓動,造成應用不可用的錯覺。

使用MediaDevices.enumerateDevices()來獲取可用媒體輸入和輸出設備的列表,例如攝像頭、麥克風、耳機等。

navigator.mediaDevices.enumerateDevices().then(devicesList ={
    console.log('------devicesList', deviceList)
})

得到的設備列表數據格式如下:

kind類型有三種,分別是audioinputaudiooutputvideoinput。分別代表音視頻的輸入和輸出。可在列表中查找目標媒體是否已經接入且可用。

若有選擇切換設備需求,可根據kind類型進行媒體設備分類,選擇目標deviceId,傳入navigator.mediaDevices.getUserMedia,完成來源切換。

 navigator.mediaDevices.getUserMedia({ video: { deviceId: xxxx } })

拍照生成圖片

拍照其實就是截取視頻中的某一幀,這裏使用canvas來實現截取。getContext() 方法可返回一個對象,該對象提供了用於在畫布上繪圖的方法和屬性。其中drawImage()方法用來向畫布上繪製圖像、畫布或視頻。

<template>
    <div class="video-page">
        <div class="video-content">
            <video ref="video" class="video-item" v-if="showVideo"></video>
            <canvas ref="canvas" v-else width="500" height="346"></canvas>
        <div class="video-buttons">
            <div @click="capture" class="button-item capture">拍照</div>
            <div @click="submit" class="button-item submit"}">提交</div>
        </div>
    </div>
</template>

export default {
   data: {
       showVideo: true, // 是否展示攝像頭畫面
   },
   methods: {
        /* 拍照按鈕點擊 */
        capture() {
          this.showVideo = false
          var context = this.$refs.canvas.getContext('2d');
          /* 要跟video的寬高一致 */
          context.drawImage(this.$refs.video, 0, 0, 1000, 692, 0, 0, 500, 346);
        }
    }
}

拍照的圖片回顯至 canvas 標籤。

上傳圖片至 CDN

上個步驟已經完成了拍照,接下來就需要將圖片上傳至 CDN,拿到圖片鏈接。這裏有兩種方式可以實現獲取圖片數據。

1. 使用HTMLCanvasElement.toBlob()

HTMLCanvasElement.toBlob() 方法生成 Blob 對象,用以展示 canvas 上的圖片。因爲直接可以拿到圖片文件,所以無需再使用方法 2 中的函數來轉化base64,直接可以獲取到圖片文件用來上傳。

語法
toBlob(callback, type, quality)
參數

callback:回調函數,參數爲Blob對象(目標圖片文件)。

type:圖片格式,默認爲image/png 可選

quality:0-1 的數字,表示圖片質量,可選

點擊提交按鈕按鈕時,先獲取圖片文件,爲上傳做準備。

methods: {
    /* 提交按鈕點擊 */
    submit() {
        const base64Url = this.$refs.canvas.toBlob(blob ={
            console.log('===blob', blob)
            const data = new FormData()
            data.append('file', blob)
            request.post('https://XXXXX/upload', data)
        }"image/jpeg", 0.95)
    }
}

console 的結果如下圖:

2. 使用HTMLCanvasElement.toDataURL()

HTMLCanvasElement.toDataURL() 方法返回一個包含圖片展示的 Data URL。

Data URL,即前綴爲 data: 協議的 URL,其允許內容創建者向文檔中嵌入小文件。

語法
canvas.toDataURL(type, encoderOptions);
參數

type 圖片格式,默認爲image/png

encoderOptions 0 到 1 之間的值,用來選定圖片質量,默認值是 0.92,超出範圍會使用默認值。

返回值

base64組成的圖片源數據,上傳前需轉爲圖片文件。這裏封裝了一個convertBase64UrlToImgFile函數用來轉換。代碼如下:

<template>
    <div class="video-page">
        <div class="video-content">
            <video ref="video" class="video-item" v-if="showVideo"></video>
            <canvas ref="canvas" v-else width="500" height="346"></canvas>
        <div class="video-buttons">
            <div @click="capture" class="button-item capture">拍照</div>
            <div @click="submit" class="button-item submit">提交</div>
        </div>
    </div>
</template>

export default {
   data: {
       /* 是否展示攝像頭畫面 */
       showVideo: true,
   },
   methods: {
        /* 將base64轉爲圖片文件 */
        convertBase64UrlToImgFile(urlData, fileType) {
            const imgData = urlData.split('base64,').splice(-1)[0]
            /* 解碼使用 base-64 編碼的字符串 轉換爲byte */
            const bytes = window.atob(imgData)
            
            /* 處理異常,將ASCII碼小於0的轉換爲大於0 */
            const ab = new ArrayBuffer(bytes.length)
            const ia = new Int8Array(ab)
            
            for (let i = 0; i < bytes.length; i++) {
                ia[i] = bytes.charCodeAt(i)
            }
            
            /* 轉換成文件,可以添加文件的type,lastModifiedDate屬性 */
            const blob = new Blob([ab]{ type: fileType })
            blob.lastModifiedDate = new Date()
            return blob
        },
        /* 提交按鈕點擊 */
        async submit() {
            const base64Url = this.$refs.canvas.toDataURL()
            const imgFile = this.convertBase64UrlToImgFile(base64Url, 'image/jpg')
            console.log('====imgFile', imgFile)
            const data = new FormData()
            data.append('file', imgFile)
            /* 上傳 */
            request.post('https://XXXXX/upload', data)
        },
    }
}

convertBase64UrlToImgFile可用於在使用canvas外的場景進行base64轉換圖片文件。和HTMLCanvasElement.toBlob()方法得到的結果一致。

以上兩種方法都可以完成圖片上傳,最終拿到 CDN 圖片鏈接後可傳給後端進行處理。獲取屏幕信息。

總結

通過以上四個步驟就完成了 Electron 應用中通過外接攝像頭拍照並上傳的功能。這裏基本用不到 Electron 的能力,和在 web 端的實現方式並無區別,Electron 在這裏起到的作用就是獲取攝像頭媒體流不需要獲取用戶權限。

Electron是基於ChromiumNode.js實現的,這就使前端開發者可以使用JavaScriptHTMLCSS輕鬆構建跨平臺的桌面應用。Electron可以使用幾乎所有的 Web 前端生態領域及Node.js生態領域的組件和技術方案。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Yb590oc6u94feS9s7FZWrQ