Mjpg-Stremer & FFmpegを使う

Raspberry Piを使って映像StreamingするならMjpg-Streamer。今回はPythonでWebサーバーを立ち上げMjpg-Streaminerをサーバーに取り込み、Streaming、録画、写真の撮影が出来るアプリを作成したいと思います。

必要なハード

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に繋いで下記を実行して下さい。

install

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分位かかりました。ダウンロードは録画終了後直ぐ出来るのですが、再生のみ時間がかかります。なぜそんなに時間がかかるのか不明です。