FFmpeg推流Nginx的HLS流无法在iPhone播放的问题求助
iPhone端HLS直播流无法加载的解决方案
问题概述
用FFmpeg把摄像头RTSP流推送到Nginx RTMP服务器转成HLS流后,桌面Safari、iPad等设备都能正常播放,但iPhone端始终加载失败,偶尔全屏快进时能播放但时间戳异常(比如显示-11:21:42)。用Apple的mediastreamvalidator检测发现部分.ts文件返回404,但浏览器调试时复现不了这个问题,推测是FFmpeg输出的PTS(显示时间戳)异常导致的。
现有配置信息
FFmpeg推流命令
ffmpeg -rtsp_transport tcp -i rtsp://user:pass@192.168.1.XXX/ -f image2 -stream_loop -1 -re -i /tmp/overlay.png -filter_complex "overlay" -vcodec libx264 -preset ultrafast -profile baseline -pix_fmt yuv420p -an -f flv -rtmp_live live -s 1280x720 rtmp://remote.server.com:1935/show/stream
Nginx RTMP配置
user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events { worker_connections 768; # multi_accept on; } http { ## # Basic Settings ## # sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # server_tokens off; # 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; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; ## # Logging Settings ## access_log /var/log/nginx/myccam-access.log; error_log /var/log/nginx/myccam-error.log; ## # Gzip Settings ## gzip on; # gzip_vary on; # gzip_proxied any; # gzip_comp_level 6; # gzip_buffers 16 8k; # gzip_http_version 1.1; # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; ## # Virtual Host Configs ## include /etc/nginx/conf.d/*.conf; # include /etc/nginx/sites-enabled/*; server { listen 8080; location ~ \.(mp4|m4a|m4v|mov)$ { add_header content_disposition filename=$request_uri; add_header accept_ranges bytes; } location /hls { # 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'; add_header Cache-Control no-cache; add_header Last-Modified ""; # 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; } root /tmp/; } } } rtmp { server { listen 1935; # Listen on standard RTMP port chunk_size 4000; application show { live on; hls on; hls_fragment_naming system; hls_fragment 1s; hls_path /tmp/hls; hls_continuous on; hls_variant _720p2628kbs BANDWIDTH=2628000; } } }
网页播放代码
<!DOCTYPE html> <html lang="en"><head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Camera</title> <style> html, body { background: none !important; overflow: hidden; margin: 0; padding: 0; } video { margin: 0 auto; width: 100%; height: 100vh; } .video-container { width: 100%; height: 100vh } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script> function waitForPublishedStream(showMsg) { if (typeof showMsg === 'undefined') showMsg = true; $.get('https://myserver.com/CameraName/hls/stream.m3u8', function (response) { if (showMsg) { $('.container-loader .alert-warning').html('Pieslēdzamies kamerai..') } if (response.state == 'success') { if (response.published) { document.location.reload(true) } else { setTimeout(function () { waitForPublishedStream(); }, 2000) } } }) } </script> </head> <body> <div class="video-container"> <video id="video" autoplay="autoplay" muted="muted" controls="controls" playsinline="" src="https://myserver.com/CameraName/hls/stream.m3u8"> <source src="https://myserver.com/CameraName/hls/stream.m3u8" type="application/vnd.apple.mpegurl"> </video> </div> <script src="https://cdn.jsdelivr.net/npm/hls.js"></script> <script> $(document).ready(function () { const video = document.getElementById('video'); if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource('https://myserver.com/CameraName/hls/stream.m3u8'); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play(); }); } }) </script> </body></html>
具体解决方案
1. 修复FFmpeg的PTS异常问题
RTSP流的PTS可能不连续,加上循环的overlay图片会进一步打乱时间戳,调整FFmpeg参数:
- 添加
-fflags +genpts强制生成连续的显示时间戳 - 添加
-use_wallclock_as_timestamps 1用系统时间作为基准时间戳,避免RTSP流自带的异常PTS影响 - 给overlay滤镜加
shortest=1,让输出流时长跟随RTSP流,避免循环图片导致时间戳无限增长
修改后的FFmpeg命令:
ffmpeg -rtsp_transport tcp -fflags +genpts -use_wallclock_as_timestamps 1 -i rtsp://user:pass@192.168.1.XXX/ -f image2 -stream_loop -1 -re -i /tmp/overlay.png -filter_complex "overlay=shortest=1" -vcodec libx264 -preset ultrafast -profile baseline -pix_fmt yuv420p -an -f flv -rtmp_live live -s 1280x720 rtmp://remote.server.com:1935/show/stream
2. 调整Nginx HLS配置适配Apple设备
Apple设备对HLS片段命名和播放列表规则要求严格,修改rtmp配置段:
- 把
hls_fragment_naming system改成hls_fragment_naming sequential,用连续序号命名片段,避免系统时间戳导致的片段名不连续 - 添加
hls_playlist_length 5s限制播放列表长度,防止旧片段被过早删除导致404 - 添加
hls_type live明确标记为直播流类型 - 移除
hls_continuous on,该参数可能导致播放列表引用已被删除的片段
修改后的rtmp配置:
rtmp { server { listen 1935; # Listen on standard RTMP port chunk_size 4000; application show { live on; hls on; hls_fragment_naming sequential; hls_fragment 1s; hls_path /tmp/hls; hls_playlist_length 5s; hls_type live; hls_variant _720p2628kbs BANDWIDTH=2628000; } } }
3. 优化网页播放代码适配iPhone Safari
iPhone Safari原生支持HLS,不需要hls.js,避免兼容性冲突:
- 移除hls.js的加载和相关代码,让Safari用原生能力播放
- 确保
playsinline属性正确设置,避免自动全屏引发的播放异常 - 给视频添加
preload="auto",帮助浏览器提前加载流信息 - 把等待连接的提示文本改成中文(可选)
修改后的网页代码:
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Camera</title> <style> html, body { background: none !important; overflow: hidden; margin: 0; padding: 0; } video { margin: 0 auto; width: 100%; height: 100vh; } .video-container { width: 100%; height: 100vh } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script> function waitForPublishedStream(showMsg) { if (typeof showMsg === 'undefined') showMsg = true; $.get('https://myserver.com/CameraName/hls/stream.m3u8', function (response) { if (showMsg) { $('.container-loader .alert-warning').html('正在连接摄像头..') } if (response.state == 'success') { if (response.published) { document.location.reload(true) } else { setTimeout(function () { waitForPublishedStream(); }, 2000) } } }) } </script> </head> <body> <div class="video-container"> <video id="video" autoplay muted controls playsinline preload="auto" src="https://myserver.com/CameraName/hls/stream.m3u8"> <source src="https://myserver.com/CameraName/hls/stream.m3u8" type="application/vnd.apple.mpegurl"> </video> </div> <script> $(document).ready(function () { const video = document.getElementById('video'); video.addEventListener('loadedmetadata', function() { video.play(); }); }) </script> </body> </html>
4. 验证修复效果
- 重启Nginx服务:
sudo systemctl restart nginx - 停止原FFmpeg推流进程,用修改后的命令重新推流
- 用
mediastreamvalidator重新检测HLS流,确认没有404错误和时间戳异常 - 在iPhone上测试播放,检查是否能正常加载、时间戳显示正确
内容的提问来源于stack exchange,提问作者Yllier123
相关产品推荐
相关产品推荐

