Raspberry Piを使って映像StreamingするならMjpg-Streamer。今回はPythonでWebサーバーを立ち上げMjpg-Streaminerをサーバーに取り込み、Streaming、録画、写真の撮影が出来るアプリを作成したいと思います。
必要なハード
- Raspberry PI 3 model B+
- MicroSD:16GB。8GBでも良いです。
- ロジクール ウェブカメラ C270 ブラック HD 720P ウェブカム
- 撮影用のUSBカメラです。
- USBカメラであれば特にこれにこだわりません。
OSのインストール
今回はOSにRaspberry Pi OS Lite(32-bit)を使用しています。インストールはPI 3 model B+のWifiを利用して、WiFiのみのインストールとAP/STAの作成に従って行いました。LANケーブル要らずかつインストール後、SSHが直ぐに使えるのでとても便利です。
インストール後、PCからRaspberry piにSSHでつなぐ場合のコマンドは ”ssh pi@raspberry.local”。パスワードは ”raspberry” です。インストール直後のホームディレクトリは下記の様になっていると思います。
Home dir
pi@raspberrypi:~ $ ls -al
total 36
drwxr-xr-x 5 pi pi 4096 Jun 14 06:14 .
drwxr-xr-x 3 root root 4096 May 7 15:42 ..
-rw------- 1 pi pi 1136 Jun 17 23:04 .bash_history
-rw-r--r-- 1 pi pi 220 May 7 15:42 .bash_logout
-rw-r--r-- 1 pi pi 3523 May 7 15:42 .bashrc
drwx------ 3 pi pi 4096 Jun 14 00:48 .gnupg
-rw-r--r-- 1 pi pi 807 May 7 15:42 .profile
後は、sudo apt-get update と sudo apt-get upgrade で最新の状態にしてOSのインストール完了です。
必要なソフトのインストール
今回は、Mjpg-StreamerとFFmpegをインストールします。先ずは、Mjpg-Streamer。ここ→’Raspberry Pi 4B カメラ動画配信「Mjpg-Streamer」のインストールと設定方法’を参考にインストールしました。このインストールでは”git”を使います。先ずは、”sudo apt-get install git
”で”git”を先にインストールして下さい。その後USBカメラをRaspberry Piに繋いで下記を実行して下さい。
sudo apt install libjpeg8-dev cmake
git clone https://github.com/jacksonliam/mjpg-streamer.git
cd mjpg-streamer/mjpg-streamer-experimental
make
sudo make install
mjpg-streamerの動作を確認します。ホームディレクトリに新たに”mjpg-streamer”が作成されています。その下の”mjpg-streamer-experimental”ディレクトリに移動してスクリプトファイル”start.sh”を実行。下記の様な画面になればOKです。
mjpg-streamerは8080ポートを使用しているので、PCのブラウザのURL欄に”raspberry.local:8080″と入力すると、下記の様な画面が表示されます。
これが、”mjpg-streamer”のHPです。HP中段下にある写真(ドアの写真)がカメラの画像です。左側にメニューを使ってスナップショットやStreamingが出来ます。
次は、FFmpegをインストールします。FFmpegはStreamingを録画する時に使用します。インストールは簡単で、”sudo apt install ffmpeg”でOKでした。
ネット環境
今回のネット環境は下記の様にRaspberry Pi とデスクトップPCは1つのルータにつながっている状態です。ルータ超えはしていません。PCのOSがUbuntuの場合、この環境下では相手指定にコンピュータ名が使用出来ます。IPアドレスをいちいち指定する必要無く便利です。
アプリの概要
下記の様にWeb画面から、Streamingと静止画(写真)、動画の録画が出来る様にしました。
プログラムの流れ。
- Raspberry Pi側で、PythonでWebサーバーを上げる。ここ参照ー>”python_HTTP_Server“
- Raspberry Piの Webサーバーで mjpg-streamerを実行する。
- サーバーのHPを上記の画面とする。
- mjpg-streamerからデータをHPのHTMLに組み込み表示する。
- サーバーのHPで、Streaming、写真、録画を行う。
mjpg-streamerの出力をHTMLに組み込む
Webで調べると色々と説明が載っているのですが、ブラウザがFirefoxの場合は簡単に組み込める事が分かりました。
- Streamingの組み込み
- <img> タグを使って、”<img src=”http://raspberrypi.local:8080/?action=stream>”とすればOK
- 静止画の場合
- mjpg-streamerサーバのhttp://raspberrypi.local:8080/?action=snapshotが静止画のアドレス。
- この画面を”wget” を使ってサーバーに”phto.jpg”として一旦保管
- <img>タグを使って、”<img src=”./phto.jpg”> として表示。
動画の録画はffmpegを使う
mjpg-streamerの出力、http://raspberrypi.local:8080/?action=streamをffmpgの入力にすれば、Streamingの録画が出来る事が分かりました。コマンドは以下の通り、
ffmpeg -i http://raspberrypi.local:8080/?action=stream -an -r 10 -vcodec libx264 ./file.mp4
- -i : 入力先の指定。今回は、mjpg-streamerのStream出力 http://raspberrypi.local:8080/?action=streamを指定
- -an :音声無し
- -r 10:1秒間に10コマ。フレーム数の指定。
- -vcodec:コーデックの指定。
- 最後に:保存ファイル名
これで録画が開始されます。録画の終了はmjpg-streamerにCtrl-Cを送ります。
プログラムの説明
プログラムは下記の5つです。
- stream.py:メインのプログラム。Pythonで書かれたWebServerです。
- stream.html:サーバーHPのHTMLファイル。
- stream.css:HPのCSSファイル。
- st_mjpg.sh:mjpg-streamer起動用のスクリプトファイル。
- favicon.ico:ファビコン用のファイル。
先ずは、stream.py。pythonで書かれたWebサーバーです。subprocess.Popen(), subprocess.run()を使って、mjpg-streamer、ffmpgを管理しています。
stream.py
from http.server import HTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse,parse_qs
import subprocess
from subprocess import PIPE
import os
flg_state = 0
proc_pt = subprocess.Popen("./st_mjpg.sh",shell=True)
class MyHandler(SimpleHTTPRequestHandler):
def do_GET(self):
global flg_state, proc_pt
self.send_response(200)
fl = 1
if self.chk_file() == 0:
parsed = urlparse(self.path)
params = parse_qs(parsed.query)
a = next(iter(params))
#----- Stream Command -----------------
if a == "1":
cmd = params['1'][0]
#----- flg_state -----------
# 0: Stream
# 1: start/Stop rec
# 2: take photo
# 3: Replay video
#----- Start Streaming -----------
if cmd == '_start':
flg_state = 0;
#----- Rec -----------
elif cmd == '_rec':
if flg_state != 1:
subprocess.run("rm file.mp4".split())
cmd = "ffmpeg -i http://raspberrypi.local:8080/?action=stream -an -r 10 -vcodec libx264 ./file.mp4"
proc_pt = subprocess.Popen(cmd, shell=True)
flg_state = 1;
else :
proc_pt.kill()
cmd = "killall -s INT ffmpeg"
proc = subprocess.run(cmd.split())
flg_state = 0;
#----- take picture -----------
elif cmd == '_photo':
flg_state = 2;
cmd = "wget -O ./phto.jpg http://raspberrypi.local:8080/?action=snapshot"
proc = subprocess.run(cmd.split())
#----- show picture -----------
elif cmd == '_rephoto':
flg_state = 2;
#----- Relay -----------
elif cmd == '_replay':
flg_state = 3
#----- Data transfer --------------
elif a == "80":
self.send_header('Content-type', "text/plain")
self.end_headers()
fl = 0;
if os.path.exists("./phto.jpg") == True :
fl = 1;
if os.path.exists("./file.mp4") == True :
fl += 2;
buf = str(flg_state) + ',' + str(fl) + ','
self.wfile.write(buf.encode())
fl = 0
#-------------------------------------
else :
fl = 0
if fl == 1:
f = open("stream.html",'rb')
self.send_header('Content-type', "text/html")
self.end_headers()
self.wfile.write(f.read())
f.close()
#-------------------------------------------------
def chk_file(self):
global flg_state
a = 0
if self.path == "/":
self.path = "/stream.html"
dataType = "text/html"
flg_state = 0
a = 1
if self.path.endswith(".css"):
dataType = "text/css"
a = 1
if self.path.endswith(".jpg"):
dataType = "image/jpeg"
a = 1
if self.path.endswith(".ico"):
dataType = "text/plain"
a = 1
if self.path.endswith(".mp4"):
dataType = "video/mp4"
a = 1
if a == 1:
self.path = "." + self.path
f = open(self.path,'rb')
self.send_header('Content-type', dataType)
self.end_headers()
self.wfile.write(f.read())
f.close()
return a
#-------------------------------------------------
host = ''
port = 8010
httpd = HTTPServer((host, port), MyHandler)
print('serving at port', port)
httpd.serve_forever()
- 8行:proc_pt = subprocess.Popen(“./st_mjpg.sh”,shell=True)
- ここで、mjpg-streamerを起動しています。
- スクリプトファイル、”st_mjpg.sh”の説明は後ほど
- 34行:if cmd == ‘_start’:
- ”Start”ボタンが押されるとここが実行されます。
- 38行:elif cmd == ‘_rec’:
- Take項目の、”Video”ボタンが押されるとここが実行されます。
- 39から43行が録画開始処理
- 44から48行が録画終了処理
- 51行:elif cmd == ‘_photo’:
- wgetを使って、mjpg-streamerの写真をサーバーに保存
- 57行:elif cmd == ‘_rephoto’:
- Replay項目の、”Photo”ボタンが押されるとここが実行されます。
- 61行:elif cmd == ‘_replay’:
- Replay項目の、”Video”ボタンが押されるとここが実行されます。
- 65から76行: サーバーの状態をクライアントに送信。送信データは2つ。最初がサーバの状態。次はファイルの有無。
- 129行:port = 8010
- Webサーバーのポートを、”8010”としています。
次は、st_mjpg.sh。stream.py内で使用されるmjpg-streamerを実行する為のスクリプトファイル。mjpg-streamerインストール時に作成された”start.sh”を参照しています。
st_mjpg.sh
#!/bin/sh
cd /home/pi/mjpg-streamer/mjpg-streamer-experimental
mjpg_streamer \
-i "input_uvc.so -f 10 -r 320x240 -d /dev/video0 -n" \
-o "output_http.so -w ./www -p 8080"
- 3行:mjpg_streamerのディレクトリへ移動
- 本ページの説明通りにmjpg_streamerをインストールしていれば、mjpg_streamerのパスはこれになります。
- もし違う場所にインストールしているのなら、パスを変更して下さい。
- 5行:-i “input_uvc.so -f 10 -r 320×240 -d /dev/video0 -y -n”
- -i 入力関係の指定。”input_uvc.so 以下にパラメータ指定
- -f:フレームの指定。今回は1秒間に10枚
- -r:解像度の指定。320x240
- -d:カメラディバイスの指定。/dev/video0と認識されたいました。
- -n:pan/tilt/focus/等の設定を行わない。このカメラにそれらの機能は無い。
- 6行:-o “output_http.so -w ./www -p 8080”
- -o 出力関係の指定。”output_http.so 以下にパラメータを指定
- -w:ウェブコンテンツのあるディレクトリ。インストール時に作られた、”www”フォルダーの位置をしてしています。
- -p:使用するポート。今回は、8080を指定。
次は、”stream.html”。 HP用のHTMLです。
stream.html
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<link rel='stylesheet' type='text/css' href='./stream.css' >
<title>Net Stream</title>
</head>
<body>
<div class='b_frame'>
<div class='t_font'><u>Stream 1.0</u></div>
<div class='p_frame'>
<img id="_stream" width="320" height="240" style='display:none'>
<video controls id='_video' width="320" height="240" style='display:none'> </video>
</div>
<div class="_menu">
<div class="input-group">
<label>Stream</label>
<button style="visibility:hidden;">none</button>
<button type='button' id='_start' onclick='onBtn_Btn("_start")'>Start</button>
<label>Take</label>
<button type='button' id='_rec' onclick='onBtn_Btn("_rec")'>Video</button>
<button type='button' id='_photo' onclick='onBtn_Btn("_photo")'>Photo</button>
<label>Replay</label>
<button type='button' id='_replay' onclick='onBtn_Btn("_replay")'>Video</button>
<button type='button' id='_rephoto' onclick='onBtn_Btn("_rephoto")'>Photo</button>
<label>Download</label>
<a href="./file.mp4" download="file.mp4"><button id='_dlv' type="button">Video</button></a>
<a href="./phto.jpg" download="phto.jpg"><button id='_dlp' type="button">Photo</button></a>
<label>Control</label>
<button style="visibility:hidden;">none</button>
<button id='_control' type='button' onclick='onBtn_Btn("_control")'>Control</button>
</div>
<form method='get' id='Btn_main'>
<input name="1" id='Btn_sub' style="display:none">
</form>
</div>
</div>
<script>
// 0:state
// 1: file 0:none 1:jpg 2:mp4 3:both
var para=['0','0'];
var xhr = new XMLHttpRequest();
var a,b;
xhr.open('GET', "http://raspberrypi.local:8010/?80=0");
xhr.send();
xhr.onreadystatechange = function()
{
if(xhr.readyState === 4 && xhr.status === 200)
{
console.log( xhr.responseText );
b=0;
for(a = 0; a < 2; a++)
{
para[a]='';
while( xhr.responseText[b] != ',')
{
para[a] += xhr.responseText[b];
b ++;
}
b ++;
}
a = Number(para[1]);
if ((a & 1) == 0)
{
a = ['_rephoto', '_dlp'];
a.forEach(element => set_Btn(element));
}
if ((a & 2) == 0)
{
a = ['_replay', '_dlv'];
a.forEach(element => set_Btn(element));
}
switch(para[0])
{
case '0': // Streaming
document.getElementById('_video').style.display = "none";
document.getElementById('_stream').src = "http://raspberrypi.local:8080/?action=stream";
document.getElementById('_stream').style.display = "block";
break;
case '1': // Start rec
document.getElementById('_rec').style.backgroundColor = 'red';
document.getElementById('_rec').innerHTML = "Stop";
document.getElementById('_video').style.display = "none";
document.getElementById('_stream').src = "http://raspberrypi.local:8080/?action=stream";
document.getElementById('_stream').style.display = "block";
a = ['_start', '_photo', '_replay', '_rephoto', '_dlv', '_dlp', '_control'];
a.forEach(element => set_Btn(element));
break;
case '2': // Photo
document.getElementById('_video').style.display = "none";
document.getElementById('_stream').src = "./phto.jpg";
document.getElementById('_stream').style.display = "block";
a = ['_rec', '_photo', '_replay', '_dlv', '_control'];
a.forEach(element => set_Btn(element));
break;
case '3': // Replay
document.getElementById('_stream').style.display = "none";
document.getElementById('_video').style.display = "block";
document.getElementById('_video').src = "./file.mp4"
document.getElementById('_video').load();
a = ['_rec', '_photo', '_replay', '_rephoto', '_dlp', '_control'];
a.forEach(element => set_Btn(element));
break;
}
}
}
function onBtn_Btn(_id) {
if( _id != '_control')
{
document.getElementById('Btn_sub').value = _id;
document.getElementById('Btn_main').submit();
}
else
window.open("http://raspberrypi.local:8080/control.htm", '_blank');
}
function set_Btn(_id) {
document.getElementById(_id).disabled = true;
document.getElementById(_id).style.backgroundColor = "gray";
}
</script>
</body>
</html>
- 13行:写真、Streamingの場合は、<Img> タグを有効にして使用しています。
- 14行:Videoの再生は、<video> タグを有効にして表示しています。
- 57行:サーバーからのパラメータの取得とHPの設定
- サーバーからのパラメータに合わせてHPの状態を設定しています。
- 63から71行:サーバーから送られてくるパラメータは2つ。この部分でpara[]に読込
- 86から120行:表示の切り替え作業とボタンの有効/無効設定。
- 124から132行:HP内でボタンを押された時の処理。
次は、”stream.css”。HP用のCSSファイルです。
stream.css
@charset "UTF-8";
.t_font {
font-size: 32px;
font-weight: bold;
font-style: italic;
text-align: center;
color: #0ff;
}
.b_frame {
width: 370px;
background: #363636;
border-radius: 50px;
border-style: ridge;
border-width: 5px 15px;
border-color: sienna;
margin: 0 auto
}
.p_frame {
width: 324px;
height: 240px;
background: #363636;
border-radius: 10px;
border-style: ridge;
border-width: 5px;
border-color: sienna;
margin: 10px auto;
text-align: center;
}
button {
display: inline;
margin: 4px 2px;
line-height: 15px;
width: 80px;
font-size: 15px;
font-weight: bold;
font-style: italic;
cursor: pointer;
color: #fff;
background: #228b22;
border-radius: 20px;
text-align: center;
padding-left: 4px;
}
.input-group>label {
display: inline-block;
font-size: 16px;
font-weight: bold;
font-style: italic;
padding-left: 15px;
min-width: 40%;
color: #0ff;
margin-left: 10px;
}
._menu {
display: block;
flex-wrap: nowrap;
min-width: 280px;
background: #363636;
border-radius: 4px;
margin: 15px auto 30px;
}
最後は、”favicon.ico” ですが、これは画像ファイルで特に説明無し。
プログラムの実行
これをダウンロードして解凍すると”stream”と言うフォルダーが出来ます。これをRaspberry Piのホームディレクトリにコピーして下さい。コピー後のホームディレクトリと”stream”ディレクトリの内容は以下の様になります。
実行ディレクトリは、”stream”ディレクトリです。”stream”ディレクトリに移動して、python3 stream.pyと実行して下さい。画面に下記の様なコメントが出力され、USBカメラのLEDが点灯すれば動作が開始した事が分かります。
PC側からブラウザを使用してRaspberryに接続します。PCのブラウザは、”Firefox”を使用して下さい。それ以外のブラウザでの動作は確認していません。ブラウザのURL欄に、”http://raspberrypi.local:8010/”と入力して下さい。下記のHP画面が表示されstreamingが開始されます。
初めての起動では、録画した動画も写真も無いのでそれらに関係するボタンは無効化(灰色)されています。状態に応じてボタンが有効になったり無効になったりします。
最後に
今回はPythonのWebサーバーを使って簡単にStreaming、録画、写真の撮影が出来ました。Raspberry PIのGPIOポートを使えば、暗くなった時の撮影用ライトの点灯、人感センサーを使えば人を感知した時の録画等の活用が可能です。
ちょっとがっかりな点は、mjpg-streamerは音声に対応していない点です。画像だけです。やっぱり音は有った方が良いです。
追加:録画してからそれが再生されるまでに若干時間がかかる様です。今回の環境で録画終了から再生されるまで2分位かかりました。ダウンロードは録画終了後直ぐ出来るのですが、再生のみ時間がかかります。なぜそんなに時間がかかるのか不明です。