5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-23

1. はじめに

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

2. HLSとは

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

3. MotionBoardとは

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

4. 前提環境

  • MotionBoard Cloud
  • Raspberry Pi 4B (4GB)
  • Raspbian Buster Lite
  • 適当な市販のUSBカメラ

5. 連携手順

Raspberry Piの初期セットアップは済んでいるものとします。

FFmpegのインストール

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

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"] = '*'
headers["Access-Control-Allow-Headers"] = '*'

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

mkdir ~/stream
cd ~/stream
vi server.py
#!/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としています)


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実行
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サーバー実行
python server.py
  • ngrokでサーバー公開
    (ngrok初期設定済みの状態で)
ngrok http 8000

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

image.png

あとはRaspberry Piの起動時にこれらを自動実行するなりなんなりすればお手軽hls配信サーバーとして動作します。
image.png

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?