今回の目標

  • 自宅などの様子を外出先から見れるライブカメラをRaspberry Pi 4で作る
    • HLSによるライブ配信を行う
    • 動画はRaspberry Pi 4のハードウェアエンコーダを用いてH.264で圧縮する

HLS(HTTP Live Streaming)とは?

HTTPを用いて動画をライブ配信するしくみ。Appleによって開発され、 iOSなどではデファクトスタンダードとなっている。 似た技術としてDASHなどがある。

カメラを有効化

今回はMIPI CSI-2接続のカメラを用いる。この場合は初期設定が必要。 (USBカメラを用いる場合は不要だが、今回の方法ではH.264のハードウェアエンコードを利用できない場合が多い) 既に設定済みの場合は/dev/video0が存在するはず。

/boot/firmware/config.txt

ブートローダの設定(config.txt)に項目が存在しない場合、 [all]または[pi4]ブロック内に下記を追記しカメラを有効にする。 設定後は再起動が必要。

1
2
start_x=1
gpu_mem=512

start_x=1でカメラデバイスを有効化する。 Raspberry Piではvcgencmdでカメラの接続状況を確認できるが、 大抵の場合/dev/video0が生えてくるので、私は直接そちらを見ていることが多い。 (ハード・プラットフォーム依存の独自コマンドなんていちいち覚えてられんので)

1
2
3
4
5
6
7
8
9
10
11
12
13
% vcgencmd get_camera
supported=1 detected=1
% sudo dmesg | grep video0
[    9.910983] bcm2835-v4l2-0: V4L2 device registered as video0 - stills mode > 1280x720
% ls -l /dev/video*
crw-rw---- 1 root video 81, 5 Jan 13 03:09 /dev/video0
crw-rw---- 1 root video 81, 1 Jan 13 03:09 /dev/video10
crw-rw---- 1 root video 81, 6 Jan 13 03:09 /dev/video11
crw-rw---- 1 root video 81, 7 Jan 13 03:09 /dev/video12
crw-rw---- 1 root video 81, 0 Jan 13 03:09 /dev/video13
crw-rw---- 1 root video 81, 2 Jan 13 03:09 /dev/video14
crw-rw---- 1 root video 81, 3 Jan 13 03:09 /dev/video15
crw-rw---- 1 root video 81, 4 Jan 13 03:09 /dev/video16

自身がvideoグループに所属しており、/dev/videoへの読み書き権限があることも必ず確認しておく。

1
2
% id
uid=10000(gloria) gid=10000(trainers) groups=10000(trainers),20(dialout),44(video)

start_x=1を有効化すると同時に、gpu_mem=512でGPU(を用いたハードウェアエンコード)に利用されるメモリの量を512MBに指定している。 これを指定しないと、ffmpegやdmesgに下記のようなエラーが出て動作しない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
% ffmpeg -f video4linux2 -input_format h264 -video_size 1280x720 -framerate 30 -i /dev/video0 -vcodec copy -an test.h264
ffmpeg version 4.4-6ubuntu5 Copyright (c) 2000-2021 the FFmpeg developers
  built with gcc 11 (Ubuntu 11.2.0-7ubuntu1)
  configuration: --prefix=/usr --extra-version=6ubuntu5 --toolchain=hardened --libdir=/usr/lib/aarch64-linux-gnu --incdir=/usr/include/aarch64-linux-gnu --arch=arm64 --enable-gpl --disable-stripping --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-pocketsphinx --enable-librsvg --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
  libavutil      56. 70.100 / 56. 70.100
  libavcodec     58.134.100 / 58.134.100
  libavformat    58. 76.100 / 58. 76.100
  libavdevice    58. 13.100 / 58. 13.100
  libavfilter     7.110.100 /  7.110.100
  libswscale      5.  9.100 /  5.  9.100
  libswresample   3.  9.100 /  3.  9.100
  libpostproc    55.  9.100 / 55.  9.100
