PHP passthru转发HLS直播流中断、子进程无法关闭问题
方案评估
你当前用PHP拉起进程做流透传的方案,在仅能使用PHP环境、无法安装额外服务的限制下是可落地的,但绝非最优解——最优方案是在服务器部署独立的转推服务,不需要每次客户端拉流才临时启动进程,也不会有进程残留、随机断流的问题。受限于你的权限没法装额外软件,只要把现有代码的进程管理、连接检测逻辑补全,就能解决你遇到的两个问题。
问题根因
30-50秒随机断流
- 你仅通过
X-Accel-Buffering: no关闭了Nginx层缓冲,没有关闭PHP-FPM的fastcgi缓冲,加上默认的PHP-FPM请求超时配置,会在连接无有效数据传输几十秒后主动断开PHP进程的输出通道,管道写满后ffmpeg会直接异常退出。 - 用
passthru()直接执行shell命令时,PHP没有主动做客户端连接状态检测,也没有做流输出的主动flush,很容易触发缓冲超时。
子进程后台驻留
passthru()会先启动一个shell进程,再由shell fork出streamlink、ffmpeg两个子进程。当PHP进程退出时,只会终止直接拉起的shell父进程,两个子进程会被系统init进程领养,继续在后台运行。- 你将ffmpeg的stderr重定向到日志文件,子进程的输入输出没有完全和PHP进程的生命周期绑定,无法感知到前端连接断开、管道关闭的事件,自然不会自动退出。
修复实现
核心改动点:
- 用
proc_open替代passthru启动进程,拿到进程组ID,后续可以一次性杀掉所有关联子进程,不会残留 - 增加客户端连接状态轮询,一旦检测到客户端断开,立刻终止整个进程组
- 关闭所有层级的缓冲,非阻塞读取流数据主动推送给客户端,避免缓冲触发超时
- 去掉多余的streamlink层,ffmpeg原生支持拉取HLS流,减少管道层级降低故障概率
修复后的完整代码:
<?php ini_set('output_buffering', '0'); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); set_time_limit(0); ob_implicit_flush(); // 忽略管道断开信号,避免进程意外崩溃 pcntl_signal(SIGPIPE, SIG_IGN); include './core/functions.php'; $PROCESS_URL = 'HTTP STREM URL GOES HERE'; $ENCODING_TYPE = 'mpegts'; $LOG_PATH = LOG_DIRECTORY . '/' . MD5($PROCESS_URL) . '_ffmpeg.txt'; // 直接用ffmpeg拉源流,去掉streamlink减少进程层级,增加自动重连参数 $CMD = FFMPEG . ' -threads 0' . ' -re' . ' -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 3' . ' -probesize 9000000'. ' -analyzeduration 9000000'. ' -i ' . escapeshellarg($PROCESS_URL) . ' -c:v copy -c:a copy' . ' -tune zerolatency' . ' -preset ultrafast' . ' -fflags +genpts' . ' -mpegts_flags initial_discontinuity' . ' -f ' . $ENCODING_TYPE . ' pipe:1 2>' . escapeshellarg($LOG_PATH); header('Content-type: application/octet-stream'); header('X-Accel-Buffering: no'); header('Cache-Control: no-cache'); // 启动进程,拿到进程句柄 $descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'] ]; $process = proc_open($CMD, $descriptors, $pipes); if (!is_resource($process)) { exit("进程启动失败"); } // 获取进程组ID,用于后续批量杀进程 $processStatus = proc_get_status($process); $pgid = posix_getpgid($processStatus['pid']); // 设置输出流为非阻塞模式 stream_set_blocking($pipes[1], 0); // 循环读取流数据推送给客户端 while (!feof($pipes[1])) { // 检测客户端连接状态,断开就直接退出循环 if (connection_aborted()) { break; } $chunk = fread($pipes[1], 8192); if ($chunk !== false && strlen($chunk) > 0) { echo $chunk; ob_flush(); flush(); } // 短暂休眠避免CPU占用过高 usleep(10000); } // 清理资源,终止所有关联进程 fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); if ($pgid) { // 先给进程组发正常终止信号 posix_kill(-$pgid, SIGTERM); // 等待2秒进程未退出则强制杀死 sleep(2); posix_kill(-$pgid, SIGKILL); } proc_close($process); ?>
备注:代码保留了全大写变量的命名风格,符合你的阅读习惯。
额外注意事项
- 如果你的PHP环境禁用了
pcntl、posix、proc_open相关函数,可以在命令前加exec前缀启动进程,启动时将PID写入临时文件,客户端断开时直接根据PID杀进程即可,逻辑和上面的实现一致。 - 不要在命令里用多层管道,每多一层管道就多一个进程残留的风险,能让ffmpeg直接处理的逻辑就不要加额外的中间进程。
内容的提问来源于stack exchange,提问作者Bluchip Studio
相关产品推荐
相关产品推荐

