2023/05/05(Fri)低遅延&お手軽なWebcamストリーミングシステムの作り方
2023/05/05 17:47
動体検知とか、カメラ移動操作とか、双方向音声とかそういうのはナシで、単にカメラ映像を手軽にストリーミングし、低遅延でブラウザから確認できるシステムをDIYしてみたメモ。
要求仕様
- カメラに映った映像・音声を低遅延(数秒程度)でライブストリーミングする
- 映像には撮影時刻を埋め込む
- 付けっぱなしの連続稼働に耐える
- Firefox, Chrome等のブラウザがあれば再生できる
- アプリ不要でスマホにも対応する
そろえる物
筆者の環境
- PC: Lenovo ThinkPad X250
- Intel Core i5-5300U (2C4T, 2.3GHz, TDP 15W, Intel QSV対応)
- DDR3L 16GB
- Crucial SSD M4-CT128M4SSD2 128GB
- Webcam: ELECOM UCAM-C820ABBK / FHD 200万画素 マイク有 3,100円くらい
- 三脚: HAKUBA ミニ三脚 eポッド3 三段伸縮 1,800円くらい
- 既設Wi-Fi 2.4G/5GHz
基本方針
どういう組み立てで作るか色々試してみた結果、- OSは再起動不要で長時間安定稼働すること、サービスの常駐化のしやすさを優先しLinux(Fedora)を採用
- カメラからの映像は、HLSでストリーミングさせる
- ストリーミングに際して必要な再変換はffmpegを使う
環境整備
まずはOSのインストール。Fedoraの最新版を取ってきて、USB bootしてインストールするだけ。もうこれだけでGUI desktopが上がる環境がサクッと出来てしまうので凄い。
LANケーブルを繋ぐなり、Wi-Fiにログインするなり、通信環境を整えておく。
独自の設定が必要なのは、クラムシェル運用時やアイドル時にサスペンドしないようにするくらい。
まずは、液晶の蓋を閉じてもサスペンドしないように設定。
$ sudo vim /etc/systemd/logind.conf HandleLidSwitch=ignore HandleLidSwitchExternalPower=ignore $ sudo systemctl restart systemd-login続いて、AC接続時に時間経過でサスペンドしないように設定。
gdmユーザで実行するのは、ログイン画面でこの設定が有効になるようにするためで、ユーザがログインしてGUIで設定した変更は、そのユーザがログインしているときにしか有効ではないので注意。
$ sudo -u gdm dbus-launch gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' $ sudo systemctl restart gdmあとは、必要なソフトウェア類。HTTPサーバ、UVCを扱うためのVideo4Linuxツール、ffmpeg、そしてrpmfusion経由でQSV支援のためのパッケージを入れる。
$ wget https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-37.noarch.rpm $ sudo rpm -i rpmfusion-free-release-37.noarch.rpm $ sudo dnf install httpd v4l2-utils ffmpeg $ sudo dnf --enablerepo=rpmfusion-nonfree-steam install intel-media-driverhttpdが/homeに置いたファイルを読めるようにSELinuxを設定しておく。
$ sudo setsebool -P httpd_enable_homedirs 1カメラをUSBに接続し、利用可能であることを確認。(今回はThinkPad自体にも内蔵カメラがあるので2台分見えている。)
$ v4l2-ctl --list-devices
FHD Camera Microphone: FHD Came (usb-0000:00:14.0-2):
/dev/video2
/dev/video3
/dev/media1
Integrated Camera: Integrated C (usb-0000:00:14.0-8):
/dev/video0
/dev/video1
/dev/media0
実行ユーザをpermitしておき、再ログインで反映。$ sudo gpasswd -a `whoami` video $ sudo gpasswd -a `whoami` audio $ sudo gpasswd -a `whoami` render $ logoutQSVを利用するためにVA-APIが権限的に使えることを確認しておく。
まず/dev/dri/renderD128が見えることを確認。
$ ls -l /dev/dri total 0 drwxr-xr-x. 2 root root 80 Feb 6 21:27 by-path/ crw-rw----+ 1 root video 226, 0 Feb 6 21:27 card0 crw-rw-rw-. 1 root render 226, 128 Feb 6 21:27 renderD128そして、コマンドでも確認。
$ vainfo
Trying display: wayland
Trying display: x11
error: can't connect to X server!
Trying display: drm
libva info: VA-API version 1.16.0
libva info: Trying to open /usr/lib64/dri/iHD_drv_video.so
libva info: Found init function __vaDriverInit_1_16
libva info: va_openDriver() returns 0
vainfo: VA-API version: 1.16 (libva 2.16.0)
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 22.5.4 ()
vainfo: Supported profile and entrypoints
VAProfileNone : VAEntrypointVideoProc
VAProfileNone : VAEntrypointStats
VAProfileMPEG2Simple : VAEntrypointVLD
VAProfileMPEG2Simple : VAEntrypointEncSlice
VAProfileMPEG2Main : VAEntrypointVLD
VAProfileMPEG2Main : VAEntrypointEncSlice
VAProfileH264Main : VAEntrypointVLD
VAProfileH264Main : VAEntrypointEncSlice
VAProfileH264Main : VAEntrypointFEI
VAProfileH264High : VAEntrypointVLD
VAProfileH264High : VAEntrypointEncSlice
VAProfileH264High : VAEntrypointFEI
VAProfileVC1Simple : VAEntrypointVLD
VAProfileVC1Main : VAEntrypointVLD
VAProfileVC1Advanced : VAEntrypointVLD
VAProfileJPEGBaseline : VAEntrypointVLD
VAProfileH264ConstrainedBaseline: VAEntrypointVLD
VAProfileH264ConstrainedBaseline: VAEntrypointEncSlice
VAProfileH264ConstrainedBaseline: VAEntrypointFEI
VAProfileVP8Version0_3 : VAEntrypointVLD
ffmpeg側も、VA-API経由のH.264エンコードなどが利用可能であることを確認。$ ffmpeg -hide_banner -encoders | grep vaapi V....D h264_vaapi H.264/AVC (VAAPI) (codec h264) V....D hevc_vaapi H.265/HEVC (VAAPI) (codec hevc) V....D mjpeg_vaapi MJPEG (VAAPI) (codec mjpeg) V....D mpeg2_vaapi MPEG-2 (VAAPI) (codec mpeg2video) V....D vp8_vaapi VP8 (VAAPI) (codec vp8) V....D vp9_vaapi VP9 (VAAPI) (codec vp9)
ライブストリーミング準備
前述のセットアップで、USBが/dev/video*としてアクセス可能で、かつVA-API有効なffmpegを準備できたので、これをffmpegに入力し、HLSを出力にする。まず準備として出力先を用意し、httpdのDocumentRootに設定しておく。特にCGI等の仕掛けは不要で、置いたファイルが単純にHTTP経由で開ける状態にしておけばよい。
$ sudo vim /etc/httpd/conf/httpd.conf - DocumentRoot "/var/www/html" + DocumentRoot "/home/kero/www" + <Directory "/home/kero/www"> + AllowOverride None + Require all granted + </Directory> $ sudo systemctl restart httpd
次にキャプチャしたいデバイスが、認識順によって/dev/video*が変わっても問題無く参照できるように名前で見つけられるようにしておく。(カメラのデバイス名はお使いの環境に合わせて修正のこと。)
$ cat > getdev.sh
#!/bin/sh
for dev in /sys/class/video4linux/*; do
devname=`cat $dev/name`
if [[ "$devname" == "FHD Camera"* ]]; then
echo "/dev/$(basename $dev)"
break
fi
done
^D
$ chmod +x getdev.sh
$ ./getdev.sh
/dev/video2
これを使ってカメラのキャプチャ可能な性能を確認しておく。
$ v4l2-ctl --device `./getdev.sh` --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
Type: Video Capture
[0]: 'MJPG' (Motion-JPEG, compressed)
Size: Discrete 640x480
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 1600x896
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 1280x720
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 1024x576
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 800x600
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 800x480
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 640x360
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 424x240
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 352x288
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 1920x1080
Interval: Discrete 0.033s (30.000 fps)
[1]: 'YUYV' (YUYV 4:2:2)
Size: Discrete 640x480
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 1600x896
Interval: Discrete 0.133s (7.500 fps)
Size: Discrete 1280x720
Interval: Discrete 0.100s (10.000 fps)
Size: Discrete 1024x576
Interval: Discrete 0.100s (10.000 fps)
Size: Discrete 800x600
Interval: Discrete 0.067s (15.000 fps)
Size: Discrete 800x480
Interval: Discrete 0.040s (25.000 fps)
Size: Discrete 640x360
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 424x240
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 352x288
Interval: Discrete 0.033s (30.000 fps)
Size: Discrete 1920x1080
Interval: Discrete 0.200s (5.000 fps)
これによると、ピクセルフォーマットはMotion JPEGとYUYV(いわゆるRAW)で取り出すことが出来、YUYVのカラーサンプリング方式は4:2:2であることが分かる。Motion JPEGでは全ての解像度で30fpsを取り出せるものの、YUYVのFHD等ではfpsに制約が出てしまうことに注意。(YUYVは非圧縮なので、性能的・データ量の兼ね合いで致し方ない気も…)
カメラによっては、H264でそのまま出てくるものもあり、ストリーミングへのトラスコード負荷が減るかも…
今回はQSVを利用するので、YUYVで取り出してみる。
オーディオは、ALSA経由でデバイス名を調べておく。
$ arecord -L
null
Discard all samples (playback) or generate zero samples (capture)
pipewire
PipeWire Sound Server
default
Default ALSA Output (currently PipeWire Media Server)
sysdefault:CARD=PCH
HDA Intel PCH, ALC3232 Analog
Default Audio Device
front:CARD=PCH,DEV=0
HDA Intel PCH, ALC3232 Analog
Front output / input
sysdefault:CARD=Microphone
FHD Camera Microphone, USB Audio
Default Audio Device
front:CARD=Microphone,DEV=0
FHD Camera Microphone, USB Audio
Front output / input
今回の場合、一番最後の"CARD=Microphone,DEV=0"がカメラ経由のマイクということがわかったのでメモしておく。で、出来上がったコマンドがこちら。
一番苦労したのは、QSVをVA-API経由で動作させつつ、drawtextを組み込むところ。drawtextを使うと基本的にソフトウェアレンダリングになるらしく、負荷が爆増してしまうので絶対に避けたかった。
$ cat > live.sh
#!/bin/sh
CURDIR=$(cd $(dirname "$0") && pwd)
for dev in /sys/class/video4linux/*; do
devname=`cat $dev/name`
if [[ "$devname" == "FHD Camera"* ]]; then
DEVICE="/dev/$(basename $dev)"
break
fi
done
if [ -z "$DEVICE" ]; then
echo "USB Camera is not connected"
exit 1
fi
rm -f $CURDIR/www/*.m3u8 $CURDIR/www/*.ts
exec ffmpeg -y -hide_banner \
-vaapi_device /dev/dri/renderD128 -hwaccel vaapi -hwaccel_output_format nv12 \
-f v4l2 -thread_queue_size 16384 -pixel_format yuv420p -video_size 800x600 -framerate 15 -i $DEVICE \
-f alsa -thread_queue_size 16384 -ac 1 -channel_layout mono -ar 44100 -i plughw:CARD=Microphone,DEV=0 \
-vf "drawtext=fontfile=/usr/share/fonts/google-roboto/Roboto-Bold.ttf: fontsize=20: text='%{localtime\:%Y/%m/%d %X}': fontcolor=white@0.9: x=10: y=10,format=nv12|vaapi,hwupload" \
-c:v h264_vaapi -pix_fmt yuv420p -fflags nobuffer -tune zerolatency -g 20 -c:a aac -b:a 48k -ac 1 -ar 44100 \
-f hls \
-hls_time 2 -hls_list_size 30 -hls_allow_cache 0 \
-hls_segment_filename $CURDIR/www/stream_%d.ts \
-hls_base_url ./ \
-hls_flags delete_segments \
$CURDIR/www/playlist.m3u8
^D
$ chmod +x live.sh
冒頭は先ほどのgetdev.shと同じく、カメラデバイス名から/dev/video*を得るロジック。未接続の場合は処理を抜ける。そして、前回のゴミファイルを削除してからffmpegの処理へ。以下パラメータを順に説明すると
- -y
- 上書き確認不要
- -hide_banner
- 冒頭のバージョン情報などの出力を削る
- -vaapi_device /dev/dri/renderD128
- QSV出力へ送るためのVA-APIデバイスファイル
- -hwaccel vaapi
- ハードウェアアクセラレーションとしてVA-API使用する。ここにqsvを直接指定しなかったのは、ffmpegをQSV有効で再コンパイルするのが面倒だったので。
- -hwaccel_output_format nv12
- Hardware/VAAPI - FFmpegによれば、『一般的にハードウェアはnv12を期待する』らしいので、例に倣って指定。勿論cuda等が使えればそちらでもよい
- -f v4l2
- 映像ソースとしてVideo for Linuxから入力
- -thread_queue_size 16384
- データのバッファサイズらしいのだが、巷でこの値がよく使われていたので真似
- pixel_format yuv420p
- nv12に入力するためにはyuv420pでなくてならないのと、HLSもYUV420pを期待するようなので明示指定する。(yuyv422のままts化してしまうと、playlistを受信しているのに映像が再生されなかった)
- -video_size 800x600
- ビデオ入力サイズ。お好みで
- -framerate 15
- ビデオFPS。お好みで
- -i $DEVICE
- ビデオデバイス名
- -f alsa
- 音声ソースとしてALSAから入力
- -thread_queue_size 16384
- こちらも同様に指定
- -ac 1
- オーディオチャネルはモノラルで十分なので1chに
- -channel_layout mono
- モノラルソースをFront Centerに配置し、再生側ではLR両方から鳴るように
- -ar 44100
- サンプリングレート 44.1kHz
- -i plughw
- CARD=Microphone,DEV=0:前述のarecord -Lで調べたオーディオデバイス名
- -vf "drawtext=..."
- 映像中にタイムスタンプを埋め込む
- fontfile=/path/to/ttf
- フォントファイル
- fontsize=20
- フォントサイズ
- text=
- 出力内容。%localtime\:%Y/%m/%d %Xと指定すると、2023/01/01 00:00:00のような出力になる
- fontcolor=white@0.9
- フォント色と透過率
- x=10, y=10
- 文字の描画位置(左上が原点)
- format=nv12|vaapi,hwupload
- デコーダがVA-APIを使える・使えないに関わらずうまいこと処理させるための条件分岐らしい。詳細はVAAPIに書いてある
- -c:v h264_vaapi
- コーデック指定でビデオはH.264をVA-APIで処理させる
- -pix_fmt yuv420p
- ピクセルフォーマットはHLSのためにyuv420p固定
- -fflags nobuffer
- 読み込みバッファを無くし即時処理を行う(低遅延のため)
- -tune zerolatency
- 低遅延ストリーミング向けの設定を指示
- -g 20
- GOPサイズ。デフォルト12で、小さいほどシークがスムーズになるが、ストリーミングなのであまり関係無いかもしれない
- -c:a aac
- オーディオのコーデックはaacとする
- -b:a 48k
- オーディオのビットレート
- -ac 1
- オーディオのチャンネル
- -ar 44100
- オーディオのサンプリングレート
- -f hls
- 出力先をHLSにする
- -hls_time 2
- HLSのチャンク(分割)サイズを2秒ごとにする。あまり大きすぎると再生開始までに時間が掛かりすぎる。小さすぎるとダウンロード回数が多くなるためオーバヘッドが大きい
- -hls_list_size 30
- 保持するHLSのチャンク数。つまり2秒x30個で1分間分のtsが保管させることになる。0を指定すると全て
- -hls_allow_cache 0
- キャッシュを許可しない
- -hls_segment_filename $CURDIR/www/stream_%d.ts
- HLSストリームの保存場所
- -hls_base_url ./
- playlistから見たストリーム保管場所への相対パス
- -hls_flags delete_segments
- playlistから消えたチャンクを削除する(ディスク溢れ対策)
- $CURDIR/www/playlist.m3u8
- playlistの出力先
これをコンソールで動かし、playlist.m3u8とstream_nnn.tsが順番に出来ていたら動作OK。
次にサービス化しておく。
ライブストリーミングサービス化
サービス化するにあたり、以下の要件を満たせるようにしておく- PCの電源が入ったら自動的にストリーミングを開始する
- カメラが途中で抜けるか、起動が終わってからカメラが接続されるかもしれないので、/dev/video*を取得できなかったり、ffmpegが異常終了しても繰り返し処理を試みる
$ sudo vim /etc/systemd/system/live.service
[Unit]
Description=Live Camera
Requires=network-online.target
StartLimitIntervalSec=30
StartLimitBurst=10
[Service]
Type=simple
ExecStart=/bin/sh /home/kero/live.sh
ExecStop=/bin/kill -TERM ${MAINPID}
Restart=always
RestartSec=5s
User=kero
Group=kero
[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable live
$ sudo systemctl start live
こうしておくと、システム起動時に30秒ごとに10回試行してくれる上、途中でffmpegが落ちても5秒後にrestartがかかるようになっている。配信ページ
最後に、ストリーミングを再生するページを作る。HLSの再生にはhls.jsを利用させていただいたので、基本的にはscriptタグで一発読み込み、playlistを指定して再生させるだけで良い。
他に色々と書いたものの、結局Chromeとかだとautoplayと書いても自動再生されない(ミュートで自動再生か、クリックして音ありで再生開始かの二択)ので、あんまり意味無し。
application/vnd.apple.mpegurlの判定は、iOSデバイスへの対応。こちらは単にvideo.srcにセットするだけで再生できるし、MediaSource Extensionsに未対応なので、Hls.isSupported()はfalseになるようなので…
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Live stream</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="container" autoplay playsinline controls></video>
<script>
var video = document.getElementById('container');
var videoSrc = './playlist.m3u8';
if(Hls.isSupported()) {
var hls = new Hls();
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
hls.loadSource(videoSrc);
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
video.play();
});
});
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
video.src = videoSrc;
video.play();
}
</script>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
}
#container {
position: absolute;
width: 100%;
height: 100%;
background-color: #333;
}
</style>
</body>
</html>
安定性など
こんな感じで3ヶ月弱問題無く動作中。CPU負荷も3割程度と、常にブン回し状態になっていないので心持ち安心。(iGPUに負荷はかかっているだろうけど…)再生側は単にstaticファイルを読み出すだけなので、httpd側の負荷はあまりない。ディスクは消耗するだろうけど、殆ど用済みのSSDを再利用しているだけなので、チップの劣化はあまり気にしていないけど、メモリが潤沢であればメモリデバイスへtsを書き出す方が若干安心かもしれない。
遅延については、配信・再生側ともにWi-Fiの環境で10秒弱と、ほぼリアルタイムに近い感じに仕上がったので満足!
top - 18:51:17 up 85 days, 21:24, 1 user, load average: 0.50, 0.41, 0.61
Tasks: 192 total, 3 running, 189 sleeping, 0 stopped, 0 zombie
%Cpu0 : 11.6 us, 1.0 sy, 0.0 ni, 86.9 id, 0.0 wa, 0.5 hi, 0.0 si, 0.0 st
%Cpu1 : 4.9 us, 0.0 sy, 0.0 ni, 94.6 id, 0.0 wa, 0.5 hi, 0.0 si, 0.0 st
%Cpu2 : 3.5 us, 1.0 sy, 0.0 ni, 88.9 id, 0.0 wa, 3.5 hi, 3.0 si, 0.0 st
%Cpu3 : 3.5 us, 1.0 sy, 0.0 ni, 95.0 id, 0.0 wa, 0.5 hi, 0.0 si, 0.0 st
KiB Mem : 16071528 total, 5162936 free, 1006648 used, 9901944 buff/cache
KiB Swap: 8388604 total, 8388604 free, 0 used. 14619244 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
113469 kero 20 0 583800 97860 80596 R 24.9 0.6 8989:31 ffmpeg
247703 root 0 -20 0 0 0 R 1.0 0.0 0:03.57 kworker/u17:1+uvcvideo
643 systemd+ 20 0 16296 6416 5548 S 0.5 0.0 211:49.08 systemd-oomd
8028 kero 20 0 16244 6908 5116 S 0.5 0.0 0:14.32 sshd
236688 apache 20 0 2420184 8524 5328 S 0.5 0.1 1:03.15 httpd
247742 kero 20 0 224880 3948 3124 R 0.5 0.0 0:00.35 top
1 root 20 0 248252 19984 10772 S 0.0 0.1 3:57.39 systemd
参考文献
- raspberrry pi と USBカメラ で30fps出すのは難しかったという話
- YUVをちゃんと理解してからRGBにコンバートしましょうね | Technology | KLablog | KLab株式会社
- ffmpeg で Apple HTTP Live Streaming(HLS)を扱う | ニコラボ
- Windows の ffmpeg で生放送する方法 - ニコニコ動画研究所
- GitHub - video-dev/hls.js: HLS.js is a JavaScript library that plays HLS in browsers with support for MSE.
- ffmpeg コマンドラインツール入門 第1回 - Morpho Tech Blog
- FV動画をHLS形式で実装した時の備忘録 | COFUS技術ブログ