[video4linux2,v4l2 @ 0xaaaaea3030e0] ioctl(VIDIOC_STREAMON): Operation not permitted
/dev/video0: Operation not permitted
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
[ 1307.390275] bcm2835-v4l2-0: Failed to enable capture port - error -1. Disabling camera port again
[ 1307.411438] ------------[ cut here ]------------
[ 1307.411447] WARNING: CPU: 2 PID: 1303 at drivers/media/common/videobuf2/videobuf2-core.c:1552 vb2_start_streaming+0xec/0x160 [videobuf2_common]
[ 1307.411479] Modules linked in: cmac algif_hash algif_skcipher af_alg bnep hci_uart btqca btrtl btbcm btintel wireguard libchacha20poly1305 chacha_neon poly1305_neon libblake2s libcurve25519_generic libchacha libblake2s_generic ip6_udp_tunnel udp_tunnel dm_multipath btsdio bluetooth ecdh_generic ecc brcmfmac brcmutil cfg80211 snd_bcm2835(C) bcm2835_codec(C) raspberrypi_hwmon bcm2835_v4l2(C) bcm2835_isp(C) snd_pcm v4l2_mem2mem bcm2835_mmal_vchiq(C) videobuf2_vmalloc videobuf2_dma_contig videobuf2_memops snd_timer videobuf2_v4l2 videobuf2_common bcm2835_gpiomem videodev snd vc_sm_cma(C) mc rpivid_mem nvmem_rmem uio_pdrv_genirq uio sch_fq_codel drm ip_tables x_tables autofs4 btrfs blake2b_generic zstd_compress raid10 raid456 async_raid6_recov async_memcpy async_pq async_xor async_tx xor xor_neon raid6_pq libcrc32c raid1 raid0 multipath linear uas usb_storage spidev dwc2 roles crct10dif_ce spi_bcm2835 udc_core i2c_bcm2835 xhci_pci xhci_pci_renesas phy_generic aes_arm64
[ 1307.411700] CPU: 2 PID: 1303 Comm: ffmpeg Tainted: G        WC        5.13.0-1016-raspi #18-Ubuntu
[ 1307.411708] Hardware name: Raspberry Pi 4 Model B Rev 1.4 (DT)
[ 1307.411712] pstate: 60400005 (nZCv daif +PAN -UAO -TCO BTYPE=--)
[ 1307.411719] pc : vb2_start_streaming+0xec/0x160 [videobuf2_common]
[ 1307.411736] lr : vb2_start_streaming+0x74/0x160 [videobuf2_common]
[ 1307.411751] sp : ffff8000106e3b70
[ 1307.411755] x29: ffff8000106e3b70 x28: ffff5db0c4ae3000 x27: 0000000040045612
[ 1307.411768] x26: ffffdd60fb596210 x25: 0000000000000000 x24: ffff8000106e3d28
[ 1307.411778] x23: ffff5db0c24d45a0 x22: ffff5db0c2a0b300 x21: ffff5db0c24d4a28
[ 1307.411789] x20: ffff5db0c24d4850 x19: 00000000ffffffff x18: ffffffffffffffff
[ 1307.411799] x17: 0000000000000000 x16: ffffdd6168b01ab0 x15: ffff8000906e37e7
[ 1307.411810] x14: 0000000000000004 x13: 0000000000000000 x12: ffff5db0c1b9ff28
[ 1307.411820] x11: ffff5db0c1b9fd98 x10: ffff5db0c1b9fd98 x9 : ffffdd60fb49da8c
[ 1307.411831] x8 : ffff5db0c1b9fdc0 x7 : 0000000000000001 x6 : ffff5db0c1b9ffc0
[ 1307.411841] x5 : 0000000000000000 x4 : 0000000000000000 x3 : ffff5db0c3440008
[ 1307.411851] x2 : 0000000000000000 x1 : ffffdd60fb47d000 x0 : 0000000000000020
[ 1307.411861] Call trace:
[ 1307.411865]  vb2_start_streaming+0xec/0x160 [videobuf2_common]
[ 1307.411881]  vb2_core_streamon+0x9c/0x1a0 [videobuf2_common]
[ 1307.411896]  vb2_ioctl_streamon+0x68/0xb4 [videobuf2_v4l2]
[ 1307.411910]  v4l_streamon+0x30/0x3c [videodev]
[ 1307.411967]  __video_do_ioctl+0x194/0x3f4 [videodev]
[ 1307.412009]  video_usercopy+0x1a8/0x5e0 [videodev]
[ 1307.412050]  video_ioctl2+0x24/0x70 [videodev]
[ 1307.412091]  v4l2_ioctl+0x4c/0x70 [videodev]
[ 1307.412132]  __arm64_sys_ioctl+0xb4/0xfc
[ 1307.412145]  invoke_syscall+0x50/0x120
[ 1307.412155]  el0_svc_common.constprop.0+0x6c/0x1a0
[ 1307.412163]  do_el0_svc+0x34/0xa0
[ 1307.412170]  el0_svc+0x2c/0x54
[ 1307.412176]  el0_sync_handler+0xa4/0x130
[ 1307.412181]  el0_sync+0x19c/0x1c0
[ 1307.412188] ---[ end trace 2d247ffa9bb8a313 ]---

Raspberry Pi用Linuxカーネルのissueには上がっている模様だが、 現状のところ対策はgpu_memの値を変更するしかなさそう。256でも動作するようだが、8GBモデルで余裕があるので512にした。

