【ラズパイDIY講座】ラズパイゼロで作る監視カメラ② 〜 UV4LのビルドインシグナリングサーバでWebRTC
※ 当ページには【広告/PR】を含む場合があります。
2021/07/05
「ラズパイでお手軽監視カメラ」
WebRTCの概要
HTTPとWebSocketとWebRTCと
TCP
UDP
TCP
UDP
WebSocket
WebRTC
http://....
ws://....
https://....
wss://....
「HTTPの双方向通信機能の強化版」
const http = require('http');
const webSocketServer = require('websocket').server;
const webSocketsServerPort = 12345;
const server = http.createServer((request, response) => {});
server.listen(webSocketsServerPort, () => {
console.log(`WebSocket Server is listening on port ${webSocketsServerPort}`);
});
//👇WebSocketサーバー
const wsServer = new webSocketServer({httpServer: server});
///....
本家WebRTC?
+ Raspberry Pi Zero W + カメラモジュールV2 (※1)
+ Debian Linux パソコン (というかブラウザが動けばなんでもOK)
+ 家庭用の無線LANルータ (※2)
WebRTCのコネクションについて
Session Description Protocol(SDP)
Interactive Connectivity Establishment(ICE)
「このピア同士のコネクションを確立させる」
WebRTC APIの利用
RTCPeerConnectionクラス
データチャンネル
メディアストリーム
RTCDataChannelクラス
//👇2つのピア間で確立したコネクション
const peerConnection = new RTCPeerConnection(configuration);
const dataChannel = peerConnection.createDataChannel();
//👇データチャンネルからデータを送信
dataChannel.send(JSON.stringify({
"message": "hogehoge piyopiyo",
"timestamp": new Date()
}));
//👇データチャンネルを閉じる
dataChannel.close();
トラック
MediaStreamTrackインターフェイス
ビデオトラック
オーディオトラック
new MediaStream()
navigator
const openMediaDevices = async (constraints) => {
return await navigator.mediaDevices.getUserMedia(constraints);
}
try {
const stream = openMediaDevices({'video':true,'audio':true});
console.log('Got MediaStream:', stream);
} catch(error) {
console.error('Error accessing media devices.', error);
}
navigator.mediaDevices.getUserMedia
シグナリングサーバー
「シグナリングサーバー」
シグナリングチャンネル
シグナリングサービス
シグナリング
オファー
アンサー
オファー
アンサー
ICE candidate
ICE candidate
ICE candidate
Candidate(候補)
Vanilla ICE方式
Trickle ICE方式
【配信側】UV4Lのビルドイン・シグナリングサーバーの利用
192.168.0.10
4300
wss://192.168.0.10:4300/stream/webrtc
ws://192.168.0.10:4300/stream/webrtc
$ uv4l --external-driver --device-name=video0 \
--server-option '--port=4300' \
--enable-server
余談〜ラズパイゼロとWebRTCのハードウェアコーディックを使うときの罠
uv4l --external-driver
u4l2-ctl
$ uv4l --external-driver --device-name=video0 \
--enable-server \
--server-option '--enable-webrtc' \
--server-option '--webrtc-enable-hw-codec' \
--server-option '--webrtc-preferred-vcodec=3' \
--server-option '--port=4300'
<notice> [core] Trying to load the the Streaming Server plug-in...
<notice> [server] HTTP/HTTPS Streaming & WebRTC Signalling Server v1.1.129 built on Feb 22 2021
<warning> [server] SSL is not enabled for the Streaming Server. Using unsecure HTTP.
<notice> [core] Streaming Server loaded!
<notice> [server] Web Streaming Server listening on port 4300
<notice> [driver] Using video device /dev/video0
<notice> [webrtc] WebRTC Renderer extension successfully loaded
<notice> [server] WebRTC, Signalling Server and STUN Server extensions successfully loaded
#....
<warning> [server] Inappropriate ioctl for device
#....
http://[ラズパイのIP]:4300/stream/video.h264
Inappropriate ioctl for device
WebRTCクライアントの実装
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>UV4L-WebRTC</title>
<script type="text/javascript">
let ws = null, pc, trickle_ice = true, remoteDesc = false, iceCandidates = [];
//👇①シグナリングサーバーのIPアドレスとポートを設定
const signalling_server_address = "192.168.0.10:4300";
const pcConfig = { iceServers: [] };
const pcOptions = { optional: [] };
const mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: false,
OfferToReceiveVideo: true
}
};
//👇②ブラウザ組込のWebRCT APIを呼び出し
RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection;
RTCSessionDescription = window.RTCSessionDescription;
RTCIceCandidate = window.RTCIceCandidate;
function createPeerConnection() {
try {
pc = new RTCPeerConnection(pcConfig, pcOptions);
pc.onicecandidate = (event) => {
if (event.candidate && event.candidate.candidate) {
const candidate = {
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
const request = {
what: "addIceCandidate",
data: JSON.stringify(candidate)
};
ws.send(JSON.stringify(request));
} else {
console.log("ICE candidateの情報取得完了");
}
}
if ('ontrack' in pc) {
pc.ontrack = (event) => {
console.log("トラックを削除");
document.getElementById('remote-video').srcObject = event.streams[0];
}
} else {
pc.onaddstream = (event) => {
console.log("リモートストリームを追加: ", event.stream);
document.getElementById('remote-video').srcObect = event.stream;
}
}
pc.onremovestream = (event) => {
document.getElementById('remote-video').srcObject = null;
document.getElementById('remote-video').src = '';
}
console.log("ピアのコネクションか確立しました!");
} catch (e) {
console.error("ピアのコネクションが失敗...");
}
}
function addIceCandidates() {
iceCandidates.forEach((candidate) => {
pc.addIceCandidate(candidate, () => {
console.log("ICE candidateが追加されました: " + JSON.stringify(candidate));
}, (error) => {
console.error("ICE追加中にエラーが発生: " + error);
}
);
});
iceCandidates = [];
}
function start() {
// [UV4L WebRTC シグナリング](https://www.linux-projects.org/webrtc-signalling/)
if ("WebSocket" in window) {
document.getElementById("stop").disabled = false;
document.getElementById("start").disabled = true;
document.documentElement.style.cursor = 'wait';
//👇③シグナリングを担当するWebSocketオブジェクトを生成
ws = new WebSocket('ws://' + signalling_server_address + '/stream/webrtc');
//👇④WebSocket開始と同時にピアコネクションも作成する
ws.onopen = (stream) => {
console.log("WebSocket: onopenイベント発生");
iceCandidates = [];
remoteDesc = false;
createPeerConnection();
const request = {
what: "call",
options: {
//👇外部ドライバではハードウェアコーディックが有効化できない
force_hw_vcodec: false,
vformat: 10,
trickle_ice: true
}
};
ws.send(JSON.stringify(request));
console.log("コールされたリクエスト: " + JSON.stringify(request));
};
ws.onmessage = (evt) => {
const msg = JSON.parse(evt.data);
const what = msg.what !== undefined ? msg.what : null;
const data = msg.what !== undefined ? msg.data : null;
switch (what) {
//👇⑤ピアコネクション確立(セッション)を確認したら、ICE情報の取得を試みる
case "offer":
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)),
() => {
remoteDesc = true;
addIceCandidates();
console.log('SDP情報: onRemoteSdpSuccesイベント');
pc.createAnswer(
(sessionDescription) => {
pc.setLocalDescription(sessionDescription);
const request = {
what: "answer",
data: JSON.stringify(sessionDescription)
};
ws.send(JSON.stringify(request));
},
(error) => {
alert("アンサーでエラーが発生: " + error);
},
mediaConstraints
);
},
(event) => {
alert('ピアコネクションエラー発生: ' + event);
stop();
}
);
break;
case "answer":
break;
case "message":
alert(msg.data);
break;
//👇⑥ICE candidateを受け取ったらICE情報の保存する
case "iceCandidate": //👈trickleICEの場合
if (!msg.data) {
console.log("ICE情報取得完了");
break;
}
const elt = JSON.parse(msg.data);
const candidate = new RTCIceCandidate({sdpMLineIndex: elt.sdpMLineIndex, candidate: elt.candidate});
iceCandidates.push(candidate);
if (remoteDesc) addIceCandidates();
document.documentElement.style.cursor = 'default';
break;
case "iceCandidates": //👈vanillaICEの場合
const candidates = JSON.parse(msg.data);
for (let i = 0; candidates && i < candidates.length; i++) {
const elt = candidates[i];
const candidate = new RTCIceCandidate({sdpMLineIndex: elt.sdpMLineIndex, candidate: elt.candidate});
iceCandidates.push(candidate);
}
if (remoteDesc) addIceCandidates();
document.documentElement.style.cursor = 'default';
break;
}
};
ws.onclose = (evt) => {
if (pc) {
pc.close();
pc = null;
}
document.getElementById("stop").disabled = true;
document.getElementById("start").disabled = false;
document.documentElement.style.cursor = 'default';
console.log("WebSocket切断完了");
};
ws.onerror = (evt) => {
alert("WebSocketエラー発生中!");
ws.close();
};
} else {
alert("このブラウザはWebSocketに対応していません");
}
}
function stop() {
document.getElementById('remote-video').srcObject = null;
document.getElementById('remote-video').src = '';
if (pc) {
pc.close();
pc = null;
}
if (ws) {
ws.close();
ws = null;
}
document.getElementById("stop").disabled = true;
document.getElementById("start").disabled = false;
document.documentElement.style.cursor = 'default';
}
window.onbeforeunload = () => {
if (ws) {
ws.onclose = () => {};
stop();
}
};
</script>
<style>
#container {
display: flex;
flex-flow: row nowrap;
align-items: flex-end;
}
video {
background: #eee none repeat scroll 0 0;
border: 1px solid #aaa;
}
</style>
</head>
<body>
<div id="container">
<div class="overlayWrapper">
<video id="remote-video" autoplay="" width="640" height="480"></video>
</div>
</div>
<div id="commands">
<button id="start" style="background-color: green; color: white" onclick="start();">Call</button>
<button disabled id="stop" style="background-color: red; color: white" onclick="stop();">Hang up</button>
</div>
</body>
</html>
まとめ
参考サイト
記事を書いた人
ナンデモ系エンジニア
電子工作を身近に知っていただけるように、材料調達からDIYのハウツーまで気になったところをできるだけ細かく記事にしてブログ配信してます。
カテゴリー