You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 02:18:35