カメラの性能や設定項目を確認

以前も確認したが、改めてカメラの機能・性能を確認する。 予めVideo4Linuxの標準ツールであるv4l-utilsをインストールしておく。

1
% sudo apt install v4l-utils
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
% v4l2-ctl -d /dev/video0 --all
Driver Info:
        Driver name      : bm2835 mmal
        Card type        : mmal service 16.1
        Bus info         : platform:bcm2835-v4l2-0
        Driver version   : 5.13.19
        Capabilities     : 0x85200005
                Video Capture
                Video Overlay
                Read/Write
                Streaming
                Extended Pix Format
                Device Capabilities
        Device Caps      : 0x05200005
                Video Capture
                Video Overlay
                Read/Write
                Streaming
                Extended Pix Format
Priority: 2
Video input : 0 (Camera 0: ok)
Format Video Capture:
        Width/Height      : 1920/1080
        Pixel Format      : 'H264' (H.264)
        Field             : None
        Bytes per Line    : 0
        Size Image        : 2088960
        Colorspace        : SMPTE 170M
        Transfer Function : Default (maps to Rec. 709)
        YCbCr/HSV Encoding: Default (maps to ITU-R 601)
        Quantization      : Default (maps to Full Range)
        Flags             :
Format Video Overlay:
        Left/Top    : 150/50
        Width/Height: 1024/768
        Field       : None
        Chroma Key  : 0x00000000
        Global Alpha: 0xff
        Clip Count  : 0
        Clip Bitmap : No
Framebuffer Format:
        Capability    : Extern Overlay
                        Global Alpha
        Flags         : Overlay Matches Capture/Output Size
        Width         : 1920
        Height        : 1088
        Pixel Format  : 'YU12'
Streaming Parameters Video Capture:
        Capabilities     : timeperframe
        Frames per second: 30.000 (30/1)
        Read buffers     : 1

User Controls

                     brightness 0x00980900 (int)    : min=0 max=100 step=1 default=50 value=50 flags=slider
                       contrast 0x00980901 (int)    : min=-100 max=100 step=1 default=0 value=0 flags=slider
                     saturation 0x00980902 (int)    : min=-100 max=100 step=1 default=0 value=0 flags=slider
                    red_balance 0x0098090e (int)    : min=1 max=7999 step=1 default=1000 value=1000 flags=slider
                   blue_balance 0x0098090f (int)    : min=1 max=7999 step=1 default=1000 value=1000 flags=slider
                horizontal_flip 0x00980914 (bool)   : default=0 value=0
                  vertical_flip 0x00980915 (bool)   : default=0 value=0
           power_line_frequency 0x00980918 (menu)   : min=0 max=3 default=1 value=1
                                0: Disabled
                                1: 50 Hz
                                2: 60 Hz
                                3: Auto
                      sharpness 0x0098091b (int)    : min=-100 max=100 step=1 default=0 value=0 flags=slider
                  color_effects 0x0098091f (menu)   : min=0 max=15 default=0 value=0
                                0: None
                                1: Black & White
                                2: Sepia
                                3: Negative
                                4: Emboss
                                5: Sketch
                                6: Sky Blue
                                7: Grass Green
                                8: Skin Whiten
                                9: Vivid
                                10: Aqua
                                11: Art Freeze
                                12: Silhouette
                                13: Solarization
                                14: Antique
                                15: Set Cb/Cr
                         rotate 0x00980922 (int)    : min=0 max=360 step=90 default=0 value=0 flags=modify-layout
             color_effects_cbcr 0x0098092a (int)    : min=0 max=65535 step=1 default=32896 value=32896

Codec Controls

             video_bitrate_mode 0x009909ce (menu)   : min=0 max=1 default=0 value=0 flags=update
                                0: Variable Bitrate
                                1: Constant Bitrate
                  video_bitrate 0x009909cf (int)    : min=25000 max=25000000 step=25000 default=10000000 value=10000000
         repeat_sequence_header 0x009909e2 (bool)   : default=0 value=0
            h264_i_frame_period 0x00990a66 (int)    : min=0 max=2147483647 step=1 default=60 value=60
                     h264_level 0x00990a67 (menu)   : min=0 max=13 default=11 value=11
                                0: 1
                                1: 1b
                                2: 1.1
                                3: 1.2
                                4: 1.3
                                5: 2
                                6: 2.1
                                7: 2.2
                                8: 3
                                9: 3.1
                                10: 3.2
                                11: 4
                                12: 4.1
                                13: 4.2
                   h264_profile 0x00990a6b (menu)   : min=0 max=4 default=4 value=4
                                0: Baseline
                                1: Constrained Baseline
                                2: Main
                                4: High

Camera Controls

                  auto_exposure 0x009a0901 (menu)   : min=0 max=3 default=0 value=0
                                0: Auto Mode
                                1: Manual Mode
         exposure_time_absolute 0x009a0902 (int)    : min=1 max=10000 step=1 default=1000 value=1000
     exposure_dynamic_framerate 0x009a0903 (bool)   : default=0 value=0
             auto_exposure_bias 0x009a0913 (intmenu): min=0 max=24 default=12 value=12
                                0: -4000 (0xfffffffffffff060)
                                1: -3667 (0xfffffffffffff1ad)
                                2: -3333 (0xfffffffffffff2fb)
                                3: -3000 (0xfffffffffffff448)
                                4: -2667 (0xfffffffffffff595)
                                5: -2333 (0xfffffffffffff6e3)
                                6: -2000 (0xfffffffffffff830)
                                7: -1667 (0xfffffffffffff97d)
                                8: -1333 (0xfffffffffffffacb)
                                9: -1000 (0xfffffffffffffc18)
                                10: -667 (0xfffffffffffffd65)
                                11: -333 (0xfffffffffffffeb3)
                                12: 0 (0x0)
                                13: 333 (0x14d)
                                14: 667 (0x29b)
                                15: 1000 (0x3e8)
                                16: 1333 (0x535)
                                17: 1667 (0x683)
                                18: 2000 (0x7d0)
                                19: 2333 (0x91d)
                                20: 2667 (0xa6b)
                                21: 3000 (0xbb8)
                                22: 3333 (0xd05)
                                23: 3667 (0xe53)
                                24: 4000 (0xfa0)
      white_balance_auto_preset 0x009a0914 (menu)   : min=0 max=10 default=1 value=1
                                0: Manual
                                1: Auto
                                2: Incandescent
                                3: Fluorescent
                                4: Fluorescent H
                                5: Horizon
                                6: Daylight
                                7: Flash
                                8: Cloudy
                                9: Shade
                                10: Greyworld
            image_stabilization 0x009a0916 (bool)   : default=0 value=0
                iso_sensitivity 0x009a0917 (intmenu): min=0 max=4 default=0 value=0
                                0: 0 (0x0)
                                1: 100000 (0x186a0)
                                2: 200000 (0x30d40)
                                3: 400000 (0x61a80)
                                4: 800000 (0xc3500)
           iso_sensitivity_auto 0x009a0918 (menu)   : min=0 max=1 default=1 value=1
                                0: Manual
                                1: Auto
         exposure_metering_mode 0x009a0919 (menu)   : min=0 max=3 default=0 value=0
                                0: Average
                                1: Center Weighted
                                2: Spot
                                3: Matrix
                     scene_mode 0x009a091a (menu)   : min=0 max=13 default=0 value=0
                                0: None
                                8: Night
                                11: Sports

JPEG Compression Controls

            compression_quality 0x009d0903 (int)    : min=1 max=100 step=1 default=30 value=30

カメラの対応フォーマットと解像度を確認

今回はハードウェアエンコードが効くらしい4のH264を用いる。

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
% v4l2-ctl -d /dev/video0 --list-formats-ext

ioctl: VIDIOC_ENUM_FMT
        Type: Video Capture

        [0]: 'YU12' (Planar YUV 4:2:0)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [1]: 'YUYV' (YUYV 4:2:2)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [2]: 'RGB3' (24-bit RGB 8-8-8)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [3]: 'JPEG' (JFIF JPEG, compressed)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [4]: 'H264' (H.264, compressed)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [5]: 'MJPG' (Motion-JPEG, compressed)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [6]: 'YVYU' (YVYU 4:2:2)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [7]: 'VYUY' (VYUY 4:2:2)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [8]: 'UYVY' (UYVY 4:2:2)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [9]: 'NV12' (Y/CbCr 4:2:0)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [10]: 'BGR3' (24-bit BGR 8-8-8)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [11]: 'YV12' (Planar YVU 4:2:0)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [12]: 'NV21' (Y/CrCb 4:2:0)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [13]: 'RX24' (32-bit XBGR 8-8-8-8)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2

カメラのパフォーマンステスト

前回はYUYVで試していたが、今回はH.264でストリーミングしたいので、4のH264を試してみる。 (ここで設定したものが後述のffmpegの動作に反映されるものではない点については注意。 これはあくまで動作テストです。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% v4l2-ctl --set-fmt-video=width=1920,height=1080,pixelformat=4
% v4l2-ctl -p 60
Frame rate set to 60.000 fps
% v4l2-ctl -d /dev/video0 --stream-mmap=3 --stream-count=600
K<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 40.40 fps
<<<<<<<<<<<<<<<<<< K<<<<<<<<<<<<<<<<<<<< 39.42 fps
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 39.19 fps
< K<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 39.00 fps
<<<<<<<<<<<<<<<<<<<<<< K<<<<<<<<<<<<<<< 38.94 fps
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 38.86 fps
<<<<< K<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 38.47 fps
<<<<<<<<<<<<<<<<<<<<<<<<<<< K<<<<<<<<<< 38.47 fps
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 38.47 fps
<<<<<<<<<< K<<<<<<<<<<<<<<<<<<<<<<<<<<<< 38.47 fps
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< K<<<<<< 38.47 fps
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 38.47 fps
<<<<<<<<<<<<<< K<<<<<<<<<<<<<<<<<<<<<<< 38.47 fps
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< K< 38.47 fps
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 38.37 fps
<<<<<<<<<<<<<<<<<<<<

するとフルHD(1920x1080)の動画がおおよそ38~40fps程度で取得できることが確認できた。 ハードウェアエンコードが効いているので、CPU負荷もほとんど無し。 前回のもこれでよかったのでは…?(検証不足でした)

ffmpegによるHLSファイル生成

HLSで配信を行う際は、H264でエンコード済みの動画をHLS用tsファイルに分割し、連番を付与しつつm3u8プレイリストを作る必要がある。 これを一発で解決するために、毎度おなじみffmpegを用いる。

今回はubuntu 21.10標準パッケージのffmpegをインストールする。

1
2
3
4
5
6
7
8
9
10
11
12
13
% sudo apt install ffmpeg
% ffmpeg -version
ffmpeg version 4.4-6ubuntu5 Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 11 (Ubuntu 11.2.0-7ubuntu1)
configuration: --prefix=/usr --extra-version=6ubuntu5 --toolchain=hardened --libdir=/usr/lib/aarch64-linux-gnu --incdir=/usr/include/aarch64-linux-gnu --arch=arm64 --enable-gpl --disable-stripping --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-pocketsphinx --enable-librsvg --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
libavutil      56. 70.100 / 56. 70.100
libavcodec     58.134.100 / 58.134.100
libavformat    58. 76.100 / 58. 76.100
libavdevice    58. 13.100 / 58. 13.100
libavfilter     7.110.100 /  7.110.100
libswscale      5.  9.100 /  5.  9.100
libswresample   3.  9.100 /  3.  9.100
libpostproc    55.  9.100 / 55.  9.100

ffmpeg -formatsでHLSとv4l2がサポートされていることが確認できればOK。

1
2
3
4
ffmpeg -formats 2>/dev/null | grep hls
 DE hls             Apple HTTP Live Streaming
 % ffmpeg -formats 2>/dev/null |grep linux
 DE video4linux2,v4l2 Video4Linux2 output device

早速実行してみる。 今回設定するオプションは下記の通り。

  • -f video4linux2: 入力にv4l2を利用
  • -input_format h264: 入力フォーマットにH264を指定
  • -video_size 1920x1080: 入力動画の解像度を1920x1080に指定
  • -framerate 30: 入力動画のフレームレートを30fpsに指定
  • -i /dev/video0: 入力デバイスを/dev/video0に指定
  • -f hls: 出力フォーマットをHLSに指定
  • -c:v copy: 出力ビデオフォーマットをcopy(入力したものをそのまま出力)に指定
  • -flags +cgop:
  • -g 30:
  • -hls_time 1: 1秒ごとに分割(セグメントの長さ)
  • -hls_base_url "/stream": プレイリストのファイルURLの親ディレクトリパスを/streamに指定
  • -hls_list_size 10: 分割された動画ファイル(セグメント)をプレイリスト上に保持する個数を10に指定
  • -hls_flags delete_segments: 古いセグメントを逐次削除(※撮影した動画ファイルを残しておきたい場合は付与しない)
  • stream/out.m3u8: 出力されるプレイリストのファイル名
1
2
3
4
5
6
7
8
9
% mkdir -p stream
% rm -f stream/*.*
% ffmpeg  -f video4linux2 -input_format h264 -video_size 1920x1080 -framerate 30 -i /dev/video0 \
        -f hls -c:v copy -flags +cgop -g 30 \
        -hls_time 1 \
        -hls_base_url "/stream/" \
        -hls_list_size 10 \
        -hls_flags delete_segments \
        stream/out.m3u8

カメラの接続・設定やアクセス権限の設定など、全てがうまくいっていれば、 下記のように数秒おきに動画ファイルが書き出され、プレイリストが更新されていく。

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
44
45
46
47
48
49
50
51
52
53
ffmpeg version 4.4-6ubuntu5 Copyright (c) 2000-2021 the FFmpeg developers
  built with gcc 11 (Ubuntu 11.2.0-7ubuntu1)
  configuration: --prefix=/usr --extra-version=6ubuntu5 --toolchain=hardened --libdir=/usr/lib/aarch64-linux-gnu --incdir=/usr/include/aarch64-linux-gnu --arch=arm64 --enable-gpl --disable-stripping --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-pocketsphinx --enable-librsvg --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
  libavutil      56. 70.100 / 56. 70.100
  libavcodec     58.134.100 / 58.134.100
  libavformat    58. 76.100 / 58. 76.100
  libavdevice    58. 13.100 / 58. 13.100
  libavfilter     7.110.100 /  7.110.100
  libswscale      5.  9.100 /  5.  9.100
  libswresample   3.  9.100 /  3.  9.100
  libpostproc    55.  9.100 / 55.  9.100
Input #0, video4linux2,v4l2, from '/dev/video0':
  Duration: N/A, start: 374.129464, bitrate: N/A
  Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080, 30 fps, 30 tbr, 1000k tbn, 2000k tbc
Output #0, hls, to 'stream/out.m3u8':
  Metadata:
    encoder         : Lavf58.76.100
  Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080, q=2-31, 30 fps, 30 tbr, 90k tbn, 1000k tbc
Stream mapping:
  Stream #0:0 -> #0:0 (copy)
Press [q] to stop, [?] for help
[hls @ 0xaaab0ef72db0] Timestamps are unset in a packet for stream 0. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly
[hls @ 0xaaab0ef72db0] Non-monotonous DTS in output stream 0:0; previous: 0, current: 0; changing to 1. This may result in incorrect timestamps in the output file.
[hls @ 0xaaab0ef72db0] Opening 'stream/out0.ts' for writinge=N/A speed=1.12x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out1.ts' for writinge=N/A speed=1.05x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out2.ts' for writinge=N/A speed=1.03x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out3.ts' for writinge=N/A speed=1.02x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out4.ts' for writinge=N/A speed=1.02x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out5.ts' for writinge=N/A speed=1.02x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out6.ts' for writinge=N/A speed=1.01x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out7.ts' for writinge=N/A speed=1.01x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out8.ts' for writinge=N/A speed=1.01x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out9.ts' for writinge=N/A speed=1.01x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out10.ts' for writing=N/A speed=1.01x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out11.ts' for writing=N/A speed=1.01x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out12.ts' for writing=N/A speed=1.01x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
[hls @ 0xaaab0ef72db0] Opening 'stream/out13.ts' for writing=N/A speed=1.01x
[hls @ 0xaaab0ef72db0] Opening 'stream/out.m3u8.tmp' for writing
frame=  801 fps= 30 q=-1.0 Lsize=N/A time=00:00:26.61 bitrate=N/A speed=1.01x
video:32606kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

カメラの読み取りとHLSフォーマットでの出力がうまくできると、 streamディレクトリ以下に下記のようなファイルが出力される。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
% ls -l stream
total 25936
-rw------- 1 gloria trainers     432 Feb 16 02:49 out.m3u8
-rw------- 1 gloria trainers 2558116 Feb 16 02:49 out10.ts
-rw------- 1 gloria trainers 2558680 Feb 16 02:49 out11.ts
-rw------- 1 gloria trainers 2558868 Feb 16 02:49 out12.ts
-rw------- 1 gloria trainers  904656 Feb 16 02:49 out13.ts
-rw------- 1 gloria trainers 2541196 Feb 16 02:48 out3.ts
-rw------- 1 gloria trainers 2594964 Feb 16 02:49 out4.ts
-rw------- 1 gloria trainers 2572780 Feb 16 02:49 out5.ts
-rw------- 1 gloria trainers 2554168 Feb 16 02:49 out6.ts
-rw------- 1 gloria trainers 2577104 Feb 16 02:49 out7.ts
-rw------- 1 gloria trainers 2551536 Feb 16 02:49 out8.ts
-rw------- 1 gloria trainers 2560184 Feb 16 02:49 out9.ts

out0.tsからの連番になっているが、-hls_flags delete_segmentsを設定しているため、 古いものから順に削除されている。

m3u8プレイリストの中身は下記のようになる。

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
% cat stream/out.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:4
#EXTINF:1.998678,
/stream/out4.ts
#EXTINF:1.998678,
/stream/out5.ts
#EXTINF:1.998678,
/stream/out6.ts
#EXTINF:1.998667,
/stream/out7.ts
#EXTINF:1.998678,
/stream/out8.ts
#EXTINF:1.998678,
/stream/out9.ts
#EXTINF:1.998678,
/stream/out10.ts
#EXTINF:1.998667,
/stream/out11.ts
#EXTINF:1.998678,
/stream/out12.ts
#EXTINF:0.700000,
/stream/out13.ts
#EXT-X-ENDLIST

次はこれらのファイルをブラウザから取得できるように、WebサーバとWebページを作成する。

動画閲覧用Webページの作成

Safari以外のブラウザはHLSに対応していないらしいので、hls.jsを利用して再生できるようにする。 (逆に言うと、SafariのためにHLSにしている…ということになる)

使い方はそれほど難しくなく、video要素に対して読み取った動画ソースをattachするだけでよい。 video要素にcontrols属性を加えておくと、再生・停止や音量調整、一定の範囲内でのシークなどもできるようになる。

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
<!DOCTYPE html>
<html>
  <head>
    <title>HLSのサンプル</title>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
  </head>

  <body>
    <h1>HLSのサンプル</h1>
    <video id="sampleVideo" controls></video>
    <script>
      var video = document.getElementById('sampleVideo');
      var videoSrc = '/stream/out.m3u8';
      if (Hls.isSupported()) {
          var hls = new Hls();
          hls.loadSource(videoSrc);
          hls.attachMedia(video);
      }
      else if (video.canPlayType('application/vnd.apple.mpegurl')) {
          video.src = videoSrc;
      }
    </script>
  </body>
</html>

ここまでのファイルが下記のように展開されている状態にし、このディレクトリをWebサーバで公開する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% find .
.
./stream
./stream/out3.ts
./stream/out4.ts
./stream/out5.ts
./stream/out6.ts
./stream/out7.ts
./stream/out8.ts
./stream/out9.ts
./stream/out10.ts
./stream/out11.ts
./stream/out12.ts
./stream/out13.ts
./stream/out.m3u8
./index.html

Webサーバは単純に上記のHTMLファイルと動画ファイルを転送するだけなのでなんでもよい。 Python3が入っているのであれば、Python標準添付のhttp.serverモジュールを用いてWebサーバが起動できる。

1
% python3 -m http.server 8000 --bind 0.0.0.0 --directory .
1
2
3
4
5
6
7
8
9
10
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.39.39.39 - - [16/Feb/2022 03:06:48] "GET / HTTP/1.1" 200 -
10.39.39.39 - - [16/Feb/2022 03:06:48] "GET /hls.js HTTP/1.1" 200 -
10.39.39.39 - - [16/Feb/2022 03:06:48] "GET /stream/out.m3u8 HTTP/1.1" 200 -
10.39.39.39 - - [16/Feb/2022 03:06:48] code 404, message File not found
10.39.39.39 - - [16/Feb/2022 03:06:48] "GET /favicon.ico HTTP/1.1" 404 -
10.39.39.39 - - [16/Feb/2022 03:06:48] "GET /stream/out.m3u8 HTTP/1.1" 304 -
10.39.39.39 - - [16/Feb/2022 03:06:48] "GET /stream/out15.ts HTTP/1.1" 200 -
10.39.39.39 - - [16/Feb/2022 03:06:48] "GET /stream/out16.ts HTTP/1.1" 200 -
10.39.39.39 - - [16/Feb/2022 03:06:48] "GET /stream/out17.ts HTTP/1.1" 200 -

ここまでの作業がうまくいっていれば、http://Raspberry Pi 4のIPアドレス:8000/にブラウザからアクセスすると、動画が閲覧できる…はず。

課題

ストレージへの書き込み量の削減

ffmpegを用いたHLSでは、一旦すべてのファイルをストレージ上に書き出して配信している。 そのため今回用いた環境(1920x1080 30fps)の場合は、数秒おきに2.5MBのファイルが書き出されては消えていく。

1
2
3
4
5
6
7
8
9
10
11
12
% du -sh stream/*.ts
2.5M    stream/out126.ts
2.5M    stream/out127.ts
2.5M    stream/out128.ts
2.5M    stream/out129.ts
2.5M    stream/out130.ts
2.5M    stream/out131.ts
2.5M    stream/out132.ts
2.5M    stream/out133.ts
2.5M    stream/out134.ts
2.5M    stream/out135.ts
2.5M    stream/out136.ts

この書込みをmicroSDXCカードのようなフラッシュストレージに行えば、あっという間に書き込み寿命を使い果たして故障してしまう可能性が高い。 (1時間で5GB、24時間で120GB、1ヶ月で3.8TB、1年で44TB程度)

今回のライブ配信システムでは、一定のサイズのファイルを書いては消しを繰り返すだけなので、 本来はストレージに書き込む(恒久的に残す)必要はないものとなっている。 このような場合は、LinuxではtmpfsのようなRAMを用いたファイルシステムを用いる。

下記のようにmount -t tmpfs -o size=tmpfsのサイズ,mode=tmpfsのパーミッション,uid=自分のUID,gid=自分のgid mount先ディレクトリとすると、 mount先ディレクトリに自分(とroot)だけが読み書きできるRAM上のファイルシステムがmountされる。

1
2
3
% sudo mount tmpfs -t tmpfs -o size=64m,mode=700,uid=`id -u`,gid=`id -g` stream
% mount -t tmpfs | grep stream
tmpfs on /home/gloria/videos/stream type tmpfs (rw,relatime,size=65536k,mode=700,uid=10000,gid=10000,inode64)

後はこれを/etc/fstabに記述しておくか、適当にシェルスクリプトにして実行前にmountするようにおけばよい。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh

mkdir -p stream
sudo umount stream
sudo mount tmpfs -t tmpfs -o size=64m,mode=700,uid=`id -u`,gid=`id -g` stream

ffmpeg  -f video4linux2 -input_format h264 -video_size 1920x1080 -framerate 30 -i /dev/video0 \
        -f hls -c:v copy -flags +cgop -g 30 \
        -hls_time 1 \
        -hls_base_url "/stream/" \
        -hls_list_size 10 \
        -hls_flags delete_segments \
        stream/out.m3u8

本番環境での利用

これを本番運用するのであれば、最低限下記についての考慮が必要となる。

  • 動画変換サービスの自動起動
    • systemd経由でてきとうに起動できるようにする
  • 本番環境用Webサーバの利用
    • nginxでもなんでもいいのでそちらを使う
  • 機器へのネットワークの接続性の確保
    • 固定IPアドレスやDynamic DNSなど
    • ルータのファイアウォールやポートフォワード機能の設定など
  • ネットワーク帯域の確保
    • 1080p30だと1クライアントだけでも2.5MB/s(20Mbps)
    • 多人数相手ならCDNなどが必須になりそう
  • (言うまでもなく)一般的なWebサイトと同様のセキュリティ機構
    • ID: pi, Password: raspberryでsshログインできるようにしていたりすると、一瞬でクラックされ乗っ取られる
  • 動画閲覧ページにおける利用者認証(必要に応じて)
    • 誰でも見れてよいということはあまりなさそう
    • 同時にHTTPS化も必要となる場合がほとんど

カメラまわりでうまくいかない場合のチェックリスト

  • Rasspberry Pi 4の電源は規定通り5V 3A出るものを用いているか
  • MIPI-CSI2カメラのフレキケーブルは正しく接続されているか
    • 表裏が逆になっていないか
    • 奥まで差し込まれているか
    • 強く折り曲げるなどして断線していないか
    • カメラセンサーと基盤の間のフレキケーブルは正しく接続されているか(今回これで数時間悩んだ)
  • /boot/firmware/config.txtが正しく記述されているか
    • Raspberry Pi OSとubuntuではファイルの場所が異なるので注意 (ubuntuは/boot/firmware/config.txt、Raspberry Pi OSは/boot/config.txt)
    • start_x=1gpu_mem=512の両方が必要
    • [pi4]または[all]ブロックのいずれかに記述する
  • /dev/video*と自分のアクセス権限は合っているか
    • ubuntuのデフォルト構成ならvideoグループに所属している必要がある
  • dmesgに/dev/video*に関するエラーが出力されていないか
  • v4l2-ctl -d /dev/video0 --allでカメラ情報を取得できているか
  • v4l2-ctl -d /dev/video0 --list-formats-extの対応フォーマット一覧にH264があるか
    • 古いUSBカメラや安価なUSBカメラだと無い場合が多い
  • v4l2-ctl -d /dev/video0 --stream-mmap=3 --stream-count=600でフレームが30fps以上で取得できているか
  • ffmpegはhlsに対応しているか、または古いバージョンでないか
    • 今回はubuntu 21.10のffmpeg-4.4を使用
  • raspistill -o sample.jpgでsample.jpgが正しく出力されるか
    • vcgencmd get_camerasupported=1 detected=1であるにもかかわらず mmal: No data received from sensor. Check all connections, including the Sunny one on the camera boardのようなエラーが出た時は、 センサーの故障やセンサーとカメラ基盤との間の接触不良の可能性がある(今回これで数時間悩んだ)
  • 信頼できる販売元から購入したか
    • 今回は謎メーカーの互換カメラを購入したが、当然うまく動かないリスクは大きくなる
    • 最初は純正品を買ったほうがよい