Skip to main content

FFmpeg + NGINX HTTPS HLS Streaming Server Ubuntu 24.04

In this tutorial, we build a lightweight streaming server on Ubuntu 24.04 that takes RTSP video feeds from IP cameras and publishes them to the web using HLS (HTTP Live Streaming).

We use FFmpeg to pull video streams from each camera, remove audio, segment the video into HLS chunks, and keep a rolling archive.
Then, NGINX with HTTPS serves those HLS playlists securely so they can be streamed in any modern browser — no plugins required.

This setup is perfect when you want:

  • Live CCTV viewing over HTTPS

  • Minimal resource usage (video passthrough, no transcoding)

  • A clean HLS output that works in browsers + mobile

  • Automatic reconnection if the RTSP camera drops

  • Simple, scalable directory-based configuration

1 — Update and install dependencies

sudo apt update
sudo apt install -y ffmpeg nginx python3-yaml openssl

2 — Directory setup

sudo mkdir -p /usr/local/ffmpeg-hls
sudo mkdir -p /var/www/html/cctv
sudo chown -R www-data:www-data /var/www/html

3 — Camera configuration file
Create file /usr/local/ffmpeg-hls/cams.yml

- name: cam1
  url: rtsp-url-here
- name: cam2
  url: rtsp-url-here
- name: cam3
  url: rtsp-url-here
# Add up to cam16 if needed...

4 — Auto FFmpeg launcher script
Create file /usr/local/ffmpeg-hls/start-all-cams.sh

#!/bin/bash
CONFIG_FILE="/usr/local/ffmpeg-hls/cams.yml"
OUTPUT_ROOT="/var/www/html/cctv"
HLS_DURATION=10            # seconds per segment
TOTAL_HISTORY_MIN=10       # total minutes to keep
SEGMENTS_TO_KEEP=$(( (TOTAL_HISTORY_MIN*60) / HLS_DURATION ))

mkdir -p "$OUTPUT_ROOT"

#Generate list of active cams
python3 - <<'PYCODE' > /tmp/camlist.txt
import yaml, sys
data = yaml.safe_load(open("/usr/local/ffmpeg-hls/cams.yml"))
for cam in data:
    print(f"{cam['name']}|{cam['url']}")
PYCODE

#Build array of active cam names
ACTIVE_CAMS=($(awk -F'|' '{print $1}' /tmp/camlist.txt))

#Clean up old folders not in YAML
for dir in "$OUTPUT_ROOT"/*/; do
  [ -d "$dir" ] || continue
  CAM_NAME=$(basename "$dir")
  if [[ ! " ${ACTIVE_CAMS[@]} " =~ " ${CAM_NAME} " ]]; then
    echo "Removing old folder: $dir"
    rm -rf "$dir"
  fi
done

#Start each active camera
while IFS="|" read -r NAME URL; do
    [ -z "$NAME" ] && continue
    mkdir -p "$OUTPUT_ROOT/$NAME"

    echo "Starting $NAME ..."
    (
      while true; do
        ffmpeg -hide_banner -loglevel warning \
          -rtsp_transport tcp \
          -i "$URL" \
          -fflags +genpts -use_wallclock_as_timestamps 1 \
          -an -c:v copy \
          -f hls \
          -hls_time $HLS_DURATION \
          -hls_list_size $SEGMENTS_TO_KEEP \
          -hls_flags delete_segments+append_list+program_date_time \
          -hls_segment_filename "$OUTPUT_ROOT/$NAME/segment_%05d.ts" \
          "$OUTPUT_ROOT/$NAME/index.m3u8"

        echo "$NAME disconnected, retrying in 5s..."
        sleep 5
      done
    ) &
done < /tmp/camlist.txt

echo "All active camera streams started!"
wait

Make it executable:

sudo chmod +x /usr/local/ffmpeg-hls/start-all-cams.sh

5 — Systemd service
Create file /etc/systemd/system/cctv-hls.service

[Unit]
Description=FFmpeg HLS Auto Streams (All Cameras)
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/ffmpeg-hls/start-all-cams.sh
Restart=always
RestartSec=10
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Apply changes

sudo nano /usr/local/ffmpeg-hls/start-all-cams.sh
# (paste script above)
sudo chmod +x /usr/local/ffmpeg-hls/start-all-cams.sh
sudo systemctl restart cctv-hls

6 — HTTPS setup with NGINX (self-signed)
Generate SSL certificate:

sudo mkdir -p /etc/ssl/cctv
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout /etc/ssl/cctv/cctv.key \
  -out /etc/ssl/cctv/cctv.crt \
  -subj "/CN=cctv.local"

Configure NGINX site:
Create file /etc/nginx/sites-available/cctv

server {
    listen 443 ssl;
    server_name _;

    ssl_certificate     /etc/ssl/cctv/cctv.crt;
    ssl_certificate_key /etc/ssl/cctv/cctv.key;

    root /var/www/html;
    index index.html;

    location /cctv/ {
        add_header Cache-Control no-cache;
        types {
            application/vnd.apple.mpegurl m3u8;
            video/mp2t ts;
        }
        autoindex on;
        add_header Access-Control-Allow-Origin *;
    }
}

server {
    listen 80;
    server_name _;
    return 301 https://$host$request_uri;
}

Enable and reload:

sudo ln -s /etc/nginx/sites-available/cctv /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo systemctl restart nginx

7 — Verify everything

sudo systemctl status cctv-hls

Then visit:

https://<server-ip>/cctv/cam1/index.m3u8
https://<server-ip>/cctv/cam2/index.m3u8
https://<server-ip>/cctv/cam3/index.m3u8