ども、@kimihom です。

WebRTC なサービス開発をしていると、デスクトップのスクリーンシェアをしたくなる時がある。その方法について一括でまとめてみる。
2018/8/28 追記
Chrome Inline Install が廃止されるらしい。そのため、以下のインラインインストールは実装できなくなるのでご注意を。
Google Developers Japan: Chrome 拡張機能の透明性向上について
スクリーンシェア概要
まずスクリーンシェアをするには、専用の Chrome 拡張 をマーケットに公開し、ユーザーがその Chrome 拡張をダウンロードしてもらう必要がある。その Chrome 拡張 で作成された Track を最終的に WebRTC に乗っける形となる。
Chrome 拡張のインストールを手軽にしてくれる、Chrome Extension inline installation ってのがある。本記事は、こちらのリンク先の情報がベースとなっているので、今回のスクリーンシェアの実装のイメージなどは上記ページの説明を参照いただきたい。以下に理想のスクリーンシェアの方法の全体像を記述していく。
Chrome 拡張の用意
Chrome 拡張機能は、Chrome ウェブストアからダウンロードできるアプリのようなものだ。読者の方も一度はこのページで何かしらの拡張をダウンロードしたことがあることだろう。
まずはここに Chrome 拡張を乗っけるための準備が必要となる。
Chrome 拡張のプログラムを用意
今回の機能を Chrome 拡張として実装するには、 chrome.js, content.js, manifest.json
の3つのファイルを用意する必要がある。最終的にこれらファイルを zip化してアップロードする形だ。
まずは manifest.json
を定義しよう。
{
"manifest_version": 2,
"name": "screen sharing",
"short_name": "screen sharing",
"description": "Screen sharing app",
"version": "0.0.1",
"background": {
"scripts": ["chrome.js"]
},
"content_scripts": [
{
"matches": ["*://www.my-awesome-app.com/*"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"externally_connectable": {
"matches": ["*://www.my-awesome-app.com/*"]
},
"permissions": [
"desktopCapture",
"tabs",
"https://www.my-awesome-app.com/*"
]
}
詳しい JSON の定義については Background Pages や Content Scripts などのページを参照いただきたい。必要に応じて適宜値を変えてほしい。
さて、続いて Background Pages で定義した chrome.js
のソース例をご紹介する。
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
switch (message && message.type) {
case 'getUserScreen':
handleGetUserScreenRequest(message.sources, sender.tab, sendResponse);
break;
case "checkExtension":
var version = chrome.runtime.getManifest().version;
sendResponse({ version: version });
break;
default:
handleUnrecognizedRequest(sendResponse);
break;
}
return true;
});
function handleGetUserScreenRequest(sources, tab, sendResponse) {
chrome.desktopCapture.chooseDesktopMedia(sources, tab, streamId => {
if (!streamId) {
sendResponse({ type: 'error', message: 'Failed to get stream ID' });
}
sendResponse({ type: 'success', streamId: streamId });
});
}
function handleUnrecognizedRequest(sendResponse) {
sendResponse({type: 'error', message: 'Unrecognized request'});
}
chrome.tabs.query({
"status": "complete",
"currentWindow": true
}, function(tabs) {
tabs.forEach(function(tab){
chrome.tabs.executeScript(tab.id, {
"file": "content.js",
"runAt": "document_start"
});
});
});
上記コードで getUserScreen
や checkExtension
メソッドを、通常の Web ページの JavaScript から呼び出すことができるようになる。
content.js
はとてもシンプルで以下のような形となる。
window.postMessage({ type: 'ContentScriptInjected' }, '*');
chrome.js
の最後のブロックで実行している chrome.tabs.query
は、Chrome 拡張のインストール完了後にスクリーンシェアを直接起動するために必要なコードだ。ここでcontent.js
が呼び出されて、ContentScriptInjected
イベントを発火し、フロントエンドでスクリーンシェアインストール後のイベントを受け取ることができるようになる。chrome.js から直接フロントエンドへ通知を送りたいところだが、それだとうまく動かないとのことのようだ。
上記をひとまず Zip 化して保存しておこう。
Chrome 拡張の公開
まず この Chrome 拡張が所有するウェブサイトの公式アイテムであることを証明するために、Search Console にウェブサイト登録する必要がある。Search Console でウェブサイト登録を行おう。ここから吐かれた aaa.txt
ファイルをhttps://www.my-awesome-app.com/aaa.txt
からアクセスできるようにする必要がある。
続いて デベロッパーダッシュボードでアプリ登録を行う必要がある。これで公開するには確か $10 くらいのお金がかかるので注意。先ほど作成した zip をアップロードし、"ウェブサイト" の項目を埋め、インライン インストール:
このアイテムではインライン インストールを使用する。 にチェックを入れる。公開設定は限定公開で問題ない。
Chrome 拡張を公開すると、 https://chrome.google.com/webstore/detail/my-awesome-app/nolkcoggbpmcgplklppcfkfgljmkmeba って感じの URL が割り当てられる。この最後の nolkcoggbpmcgplklppcfkfgljmkmeba
が今後使う Chrome 拡張の ID となる。この公開の適用は、10分くらいかかるので気長に待とう。
無事公開できたら、ようやく Web ページ側の フロントエンドのコーディングに移っていく。
フロントエンドのコーディング
さて、ここも一気にソースコードを紹介する形で進めていく。
まずは HTML のヘッダに以下を追記。
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/nolkcoggbpmcgplklppcfkfgljmkmeba">
続いて JavaScript。
chrome.runtime.sendMessage("nolkcoggbpmcgplklppcfkfgljmkmeba", { type: 'checkExtension', sources: ['window', 'screen', 'tab'] }, function(response) {
if (response && response.version) {
}
});
if ("Chrome 拡張が有効であるフラグ == true") {
enableScreen();
} else {
chrome.webstore.install("", function(){
window.addEventListener('message', function(ev) {
if (ev.data.type === "ContentScriptInjected") {
enableScreen();
}
}, false);
}, function(err, errCode) {
console.log(err);
});
}
function enableScreen() {
getUserScreen(['window', 'screen', 'tab'], "nolkcoggbpmcgplklppcfkfgljmkmeba").then(function(stream) {
}, function(err) {
console.error(err);
});
}
function getUserScreen(sources, extensionId) {
return new Promise(function(resolve, reject) {
chrome.runtime.sendMessage(extensionId, { type: 'getUserScreen', sources: sources }, function(response) {
switch (response && response.type) {
case 'success':
resolve(response.streamId);
break;
case 'error':
console.error(response);
break;
default:
reject(new Error('Unknown response'));
break;
}
});
}).then(function(streamId) {
return navigator.mediaDevices.getUserMedia({
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: streamId
}
}
});
});
}
上記コードの概要は以下のようなものだ。
- ページに来た時点で専用のスクリーンシェア Chrome 拡張がインストールされているかをチェック
- インストールされていなくて、かつスクリーンシェアのボタンをクリックした場合、スクリーンシェア拡張 のダウンロードをしてからスクリーンシェアを実行
- インストールされていればそのままスクリーンシェアを実行
最終的にスクリーンシェア用の Stream さえ取ってこれれば、あとは WebRTC に乗っけるだけなので、それぞれの方法に応じて実装してほしい。
おわりに
今回は理想的なスクリーンシェアを Chrome の WebRTC で実現するために必要な流れを解説した。最近は WebRTC の記事ばかり書いてるけど、こうして WebRTC に挑戦する方が増えていくことを願っている。
今回の方法は 本当に Chrome 限定のコードなので、 Firefox アドオンとかにも適用したいってなると面倒な if 文でなんとかしないといけなくなる。
スクリーンシェアがアプリとしてインストールする必要があること自体、面倒なことなので、ここら辺は今後の HTML の発展に期待したいところでもある。
Happy WebRTC Coding!