前回UV4Lを使った自前のHPを作りました。今回はRaspberry PIにサーバを立ち上げて更に機能を追加して行きたと思います。
サーバは何をするのか
現在のPCとRaspberry PIとの構成を確認すると以下の様になっています。
- 先ず、Raspberry PIを立ち上げる。立ち上がりと同時いUV4Lが起動する。
- PC側にintercom.htmlというHTMLファイルが有り、これをWebブラウザーで読み込む。
- PC側からUV4LのシグナルサーバにアクセスしRaspberry PIとP2P接続を行う。
- P2P接続後WebRTC機能を使ってRaspberry PIと映像と音声のStreamingを行う。
この状態でWebRTC関係の変更は簡単に出来るのですが、PCのHPからRaspberry PIのLEDを点滅させる(GPIOを操作する)事は出来ません。Raspberry PIにPythonのWebserverを立ち上げる方法も有りますが、UV4LとWebRTCと新たに立ち上げるPythonのWebserverの関係がちょっと複雑になる様に思えます。一方で追加したい機能は、
- カメラの角度を変更する。
- 暗くなったらLEDライトを点灯する。
- 不在時の来客の録画とメールの送信
位です。それなら簡単なWebsocketサーバで十分。
Websocketサーバのインストール
Raspberry PIへのWebsocketサーバ(python)のインストール。
$ pip install git+https://github.com/Pithikos/python-websocket-server
pip がインストールされていない時は、下記でインストール。
$ sudo apt install pip
更に git が無いと言われたら
$ sudo apt install git
を実行して下さい。
先ずはLEDを点滅と留守録画
先ずはLEDオンオフ機能と留守録画機能を追加します。留守録画の開始トリガーは来客になりますが、今回はその代役としてHPのボタンをトリガーに使用しています。
修正は前回PC側に保存した3つのファイル(intercom.html、intercom.js、intercom.jcss)のうちintercom.htmlとintercom.jsに行います。また新規に作製したサーバ用のコード(websock.py)をRaspberry PI側に保存します。
PC側のソフト
先ずはintercom.htmlを変更します。以下の通りに変更し、intercom_01.html とPCに保存。
intercom_01.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel='stylesheet' type='text/css' href='./intercom.css' >
<title>UV4L WebRTC</title>
</head>
<body>
<div class='b_frame'>
<div class='t_font'><u>UV4L WebRTC 1.0</u></div>
<div class='p_frame'>
<video id="remote-video" autoplay="" width="640" height="480"> </video>
<video id="play-video" controls style='display:none'>Your browser does not support the video tag.</video>
</div>
<div class="_menu">
<div class="input-group">
<label>Stream</label>
<button type='button' id='start' onclick='start();'>Start</button>
<button type='button' id="pause" onclick="pause();" >Pause</button>
<button type='button' id="mute" onclick="mute();" >Mute</button>
<button type='button' id='stop' onclick="stop();">Stop</button><br>
<label>Take & Play</label>
<button type='button' id='photo' onclick="take_photo();">Photo</button>
<button type='button' id="record" onclick="start_stop_record();">Video</button>
<button type='button' id="play" onclick="play_video();">Play</button><br>
<label>Download</label>
<button type='button' id='dn_photo' onclick='dn_photo();'>Photo</button>
<button type='button' id='dn_video' onclick='download();'>Video</button><br>
<label>TEST</label>
<button type='button' id='bt_LED' onclick='bt_led();'>LED_OFF</button>
<button type='button' id='bt_REC' onclick='bt_rec();'>REC</button>
<canvas style='display:none'></canvas>
</div>
</div>
</div>
<script src='./intercom_01.js'></script>
</body>
</html>
- 33行: LEDオンオフ用のボタン。クリックで ‘bt_led()’ が起動
- 34行: 留守録画開始用のボタン。 クリックで ”bt_rec()’ が起動
- 41行: 読み込むJavescript Fileを変更した新しい’intercom_01.js’ に指定
次に、intercom.js。変更後にintercom_01.jsと名前を変えて保存して下さい。
intercom_01.js
var ws = null;
var pc;
var audio_video_stream;
var recorder = null;
var aa_streams = [];
var mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
var iceCandidates = [];
var botton_buff = ["pause", "mute", 'stop', 'photo', "record", "play", 'dn_photo', 'dn_video'];
var flg_view = 0;
var flg_record = 0;
var remoteVideo = document.getElementById("remote-video");
var led = 0;
var rec_flg = 1;
ws00 = new WebSocket('ws://raspberrypi.local:8070');
ws00.onmessage = function (evt) {
var msg = evt.data;
console.log(msg);
switch(msg)
{
case 'start':// Start Stream without webrtc
rec_flg = 0;
start();
break;
case 'start_record': // Start Stop recording
start_stop_record();
break;
case 'stop_record': // Start Stop recording
start_stop_record();
rec_flg = 1;
break;
case 'download': // Download
download();
break;
case 'puase': // back or pause
puase();
break;
case 'stop': // stop
stop();
break;
}
};
function bt_led() {
led = led ^ 1;
if(led)
{
document.getElementById("bt_LED").textContent = 'LED_ON'
document.getElementById("bt_LED").style.backgroundColor = 'red';
}
else
{
document.getElementById("bt_LED").textContent = 'LED_OFF'
document.getElementById("bt_LED").style.backgroundColor = "#228b22";
}
ws00.send('LED,' + String(led));
}
function bt_rec() {
ws00.send('REC');
}
botton_buff.forEach(element => set_Btn(element, 0));
function createPeerConnection() {
try {
pc = new RTCPeerConnection();
pc.onicecandidate = onIceCandidate;
pc.ontrack = onTrack;
pc.onremovestream = onRemoteStreamRemoved;
console.log("peer connection successfully created!");
} catch (e) {
console.error("createPeerConnection() failed");
}
}
function onIceCandidate(event) {
if (event.candidate && event.candidate.candidate) {
var candidate = {
sdpMLineIndex: event.candidate.sdpMLineIndex,
sdpMid: event.candidate.sdpMid,
candidate: event.candidate.candidate
};
var request = {
what: "addIceCandidate",
data: JSON.stringify(candidate)
};
ws.send(JSON.stringify(request));
} else {
console.log("End of candidates.");
}
}
function addIceCandidates() {
iceCandidates.forEach(function (candidate) {
pc.addIceCandidate(candidate,
function () {
console.log("IceCandidate added: " + JSON.stringify(candidate));
},
function (error) {
console.error("addIceCandidate error: " + error);
}
);
});
iceCandidates = [];
}
function onTrack(event) {
console.log("Remote track!");
var remoteVideoElement = document.getElementById('remote-video');
remoteVideoElement.srcObject = event.streams[0];
aa_streams[0] = event.streams[0];
botton_buff = ["pause", "mute", 'stop', 'photo', "record"];
botton_buff.forEach(element => set_Btn(element, 1));
}
function onRemoteStreamRemoved(event) {
var remoteVideoElement = document.getElementById('remote-video');
remoteVideoElement.srcObject = null;
remoteVideoElement.src = ''; // TODO: remove
}
function start() {
set_Btn('start', 0)
if ("WebSocket" in window) {
document.getElementById("stop").disabled = false;
document.getElementById("start").disabled = true;
document.documentElement.style.cursor = 'wait';
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
ws = new WebSocket(protocol + '//raspberrypi.local:8090/stream/webrtc');
ws.onopen = async function () {
iceCandidates = [];
remoteDesc = false;
if(rec_flg) audio_video_stream = await navigator.mediaDevices.getUserMedia({video: false, audio: true});
createPeerConnection();
if(rec_flg) audio_video_stream.getTracks().forEach(track => pc.addTrack(track, audio_video_stream));
var request = {
what: "call",
options: {
force_hw_vcodec: false,
vformat: "30",
trickle_ice: true
}
};
ws.send(JSON.stringify(request));
console.log("call(), request=" + JSON.stringify(request));
};
ws.onmessage = function (evt) {
var msg = JSON.parse(evt.data);
if (msg.what !== 'undefined') {
var what = msg.what;
var data = msg.data;
}
console.log("message =" + what);
switch (what) {
case "offer":
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)),
function onRemoteSdpSuccess() {
remoteDesc = true;
addIceCandidates();
console.log('onRemoteSdpSucces()');
pc.createAnswer(function (sessionDescription) {
pc.setLocalDescription(sessionDescription);
var request = {
what: "answer",
data: JSON.stringify(sessionDescription)
};
ws.send(JSON.stringify(request));
console.log(request);
}, function (error) {
alert("Failed to createAnswer: " + error);
}, mediaConstraints);
},
function onRemoteSdpError(event) {
alert('Failed to set remote description (unsupported codec on this browser?): ' + event);
stop();
}
);
break;
case "iceCandidate": // when trickle is enabled
if (!msg.data) {
console.log("Ice Gathering Complete");
break;
}
var elt = JSON.parse(msg.data);
let candidate = new RTCIceCandidate({sdpMLineIndex: elt.sdpMLineIndex, candidate: elt.candidate});
iceCandidates.push(candidate);
if (remoteDesc)
addIceCandidates();
document.documentElement.style.cursor = 'default';
break;
}
};
ws.onclose = function (evt) {
if (pc) {
pc.close();
pc = null;
}
document.getElementById("stop").disabled = true;
document.getElementById("start").disabled = false;
document.documentElement.style.cursor = 'default';
};
ws.onerror = function (evt) {
alert("An error has occurred!");
ws.close();
};
} else {
alert("Sorry, this browser does not support WebSockets.");
}
}
function stop() {
botton_buff = ["pause", "mute", 'stop', 'photo', "record", "play", 'dn_photo', 'dn_video'];
botton_buff.forEach(element => set_Btn(element, 0));
set_Btn('start', 1);
if (audio_video_stream) {
try {
if (audio_video_stream.getVideoTracks().length)
audio_video_stream.getVideoTracks()[0].stop();
if (audio_video_stream.getAudioTracks().length)
audio_video_stream.getAudioTracks()[0].stop();
audio_video_stream.stop(); // deprecated
} catch (e) {
for (var i = 0; i < audio_video_stream.getTracks().length; i++)
audio_video_stream.getTracks()[i].stop();
}
audio_video_stream = null;
}
document.getElementById('remote-video').srcObject = null;
document.getElementById('remote-video').src = ''; // TODO; remove
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';
}
function pause() {
botton_buff = ["play", 'dn_photo', 'dn_video'];
botton_buff.forEach(element => set_Btn(element, 0));
botton_buff = ["mute", 'stop', 'photo', "record"];
botton_buff.forEach(element => set_Btn(element, remoteVideo.paused));
if (remoteVideo.paused){
if(flg_view == 1){
remoteVideo.style.display = "";
remoteVideo.muted = "false";
document.getElementById('play-video').style.display = "none";
flg_view = 0;
remoteVideo.muted = "false";
}
document.getElementById("pause").innerHTML = 'Pause';
remoteVideo.play();
}
else{
remoteVideo.pause();
document.getElementById('pause').innerHTML = 'Back';
}
set_Btn("pause",1);
}
function mute() {
remoteVideo.muted = !remoteVideo.muted;
if (remoteVideo.muted){
document.getElementById("mute").innerHTML = 'No mute';
}
else{
document.getElementById('mute').innerHTML = 'Mute';
}
}
function download() {
if (recordedBlobs !== undefined) {
var blob = new Blob(recordedBlobs, {type: 'video/webm'});
// var url = window.URL.createObjectURL(blob);
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'video.webm';
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
// window.URL.revokeObjectURL(url);
URL.revokeObjectURL(url);
}, 100);
}
}
function start_stop_record() {
botton_buff = ["start", "pause", "mute", 'stop', 'photo', "play", 'dn_video', 'dn_photo'];
botton_buff.forEach(element => set_Btn(element, 0));
set_Btn("record",1);
document.getElementById('pause').innerHTML = 'Back';
if (pc && !flg_record) {
if (aa_streams.length) {
console.log("Starting recording");
startRecording(aa_streams[0]);
document.getElementById('record').innerHTML = 'Stop';
flg_record = 1;
}
}
else {
console.log("Stop recording");
botton_buff = ["pause", "record", "play", 'dn_video'];
botton_buff.forEach(element => set_Btn(element, 1));
stop_record();
document.getElementById('record').innerHTML = 'Video';
remoteVideo.pause();
set_Btn("record",0);
flg_record = 0;
}
}
function startRecording(stream) {
recordedBlobs = [];
var options = {mimeType: 'video/webm;codecs=vp9,opus'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.log(options.mimeType + ' is not Supported');
//options = {mimeType: 'video/webm;codecs=vp8'};
options = {mimeType: 'video/webm;codecs=vp8,opus'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.log(options.mimeType + ' is not Supported');
options = {mimeType: 'video/webm;codecs=h264'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.log(options.mimeType + ' is not Supported');
options = {mimeType: 'video/webm'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.log(options.mimeType + ' is not Supported');
options = {mimeType: ''};
}
}
}
}
try {
recorder = new MediaRecorder(stream, options);
} catch (e) {
console.error('Exception while creating MediaRecorder: ' + e);
alert('Exception while creating MediaRecorder: ' + e + '. mimeType: ' + options.mimeType);
return;
}
console.log('Created MediaRecorder', recorder, 'with options', options);
recorder.ondataavailable = handleDataAvailable;
recorder.onwarning = function (e) {
console.log('Warning: ' + e);
};
recorder.start();
console.log('MediaRecorder started', recorder);
}
function stop_record() {
if (recorder) {
recorder.stop();
console.log("recording stopped");
document.getElementById('record').innerHTML = 'Video';
recorder = null;
}
}
function handleDataAvailable(event) {
//console.log(event);
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
}
function play_video() {
botton_buff = ["start", "mute", 'stop', 'photo', "record", 'dn_photo'];
botton_buff.forEach(element => set_Btn(element, 0));
botton_buff = ["pause", "play", 'dn_video'];
botton_buff.forEach(element => set_Btn(element, 1));
document.getElementById('pause').innerHTML = 'Back';
console.log('Play video');
recorder = null;
flg_view = 1;
remoteVideo.style.display = "none";
remoteVideo.pause();
remoteVideo.muted = "true";
document.getElementById('play-video').style.display = "";
var superBuffer = new Blob(recordedBlobs, {type: 'video/webm'});
var recordedVideoElement = document.getElementById('play-video');
recordedVideoElement.src = URL.createObjectURL(superBuffer);
}
function dn_photo(){
var canvas = document.querySelector('canvas');
var url = canvas.toDataURL('image/jpeg', 1.0);
var a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'pic.jpg';
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
function take_photo() {
botton_buff = ['start', "mute", 'stop', "record", "play", 'dn_video'];
botton_buff.forEach(element => set_Btn(element, 0));
botton_buff = ['pause', "photo", 'dn_photo'];
botton_buff.forEach(element => set_Btn(element, 1));
document.getElementById('pause').innerHTML = 'Back';
remoteVideo.pause();
var canvas = document.querySelector('canvas');
canvas.width = remoteVideo.videoWidth;
canvas.height = remoteVideo.videoHeight;
canvas.getContext('2d').drawImage(remoteVideo, 0, 0, canvas.width, canvas.height);
}
function set_Btn(_id, flg) {
var flg_able = true;
var flg_back = "gray";
if(flg){
flg_able = false;
flg_back = "#228b22";
}
document.getElementById(_id).disabled = flg_able;
document.getElementById(_id).style.backgroundColor = flg_back;
}
実は、intercom.jsで既にWebsocketサーバを使用しています。P2P接続の初期の段階でお互いの情報を交換する為に使ったシグナルサーバがWebsocketサーバです。その部分を参考にソースを変更しています。変更箇所(追加分)は19行から74行と 148,150行です。
- 22行: ws00 = new WebSocket(‘ws://raspberrypi.local:8070’);
- サーバとの接続。ポートは8070を使用。
- 24行: ws00.onmessage = function (evt) {
- Raspberry PIからメッセージが送られるとここが実行されます
- 今回は、留守録画時に幾つかのメーッセージが送られて来ます。それらをここで対応します。
- サーバから送られて来たメッセージを判断し要求された動作を行います。
- HPのボタンを押された時と同じ関数が起動する様に設定しています。
- Streamを開始するStart()のみ一部変更しています。
- PC側から音声を送る設定にしているので、使用許可のポップアップが起動します。
- このポップアップに回答しないと次に進まないこと、留守録では音声を送る必要が無い事から留守録時は音声を送らない設定にしています。ー> 148,150行
- 57行: function bt_led()
- LEDボタンが押された時の処理を行っています。
- LEDの状態に合わせてボタンの表示を変更
- 69行: ws00.send(‘LED,’ + String(led));
- ここでサーバにメッセージを送信しています。
- メッセージはカンマ ’,’ で区切られ、最初がコマンド、次がデータと設定しています。
- ここでのメッセージは、’LED,x’ xは0か1。
- LED,0: ー> LEDオフ
- LED,1: ー> LEDオン
- 72行: function bt_rec() {
- RECボタンが押されて時に実行。Raspberry PIに ‘REC’ を送信。
Raspberry PI側のソフト
Raspberry PI側Websocketサーバ用コードです。ファイル名、websock.py でRaspberry PIで保存して下さい。
websock.py
#!/usr/bin/env python3
from websocket_server import WebsocketServer
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
GPIO.setup(23, GPIO.OUT)
GPIO.output(23, 0)
# new client
def new_client(client, server):
print('New client {} has joined.'.format(client['address'][0]))
# client close
def client_left(client, server):
print('Client {} has left.'.format(client['address'][0]))
# message from client
def message_received(client, server, message):
print('command: ', message)
cmd = message.split(',')
if(cmd[0] == 'LED'):
GPIO.output(23, int(cmd[1]))
elif(cmd[0] == 'REC'):
server.send_message(client, 'start')
time.sleep(5)
server.send_message(client, 'start_record')
time.sleep(10)
server.send_message(client, 'stop_record')
time.sleep(1)
server.send_message(client, 'download')
time.sleep(1)
server.send_message(client, 'pause')
time.sleep(1)
server.send_message(client, 'stop')
# set server port @8070
server = WebsocketServer(port=8070, host='raspberrypi.local')
# define event handler
server.set_fn_new_client(new_client)
server.set_fn_client_left(client_left)
server.set_fn_message_received(message_received)
# start
print('Start Server')
server.run_forever()
- 2行: from websocket_server import WebsocketServer
- websocket_server モジュールのインポート
- 3行: import RPi.GPIO as GPIO
- GPIO モジュールのインポート
- 6〜8行: LED用ポートの設定
- LED ポートとして GPIO:23を出力とし、初期値0を設定。
- 11行: def new_client(client, server):
- 新しいClientが接続して来たらこの関数が起動します。
- 今回、’New client xxxxxxx has joined.’ と表示。(”xxxxxxx”はClientのIPアドレス)
- 15行: def client_left(client, server):
- Clientが接続をCloseした時にこの関数が実行されます。
- 今回、’Client xxxxxxx has left.’ と表示。(”xxxxxxx”はClientのIPアドレス)
- 19行: def message_received(client, server, message):
- Clientからメッセージが来るとこの関数が実行される。
- Clientから送信されたメッセージは変数 message に保管される。
- messageをコマンドとデータに分離して各処理を行う
- 22行: if(cmd[0] == ‘LED’):
- LEDのオンオフを行う
- GPIO.output(23, int(cmd[1])) でデータに合わせてポートの状態を設定
- 25行: elif(cmd[0] == ‘REC’):
- 留守録画を行う。
- メッセージを送る関数、server.send_message(client, ‘xxxxx’)を使ってPCへメッセージを送る
- 録画を行う為のメッセージ
- Stream開始 ー> server.send_message(client, ‘start’)
- 5秒待ち ー> WebRTCが準備出来るまでの待ち
- 録画開始 ー> server.send_message(client, ‘start_record’)
- 10秒待ち ー> 10秒間録画する。
- 録画終了 ー> server.send_message(client, ‘stop_record’)
- 1秒待ち
- ファイルの保存 ー> server.send_message(client, ‘download’)
- 1秒待ち
- 録画モード終了 ー> server.send_message(client, ‘pause’)
- 1秒待ち
- Stream終了 ー> server.send_message(client, ‘stop’)
- 39行: server = WebsocketServer(port=8070, host=’raspberrypi.local’)
- サーバの宣言
- port=8070。host=’raspberrypi.local’ と設定
- 41行: server.set_fn_new_client(new_client)
- 新規Clientが接続して来た時に対応する関数を宣言
- 42行: server.set_fn_client_left(client_left)
- Clientが接続を終了して時に対応する関数を宣言
- 43行: server.set_fn_message_received(message_received)
- Clientからメッセージが送られて来た時に対応する関数を宣言
- 46行: server.run_forever()
- サーバの開始
Raspberry PI側のハード
LEDを以下の様にポート23に配線しています。
実行
- Raspberry PI側
- 電源を入れてレディーになるのを待つ。この時点でUV4Lは起動している。
- websock.pyが保存されているフォルダーに移動。
- そこで、モニターを上げ、python websock.py とサーバーを立ち上げる。
- モニターに Start Server と表示されたら サーバが起動しています。
- PC側
- FireFoxを使って”intercom_01.html”を立ち上げる。
- 赤枠の部分が追加したボタンです。
- HPが表示された時点でRaspberry PI側のモニターに新しいClientが接続して来た事が表示されます。
- HPのLEDボタンを押すと
- LED_OFFボタンを押す: 表示が LED_ONに変わって Raspberry PI側のLEDが点灯
- LED_ONボタンを押す: 表示が LED_OFFに変わって Raspberry PI側のLEDが消灯
- HPのRECボタンを押すと
- Raspberry PI側の指示に従いHPが動作し自動で録画します。
まとめ
websocket-serverを使用してRaspberry PIを制御出来る事が分かりHPから LEDの点灯、カメラ角度の変更等が出来る事様になりました。
また、Raspberry PI側からHPの関数を起動させれる事が分かったのですが、HPの機能を使うにはHPが(PCが)立ち上がっている必要が有る事に気付きました。例えば、今回行った留守録機能ですが、留守の間もPCが(HPが)上がっていなければ動作しない事に気付きました。留守中はPCを上げない事を想定していました。ちょっと残念です。
次回は趣向を変えてRouterの外からUV4Lにアクセスする方法を考えて見たいと思います。
今回使用したソフトをここに保存します。ー>