ラズパイにhlsサーバー建てて、USBカメラ映像をMotionBoardに直接ストリーミング配信する

# 1. はじめに
MotionBoard Cloudのストリーミング動画の再生機能を使い、USBカメラの映像をリアルタイムに表示したいと思っていろいろ調べてみました。
現状ではHLSとMPEG-DASHの再生に対応していますが、以下にはHLSでの配信実験結果を書きます。

# 2. HLSとは
HTTP Live Streamingの略で、HTTPベースで動画のストリーミングができるプロトコルです。

# 3. MotionBoardとは
データベースやセンサーデータなどを可視化できるソフトウェアです。
商用ソフトウェアですが、各種データベースとの接続や高度な可視化・分析機能が使用できます。

- [MotionBoard](http://www.wingarc.com/product/motionboard/)


# 4. 前提環境
- MotionBoard Cloud
- Raspberry Pi 4B (4GB)
- Raspbian Buster Lite
- 適当な市販のUSBカメラ

# 5. 連携手順
Raspberry Piの初期セットアップは済んでいるものとします。

## FFmpegのインストール
Webカメラの映像をts形式に変換すると同時にm3u8形式のプレイリストを生成する為に使用します。

```bash
sudo apt-get install ffmpeg
```

## Webサーバー
pythonを使用して簡易的なものを建てます。
以下の例ではポート番号は8000としています。

レスポンスヘッダにAccept-Ranges、Access-Control-Allow-Origin、Access-Control-Allow-Headersを含めないとMotionBoard側でエラーとなり、動かないので注意します。
headers["Accept-Ranges"] = 'bytes'
headers["Access-Control-Allow-Origin"] = '&#x002a;'
headers["Access-Control-Allow-Headers"] = '&#x002a;'

server.pyとして~/streamに作成。

```bash
mkdir ~/stream
cd ~/stream
vi server.py
```

```python
#!/usr/bin/env python

import codecs
import mimetypes
import os
import re
import sys
import threading
import webbrowser

if sys.version_info[0] > 2:
    from urllib.parse import urlparse
    from http.server import HTTPServer
    from http.server import BaseHTTPRequestHandler
else:
    from urlparse import urlparse
    from BaseHTTPServer import HTTPServer
    from BaseHTTPServer import BaseHTTPRequestHandler

folder = "."
port = 8000
browse = False
redirects = []
index_page = "index.html"
not_found_page = ""

args = sys.argv[1:]
while len(args) > 0:
    arg = args.pop(0)
    if (arg == "--port" or arg == "-p") and len(args) > 0 and args[0].isdigit(): 
        port = int(args.pop(0))
    if (arg == "--index-page" or arg == "-i") and len(args) > 0: 
        index_page = args.pop(0)
    if (arg == "--not-found-page" or arg == "-e") and len(args) > 0: 
        not_found_page = args.pop(0)
    elif (arg == "--redirect-map" or arg == "-r") and len(args) > 0:
        with codecs.open(args.pop(0), "r", "utf-8") as open_file:
            data = open_file.read()
            lines = re.split(r"\r\n?|\n", data)
            while len(lines) > 0:
                line = lines.pop(0)
                match = re.compile("([^ ]*) *([^ ]*)").match(line)
                if match and len(match.groups()[0]) > 0 and len(match.groups()[1]) > 0:
                    redirects.append({ 
                        "source": match.groups()[0], 
                        "target": match.groups()[1] })
    elif arg == "--browse" or arg == "-b":
        browse = True
    elif not arg.startswith("-"):
        folder = arg

class HTTPRequestHandler(BaseHTTPRequestHandler):
    def handler(self):
        pathname = urlparse(self.path).path
        location = folder + pathname;
        status_code = 0
        headers = {}
        buffer = None
        for redirect in redirects:
            if redirect["source"] == pathname:
                status_code = 301
                headers = { "Location": redirect["target"] }
        if status_code == 0:
            if os.path.exists(location) and os.path.isdir(location):
                if location.endswith("/"):
                    location += index_page
                else:
                    status_code = 302
                    headers = { "Location": pathname + "/" }
        if status_code == 0:
            if os.path.exists(location) and not os.path.isdir(location):
                status_code = 200
            else:
                status_code = 404
                location = folder + "/" + not_found_page
            if os.path.exists(location) and not os.path.isdir(location):
                with open(location, "rb") as binary:
                    buffer = binary.read()
                headers["Content-Length"] = len(buffer)
                headers["Accept-Ranges"] = 'bytes'
                headers["Access-Control-Allow-Origin"] = '*'
                headers["Access-Control-Allow-Headers"] = '*'
                headers["cache-control"] = 'public, max-age=0, must-revalidate'
                extension = os.path.splitext(location)[1]
                if extension == '.m3u8':
                    content_type = 'audio/x-mpegurl'
                elif extension == '.ts':
                    content_type = 'video/MP2T'
                else:
                    content_type = mimetypes.types_map[extension]
                if content_type:
                    headers["Content-Type"] = content_type
        print(str(status_code) + " " + self.command + " " + self.path)
        sys.stdout.flush()
        self.send_response(status_code)
        for key in headers:
            self.send_header(key, headers[key])
        self.end_headers()
        if self.command != "HEAD":
            if status_code == 404 and buffer == None:
                self.wfile.write(str(status_code))
            elif (status_code == 200 or status_code == 404) and buffer != None:
                self.wfile.write(buffer)
    def do_GET(self):
        self.handler();
    def do_HEAD(self):
        self.handler();
    def log_message(self, format, *args):
        return

server = HTTPServer(("localhost", port), HTTPRequestHandler)
url = "http://localhost:" + str(port)
print("Serving '" + folder + "' at " + url + "...")
if browse:
    threading.Timer(1, webbrowser.open, args=(url,)).start()
sys.stdout.flush()
try:
    server.serve_forever()
except (KeyboardInterrupt, SystemExit):
    server.server_close()

```

##FFmpegのテスト
（Webカメラの映像は/dev/video0としています）

```bash

ffmpeg -f alsa -thread_queue_size 1024 \ -i hw:1 \
                    -f v4l2 -thread_queue_size 512 -input_format yuyv422 -video_size 800x600 \
                     -i /dev/video0 \
                     -filter_complex scale=800x600,fps=12 \
                     -c:v h264_omx -b:v 764k -g 24 \
                     -c:a aac -b:a 64k \
                     -flags +cgop+global_header \
                     -f hls \ -hls_time 2 -hls_list_size 3
                     -hls_allow_cache 0 \
                     -hls_segment_filename stream_%d.ts \
                     -hls_flags delete_segments \
                        playlist.m3u8
```

##動作確認

- FFmpeg実行

```bash
cd ~/stream
ffmpeg -f alsa -thread_queue_size 1024 \ -i hw:1 \
                    -f v4l2 -thread_queue_size 512 -input_format yuyv422 -video_size 800x600 \
                     -i /dev/video0 \
                     -filter_complex scale=800x600,fps=12 \
                     -c:v h264_omx -b:v 764k -g 24 \
                     -c:a aac -b:a 64k \
                     -flags +cgop+global_header \
                     -f hls \ -hls_time 2 -hls_list_size 3
                     -hls_allow_cache 0 \
                     -hls_segment_filename stream_%d.ts \
                     -hls_flags delete_segments \
                        playlist.m3u8
```



- Webサーバー実行

```bash
python server.py
```

- [ngrok](https://ngrok.com/)でサーバー公開
（ngrok初期設定済みの状態で）

```bash
ngrok http 8000
```

と実行し、
ngrokの管理画面上からhttpsのURLを確認してMotionBoardに設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70061/977c6ba8-a6f4-c6e2-8206-98a0cec9da6b.png)

あとはRaspberry Piの起動時にこれらを自動実行するなりなんなりすればお手軽hls配信サーバーとして動作します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70061/9678d272-d772-1628-7a48-b23ccd13bd73.png)