2026年11月14日
2026年11月14日
HLS Streaming with Nginx: ffmpeg Conversion and RTMP Live Setup
This article is a localized edition of the Japanese original. Read the Japanese original
HLS (HTTP Live Streaming) splits a video into files a few seconds long (segments) and delivers them over plain HTTP together with a list of them (a playlist, extension .m3u8). Apple designed it, and it is published as RFC 8216. The player reads the playlist and fetches the segments one after another.
From Nginx's point of view, serving HLS just means returning static .m3u8 and .ts files. Open source Nginx cannot convert video to HLS, so the conversion is done in one of two ways.
| Use case | What converts the video | Role of Nginx |
|---|---|---|
| Recorded video (VOD) | ffmpeg creates the playlist and segments in advance | Serves them as static files |
| Live streaming | The nginx-rtmp module cuts the incoming RTMP stream into segments on the fly | Receives RTMP and serves the generated files over HTTP |
The hls directive in the official documentation (ngx_http_hls_module, which converts MP4 to HLS on request) is available only in the commercial NGINX Plus. hls_fragment and hls_playlist_length are nginx-rtmp module directives and cannot be used in a location inside the http block.
The steps in this article were tested end to end (conversion, delivery and live ingest) on Debian 13 with Nginx 1.26.3, ffmpeg 7.1.5 and libnginx-mod-rtmp 1.2.2 (as of November 2026).
Serving recorded video over HLS
1. Install ffmpeg and Nginx
sudo apt update
sudo apt install nginx ffmpeg
2. Convert an MP4 to HLS
sudo mkdir -p /var/www/hls/vod
sudo ffmpeg -i input.mp4 \
-c:v libx264 -preset veryfast -crf 23 -g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k \
-f hls -hls_time 6 -hls_playlist_type vod \
-hls_segment_filename /var/www/hls/vod/seg_%03d.ts \
/var/www/hls/vod/index.m3u8
| Option | Meaning |
|---|---|
-c:v libx264 / -c:a aac |
Convert to H.264 and AAC, which most devices can play |
-g 60 -keyint_min 60 -sc_threshold 0 |
Fix the keyframe interval at 60 frames (every 2 seconds at 30 fps) |
-hls_time 6 |
Target segment length in seconds. Segments are cut at keyframes |
-hls_playlist_type vod |
Write a playlist listing every segment and end it with #EXT-X-ENDLIST |
-hls_segment_filename |
Segment file name. %03d becomes a sequence number |
If the source is already H.264 and AAC, you can use -c copy instead of -c:v libx264 ... and -c:a aac ... to split it without re-encoding. Segments can only be cut at keyframes, though, so with long keyframe intervals the segments end up longer than -hls_time.
Converting a 20-second test video produced the following playlist and four segments.
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:6.000000,
seg_000.ts
#EXTINF:6.000000,
seg_001.ts
#EXTINF:6.000000,
seg_002.ts
#EXTINF:2.000000,
seg_003.ts
#EXT-X-ENDLIST
The -hls_segment_time option seen in older articles does not exist in ffmpeg. Set the segment length with -hls_time.
3. Serve the files with Nginx
The MIME types for .m3u8 and .ts are already in /etc/nginx/mime.types in Debian 13's Nginx. Check your environment as well:
grep -n "m3u8\|mp2t" /etc/nginx/mime.types
37: application/vnd.apple.mpegurl m3u8;
88: video/mp2t ts;
If nothing is shown, add types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } to the location.
Here is the delivery configuration. On Debian/Ubuntu, create a file in /etc/nginx/sites-available/ and link it into sites-enabled.
server {
listen 80;
server_name video.example.com;
location /hls/ {
root /var/www;
location ~ \.m3u8$ {
add_header Cache-Control "no-cache";
add_header Access-Control-Allow-Origin "*";
}
location ~ \.ts$ {
add_header Cache-Control "max-age=86400";
add_header Access-Control-Allow-Origin "*";
}
}
}
- With
root /var/www;,/hls/vod/index.m3u8returns/var/www/hls/vod/index.m3u8. - Playlists can change, so they get
no-cache; segments never change, so they are cached longer. Access-Control-Allow-Originis needed so a player on a page served from another domain (hls.js and the like) can load the files. Instead of*, you can restrict it to the origin of the site hosting the player (for examplehttps://www.example.com).
sudo nginx -t
sudo systemctl reload nginx
curl -sI http://video.example.com/hls/vod/index.m3u8
In the test environment the response carried Content-Type: application/vnd.apple.mpegurl and the headers configured above. Reading the URL with ffprobe recognizes it as a 20-second video.
ffprobe -v error -show_entries format=duration http://video.example.com/hls/vod/index.m3u8
4. Play it in a browser
Safari (macOS and iOS) plays it when you simply put the .m3u8 URL in a <video> element. Some browsers cannot play it that way, so a JavaScript player such as hls.js lets it play in more browsers. To check playback, you can also enter the URL in VLC media player under "Open Network Stream".
Live streaming (receive RTMP, deliver HLS)
Streaming software such as OBS Studio sends video over RTMP. With the nginx-rtmp module, Nginx receives the RTMP stream and writes out HLS segments. The module is third-party; on Debian 13 it is installed with the libnginx-mod-rtmp package.
sudo apt install libnginx-mod-rtmp
RTMP configuration
The rtmp block goes at the top level of nginx.conf, not inside the http block. Here it is kept in a separate file and included at the end of nginx.conf (outside http { ... }).
# /etc/nginx/rtmp.conf
rtmp {
server {
listen 1935;
chunk_size 4096;
application live {
live on;
record off;
allow publish 127.0.0.1;
deny publish all;
allow play all;
hls on;
hls_path /var/www/hls/live;
hls_fragment 3s;
hls_playlist_length 60s;
}
}
}
# append to the end of /etc/nginx/nginx.conf
include /etc/nginx/rtmp.conf;
| Directive | Meaning |
|---|---|
allow publish / deny publish |
IP addresses allowed to send video. Allow the address of the machine running your streaming software |
hls on |
Convert the incoming stream to HLS |
hls_path |
Directory where segments and playlists are written |
hls_fragment |
Segment length. In practice segments are cut at keyframes |
hls_playlist_length |
How much time the playlist covers |
Make the output directory writable by the Nginx worker user (www-data on Debian/Ubuntu).
sudo mkdir -p /var/www/hls/live
sudo chown www-data:www-data /var/www/hls/live
sudo nginx -t
sudo systemctl reload nginx
If you use a firewall, make sure TCP port 1935 is reachable from wherever the streaming software runs.
Send a stream and check it
In OBS Studio, set the server in the stream settings to rtmp://server-address/live and the stream key to a name such as stream. To test with ffmpeg:
ffmpeg -re -i input.mp4 -c:v libx264 -preset veryfast -g 60 -c:a aac -f flv rtmp://127.0.0.1/live/stream
While it is streaming, a playlist and segments named after the stream key appear in hls_path.
$ ls /var/www/hls/live
stream-0.ts stream-1.ts stream-2.ts stream-3.ts stream.m3u8
With the location /hls/ configuration from the previous section, the viewing URL is http://video.example.com/hls/live/stream.m3u8. In the test environment, a stream with a keyframe every 2 seconds and hls_fragment 3s produced segments of about 4 seconds. To get the segment length you asked for, match the keyframe interval in your streaming software.
Things to watch
- Anyone who knows an HLS URL can play it. Protect videos you do not want public with Basic authentication, IP address restrictions or signed URLs (the
secure_linkmodule). - If you open the RTMP ingest (
allow publish) to everyone, third parties can push video into it. Restrict the allowed IP addresses. - As viewers increase, traffic is roughly "bitrate × concurrent viewers". Check the limits of your connection and the server's transfer allowance, and put a CDN in front if needed.
- Serve over HTTPS in production. A player on an HTTPS page cannot load video over HTTP.
CORS settings for Nginx are covered in How to configure CORS in Nginx, and moving to HTTPS in How to set up SSL certificates with Nginx and Certbot.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)