I've been working on setting up an HLS stream on my Raspberry Pi to broadcast video from a security camera that's physically connected to my Raspberry Pi through my web server, making it accessible via my website. The .ts video files and the .m3u8 playlist are correctly being served from /var/www/html/hls. However, when I attempt to load the stream on Safari (as well as other browsers), the video continuously appears to be loading without ever displaying any content.
Here are some details about my setup:
/dev/video0..ts files directly from the browser, they only show a black screen but they do play.Given the situation, I suspect there might be an issue with my FFmpeg command or possibly with my Nginx configuration.
Here is what I have:
ffmpeg stream service:
/etc/systemd/system/ffmpeg-stream.service
[Unit]
Description=FFmpeg RTMP Stream
After=network.target
[Service]
ExecStart=/usr/local/bin/start_ffmpeg.sh
Restart=always
User=jacobanderson
Group=jacobanderson
StandardError=syslog
SyslogIdentifier=ffmpeg-stream
Environment=FFMPEG_LOGLEVEL=error
[Install]
WantedBy=multi-user.target
ffmpeg command:
/usr/local/bin/start_ffmpeg.sh
#!/bin/bash
/usr/bin/ffmpeg -f v4l2 -input_format mjpeg -video_size 1280x720 -framerate 30 -i /dev/video0 -vcodec libx264 -preset veryfast -acodec aac -strict -2 -f flv rtmp://localhost/live/
nginx.conf:
/etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
rtmp {
server {
listen 1935;
chunk_size 4096;
#allow publish 127.0.0.1;
#deny publish all;
application live {
#allow 192.168.0.100;
live on;
hls on;
hls_path /var/www/html/hls;
hls_fragment 3;
hls_nested on;
#hls_fragment_naming stream;
hls_playlist_length 120;
hls_cleanup on;
hls_continuous on;
#deny play all;
}
}
}
http {
##
# Basic Settings
##
sendfile on;
#sendfile off;
tcp_nopush on;
types_hash_max_size 2048;
# server_tokens off;
# Additional for video
directio 512;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
#ssl_protocols TLSv1.2 TLSv1.3; # Use only secure protocols
ssl_prefer_server_ciphers on;
#ssl_ciphers "HIGH:!aNULL:!MD5";
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
#gzip on;
gzip off; # Ensure gzip is off for HLS
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
sites-available:
/etc/nginx/sites-available/myStream.mysite.com
server {
listen 443 ssl;
server_name myStream.mysite.com;
ssl_certificate /etc/letsencrypt/live/myStream.mysite.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/myStream.mysite.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location / {
root /var/www/html/hls;
index index.html;
}
location /hls {
# Password protection
auth_basic "Restricted Content";
auth_basic_user_file /etc/nginx/.htpasswd;
# Disable cache
add_header Cache-Control no-cache;
# CORS setup
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length';
# Allow CORS preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
text/html html;
text/css css;
}
root /var/www/html;
}
}
server {
listen 80;
server_name myStream.mysite.com;
if ($host = myStream.mysite.com) {
return 301 https://$host$request_uri;
}
return 404; # managed by Certbot
}
index.html:
/var/www/html/hls/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HLS Stream</title>
<link href="https://vjs.zencdn.net/7.10.2/video-js.css" rel="stylesheet" />
<script src="./js/hls.min.js"></script>
</head>
<body>
<video-js id="my-video" class="video-js vjs-default-skin" controls preload="auto" width="640" height="264"
autoplay muted data-setup='{"fluid": true}'>
<!--<source src="https://myStream.mysite.com/hls/index.m3u8" type="application/x-mpegURL">-->
<source src="https://myStream.mysite.com/hls/index.m3u8" type="application/vnd.apple.mpegurl">
</video-js>
<script src="https://vjs.zencdn.net/7.10.2/video.js"></script>
<script>
if (Hls.isSupported()) {
var video = document.getElementById('my-video_html5_api'); // Updated ID to target the correct video element
var hls = new Hls();
hls.loadSource('https://myStream.mysite.com/hls/index.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'https://myStream.mysite.com/hls/index.m3u8';
video.addEventListener('loadedmetadata', function() {
video.play();
});
}
</script>
</body>
</html>
Has anyone experienced similar issues or can spot an error in my configuration? Any help would be greatly appreciated as I have already invested over 30 hours trying to resolve this.
I finally figured out a solution, it was to change:
#!/bin/bash
/usr/bin/ffmpeg -f v4l2 -input_format mjpeg -video_size 1280x720 -framerate 30 -i /dev/video0 -vcodec libx264 -preset veryfast -acodec aac -strict -2 -f flv rtmp://localhost/live/
to
#!/bin/bash
/usr/bin/ffmpeg -f v4l2 -input_format mjpeg -video_size 1280x720 -framerate 30 -i /dev/video0 -vcodec libx264 -preset veryfast -tune zerolatency -g 60 -sc_threshold 0 -acodec aac -b:a 128k -ar 44100 -maxrate 1500k -bufsize 3000k -f hls -hls_time 4 -hls_playlist_type event -hls_segment_filename '/var/www/html/hls/stream%03d.ts' /var/www/html/hls/index.m3u8
I think this solution works as it bypasses RTMP, if someone knows of a good alternative solution that involves actually using the RTMP please lmk in the comments or as an alternate solution and I will accept it (if it works).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With