昨天已經將video stream添加到video元素中,今天要來繼續實作當用戶按下「拍照鈕」後,會將最新的「video stream snapshot」傳送到canvas元素,接著就可以將放在canvas元素中的圖片存到後台資料庫。
看一下要怎麼實現上面的邏輯吧:
首先要再feed.js中新增一個「當用戶點擊拍照鍵時的event listener」
captureButton.addEventListener('click', function(event) {
canvasElement.style.display = 'block';
videoPlayer.style.display = 'none';
captureButton.style.display = 'none';
var context = canvasElement.getContext('2d');
context.drawImage(videoPlayer, 0, 0, canvas.width, videoPlayer.videoHeight / (videoPlayer.videoWidth / canvas.width));
videoPlayer.srcObject.getVideoTracks().forEach(function(track) {
track.stop();
})
});
在這個監聽器中,針對UI的部分,先將canvas元素顯示出來和video元素隱藏起來,最後也順便將拍照鍵隱藏起來。接著針對canvas這個element,顧名思義這是一個畫布網頁元素,可以在上面放置任何圖像。
所以我在這裡先透過「canvasElement.getContext()
這個方法」來定義要從這個畫布上獲取的內容類型(也就是一個"2d"的影像)。
然後使用drawImage()方法
將影像放置於畫布上,這個方法一共可以輸入5個參數,前3個代表「從座標點(0, 0)開始畫上videoPlayer指定的影像來源」,最後的兩個參數是讓我們在畫布上放置影像的同時可以進行「縮放影像」(這裡我將縮放寬度訂為canvas.width,而高度則是根據video元素的大小成比例縮放)。
最後,記得要將video元素(videoPlayer)給關閉,否則裝置的攝像鏡頭就會一直開著。關閉的方式有很多中,這裡是使用videoPlayer.srcObject.getVideoTracks()
取得所有的video tracks(雖然這裡只有一個track),並使用forEach()來處理回傳的track陣列,針對每個track呼叫stop() method。
目前影像是存放在canvas元素中,那要如何將它儲存成一個檔案並傳送到後台的firebase storage?
首先,要知道儲存在canvas中的影像預設類型是一個base64 url string
要將「base64字串」轉換成「一般檔案類型」,這裡需要引用別人寫好的code(並把它放在utility.js中):
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1]);
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
var blob = new Blob([ab], {type: mimeString});
return blob;
}
接下來在剛剛寫好的「拍照鈕event listener」中,加入下列第二行的程式碼。也就是使用canvasElement.toDataURL()
回傳含有圖像和參數設置特定格式的base64 URI string(預設為PNG格式),最後使用別人寫好的function將圖像轉換為一般檔案類型。
var picture; // 這行要放在global scope中
picture = dataURItoBlob(canvasElement.toDataURL());
還記得我們有在兩個地方(feed.js和sw.js中)有呼叫Firebase Cloud Funcitons,將用戶發佈的貼文資訊傳送到資料庫中嗎?當初傳送時的image欄位都是寫死的,而且傳送的檔案類型都是json format。
不過現在為了要傳送檔案,所以不能再使用json格式了,必須要使用「表單資料(Form Data)」來傳送用戶的發文資料。
先來看一下要怎麼在sw.js中修改,首先在監聽sync事件傳送post data時,要將原本body中的json format改成FormData的格式。
self.addEventListener('sync', function(event) {
console.log('[Service Worker] Background syncing', event);
if(event.tag === 'sync-new-posts') {
console.log('[Service Worker] Syncing new Posts');
event.waitUntil(
readAllData('sync-posts').then(function(data) {
for(var dt of data) {
// 新增表單資料
var postData = new FormData();
// 透過append()來新增key-value資料
postData.append('id', dt.id);
postData.append('title', dt.title);
postData.append('location', dt.location);
postData.append('file', dt.picture, dt.id + '.png');
fetch('https://us-central1-trip-diary-f56de.cloudfunctions.net/storePostData', {
method: 'POST',
body: postData
}).then(function(res) {
console.log('Send data', res);
if(res.ok) {
res.json().then(function(resData) {
deleteItemFromData('sync-posts', resData.id);
});
}
}).catch(function(err) {
console.log('Error while sending data', err);
});
}
})
);
}
});
接著修改在feed.js中的sendData() function,跟剛剛修改的方式相同,將原本body中的json format改成FormData的格式:
function sendData() {
var id = new Date().toISOString();
var postData = new FormData();
postData.append('id', id);
postData.append('title', titleInput.value);
postData.append('location', locationInput.value);
postData.append('file', picture, id + '.png');
fetch('https://us-central1-trip-diary-f56de.cloudfunctions.net/storePostData', {
method: 'POST',
body: postData
}).then(function(res) {
console.log('Send data', res);
// updateUI();
})
}
之前說明過在feed.js中監聽submit event時,會先將「要背景同步的用戶貼文資料」暫時寫入indexedDB,當初並沒有將picture存進去,記得要加上去,否則剛剛在sw.js新增的FormData中,dt.picture
就會是null:
... 上方省略 ...
if('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.ready.then(function(sw) {
var post = {
id: new Date().toISOString(),
title: titleInput.value,
location: locationInput.value,
picture: picture
};
writeData('sync-posts', post).then(function() {
return sw.sync.register('sync-new-posts');
})
... 下方省略 ...
最後要開始在Firebase Cloud Funcitons中針對剛剛傳入的FormData進行處理
首先,要先安裝一些等一下會使用到的套件:
npm install --save busboy @google-cloud/storage@^1.2.1 uuid-v4
接下來在functions/index.js
中,導入上述的這些模組吧:
var fs = require('fs'); // node原生模組,是用來操作(read/write)實體檔案
var UUID = require('uuid-v4');
var Busboy = require('busboy');
var os = require('os');
var path = require('path');
在導入google cloud storage模組前要先初始化一些設定,包話「專案ID」和「專案的私鑰資訊」
var gcconfig = {
projectId: 'trip-diary-f56de',
keyFilename: 'trip-diary-firebase-key.json'
};
var gcs = require('@google-cloud/storage')(gcconfig);
終於可以開始處理傳入的FormData,這裡我直接在程式碼註解中一行行說明:
exports.storePostData = functions.https.onRequest((request, response) => {
cors(request, response, function() {
// 產生唯一識別的亂數
var uuid = UUID();
// request為node的原生請求
const busboy = new Busboy({ headers: request.headers });
// 之後會定義要上傳的檔案路徑和類型
let upload;
// 建立fields object,之後將解析完成的request field加入
const fields = {};
// 開始監聽file解析事件(也就是用戶拍照的圖片)
busboy.on("file", (fieldname, file, filename, encoding, mimetype) => {
// 定義file要保存的路徑
const filepath = path.join(os.tmpdir(), filename);
// 定義要上傳的檔案路徑和類型
upload = { file: filepath, type: mimetype };
// 將圖片(file)保存到特定路徑
file.pipe(fs.createWriteStream(filepath));
});
// 開始監聽request中的表單欄位(field)
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
fields[fieldname] = val;
});
// 監聽結束事件
// 也就是當解析完POST request後,要開始上傳到google cloud storage的bucket了
busboy.on("finish", () => {
var bucket = gcs.bucket("trip-diary-f56de.appspot.com");
bucket.upload(
upload.file, // 要上傳的檔案路徑
{
uploadType: "media",
metadata: {
metadata: {
contentType: upload.type, // 要上傳的檔案類型
firebaseStorageDownloadTokens: uuid // 圖片下載連結的唯一識別碼
}
}
},
// 上傳完成後,執行該callback function
function(err, uploadedFile) {
if(!err) {
// 若無錯誤,開始執行原本push notification的程式碼
// 將push到資料庫的欄位,將「request.body」改成解析完成的「fields」
admin.database().ref('posts').push({
id: fields.id,
title: fields.title,
location: fields.location,
image: "https://firebasestorage.googleapis.com/v0/b/" + bucket.name + "/o/" + encodeURIComponent(uploadedFile.name) + "?alt=media&token=" + uuid
}).then(function() {
webpush.setVapidDetails('mailto:j84077200345@gmail.com', 'BDIOql6aKK-00AGzVKggeN9LSpjGd2golLzuiCvmUG0NAIa3wi-FmG17HElLHhXtzQBQQ9faZmJ2MWW87VI8bgg', 'JxB633wEwprQT3hahwrNPoimHshPRj0Kd9OK11IXlQ8');
return admin.database().ref('subscriptions').once('value');
}).then(function(subscriptions) {
subscriptions.forEach(function(sub) {
var pushConfig = {
endpoint: sub.val().endpoint,
keys: {
auth: sub.val().keys.auth,
p256dh: sub.val().keys.p256dh
}
};
webpush.sendNotification(pushConfig, JSON.stringify({title: '新貼文', content: '有新增的貼文!!', openUrl: '/'})).catch(function(err) {
console.log(err);
});
});
response.status(201).json({message: 'Data Stored', id: fields.id});
}).catch(function(err) {
response.status(500).json({error: err});
})
} else {
console.log(err);
}
}
);
});
busboy.end(request.rawBody);
});
});
最後如果用戶沒有攝像鏡頭的話,我們有一個image picker來讓用戶選擇想要的圖片上傳。這個實作其實非常簡單,來看一下在feed.js中要怎麼寫:
imagePicker.addEventListener('change', function(event) {
picture = event.target.files[0];
});
監聽imagePicker改變(change)的事件,當用戶選好檔案,可以使用event.target.files[0]
來取得選擇的檔案array(這裡我預設只有選擇一張圖片)。
Day26 結束!!