• 欢迎来到我的博客
  • [email protected]

PHP中转资源服务器文件(包括MP4)

学习笔记 tianlan 9个月前 (08-10) 496次浏览 0个评论 扫描二维码
文章目录[隐藏]

业务说明

浏览器请求PHP,PHP再去请求资源服务器的文件

核心思路

PHP 与 资源服务器 之间 stream_socket 传输数据。

1.PHP 自己拼接 http 请求头,其中把 浏览器 请求 PHP 的 headers 也拼接上了

2.PHP 发送 【浏览器请求PHP的请求体】 给 资源服务器

3.PHP 从 资源服务器 中读取 响应头 并且返回给 浏览器

4.循环从 stream_socket 连接中读取数据,并且返回到浏览器

实现代码

本篇博文是参考 @老虎会游泳 的代码而成,下面的代码也是他写的。


<?php
/**
 * 老虎写的 文件中间件
 */
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
@ignore_user_abort(0);
@set_time_limit(0);

$url = trim($_GET['url']);
$play = (bool)$_GET['play'];

if ($play && !empty($url)) {
    $url = urlencode($url);
    header('Content-Type: text/html; charset=utf-8');
    echo <<<EOF
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>视频中转</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=1">
    <style>
        html, body, video {
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
    <video src="?url=$url" controls>
</body>
</html>
EOF;
    return;
}

if(!empty($url)) {
//    这行可以去掉
//    header("Content-Disposition: attachment; filename=".basename($url));//设置下载文件名


    /**
     * 阅读备注
     * 解析浏览器请求,获取其中的 headers 等
     */
    $urlArgs = parse_url($url);

    $host = $urlArgs['host'];
    $requestUri = $urlArgs['path'];

    if (isset($urlArgs['query'])) {
        $requestUri .= '?' . $urlArgs['query'];
    }

    $protocol = ($urlArgs['scheme'] == 'http') ? 'tcp' : 'ssl';
    $port = $urlArgs['port'];

    if (empty($port)) {
        $port = ($protocol == 'tcp') ? 80 : 443;
    }

    $header = "{$_SERVER['REQUEST_METHOD']} {$requestUri} HTTP/1.1\r\nHost: {$host}\r\nUser-Agent:  Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5 Quark/2.4.4.993\r\n";

    unset($_SERVER['HTTP_HOST']);
    $_SERVER['HTTP_CONNECTION'] = 'close';

    if ($_SERVER['CONTENT_TYPE']) {
        $_SERVER['HTTP_CONTENT_TYPE'] = $_SERVER['CONTENT_TYPE'];
    }

    foreach ($_SERVER as $x => $v) {
        if (substr($x, 0, 5) !== 'HTTP_') {
            continue;
        }
        $x = strtr(ucwords(strtr(strtolower(substr($x, 5)), '_', ' ')), ' ', '-');
        $header .= "{$x}: {$v}\r\n";
    }

    $header .= "\r\n";

    $remote = "{$protocol}://{$host}:{$port}";

    /**
     * 阅读备注
     * 请求资源服务器,并且将之前解析到的浏览器请求 headers 发送
     */

    $context = stream_context_create();
    stream_context_set_option($context, 'ssl', 'verify_host', false);

    $p = stream_socket_client($remote, $err, $errstr, 60, STREAM_CLIENT_CONNECT, $context);

    if (!$p) {
        exit;
    }

    fwrite($p, $header);

    /**
     * 阅读备注
     * 解析浏览器请求,获取 请求体,并且发送到资源服务器
     */
    $pp = fopen('php://input', 'rb');

    while ($pp && !feof($pp)) {
        fwrite($p, fread($pp, 1024));
    }

    fclose($pp);

    /**
     * 阅读备注
     * 解析浏资源服务器响应
     */

    $header = '';

    $headerParsed = false; //标记,是否读取过 headers
    $len = false;
    $off = 0;

    while (!feof($p)) {
        if (!$headerParsed) {
            /**
             * 阅读备注
             * 解析浏资源服务器响应的 headers
             */
            $header .= fread($p, 1024);

            if (($i = strpos($header, "\r\n\r\n")) !== false) {

                $headerParsed = true;
                $resourceResponseBodyPart = substr($header, $i + 4);
                $header = substr($header, 0, $i);
                $header = explode("\r\n", $header);
                foreach ($header as $m) {
                    if (preg_match('!^\\s*content-length\\s*:!is', $m)) {
                        $len = trim(substr($m, 15));
                    }
                    header($m);
                }
                $off = strlen($resourceResponseBodyPart);
                echo $resourceResponseBodyPart;
                flush();
            }
        } else {
            if ($len !== false && $off >= $len) {
                break;
            }
            $resourceResponseBodyPart = fread($p, 1024);
            $off += strlen($resourceResponseBodyPart);
            echo $resourceResponseBodyPart;
            flush();
        }
    }

    fclose($p);
    return;
}

header('Content-Type: text/html; charset=utf-8');
echo <<<EOF
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>文件中转</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=1">
</head>
<body>
    <h1>文件中转</h1>
    <form action="" method="get">
        文件地址:<input name="url">
        <input type="submit" value="下载">
        <input type="submit" value="播放" name="play">
    </form>
</body>
</html>
EOF;


回首

之前我尝试用大文件下载代码来中转MP4,但是不行。

原因:dplayer加载MP4时,会分段发送多个请求(盲猜是dashjs的功能),通过请求头Range实现(如 Range: bytes=5754914-),这个请求头指定要获取MP4文件的范围。

而大文件下载代码是在一个请求中返回文件所有内容,并没有对分段请求做处理(没有根据 Range 做处理)。

所以当浏览器分段请求MP4时,PHP一直返回MP4文件开头的内容。

所以我百度找了专门针对中转MP4文件的代码:

function GetMp4File($file) {
    $header_array = get_headers($file, true);
    $size = $header_array['Content-Length'];
    header("Content-type: video/mp4");
    header("Accept-Ranges: bytes");
    if(isset($_SERVER['HTTP_RANGE'])){
        header("HTTP/1.1 206 Partial Content");
        list($name, $range) = explode("=", $_SERVER['HTTP_RANGE']);
        list($begin, $end) =explode("-", $range);
        if($end == 0){
            $end = $size - 1;
        }
    }else {
        $begin = 0; $end = $size - 1;
    }
    header("Content-Length: " . ($end - $begin + 1));
    header("Content-Disposition: filename=".basename($file));
    header("Content-Range: bytes ".$begin."-".$end."/".$size);
    $fp = fopen($file, 'rb');
    fseek($fp, $begin);
    while(!feof($fp)) {
        $p = min(1024, $end - $begin + 1);
        $begin += $p;
        echo fread($fp, $p);
    }
    fclose($fp);
}

这段代码相比大文件下载代码,支持分段请求(使用 fseek() 对 range 进行了处理),而且还设置的 http status code 206。

这当MP4在PHP本地时没什么问题,但是如果MP4是远程文件,不在PHP本地时,那么 fseek() 就无法成功运行,也就是说,此时分段处理失败,效果和大文件下载代码差不多。


天蓝, 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:PHP中转资源服务器文件(包括MP4)
喜欢 (0)
[[email protected]]
分享 (0)

您必须 登录 才能发表评论!