Skip to content

Instantly share code, notes, and snippets.

@Bartvelp
Created May 10, 2018 17:13
Show Gist options
  • Save Bartvelp/04219df1314da36787d15f3d37197233 to your computer and use it in GitHub Desktop.
Save Bartvelp/04219df1314da36787d15f3d37197233 to your computer and use it in GitHub Desktop.
PHP seekable stream external video with url and range header support
<?php
/*
Hi!
So this gist is not a very good implementation, it works great in chrome but the seeking is sort of glitched in firefox.
But it does the job and I hope it can help out!
(Please let me know how to improve this)
*/
ini_set('max_execution_time', 0);
$url = "http://mirrors.standaloneinstaller.com/video-sample/jellyfish-25-mbps-hd-hevc.m4v"; //just some sample url, please dont overload their servers
$stream = new VideoStream($url);
$stream->start();
class VideoStream
{
private $url = "";
private $sizeReq = 10 * 1000000; //get 10MB per range request
private $curlStream = NULL;
private $followRedirects = true; //maybe disable if not trusted
private $start = -1;
private $end = -1;
private $size = -1;
function __construct($URL)
{
$this->url = $URL;
$this->setSize(); //set the size and check if the video is available at the same time
}
/**
* Set proper header to serve the video content
*/
private function setHeader()
{
ob_get_clean();
header("Content-Type: video/mp4");
header("Cache-Control: max-age=2592000, public");
header("Expires: ".gmdate('D, d M Y H:i:s', time()+2592000) . ' GMT');
$this->start = 0;
$this->end = $this->size;
header("Accept-Ranges: 0-" . ($this->size + 1));
if (isset($_SERVER['HTTP_RANGE'])) {
$ranges = explode("-", explode("=", $_SERVER['HTTP_RANGE'])[1]); //explode twice to get the ranges in an array
$this->start = $ranges[0]; //starting range
if($ranges[1] === ""){ //no end specified, set it ourselves
$this->end = $ranges[0] + $this->sizeReq;
if($this->end > $this->size) $this->end = $this->size; //set it to the end if the request would be too big
} else { //set it to the requested length
$this->end = $ranges[1];
}
$length = $this->end - $this->start + 1;
header('HTTP/1.1 206 Partial Content');
header("Content-Length: ".$length);
header("Content-Range: bytes $this->start-$this->end/" . ($this->size + 1));
}
else
{
header("Content-Length: ". ($this->end - $this->start + 1));
}
}
/**
* close curretly opened stream
*/
private function end()
{
curl_close($this->curlStream);
exit;
}
/**
* perform the streaming of calculated range
*/
private function stream()
{
$splitter = new SplitCurlByLines();
$this->curlStream = curl_init();
curl_setopt($this->curlStream, CURLOPT_URL, $this->url);
curl_setopt($this->curlStream, CURLOPT_WRITEFUNCTION, array($splitter, 'curlCallback')); //we need this so we "live"stream our results
curl_setopt($this->curlStream, CURLOPT_ENCODING, 'gzip, deflate');
$headers = array();
$headers[] = "Pragma: no-cache";
$headers[] = "Dnt: 1";
$headers[] = "Accept-Encoding: identity;q=1, *;q=0";
$headers[] = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36";
$headers[] = "Accept: */*";
$headers[] = "Cache-Control: no-cache";
$headers[] = "Connection: keep-alive";
$headers[] = "Range: bytes={$this->start}-{$this->end}";
curl_setopt($this->curlStream, CURLOPT_HTTPHEADER, $headers);
curl_exec($this->curlStream);
$splitter->processLine($splitter->currentLine);
}
/**
* get the size of the external video
*/
private function setSize()
{
$headers = get_headers($this->url, 1);
if(isset($headers["Location"]) && $this->followRedirects){ //following the rabit hole, might be risky
$this->url = $headers["Location"];
$this->setSize();
return;
}
if(strpos($headers[0], '200 OK') === false){ //URL is not OK
throw new Exception("URL not valid, not a 200 reponse code");
}
if(!isset($headers["Content-Length"])){
throw new Exception("URL not valid, could not find the video size");
}
$this->size = (int) $headers["Content-Length"];
}
/**
* Start streaming video content
*/
function start()
{
$this->setHeader();
$this->stream();
$this->end();
}
}
class SplitCurlByLines
{
public function curlCallback($curl, $data) {
$this->currentLine .= $data;
$lines = explode("\n", $this->currentLine);
// The last line could be unfinished. We should not
// proccess it yet.
$numLines = count($lines) - 1;
$this->currentLine = $lines[$numLines]; // Save for the next callback.
for ($i = 0; $i < $numLines; ++$i) {
$this->processLine($lines[$i]); // Do whatever you want
++$this->totalLineCount; // Statistics.
$this->totalLength += strlen($lines[$i]) + 1;
}
return strlen($data); // Ask curl for more data (!= value will stop).
}
public function processLine($str) {
// Do what ever you want (split CSV, ...).
echo $str . "\n";
}
public $currentLine = '';
public $totalLineCount = 0;
public $totalLength = 0;
}
@mzughbor
Copy link

mzughbor commented Jun 2, 2023

Hi, does this work with a .m3u8 file, that generates fragments .ts files? so I have the link ending with .ts or the root one with .m3u8 can I open them through HTTPs, my problem with the Wordpress site is, I can't use hls.js to open them because of the Https and they stream from HTTP not with 'https' so what is the solution here?

@kclauhk
Copy link

kclauhk commented Jun 19, 2023

It works in Firefox if line 43 is replaced with header("Accept-Ranges: bytes");

info. about Accept-Ranges HTTP response header:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges

@mirco1000
Copy link

thank you, but the seeking not working in vlc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment