When you want to send video captured from an HDMI capture device or similar source to another PC on the same LAN, one issue that can be surprisingly significant is video latency.
If you simply encode the video as H.264 and stream it over the network, small amounts of buffering can occur at multiple stages, such as:
- Capture
- FFmpeg’s internal queues
- Encoding
- Multiplexing
- Network transport
- Player-side buffering
- Decoding and display
As a result, the final latency can range from several hundred milliseconds to several seconds.
This time, we will build a low-latency streaming setup that minimizes buffering as much as possible using the following configuration:
V4L2 + ALSA → FFmpeg → MediaMTX → RTSP/UDP → ffplay
In the current FFmpeg documentation, nobuffer is defined as an option for reducing latency caused by buffering during input analysis, while low_delay is defined as a flag that forces low-delay operation.
Configuration
The setup used here looks like this:
HDMI input
↓
Capture device
/dev/video0
↓
FFmpeg
├─ Video input via V4L2
├─ Audio input via ALSA
├─ H.264 encoding with Intel QSV
↓
RTSP / UDP
↓
MediaMTX
↓
LAN
↓
ffplay
↓
Display
Rather than having MediaMTX encode the video itself, we use it as an RTSP server that relays the stream.
MediaMTX is a media server that supports publishing and reading real-time video and audio streams, including RTSP, and it can also launch external commands as hooks.
This time, we will use its runOnDemand feature.
MediaMTX Configuration
The mediamtx.yml file is configured as follows:
paths:
hdmi:
runOnDemand: >-
ffmpeg -hide_banner -y
-loglevel warning
-fflags nobuffer
-flags low_delay
-init_hw_device qsv=qsv
-filter_hw_device qsv
-thread_queue_size 4
-f alsa
-ac 2
-ar 48000
-i hw:1,0
-thread_queue_size 1
-f v4l2
-input_format yuyv422
-video_size 1920x1080
-framerate 30
-i /dev/video0
-map 1:v:0
-map 0:a:0
-vf 'hwupload=extra_hw_frames=0,vpp_qsv=format=nv12'
-c:v h264_qsv
-preset veryfast
-global_quality:v 35
-async_depth 1
-bf 0
-g 30
-c:a libopus -b:a 192k
-f rtsp
-rtsp_transport udp
-muxdelay 0
rtsp://127.0.0.1:8554/hdmi
runOnDemandRestart: yes
runOnDemandStartTimeout: 10s
runOnDemandCloseAfter: 5s
At first glance, this may seem like a lot of options, but from a low-latency perspective, it becomes easier to understand if you break them down into a few key areas.
Start FFmpeg Only When Needed with runOnDemand
First, let’s look at the MediaMTX side.
runOnDemand: >-
ffmpeg ...
runOnDemand is a feature that starts an external command when a client accesses the corresponding path.
In other words, it works like this:
Nobody is watching
↓
FFmpeg stopped
ffplay connects to /hdmi
↓
MediaMTX starts FFmpeg
↓
FFmpeg publishes to /hdmi
↓
Playback starts
The official MediaMTX documentation also describes runOnDemand as a mechanism that starts the specified command when a reader requests the path.
Because FFmpeg does not need to run continuously, this is convenient when you only want to use HDMI streaming when needed.
In addition, we use:
runOnDemandRestart: yes
so that if FFmpeg exits for some reason, it will be restarted. The current MediaMTX configuration reference also defines this option as a setting that restarts the command after it exits.
Minimize Input-Side Buffers
An important principle in low-latency streaming is:
Avoid “buffering first, processing later.”
For that reason, the FFmpeg command begins with:
-fflags nobuffer
-flags low_delay
-fflags nobuffer
-fflags nobuffer
This setting reduces latency caused by buffering during input stream analysis. The official FFmpeg documentation also describes it as an option for reducing latency caused by buffering during initial input analysis.
For real-time input, it is important to configure the pipeline in the following direction:
Avoid accumulating packets as much as possible
↓
Pass incoming data to the next processing stage immediately
-flags low_delay
-flags low_delay
As the name suggests, this flag configures codec processing for low latency. In FFmpeg, low_delay is defined as “Force low delay.”
Capture Video and Audio from Separate Devices
In this setup, audio is captured through ALSA:
-f alsa
-ac 2
-ar 48000
-i hw:1,0
while video is captured through V4L2:
-f v4l2
-input_format yuyv422
-video_size 1920x1080
-framerate 30
-i /dev/video0
From FFmpeg’s perspective, the inputs are therefore:
input 0 = ALSA
input 1 = V4L2
The output streams are explicitly selected with:
-map 1:v:0
-map 0:a:0
In other words:
Video → video 0 from input 1
Audio → audio 0 from input 0
Keep thread_queue_size Small
For the inputs, we specify:
-thread_queue_size 4
and:
-thread_queue_size 1
thread_queue_size determines how many packets read from an input device or other source FFmpeg may retain in its internal queue. The official FFmpeg documentation describes it, for inputs, as the maximum number of queued packets when reading from a device or file.
If you increase the queue size, behavior tends to become:
Processing stalls slightly
↓
Packets accumulate in the queue
↓
Frames are processed without being dropped
However, in low-latency applications, this can become a problem:
Processing cannot keep up
↓
Old video accumulates in the queue
↓
Displayed video falls further and further behind real time
For that reason, we use very small values here.
The design philosophy is:
Prioritize displaying video that is close to the current time over guaranteeing that no frame is ever dropped.
This is a very important concept in low-latency streaming.
Encode with Intel Quick Sync Video
If 1920×1080 30 fps video is encoded to H.264 in software, the CPU load itself can become a source of latency.
For that reason, we use Intel Quick Sync Video, commonly known as QSV.
-init_hw_device qsv=qsv
-filter_hw_device qsv
Then the video is uploaded to the QSV device and converted to NV12 with:
-vf 'hwupload=extra_hw_frames=0,vpp_qsv=format=nv12'
The encoder is:
-c:v h264_qsv
This allows the Intel GPU’s hardware encoder to handle H.264 encoding.
Reduce Encoder “Lookahead” as Well
This is where some of the most important low-latency settings appear.
-preset veryfast
-global_quality:v 35
-async_depth 1
-bf 0
-g 30
-preset veryfast
-preset veryfast
QSV presets include:
veryfast
faster
fast
medium
slow
slower
veryslow
In FFmpeg, the veryfast side prioritizes speed, while the veryslow side prioritizes quality.
For live streaming, rather than pushing encoding quality to the limit, we prioritize:
Encoding each frame quickly and sending it to the network.
That is why we choose veryfast.
async_depth 1 Is Important
-async_depth 1
This setting also helps reduce latency.
QSV can improve throughput by processing multiple frames asynchronously.
However:
Process multiple frames in parallel
also means that multiple frames may exist inside the encoder at the same time.
The FFmpeg QSV documentation defines async_depth as a parameter related to the number of asynchronous operations, and for the QSV decoder it explicitly notes that increasing the value also increases latency.
For that reason, we reduce it to:
-async_depth 1
The goal is a simple pipeline:
Frame input
↓
Encode
↓
Output immediately
We prioritize latency over throughput.
Do Not Use B-Frames
For low-latency H.264, this setting is especially important:
-bf 0
When B-frames are used, encoding and decoding a frame may require referencing frames that occur later in time.
Conceptually, the structure may look like:
I P B B P
As a result, the encoder and decoder may need to reorder frames, which is unfavorable for low-latency use cases.
Therefore, we completely disable B-frames with:
-bf 0
Compression efficiency is sacrificed to some extent, but this is easier to handle in real-time applications.
Use a One-Second GOP
-g 30
This is another key setting.
Here, we use:
-framerate 30
so the frame rate is 30 fps.
Therefore:
30 frames ÷ 30 fps = 1 second
which means the GOP is divided at roughly one-second intervals.
In FFmpeg, -g specifies the GOP size.
Shortening the GOP slightly reduces compression efficiency, but it makes the stream easier to handle in situations such as:
- Playback startup
- Recovery after packet loss
- Joining the stream mid-session
For low-latency applications, compression ratio is not the only concern. It is also important to catch up to the current video quickly.
global_quality:v 35
-global_quality:v 35
This sets the QSV quality level.
When global_quality:v is specified with h264_qsv, quality-based rate control such as ICQ may be used depending on the conditions. The FFmpeg QSV documentation states that the ICQ range is 1–51, with 1 being the highest quality.
In other words:
Smaller value
↓
Higher quality, larger data volume
Larger value
↓
Lower quality, smaller data volume
35 is fairly compression-oriented, so you can adjust it while monitoring network bandwidth and image quality.
For example, if you want better quality, you could lower it to:
-global_quality:v 28
and compare the result.
Audio Is Opus at 192 kbps
Audio is configured as:
-c:a libopus -b:a 192k
Compared with video, audio encoding has relatively low computational and bandwidth requirements, so we use Opus at 192 kbps.
However, for monitoring applications where you only need to see the video, you can remove audio with:
-an
to simplify the setup further.
Use UDP for RTSP
FFmpeg outputs to MediaMTX with:
-f rtsp
-rtsp_transport udp
-muxdelay 0
rtsp://127.0.0.1:8554/hdmi
For reducing latency, the key setting is:
-rtsp_transport udp
FFmpeg allows UDP, TCP, and other methods to be selected as the RTSP lower transport. With UDP, media is sent over UDP; with TCP, the data is interleaved inside the RTSP control channel.
Unlike TCP, UDP does not have a mechanism like:
Packet loss
↓
Wait for retransmission
↓
Subsequent processing also waits until the packet arrives
Therefore, it is well suited to real-time applications where the priority is:
Even if the image becomes slightly corrupted, do not wait for old video—show the current video instead.
Naturally, however, video may become corrupted in environments where packet loss is more likely, such as:
- Wi-Fi
- Congested LANs
- Streaming over the internet
If reliability is more important, another option is:
-rtsp_transport tcp
Low latency and transmission reliability are a tradeoff.
muxdelay 0
We also specify:
-muxdelay 0
muxdelay is a delay-related parameter used on FFmpeg’s output side. The official FFmpeg RTSP sending example also includes a case using -muxdelay 0.1.
Here, we push it even further with:
-muxdelay 0
to configure the system so that:
As little time as possible is spent waiting to batch packets together.
Do Not Make MediaMTX Encode the Video
An important part of this setup is that FFmpeg publishes once to the local MediaMTX instance:
FFmpeg
↓
rtsp://127.0.0.1:8554/hdmi
Clients then access:
rtsp://x.x.x.x:8554/hdmi
In other words, MediaMTX acts as the relay point:
FFmpeg
↓
MediaMTX
↓
Multiple clients
With this configuration, individual clients do not need direct access to the capture device or encoder.
It is also easier to manage when distributing the stream to multiple devices.
Reduce Buffering on the ffplay Side as Well
Even if the sender is optimized for low latency, there is little benefit if the receiving player buffers one second of video before starting playback.
For that reason, ffplay is also configured as follows:
ffplay \
-fflags nobuffer \
-flags low_delay \
-framedrop \
-rtsp_transport udp \
rtsp://x.x.x.x:8554/hdmi
-fflags nobuffer
As on the sending side, we specify:
-fflags nobuffer
to reduce latency caused by buffering during input analysis.
-flags low_delay
-flags low_delay
This also configures decoding for low-latency operation.
-framedrop
-framedrop
This is another important option for real-time applications.
ffplay provides the framedrop option, which allows video frames to be dropped when playback falls behind synchronization.
For low-latency use, instead of:
Faithfully displaying every single frame
it is more important to:
If processing falls behind, discard old frames
↓
Catch up to the current time
This setting sacrifices some visual continuity in order to prevent latency from increasing.
In Low-Latency Streaming, “Dropping” Is Important
Looking at all of these settings, there is a common philosophy:
Do not retain old data any longer than necessary.
In normal video playback, it is important to:
Avoid packet loss
Avoid dropping frames
Keep playback smooth
However, real-time video has different priorities.
For example, when displaying a game screen or camera feed, it may be more useful to show:
Video from around 100 ms ago, even if some frames are skipped
than to perfectly display:
Video from 3 seconds ago
For that reason, this setup reduces waiting time at every stage of the pipeline:
Small input queues
↓
No B-frames
↓
Reduced QSV asynchronous depth
↓
Reduced muxer waiting time
↓
UDP
↓
Reduced buffering in ffplay
↓
Drop frames if playback falls behind
Where Does Latency Occur?
When optimizing for low latency, you need to look at the entire pipeline, not just the network:
Capture
↓
Queue
↓
Filter
↓
Encode
↓
Mux
↓
Network
↓
Demux
↓
Decode
↓
Render
For example, even if LAN ping is:
1 ms
if you have:
Encoder: 100 ms
Player buffer: 500 ms
then improving the network further will not have much impact.
In fact, with real-time video:
Buffers inside the encoder, decoder, and player can sometimes contribute more latency than the network itself.
That is why this configuration uses so many low-latency options.
If You Want to Reduce Latency Even Further
If latency is still noticeable with this configuration, there are several additional things you can try.
First, ffplay’s RTSP reception uses a buffer for reordering UDP packets. The FFmpeg documentation states that packet reordering during UDP reception can be disabled by setting max_delay to 0.
For example, you can try:
ffplay \
-fflags nobuffer \
-flags low_delay \
-framedrop \
-rtsp_transport udp \
-max_delay 0 \
rtsp://x.x.x.x:8554/hdmi
However, this further reduces tolerance for out-of-order packets, so video may become unstable depending on network quality.
You can also reduce:
-g 30
to something like:
-g 15
However, shorter GOPs generally reduce compression efficiency.
Low-latency optimization always involves tradeoffs among:
Latency
Image quality
Bandwidth
Stability
CPU/GPU load
UDP Does Not Automatically Mean Low Latency
One important point to keep in mind is that:
UDP = always low latency
is not necessarily true.
UDP can help avoid retransmission delays, but if network quality is poor, you may instead see:
Packet loss
↓
Video corruption
↓
Wait until the next recoverable frame
Therefore, the ideal environment is something like:
Wired LAN + UDP
where packet loss is low and the network is stable.
In a Wi-Fi environment, it is better to test both UDP and TCP and compare:
Actual latency
Video corruption
Stability
Summary
In this setup, we built the following pipeline:
HDMI
↓
V4L2 / ALSA
↓
FFmpeg
↓
Intel QSV H.264
↓
RTSP / UDP
↓
MediaMTX
↓
LAN
↓
ffplay
The key to reducing latency is not simply using a fast encoder.
What matters is removing, as much as possible, the buffers at each stage that exist to:
“Hold a little data just in case.”
The main settings used here can be summarized as follows:
-fflags nobuffer
Reduce input-side buffering
-flags low_delay
Configure codecs for low latency
-thread_queue_size
Keep capture input queues small
-c:v h264_qsv
Encode H.264 quickly with Intel QSV
-preset veryfast
Prioritize encoding speed
-async_depth 1
Reduce the asynchronous processing depth inside QSV
-bf 0
Disable B-frames
-g 30
About a one-second GOP at 30 fps
-rtsp_transport udp
Favor avoiding retransmission delays
-muxdelay 0
Reduce waiting time in the muxer
-framedrop
Drop old frames if playback falls behind
The important thing to understand about low-latency video streaming is that:
“Deliver every frame reliably” and “deliver the current video” are different goals.
For recording, not losing frames is important.
On the other hand, for real-time monitoring, remote control, game screens, camera surveillance, and similar applications, it is often more important to display the latest video even if a few frames are missing than to receive old video perfectly.
By tuning the capture, encoding, network, and player stages around that principle, you can achieve very low-latency video transmission even with RTSP.
FFmpeg + MediaMTXで映像をできるだけ低遅延にネットワーク配信する
HDMIキャプチャなどから取り込んだ映像をLAN内の別PCへ送りたいとき、意外と問題になるのが映像の遅延です。
普通にH.264へエンコードしてネットワーク配信すると、
- キャプチャ
- FFmpeg内部のキュー
- エンコード
- マルチプレクサ
- ネットワーク
- プレイヤー側のバッファ
- デコード・表示
といった複数の段階で少しずつバッファリングされ、最終的には数百ms〜数秒の遅延になることがあります。
そこで今回は、
V4L2 + ALSA → FFmpeg → MediaMTX → RTSP/UDP → ffplay
という構成で、可能な限りバッファを減らした低遅延ストリーミングを構築します。
FFmpegの現行ドキュメントでも、nobuffer は入力解析時のバッファリングによる遅延を減らすためのオプション、low_delay は低遅延動作を強制するフラグとして定義されています。
構成
今回の構成は次のようになります。
HDMI入力
↓
キャプチャデバイス
/dev/video0
↓
FFmpeg
├─ V4L2で映像入力
├─ ALSAで音声入力
├─ Intel QSVでH.264エンコード
↓
RTSP / UDP
↓
MediaMTX
↓
LAN
↓
ffplay
↓
画面表示
MediaMTXには映像そのものをエンコードさせるのではなく、RTSPサーバーとして中継を担当させます。
MediaMTXはRTSPを含むリアルタイム映像・音声ストリームのpublish/readに対応するメディアサーバーで、外部コマンドをhookとして起動することもできます。
今回はそのrunOnDemand機能を使います。
MediaMTXの設定
mediamtx.ymlを次のようにしています。
paths:
hdmi:
runOnDemand: >-
ffmpeg -hide_banner -y
-loglevel warning
-fflags nobuffer
-flags low_delay
-init_hw_device qsv=qsv
-filter_hw_device qsv
-thread_queue_size 4
-f alsa
-ac 2
-ar 48000
-i hw:1,0
-thread_queue_size 1
-f v4l2
-input_format yuyv422
-video_size 1920x1080
-framerate 30
-i /dev/video0
-map 1:v:0
-map 0:a:0
-vf 'hwupload=extra_hw_frames=0,vpp_qsv=format=nv12'
-c:v h264_qsv
-preset veryfast
-global_quality:v 35
-async_depth 1
-bf 0
-g 30
-c:a libopus -b:a 192k
-f rtsp
-rtsp_transport udp
-muxdelay 0
rtsp://127.0.0.1:8554/hdmi
runOnDemandRestart: yes
runOnDemandStartTimeout: 10s
runOnDemandCloseAfter: 5s
一見するとオプションがかなり多いですが、低遅延化という観点ではいくつかのポイントに分けて考えると分かりやすくなります。
runOnDemandで必要なときだけFFmpegを起動する
まずMediaMTX側です。
runOnDemand: >-
ffmpeg ...
runOnDemandは、クライアントからそのpathへのアクセスが発生したときに外部コマンドを起動する機能です。
つまり、
誰も見ていない
↓
FFmpeg停止
ffplayから /hdmi に接続
↓
MediaMTXがFFmpegを起動
↓
FFmpegが /hdmi にpublish
↓
再生開始
という動作になります。
MediaMTX公式ドキュメントでも、runOnDemandに指定したコマンドはreaderからpathが要求されたタイミングで開始される仕組みになっています。
常時FFmpegを動かしておく必要がないため、HDMI配信を必要なときだけ使いたい場合に便利です。
さらに、
runOnDemandRestart: yes
としているので、FFmpegが何らかの理由で終了した場合にも再起動されます。現行のMediaMTX設定リファレンスにも、このオプションはコマンド終了時に再起動する設定として定義されています。
入力側のバッファを極力小さくする
低遅延ストリーミングで重要なのが、
「溜めてから処理する」のを避けること
です。
そのためFFmpegの冒頭で、
-fflags nobuffer
-flags low_delay
を指定しています。
-fflags nobuffer
-fflags nobuffer
は入力ストリーム解析時に発生するバッファリングによる遅延を減らすための設定です。FFmpeg公式ドキュメントでも、初期入力解析時のバッファリングによるレイテンシーを削減するオプションとされています。
リアルタイム入力では、
できるだけパケットを貯めない
↓
届いたデータをすぐ次の処理へ渡す
という方向に設定しておくことが重要です。
-flags low_delay
-flags low_delay
も名前の通り、コーデック処理を低遅延方向へ設定するためのフラグです。FFmpegではlow_delayが「Force low delay」と定義されています。
映像と音声を別デバイスから入力する
今回の構成では音声をALSA、
-f alsa
-ac 2
-ar 48000
-i hw:1,0
映像をV4L2、
-f v4l2
-input_format yuyv422
-video_size 1920x1080
-framerate 30
-i /dev/video0
から取り込んでいます。
したがってFFmpegから見ると入力は、
input 0 = ALSA
input 1 = V4L2
になります。
出力するストリームを、
-map 1:v:0
-map 0:a:0
と明示しています。
つまり、
映像 → input 1のvideo 0
音声 → input 0のaudio 0
です。
thread_queue_sizeを小さくする
入力にはそれぞれ、
-thread_queue_size 4
-thread_queue_size 1
を指定しています。
thread_queue_sizeは、入力デバイスなどから読み込んだパケットをFFmpeg内部で何個までキューに保持するかを決める設定です。FFmpeg公式ドキュメントでも、入力の場合はデバイスやファイルから読み込む際のqueued packetsの最大数と説明されています。
キューを大きくすると、
多少処理が詰まる
↓
キューへ蓄積
↓
フレームを捨てず処理
しやすくなります。
一方で低遅延用途では、
処理が追いつかない
↓
過去の映像がキューへ溜まる
↓
表示がどんどん現実時間から遅れる
という問題になります。
そのため今回はかなり小さい値にしています。
これは、
フレームを絶対に落とさないことより、現在時刻に近い映像を表示することを優先する
という設計です。
低遅延ストリーミングでは非常に重要な考え方です。
Intel Quick Sync Videoでエンコードする
1920×1080 30fpsの映像をH.264へソフトウェアエンコードすると、CPU負荷によっては処理そのものが遅延原因になります。
そこでIntel Quick Sync Video、いわゆるQSVを利用しています。
-init_hw_device qsv=qsv
-filter_hw_device qsv
そして映像を、
-vf 'hwupload=extra_hw_frames=0,vpp_qsv=format=nv12'
でQSV側へアップロードし、NV12へ変換します。
エンコーダーは、
-c:v h264_qsv
です。
これによってH.264エンコードをIntel GPUのハードウェアエンコーダーへ担当させます。
エンコーダーでも「先読み」を減らす
ここからが低遅延化の重要部分です。
-preset veryfast
-global_quality:v 35
-async_depth 1
-bf 0
-g 30
としています。
-preset veryfast
-preset veryfast
QSVのpresetは、
veryfast
faster
fast
medium
slow
slower
veryslow
という選択肢があり、FFmpegではveryfast側が速度優先、veryslow側が画質優先として定義されています。
ライブ配信ではエンコード品質を限界まで高めるより、
1フレームを素早くエンコードしてネットワークへ送る
ことを優先します。
そのためveryfastを選択しています。
async_depth 1が重要
-async_depth 1
も低遅延化に効く設定です。
QSVは複数フレームを非同期に処理することでスループットを高められます。
ただし、
複数フレームを並列処理
するということは、逆に言えばエンコーダー内部に複数フレームが存在することにもなります。
FFmpegのQSVドキュメントでもasync_depthは非同期処理数に関係するパラメータとして定義されており、QSV decoderについては値を増やすほどレイテンシーも増えると明記されています。
そこで、
-async_depth 1
まで下げています。
狙っているのは、
フレーム入力
↓
エンコード
↓
すぐ出力
という単純なパイプラインです。
スループットよりlatencyを優先しています。
Bフレームを使わない
低遅延H.264では、
-bf 0
が特に重要です。
Bフレームを使用すると、あるフレームをエンコード・デコードする際に未来側のフレームを参照することがあります。
概念的には、
I P B B P
のような構造です。
そのためエンコーダーやデコーダーでフレーム並び替えが必要になり、低遅延用途では不利になります。
そこで、
-bf 0
としてBフレームを完全に無効化しています。
圧縮効率は多少犠牲になりますが、リアルタイム用途ではこちらの方が扱いやすくなります。
GOPを1秒にする
-g 30
もポイントです。
今回は、
-framerate 30
なので30fpsです。
したがって、
30フレーム ÷ 30fps = 1秒
となり、おおよそ1秒ごとにGOPが区切られます。
FFmpegでは-gはGOP sizeを指定するパラメータです。
GOPを短くすると圧縮効率は多少悪くなりますが、
- 再生開始
- パケットロス後の復帰
- ストリームへの途中参加
といった場面で扱いやすくなります。
低遅延用途では圧縮率だけでなく、素早く現在の映像へ追いつけることも重要です。
global_quality:v 35
-global_quality:v 35
ではQSVの品質を指定しています。
h264_qsvでglobal_quality:vを指定すると、条件に応じてICQなどの品質ベースのレート制御が利用されます。FFmpegのQSVドキュメントではICQ時の範囲は1〜51で、1が最高品質とされています。
つまり、
小さい値
↓
高画質・大きなデータ量
大きい値
↓
低画質・小さなデータ量
という方向です。
35はかなり圧縮寄りの設定なので、ネットワーク帯域や画質を見ながら調整できます。
たとえば画質を上げるなら、
-global_quality:v 28
などへ下げて比較すると分かりやすいでしょう。
音声はOpus 192kbps
音声は、
-c:a libopus -b:a 192k
としています。
映像に比べれば音声エンコードの負荷や帯域は小さいため、Opus 192kbpsとしています。
ただし「映像だけ確認できればよい」という監視用途などでは音声を削除して、
-an
とすることで構成をさらに単純化できます。
RTSPはUDPを使う
FFmpegからMediaMTXへの出力は、
-f rtsp
-rtsp_transport udp
-muxdelay 0
rtsp://127.0.0.1:8554/hdmi
です。
低遅延化では、
-rtsp_transport udp
がポイントです。
FFmpegではRTSPのlower transportとしてUDPまたはTCPなどを選択できます。UDPの場合はUDP、TCPの場合はRTSP control channel内へインターリーブしてデータが流れます。
UDPにはTCPのような、
パケット消失
↓
再送待ち
↓
到着するまで後続処理も待つ
という仕組みがありません。
そのため、
多少映像が乱れてもよいので、古い映像を待たず現在の映像を出す
というリアルタイム用途と相性がよい方式です。
ただし当然ながら、
- Wi-Fi
- 混雑したLAN
- インターネット越し
などパケットロスが発生しやすい環境では映像が乱れる可能性があります。
逆に安定性を優先するなら、
-rtsp_transport tcp
という選択肢もあります。
低遅延と伝送の確実性はトレードオフです。
muxdelay 0
さらに、
-muxdelay 0
としています。
muxdelayはFFmpegの出力側で使用される遅延関連パラメータです。FFmpeg公式のRTSP送信例でも-muxdelay 0.1を指定した例があります。
今回はさらに攻めて、
-muxdelay 0
とし、
パケットをまとめるために待つ時間を極力発生させない
方向へ寄せています。
MediaMTX自身にはエンコードさせない
今回の構成で重要なのは、
FFmpeg
↓
rtsp://127.0.0.1:8554/hdmi
として、一度localhostのMediaMTXへpublishしていることです。
クライアントは、
rtsp://x.x.x.x:8554/hdmi
へアクセスします。
つまりMediaMTXは、
FFmpeg
↓
MediaMTX
↓
複数クライアント
という中継点になります。
この構成にすると、キャプチャデバイスやエンコーダーを各クライアントが直接触る必要がありません。
複数端末へ配信したい場合にも扱いやすくなります。
ffplay側でもバッファを削る
送信側を低遅延化しても、受信側のプレイヤーが映像を1秒溜めてから再生していたら意味がありません。
そこでffplay側も、
ffplay \
-fflags nobuffer \
-flags low_delay \
-framedrop \
-rtsp_transport udp \
rtsp://x.x.x.x:8554/hdmi
としています。
-fflags nobuffer
送信側と同様に、
-fflags nobuffer
を指定し、入力解析時のバッファリングによるレイテンシーを減らします。
-flags low_delay
-flags low_delay
でデコードも低遅延方向へ設定します。
-framedrop
-framedrop
もリアルタイム用途では重要です。
ffplayでは、映像が同期から遅れた場合にvideo frameをdropできるオプションとしてframedropが用意されています。
低遅延用途では、
すべてのフレームを律儀に表示する
よりも、
処理が遅れたら古いフレームを捨てる
↓
現在時刻へ追いつく
ほうが重要です。
これは映像品質を多少犠牲にしてでも遅延を増大させないための設定です。
低遅延配信では「捨てる」ことが重要
ここまでの設定を見ると、共通した思想があります。
それは、
古いデータをなるべく保持しない
ということです。
通常の動画再生では、
パケットロスさせない
フレームを落とさない
映像を滑らかにする
ことが重要です。
しかしリアルタイム映像では事情が違います。
例えばゲーム画面やカメラ映像を表示するとき、
3秒前の映像を完璧に表示
することより、
多少フレームが飛んでも100ms前後の映像を表示
できるほうが有用な場合があります。
そのため今回の設定では、
小さい入力キュー
↓
Bフレームなし
↓
QSVの非同期深度を削減
↓
muxerの待ち時間を削減
↓
UDP
↓
ffplayでもバッファ削減
↓
遅れたらframe drop
という形で、パイプラインの各段階から待ち時間を削っています。
遅延はどこで発生するのか
低遅延化するときは、ネットワークだけを見るのではなく、
Capture
↓
Queue
↓
Filter
↓
Encode
↓
Mux
↓
Network
↓
Demux
↓
Decode
↓
Render
というパイプライン全体を見る必要があります。
例えばLANのpingが、
1ms
だったとしても、
エンコーダー 100ms
プレイヤーバッファ 500ms
なら、ネットワークをいくら改善しても大きな効果はありません。
むしろリアルタイム動画の場合、
ネットワークそのものよりエンコーダー・デコーダー・プレイヤー内部のバッファが大きな遅延源になる
ことがあります。
今回多数の低遅延オプションを指定しているのはそのためです。
さらに遅延を詰めたい場合
この設定でもまだ遅延が気になる場合、いくつか試せるポイントがあります。
まずffplayのRTSP受信では、UDPパケットの並び替え用バッファが存在します。FFmpegドキュメントでは、UDP受信時のpacket reorderingをmax_delayを0にすることで無効化できるとされています。
例えば、
ffplay \
-fflags nobuffer \
-flags low_delay \
-framedrop \
-rtsp_transport udp \
-max_delay 0 \
rtsp://x.x.x.x:8554/hdmi
という設定も試せます。
ただしこれはパケット順序の乱れに対する耐性をさらに削るため、ネットワーク品質によっては映像が不安定になります。
また、
-g 30
を、
-g 15
などへ縮めることもできます。
ただしGOPを短くするほど一般に圧縮効率は悪化します。
低遅延化には常に、
遅延
画質
帯域
安定性
CPU/GPU負荷
のトレードオフがあります。
UDPだから必ず低遅延、ではない
注意したいのが、
UDP = 必ず低遅延
ではないということです。
UDPにすると再送待ちを避けやすい一方、ネットワーク品質が悪い場合は、
packet loss
↓
映像破損
↓
次の復旧可能なフレームまで待つ
ということもあります。
したがって実際には、
有線LAN + UDP
のような、低ロスかつ安定したネットワークで使うのが理想的です。
Wi-Fi環境ならUDPとTCPを両方試し、
実際の遅延
映像の乱れ
安定性
を比較したほうがよいでしょう。
まとめ
今回の構成では、
HDMI
↓
V4L2 / ALSA
↓
FFmpeg
↓
Intel QSV H.264
↓
RTSP / UDP
↓
MediaMTX
↓
LAN
↓
ffplay
というパイプラインを作りました。
低遅延化のポイントは、単に高速なエンコーダーを使用することではありません。
各段階で発生する、
「念のため少し溜めておく」バッファを可能な限り取り除くこと
が重要です。
今回の主要な設定をまとめると、
-fflags nobuffer
入力側のバッファリングを削減
-flags low_delay
コーデックを低遅延方向へ
-thread_queue_size
キャプチャ入力のキューを小さくする
-c:v h264_qsv
Intel QSVで高速にH.264エンコード
-preset veryfast
エンコード速度優先
-async_depth 1
QSV内部の非同期処理深度を削減
-bf 0
Bフレームを無効化
-g 30
30fpsなら約1秒GOP
-rtsp_transport udp
再送待ちを避ける方向へ
-muxdelay 0
muxer側の待ち時間を削減
-framedrop
再生が遅れたら古いフレームを捨てる
となります。
低遅延映像配信で大切なのは、
「すべてのフレームを確実に届ける」ことと「今の映像を届ける」ことは別の目標
だと理解することです。
録画ならフレームを失わないことが重要です。
一方、リアルタイムモニター、遠隔操作、ゲーム画面、カメラ監視などでは、古い映像が完全に届くよりも、多少フレームが欠けても最新の映像が表示されることのほうが重要です。
その方針でキャプチャ・エンコード・ネットワーク・プレイヤーのすべてを調整すると、RTSPでもかなり低遅延な映像伝送を構成できます。
Top comments (0)