Mjpg-Stremer & FFmpegを使う

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

必要なハード

OSのインストール

インストールはPI 3 model B+のWifiを利用して、Raspberry Pi にOSをInstallに従って行いました。
設定は下記の通り。

  • OS    : Raspberry Pi OS Lite(32-bit)     
  • ホスト名 : raspberry
  • ユーザー名: pi
  • パスワード: raspberry

今回はサーバがメインなのでGUIの必要無し。Raspberry PIの負担を下げる為にCUIを選択しました。LANケーブル無しでインストール出来、インストール後直ぐにSSHが使えるのでとても便利です。

PCからRaspberry piに “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とFFmpegをインストールします。

先ずはMjpg-Streamer。

このインストールでは”git”を使います。先ずは、”sudo apt-get install git”で”git”を先にインストールして下さい。その後USBカメラをRaspberry Piに繋いで下記を実行して下さい。

install

cd ~/
sudo apt install -y git cmake libjpeg-dev
git clone https://github.com/neuralassembly/mjpg-streamer.git
cd mjpg-streamer/mjpg-streamer-experimental
make
sudo make install

mjpg-streamerの動作を確認します。ホームディレクトリに新たに”mjpg-streamer”が作成されています。その下の”mjpg-streamer-experimental”ディレクトリに移動して(手順通りにInstallしていれば、そこにいるはず)スクリプトファイル”start.sh”を実行。下記の様な画面になればOKです。

mjpg-streamerは8080ポートを使用しているので、PCのブラウザのURL欄に”raspberry.local:8080″と入力すると、下記の様な画面が表示されます。

これが、”mjpg-streamer”のHPです。HP中段下にある写真(ドアの写真)がカメラの画像です。左側にメニューを使ってスナップショットやStreamingが出来ます。

”start.sh”は殆どがコメントでコマンドは下記の2行のみです。

export LD_LIBRARY_PATH="$(pwd)"
./mjpg_streamer -i "./input_uvc.so" -o "./output_http.so -w ./www"

1行目は環境変数の設定。2行目がMjpg-streamerの実行コマンドです。Mjpg-streamerは実行時に幾つかのパラメタを設定する必要が有ります。主はパラメタ一は以下の通り。

OptionparametersDescription
-iinput-plugin.soの指定
  PI_カメラ: “./input_raspicam.so [parameters]”
  USB_カメラ: “./input_uvc.so [parameters]”
-dカメラの指定
  Default: /dev/video0(現在Activeなカメラ)
-r画面の解像度の指定
  QQVGA QCIF CGA QVGA CIF PAL VGA SVGA XGA
  HD SXGA UXGA FHD
  または、 数字で指定 640×480
Default: 640×480
-f1秒間にフレーム数
(camera may coerce to different value)
-qJPEGの品質(0-100)を指定。Default: 85
-ooutput-plugin.soの指定 ’./output_http.so [parameters]”
-wWeb関係のファイルが入っているfolderのパス
(サブフォルダは不可)
-pTCP が使うポート Default: 8080
-lIPアドレスの指定。
-hヘルプに表示
-v version information
-b backgroundへ

オプション -i と -oは必ず指定します。./mjpg_streamer -i “./input_uvc.so” -o “./output_http.so -w ./www” は、この表から

  • 入力(-i)
    • カメラ:USBカメラ
    • 解像度:640x480
  • 出力(-o)
    • Web関係のファイル: ./www
    • 使用ポート: 8080

と設定した事になります(正確にはこの他にもデフォルト値が適応さていますが)。

次は、FFmpegをインストール

FFmpegはStreamingを録画する時に使用します。インストールは簡単で、”sudo apt install ffmpeg”でOKでした。

ネット環境

今回のネット環境は下記の様にRaspberry Pi とデスクトップPCを1つのルータ内に配置しています。Raspberry Pi にはSSHでPCから接続。さらに、Raspberry Pi にSAMBAを立ち上げています。Raspberry Pi には電源のみの接続です。

アプリの製作

必要なアプリをインストールしたのでアプリの製作に入ります。

アプリの概要

下記の様にStreamingと写真の表示と保存、保存したデータの再生が出来るアプリを目指します。

  • Pythonでサーバを立ち上げそのHP画面を上記の様に設定。
  • ユーザーから要求を受けたPythonでサーバはStreaming、静止画に対し以下で対応。
    • 表示: Mjpg-streamerサーバと
    • 録画: ffmpeg、Mjpg-streamerサーバと

静止画とStreamingの取り込みはHTMLコードはデモプログラムから以下に通り。

  • 静止画の取り込み: <img src = “./?action=snapshot ” />
  • Streamingの場合: <img src=”./?action=stream” />

動画の録画は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'> 
                    <p>動画の再生に対応していません</p>
                </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[0]);
                    switch(a) {
                        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";
                                break;
        
                        case 2:   // Photo
                                document.getElementById('_video').style.display = "none";
                                document.getElementById('_stream').src = "./phto.jpg";
                                document.getElementById('_stream').style.display = "block";
                                break;
        
                        case 3:   // Replay
                                document.getElementById('_stream').style.display = "none";
                                document.getElementById('_video').style.display = "block";
                                document.getElementById('_video').src = "./file.mp4";
                                break;
                    }

                    if(a == 1) b = ['_start', '_photo', '_control'];
                    if(a > 1) b = ['_rec', '_photo', '_control'];
                    if(a) b.forEach(element => set_Btn(element));

                    a = Number(para[1]);
                    switch(a){
                        case 0: b = ['_replay', '_rephoto', '_dlv', '_dlp'];
                                break;

                        case 1: b = ['_replay', '_dlv'];
                                break;

                        case 2: b = ['_rephoto', '_dlp'];
                                break;
                    }
                    if(a < 3) b.forEach(element => set_Btn(element));
       	        }
    	     }
    
    	    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> タグを有効にして表示しています。
  • 59行:サーバーからのパラメータの取得とHPの設定
  • 120から128行: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分位かかりました。ダウンロードは録画終了後直ぐ出来るのですが、再生のみ時間がかかります。なぜそんなに時間がかかるのか不明です。