繁星淼淼
正如我在评论中提到的,以下代码仅在一小部分 MP4 文件上进行了测试。它可能需要做更多的工作,但它确实满足了您的即时需求。它使用 exec() 生成一个单独的进程,在需要时(即在第一次请求时或 5 小时后)生成缓存文件。每个视频必须有自己的缓存文件夹,因为缓存的块简称为 1、2、3 等。请参阅代码中的其他注释。play.php - 这是用户从浏览器调用的脚本<?phpini_set('memory_limit', '1024M');set_time_limit(3600);$remoteFile = 'blabla.com/video_5GB.mp4';play($remoteFile);/** * @param string $url * * This will serve the video from the remote url */function playFromRemote($url){ ob_start(); $opts = array(); if(isset($_SERVER['HTTP_RANGE'])) { $opts['http']['header'] = "Range: ".$_SERVER['HTTP_RANGE']; } $opts['http']['method'] = "HEAD"; $conh = stream_context_create($opts); $opts['http']['method'] = "GET"; $cong = stream_context_create($opts); $out[] = file_get_contents($url, false, $conh); $out[] = $http_response_header; ob_end_clean(); $fh = fopen('response.log', 'a'); if($fh !== false) { fwrite($fh, print_r($http_response_header, true)."\n\n\n\n"); fclose($fh); } array_map("header", $http_response_header); readfile($url, false, $cong);}/** * @param string $cacheFolder Directory in which to find the cached chunk files * @param string $url * * This will serve the video from the cache, it uses a "completed.log" file which holds the byte ranges of each chunk * this makes it easier to locate the first chunk of a range request. The file is generated by the cache script */function playFromCache($cacheFolder, $url){ $bytesFrom = 0; $bytesTo = 0; if(isset($_SERVER['HTTP_RANGE'])) { //the client asked for a specific range, extract those from the http_range server var //can take the form "bytes=123-567" or just a from "bytes=123-" $matches = array(); if(preg_match('/^bytes=(\d+)-(\d+)?$/', $_SERVER['HTTP_RANGE'], $matches)) { $bytesFrom = intval($matches[1]); if(!empty($matches[2])) { $bytesTo = intval($matches[2]); } } } //completed log is a json_encoded file containing an array or byte ranges that directly //correspond with the chunk files generated by the cache script $log = json_decode(file_get_contents($cacheFolder.DIRECTORY_SEPARATOR.'completed.log')); $totalBytes = 0; $chunk = 0; foreach($log as $ind => $bytes) { //find the first chunk file we need to open if($bytes[0] <= $bytesFrom && $bytes[1] > $bytesFrom) { $chunk = $ind + 1; } //and while we are at it save the last byte range "to" which is the total number of bytes of all the chunk files $totalBytes = $bytes[1]; } if($bytesTo === 0) { if($totalBytes === 0) { //if we get here then something is wrong with the cache, revert to serving from the remote playFromRemote($url); return; } $bytesTo = $totalBytes - 1; } //calculate how many bytes will be returned in this request $contentLength = $bytesTo - $bytesFrom + 1; //send some headers - I have hardcoded MP4 here because that is all I have developed with //if you are using different video formats then testing and changes will no doubt be required header('Content-Type: video/mp4'); header('Content-Length: '.$contentLength); header('Accept-Ranges: bytes'); //Send a header so we can recognise that the content was indeed served by the cache header('X-Cached-Date: '.(date('Y-m-d H:i:s', filemtime($cacheFolder.DIRECTORY_SEPARATOR.'completed.log')))); if($bytesFrom > 0) { //We are sending back a range so it needs a header and the http response must be 206: Partial Content header(sprintf('content-range: bytes %s-%s/%s', $bytesFrom, $bytesTo, $totalBytes)); http_response_code(206); } $bytesSent = 0; while(is_file($cacheFolder.DIRECTORY_SEPARATOR.$chunk) && $bytesSent < $contentLength) { $cfh = fopen($cacheFolder.DIRECTORY_SEPARATOR.$chunk, 'rb'); if($cfh !== false) { //if we are fetching a range then we might need to seek the correct starting point in the first chunk we look at //this check will be performed on all chunks but only the first one should need seeking so no harm done if($log[$chunk - 1][0] < $bytesFrom) { fseek($cfh, $bytesFrom - $log[$chunk - 1][0]); } //read and send data until the end of the file or we have sent what was requested while(!feof($cfh) && $bytesSent < $contentLength) { $data = fread($cfh, 1024); //check we are not going to be sending too much back and if we are then truncate the data to the correct length if($bytesSent + strlen($data) > $contentLength) { $data = substr($data, 0, $contentLength - $bytesSent); } $bytesSent += strlen($data); echo $data; } fclose($cfh); } //move to the next chunk $chunk ++; }}function play($url){ //I have chosen a simple way to make a folder name, this can be improved any way you need //IMPORTANT: Each video must have its own cache folder $cacheFolder = sha1($url); if(!is_dir($cacheFolder)) { mkdir($cacheFolder, 0755, true); } //First check if we are currently in the process of generating the cache and so just play from remote if(is_file($cacheFolder.DIRECTORY_SEPARATOR.'caching.log')) { playFromRemote($url); } //Otherwise check if we have never completed the cache or it was completed 5 hours ago and if so spawn a process to generate the cache elseif(!is_file($cacheFolder.DIRECTORY_SEPARATOR.'completed.log') || filemtime($cacheFolder.DIRECTORY_SEPARATOR.'completed.log') + (5 * 60 * 60) < time()) { //fork the caching to a separate process - the & echo $! at the end causes the process to run as a background task //and print the process ID returning immediately //The cache script can be anywhere, pass the location to sprintf in the first position //A base64 encoded url is passed in as argument 1, sprintf second position $cmd = sprintf('php %scache.php %s & echo $!', __DIR__.DIRECTORY_SEPARATOR, base64_encode($url)); $pid = exec($cmd); //with that started we need to serve the request from the remote url playFromRemote($url); } else { //if we got this far then we have a completed cache so serve from there playFromCache($cacheFolder, $url); }}cache.php - 该脚本将由 play.php 通过 exec() 调用<?php//This script expects as argument 1 a base64 encoded urlif(count($argv)!==2){ die('Invalid Request!');}$url = base64_decode($argv[1]);//make sure to use the same method of obtaining the cache folder name as the main play script//or change the code to pass it in as an argument$cacheFolder = sha1($url);if(!is_dir($cacheFolder)){ die('Invalid Arguments!');}//double check it is not already runningif(is_file($cacheFolder.DIRECTORY_SEPARATOR.'caching.log')){ die('Already Running');}//create a file so we know this has started, the file will be removed at the end of the scriptfile_put_contents($cacheFolder.DIRECTORY_SEPARATOR.'caching.log', date('d/m/Y H:i:s'));//get rid of the old completed logif(is_file($cacheFolder.DIRECTORY_SEPARATOR.'completed.log')){ unlink($cacheFolder.DIRECTORY_SEPARATOR.'completed.log');}$bytesFrom = 0;$bytesWritten = 0;$totalBytes = 0;//this is the size of the chunk files, currently 10MB$maxSizeInBytes = 10 * 1024 * 1024;$chunk = 1;//open the url for binary reading and first chunk for binary writing$fh = fopen($url, 'rb');$cfh = fopen($cacheFolder.DIRECTORY_SEPARATOR.$chunk, 'wb');if($fh !== false && $cfh!==false){ $log = array(); while(!feof($fh)) { $data = fread($fh, 1024); fwrite($cfh, $data); $totalBytes += strlen($data); //use actual length here $bytesWritten += strlen($data); //if we are on or passed the chunk size then close the chunk and open a new one //keeping a log of the byte range of the chunk if($bytesWritten>=$maxSizeInBytes) { $log[$chunk-1] = array($bytesFrom,$totalBytes); $bytesFrom = $totalBytes; fclose($cfh); $chunk++; $bytesWritten = 0; $cfh = fopen($cacheFolder.DIRECTORY_SEPARATOR.$chunk, 'wb'); } } fclose($fh); $log[$chunk-1] = array($bytesFrom,$totalBytes); fclose($cfh); //write the completed log. This is a json encoded string of the chunk byte ranges and will be used //by the play script to quickly locate the starting chunk of a range request file_put_contents($cacheFolder.DIRECTORY_SEPARATOR.'completed.log', json_encode($log)); //finally remove the caching log so the play script doesn't think the process is still running unlink($cacheFolder.DIRECTORY_SEPARATOR.'caching.log');}