せっかくのカメラのついたエッジAIデバイスなのでネットワークカメラを作ってみることにした。Jetson Nanoを題材にしているが、今回の記事の時点ではまだJetson特有の機能を使用していないので、Raspberry Piでも同じような手順でできるはず。
Overview
構築するストリーミングシステムの概要図を示す。
CSIカメラ(Raspberry Pi camera v2)からの映像をGstreamerパイプラインでエンコードし、ストリームデータとして出力。
コンソールからコマンド一発でここまで実現できる。最高!
ウェブサーバーとしてJetsonへlighttpdを実装する。lighttpdは軽量・高速を謳うウェブサーバーOSS。apacheでも良いのだろうけど、実装する機能は限られているし、組み込みで容量を抑えたいこともあってlighttpdを採用した。
ストリーミングのプロトコルにはHLSを使用する。httpを使用して難しいセットアップもなしに実装できる。 詳しい仕組みについては後述。
なお、本記事では192.168.0.8をJetsonのIPアドレスとする。
HLS(HTTP Live Streaming)
HLSとは
HLS(HTTP Live Streaming)はメディアをストリーミングするために定められたプロトコル。Appleによって実装されている。
カメラのストリーミング映像や画像データをインデックスファイル(.m3u8ファイル)とts file(.tsファイル)に分割する。ユーザーは普通にホームページを見るのと同様にこのデータをダウンロードし、ブラウザで動画として表示する。
インデックスファイルとセグメントファイル
実はこれは昔のmp3プレイヤーから使われている技術。自分の好きな曲を選択して「プレイリスト」をつくるとm3uファイルとして保存される。m3uファイルは選択されたmp3ファイルの場所と再生順序を記録している。プレイヤーはこのm3uファイルを参照して、プレイリストの通りに曲を再生する という仕組み。
HLSはこれを動画に当てはめて考えるだけ。
ストリーミングでは、セグメントファイルが動的に生成され、同時に古いファイルが破棄される。インデックスファイルの中身はセグメントファイルの順番を示していて、セグメントファイルが生成/破棄される度に内容が更新される。
これによって、例えば巨大な動画ファイルを動的にセグメントファイルに分割して配信する なんてこともできる。(言ってみれば、カメラのライブ映像って容量無限の動画ファイルみたいなもんだ)
ちなみにmp4などの動画ファイルの場合はffmpegというツールをつかってセグメントファイルに分割する。
lighttpdのセットアップ
ligthttpdをインストール
下記のコマンドでインストールする。
1 |
$sudo apt install lighttpd |
次に下記のコマンドで起動する。簡単!
1 |
sudo systemctl enable --now lighttpd |
と思いきや、そのままサービススタートしようとすると以下のようなエラーになる。
1 2 3 4 5 |
hiko@hiko-desktop:~/projects/face_detect$ sudo systemctl enable --now lighttpd Synchronizing state of lighttpd.service with SysV service script with /lib/systemd/systemd-sysv-install. Executing: /lib/systemd/systemd-sysv-install enable lighttpd Job for lighttpd.service failed because the control process exited with error code. See "systemctl status lighttpd.service" and "journalctl -xe" for details. |
言われている通り、systemctl status
でステータスを確認してみる。
1 2 3 4 5 6 7 8 9 10 11 12 |
hiko@hiko-desktop:~/projects/face_detect$ sudo systemctl status lighttpd ● lighttpd.service - Lighttpd Daemon Loaded: loaded (/lib/systemd/system/lighttpd.service; enabled; vendor preset: enabled) Active: failed (Result: exit-code) since Mon 2020-11-23 10:53:57 JST; 9s ago Process: 13204 ExecStartPre=/usr/sbin/lighttpd -tt -f /etc/lighttpd/lighttpd.conf (code=exited, status=127) 11月 23 10:53:57 hiko-desktop systemd[1]: lighttpd.service: Service hold-off time over, scheduling restart. 11月 23 10:53:57 hiko-desktop systemd[1]: lighttpd.service: Scheduled restart job, restart counter is at 5. 11月 23 10:53:57 hiko-desktop systemd[1]: Stopped Lighttpd Daemon. 11月 23 10:53:57 hiko-desktop systemd[1]: lighttpd.service: Start request repeated too quickly. 11月 23 10:53:57 hiko-desktop systemd[1]: lighttpd.service: Failed with result 'exit-code'. 11月 23 10:53:57 hiko-desktop systemd[1]: Failed to start Lighttpd Daemon. |
状態はfailed
となっていて、サービスの立ち上げに失敗する。
これはJetsonの問題というよりubuntuの問題な気がするので、同様のエラー報告がないかググってみたところ、下記のフォーラムのスレッドがヒットした。
Failed to start lighttpd Daemon on boot – Ask Ubuntu
https://askubuntu.com/questions/1072840/failed-to-start-lighttpd-daemon-on-boot
上記の通り、gaminというパッケージをインストールすると解消できる。理由は不明 (笑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
hiko@hiko-desktop:~/projects/face_detect$ sudo systemctl enable --now lighttpd Synchronizing state of lighttpd.service with SysV service script with /lib/systemd/systemd-sysv-install. Executing: /lib/systemd/systemd-sysv-install enable lighttpd hiko@hiko-desktop:~/projects/face_detect$ sudo systemctl status lighttpd ● lighttpd.service - Lighttpd Daemon Loaded: loaded (/lib/systemd/system/lighttpd.service; enabled; vendor preset: enabled) Active: active (running) since Mon 2020-11-23 11:03:27 JST; 8s ago Process: 15101 ExecStartPre=/usr/sbin/lighttpd -tt -f /etc/lighttpd/lighttpd.conf (code=exited, status=0/SUCCESS) Main PID: 15137 (lighttpd) Tasks: 1 (limit: 4174) CGroup: /system.slice/lighttpd.service └─15137 /usr/sbin/lighttpd -D -f /etc/lighttpd/lighttpd.conf 11月 23 11:03:27 hiko-desktop systemd[1]: Starting Lighttpd Daemon… 11月 23 11:03:27 hiko-desktop systemd[1]: Started Lighttpd Daemon. |
試しに、webサーバーにアクセスしてみる。
クライアントPCのwebブラウザにJetsonのIPアドレス192.168.0.8を入力。
上の画像のような画面が表示されればOK。この画面は、/var/www/html/index.lighttpd.html
の内容が表示されている。このhtmlファイルはlighttpdをインストールした際に自動で生成されるテスト用のトップ画面。
ここまででwebサーバの動作確認は完了。
lighttpdの設定
次に、HLSでストリーム配信をするめにlighttpdのコンフィグレーションファイルの変更を行う。
/etc/lighttpd/lighttpd.conf/
を編集する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
server.modules = ( "mod_rewrite", "mod_alias", "mod_expire", "mod_fastcgi", "mod_accesslog" ) server.document-root = "/var/www/html" server.upload-dirs = ( "/var/cache/lighttpd/uploads" ) server.errorlog = "/var/log/lighttpd/error.log" server.pid-file = "/var/run/lighttpd.pid" server.username = "www-data" server.groupname = "www-data" server.port = 80 mimetype.assign = ( ".html" => "text/html", ".txt" => "text/plain", ".ico" => "image/vnd.microsoft.icon", ".jpg" => "image/jpeg", ".png" => "image/png", ".css" => "text/css", ".js" => "text/javascript", ".m3u" => "audio/mpegURL", ".m3u8" => "audio/mpegURL" ".ts" => "video/MP2T" ) index-file.names = ( "index.php", "index.html", "index.lighttpd.html" ) url.access-deny = ( "~", ".inc" ) #static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" ) static-file.exclude-extensions = ( ".fcgi", "php", ".rb", "~", ".inc" ) compress.cache-dir = "/var/cache/lighttpd/compress/" compress.filetype = ( "application/javascript", "text/css", "text/html", "text/plain" ) alias.url = ( "/alias/path/" => "/var/www/") # default listening port for IPv6 falls back to the IPv4 port ## Use ipv6 if available #include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port server.pid-file = "/var/run/lighttpd.pid" #include_shell "/usr/share/lighttpd/create-mime.assign.pl" include_shell "/usr/share/lighttpd/include-conf-enabled.pl" |
メディアタイプの定義(mimetype)
webサーバーのディレクトリに配置されたコンテンツがどのような種類のファイルであるのかを拡張子から定義する。
定義は[大分類]/[小分類]という感じで定義される。例えばhtmlファイルであれば、大分類はテキストファイル、小分類はhtmlとして定義し、".html" => "text/html",
と記述する。
.htmlや.css, .phpなどの基本的なファイル形式に加え、今回はHLSのインデックスファイルとセグメントファイル(.m3u8, .ts)への定義を追加する。
1 |
".m3u8" => "audio/mpegURL"<br> ".ts" => "video/MP2T" |
m3u8がaudioになってるのが気になるけど、これはもう決まりなので気にしない。
詳細はwikipediaを参照
https://ja.wikipedia.org/wiki/メディアタイプ
ちなみに、include_shell "/usr/share/lighttpd/create-mime.assign.pl"
を記述すると、大体のファイル形式の自動で定義してくれる。こちらを使う場合はmimetype.assign = (...)
の記述は削除すること。
尚、.m3u8の定義は create-mime.assign.pl
には入っていないので、自分で追記する必要がある。
ストリーム配信ページの作成
ストリーム配信ページの作成を行う。下記のパスへ格納。/var/www/html/hls.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"> <title>HTTP Live Streaming Test</title> </head> <body> <header> <h1>HTTP Live Streaming Test</h1> </header> <div> <video width="1280" height="720" src="playlist.m3u8" preload="none" onclick="this.play()" controls /> </div> </body> </html> |
h1タグのタイトルとビデオの埋め込みがあるだけのシンプルなテストページ。
videoタグでビデオの埋め込みをする。src
でインデックスファイル(.m3u8ファイル)のパスを指定する。hls.htmlと同じ場所に格納する予定なので、ファイル名のみ記述している。
Gstreamer
Gstreamerの以下のコマンドを打つ
1 |
$ sudo gst-launch-1.0 nvarguscamerasrc ! 'video/x-raw(memory:NVMM), width=(int)1280, height=(int)720, framerate=(fraction)30/1' ! omxh264enc bitrate=1000000 insert-aud=true ! video/x-h264, profile=baseline ! mpegtsmux ! hlssink max-files=5 playlist-location=/var/www/html/playlist.m3u8 location=/var/www/html/segment%05d.ts playlist-root=http://192.168.0.8:80/ |
一行になっていて見難いと思うので、改行を追加したものを以下に示す。
1 2 3 4 5 6 7 8 9 |
$ sudo gst-launch-1.0 nvarguscamerasrc ! 'video/x-raw(memory:NVMM), width=(int)1280, height=(int)720, framerate=(fraction)30/1' ! omxh264enc bitrate=1000000 insert-aud=true ! video/x-h264, profile=baseline ! mpegtsmux ! hlssink max-files=5 playlist-location=/var/www/html/playlist.m3u8 location=/var/www/html/segment%05d.ts playlist-root=http://192.168.0.8:80/ |
主要なエレメントとパラメータをざっくり解説していきます。
omxh264end
カメラからのストリームデータを.h264にエンコードする。
ビットレートとオーディオデータの有無を選択できる。
mpegtsmux
メディアデータをMPEG Trasport Streamデータへ変換する。HLSの.tsファイルと.m3u8ファイルを作る。
hlssink
シンク側の選択。hlsを指定。
max-files
tsファイルをいくつ作るかを指定する。
playlist-location
インデックスファイル(.m3u8ファイル)を生成する場所を指定する。
先ほど作成したhls.htmlの中ではhtmlファイルと同ディレクトリに設定しているので、/var/www/html/playlist.m3u8 と指定しておく。
location
.tsファイルを生成する場所。playlistと同じディレクトリを指定。
location=/var/www/html/segment%05d.ts
%05dとすることで、5ケタの連番ファイルが作成される。
動作確認
gstreamerのコマンドを実行する。下記のようなメッセージが表示されればOK。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
パイプラインを一時停止 (PAUSED) にしています... Pipeline is live and does not need PREROLL ... Framerate set to : 30 at NvxVideoEncoderSetParameterパイプラインを再生中 (PLAYING) にしています... NvMMLiteOpen : Block : BlockType = 4 New clock: GstSystemClock ===== NVMEDIA: NVENC ===== NvMMLiteBlockCreate : Block : BlockType = 4 H264: Profile = 66, Level = 40 GST_ARGUS: Creating output stream CONSUMER: Waiting until producer is connected... GST_ARGUS: Available Sensor modes : GST_ARGUS: 3264 x 2464 FR = 21.000000 fps Duration = 47619048 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000; GST_ARGUS: 3264 x 1848 FR = 28.000001 fps Duration = 35714284 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000; GST_ARGUS: 1920 x 1080 FR = 29.999999 fps Duration = 33333334 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000; GST_ARGUS: 1280 x 720 FR = 59.999999 fps Duration = 16666667 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000; GST_ARGUS: 1280 x 720 FR = 120.000005 fps Duration = 8333333 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000; GST_ARGUS: Running with following settings: Camera index = 0 Camera mode = 4 Output Stream W = 1280 H = 720 seconds to Run = 0 Frame Rate = 120.000005 GST_ARGUS: Setup Complete, Starting captures for 0 seconds GST_ARGUS: Starting repeat capture requests. CONSUMER: Producer has connected; continuing. |
次に、/var/www/htmlに.tsファイルが動的に作成されていることを確認する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ ls /var/www/html/ hls.html segment00002.ts index.lighttpd.html segment00003.ts playlist.m3u8 segment00004.ts segment00001.ts segment00005.ts $ ls /var/www/html/ hls.html segment00003.ts index.lighttpd.html segment00004.ts playlist.m3u8 segment00005.ts segment00002.ts segment00006.ts $ ls /var/www/html/ hls.html segment00004.ts index.lighttpd.html segment00005.ts playlist.m3u8 segment00006.ts segment00003.ts segment00007.ts |
ここまで確認できれば、あとはクライアントPCからブラウザで映像が見えるか確認する。
ブラウザから 192.168.0.8/hls.html にアクセスし、カメラの画像が表示されればOK。
かなりレイテンシがおおきく、30~50secくらいの遅延がある。