什麼是服務端錄製,通俗來講就是在服務器上把網站錄製下來,包括網站的 聲音、動做、刷新、跳轉 等。並保存成一個視頻文件node
經過虛擬桌面 xvfb
技術啓動 Puppeteer
,Puppeteer
打開 Chrome
,再調用 Chrome Extension API
進行錄製生成 Stream
,最終經過 H5 API 把 Stream 轉換成 webm
格式的視頻文件git
如何錄製聲音、在服務器上、作成自動化github
在前期調研階段,想到了各類方案,如:web
Chrome
及 H5
的各個 API可是通過各個方面的測試,最終肯定下來,使用 Chrome 插件提供的一個 API: chrome.tabCapture.capture
,這個 API 其實在 Chrome 插件文檔裏的介紹是這樣的:chrome
捕獲當前活動標籤頁的可視區域。該方法只能在擴展程序被調用以後在當前活動網頁上使用,與 activeTab 的工做方式相似。數據庫
捕獲當前活動標籤頁的可視區域 這段話表明了這個 API 的功能,後面的話表明了這個插件的限制,也就是說你不能直接調用。須要一個用戶操做才能去調用這個 API(不得不說,Chrome對安全問題是很重視的)api
這個限制就是當時開發遇到的第一個問題,由於整個錄製都是在服務器上運行的,是不可能有人工干預的狀況。因而翻了下 Chrome
的源碼,果真在 tab_capture_api.cc 找到了,核心代碼以下:瀏覽器
// Make sure either we have been granted permission to capture through an
// extension icon click or our extension is whitelisted.
if (!extension()->permissions_data()->HasAPIPermissionForTab(
SessionTabHelper::IdForTab(target_contents).id(),
APIPermission::kTabCaptureForTab) &&
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kWhitelistedExtensionID) != extension_id &&
!SimpleFeature::IsIdInArray(extension_id, kMediaRouterExtensionIds,
base::size(kMediaRouterExtensionIds))) {
return RespondNow(Error(kGrantError));
}
複製代碼
其中下面的代碼是最主要的:安全
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kWhitelistedExtensionID) != extension_id &&
!SimpleFeature::IsIdInArray(extension_id, kMediaRouterExtensionIds,
base::size(kMediaRouterExtensionIds))
複製代碼
這段代碼會檢測當前插件的的id是否和 kWhitelistedExtensionID
同樣,而 kWhitelistedExtensionID
就是一個特權列表,當相同時,就能夠繞過用戶操做,作成自動化。bash
而 kWhitelistedExtensionID
聲明是在 switches.cc 文件裏的,定義以下:
// Adds the given extension ID to all the permission whitelists.
const char kWhitelistedExtensionID[] = "whitelisted-extension-id";
複製代碼
如今就很清楚了,我只須要在啓動 Chrome 的時候,增長一個 --whitelisted-extension-id
參數,來指定當前 Chrome 插件ID 就好了。
惟一的缺陷就是在調用這個 api 的時候,必須保證要錄製的tab是激活狀態,調用以後就能夠跳轉到其餘頁面了
因此如今新的問題來了,我須要讓我每次生成的 Chrome 插件,ID都是固定的,不然每次生成的插件,ID都不同就有問題了。在 Stack Overflow
搜了一下,找到了相關的解決方案: Making a unique extension id and key for Chrome extension?
這也就是爲何我會在項目裏的 插件目錄放置一個 key.pem 文件,本質就是爲了讓插件ID固定下來。
可能有的小夥伴已經發現,這個API沒有提供其餘的方法了,因此須要咱們手動去完成 暫停 / 恢復 / 中止
的方法,這個時候咱們就能夠藉助 H5 的 MediaRecorder
API 來完成這件事情。
不理解這個API的小夥伴,能夠先初步理解成用來管理音視頻流的
chrome.tabCapture.capture
這個方法會返回一個 Stream
對象,而這個 Stream
包含了 音/視頻。因此咱們就可使用 MediaRecorder
來完成剩下的功能了。
在調用 chrome.tabCapture.capture
後,咱們會建立一個變量。這個變量由 MediaRecorder
實例化而來,而且同時監聽新的流進來。
如今咱們寫了幾個方法(暫停 / 恢復 / 中止
),其實本質就是調用 MediaRecorder
的方法。由於 MediaRecorder
自己就提供了: pause / resume / stop
的方法,咱們只須要作一層包裝便可。
固然這裏有個小問題,就是當你調用 MediaRecorder
的 stop
方法時,還須要遍歷每一個 Tracks
,否則會照成持續的內存佔用。代碼以下:
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track => {
track.stop();
});
複製代碼
你能夠初步理解成,這個 stop 只是中止接收流,可是以前的流尚未被關閉/釋放。
接下來還出現了一個問題,這個問題讓我差點當場去世。
上面說了那麼多,基本的錄製結構都OK了。不管有沒有看懂,都應該知道這個項目的核心是瀏覽器插件。可是 Chrome 不支持在 headless
模式下注入插件。
能夠把headless理解成,在命令行啓動 Chrome,經過 命令/API 進行交互,而且沒有可視化頁面。
由於是在服務器上,而且之後確定是要走 Docker
的方式,這些都是無桌面的,不能使用 headless
模式的話,至關於以上全部的工做都是白費的。
隨後翻遍了 Google
,找到了一個解決方案,就是使用 xvfb
。你能夠理解成這個軟件會幫我虛擬出一個桌面出來,個人代碼(Chrome) 就會在這個虛擬桌面運行。完美解決剛剛的窘迫。
因此你能在 entrypoint.sh 文件裏看到下面的代碼:
# open virtual desktop
xvfb-run --listen-tcp --server-num=76 --server-arg="-screen 0 2048x1024x24" --auth-file=$XAUTHORITY node index.js &
複製代碼
至此,整個工做其實已經算是OK了,接下來就是一些優化的方案
如今整個項目已經能夠安安心心的在服務器(Docker)上進行錄製了,可是咱們看不到具體裏面的內容。我不知道里面如今處於什麼的狀況,想進行一些調試。因此我在原有的基礎上增長了 VNC
和 Chrome Remote Debug
調試模式。
VNC
的話,很簡單,只要在 Docker 裏安裝了 VNC 的套件,再在 entrypoint.sh 文件裏增長以下代碼便可:
x11vnc -display :76 -passwd password -forever -autoport 5920 &
複製代碼
Chrome Remote Debug
則有些麻煩,須要在 Chrome 啓動參數里加上 --remote-debugging-port=9222
,而後須要在 Docker 裏安裝 socat
軟件,進行端口轉發。
由於9222是 Chrome Remote Debug
的端口,可是 Chrome 不支持除本機之外的機器訪問它。因此咱們須要使用 socat
把 9222 端口轉發到 9223 便可,在 entrypoint.sh 文件的代碼以下:
# forward chrome remote debugging protocol port
socat tcp-listen:9223,fork tcp:localhost:9222 &
複製代碼
由於這個 Docker 之後可能會部署到 k8s 上,或者其餘地方,而部署後,總會遇到被通知說,你自殺吧(通常當集羣資源不夠時、CPU佔用率太高時會通知)。那咱們應該作成,當他們通知到這個 Docker(k8s爲Pod)時,應該及時的回滾數據等操做。因此在 entrypoint.sh 文件裏有這麼一段代碼:
# get nodejs process pid
NODE_PID=$(lsof -i:80 | grep node | awk 'NR==1,$NF=" "{print $2}')
# forward SIGINT/SIGKILL/SIGTERM to nodejs process
trap 'kill -n 15 ${NODE_PID}' 2 9 15
# waiting nodejs exit
while [[ -e /proc/${NODE_PID} ]]; do sleep 1; done
複製代碼
先獲取 node 進程的 PID,再把消息通知到 node 進程裏。而 node 代碼中又有這麼一段:
let status = false;
const exit = message => {
if (status) return;
console.log('the process was kill:', message);
// 回滾操做
status = true;
process.exit();
};
process.once('exit', () => exit('exit'));
process.once('SIGTERM', () => exit('sigterm'));
process.on('message', message => {
if (message === 'shutdown') {
exit('shutdown');
}
});
複製代碼
咱們公司由於使用的 k8s 來部署的,因此咱們目前的部署方式是這樣的:
首先 Server 那裏派發一個錄製任務插入到數據庫裏,這個時候我寫了另外一個項目,這個項目會按期去掃數據庫(目前爲3分鐘),掃到一個數據就會調用 k8s 的 API 去建立 Job→Pod。完成一次錄製任務,有興趣能夠看我以前寫的文章: 基於任務量進行k8s集羣的靈活調度處理
目前項目已經開源,歡迎 Star 或 PR: github.com/alo7/rebirt…