OBSのブラウザソースは、Chromium Embedded Framework (CEF)で出来ているそうだ。ローカルファイルのHTMLをロードすることもできる。それを使ってAndroidスマホのカメラからの映像を表示しようと思った。WebRTCだ。
OBSのブラウザソースで表示したHTMLは、ボタンをクリックしたりテキスト入力したりできるがやりにくい。ページが読み込まれたら自動でWebRTCの接続をするようにした。
github.com
Pythonでこの用途限定のシグナリングサーバーを書いた。
GET /offer
スマホ側がofferポーリングする
POST /offer
OBS側がofferをポストする
GET /answer
スマホ側がanswerをポーリングする
POST /answer
OBS側がanswerをポストする
スマホ側もOBS側もローカルファイルからHTMLを読み込むので origin が "null"になる。CORSヘッダーで許可する必要がある。"null"の場合は、"*"ワイルドカードでの許可はできない。
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
offer = None
answer = None
def send_corsHeaders(self):
self.send_header("Access-Control-Allow-Origin", "null")
self.send_header("Access-Control-Allow-Method", "POST, GET")
self.send_header("Access-Control-Allow-Headers", "*")
def do_OPTIONS(self):
self.send_response(200)
self.send_corsHeaders()
self.end_headers()
def do_GET(self):
if self.path == '/offer' and self.offer:
size, data = self.offer
elif self.path == '/answer' and self.answer:
size, data = self.answer
else:
self.send_response(204)
self.send_corsHeaders()
self.end_headers()
return
self.send_response(200)
self.send_corsHeaders()
self.send_header("Content-Type","application/json")
self.send_header("Content-Length", size)
self.end_headers()
self.wfile.write(data)
def do_POST(self):
size = int(self.headers["content-length"])
data = self.rfile.read(size)
if self.path == '/offer':
self.__class__.offer = [size, data]
elif self.path == '/answer':
self.__class__.answer = [size, data]
else:
self.send_response(204)
self.send_corsHeaders()
self.end_headers()
return
self.send_response(200)
self.send_corsHeaders()
self.end_headers()
HTTPServer.allow_reuse_address = True
HTTPServer.timeout = None
with HTTPServer(("0.0.0.0", 8080), Handler) as server:
server.serve_forever()
スマホ側のJavaScriptコード。OBS側もほぼ一緒。Androidの保存場所はダウンロードフォルダ(Download)にした、他の場所は許可のこととかわからんかったから。Chromeからは file:///sdcard/Download/camera.htmlでアクセスできる。file:///はスラッシュ3つ。開発中はパソコンにUSBで繋いでChromeのdevtoolsのremote devicesから開いて操作した。
SDPのメッセージには改行が含まれているので、JSON.stringifyしてもJSON.parseでエラーになってしまう。stringify、parseメソッドの第2引数にreplacer, reviver関数を渡して改行を処理した。
LAN内で接続するだけなので、RTCPeerConnectionにTURNサーバーを設定しない。それでもiceGatheringのために関数を抜ける必要があるので、awaitでiceGatheringStateがcompleteになるのを待つ。
const pc = new RTCPeerConnection();
const mediaConstraints = {
video: { facingMode: "user", width: 200, height: 200 }
};
const url_offer = "http://192.168.1.2:8080/offer";
const url_answer = "http://192.168.1.2:8080/answer";
const sleep = ms => new Promise(r => setTimeout(r, ms));
const replacer = (_, v) => {
if (typeof v == "string") {
return v.replace(/\r\n/g, "\n");
}
return v;
};
const reviver = (_, v) => {
if (typeof v == "string") {
return v.replace(/\n/g, "\r\n");
}
return v;
};
(async () => {
const ms = await navigator.mediaDevices.getUserMedia(mediaConstraints);
pc.addTrack(ms.getTracks()[0], ms);
pc.setLocalDescription(await pc.createOffer());
const msg = await new Promise(rslv => {
const f = () => {
if (pc.iceGatheringState == "complete") {
pc.removeEventListener("icegatheringstatechange", f);
rslv(pc.localDescription);
}
};
pc.addEventListener("icegatheringstatechange", f);
});
let res = await fetch(url_offer, {
mode: "cors",
method: "POST",
body: JSON.stringify(msg, replacer),
headers: {
"Content-Type": "application/json"
}
});
let ans;
while (true) {
const res = await fetch(url_answer, {
mode: "cors",
method: "GET"
});
if (res.status === 200) {
ans = await res.text().then(txt => JSON.parse(txt, reviver));
break;
}
await sleep(1000);
}
pc.setRemoteDescription(ans);
})();
その他
Chromeのビデオの自動再生は、mutedの場合だけ有効。1
fetch api の response.json()に reviverがない理由はBody.jsonがネイティブで実装されているから2