feat: Split Youtube downloads in smaller chunks

Fixes #217
This commit is contained in:
Pierre Rudloff 2019-04-22 20:20:04 +02:00
parent bba2087a55
commit 1a6ff90eac
4 changed files with 256 additions and 5 deletions

View file

@ -24,6 +24,7 @@ use Symfony\Component\Process\Process;
* @property-read array $entries List of videos (if the object contains information about a playlist)
* @property-read array $rtmp_conn
* @property-read string|null $_type Object type (usually "playlist" or null)
* @property-read stdClass $downloader_options
*/
class Video
{
@ -62,6 +63,12 @@ class Video
*/
private $json;
/**
* URLs of the video files.
* @var array
*/
private $urls;
/**
* VideoDownload constructor.
*
@ -219,13 +226,16 @@ class Video
* */
public function getUrl()
{
$urls = explode("\n", $this->getProp('get-url'));
// Cache the URLs.
if (!isset($this->urls)) {
$this->urls = explode("\n", $this->getProp('get-url'));
if (empty($urls[0])) {
throw new EmptyUrlException(_('youtube-dl returned an empty URL.'));
if (empty($urls[0])) {
throw new EmptyUrlException(_('youtube-dl returned an empty URL.'));
}
}
return $urls;
return $this->urls;
}
/**

View file

@ -0,0 +1,196 @@
<?php
/**
* YoutubeChunkStream class.
*/
namespace Alltube;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\StreamInterface;
/**
* This is a wrapper around GuzzleHttp\Psr7\Stream.
* It is required because Youtube HTTP responses are buggy if we try to read further than the end of the response.
*/
class YoutubeChunkStream implements StreamInterface
{
/**
* HTTP response containing the video chunk.
* @var Response
*/
private $response;
/**
* YoutubeChunkStream constructor.
*
* @param Response $response HTTP response containing the video chunk
*/
public function __construct(Response $response)
{
$this->response = $response;
}
/**
* Read data from the stream.
*
* @param int $length Read up to $length bytes from the object and return
*
* @return string
*/
public function read($length)
{
$size = $this->response->getHeader('Content-Length')[0];
if ($size - $this->tell() < $length) {
// Don't try to read further than the end of the stream.
$length = $size - $this->tell();
}
return $this->response->getBody()->read($length);
}
/**
* Reads all data from the stream into a string, from the beginning to end.
*/
public function __toString()
{
return (string) $this->response->getBody();
}
/**
* Closes the stream and any underlying resources.
*
* @return mixed
*/
public function close()
{
return $this->response->getBody()->close();
}
/**
* Separates any underlying resources from the stream.
*
* @return resource|null
*/
public function detach()
{
return $this->response->getBody()->detach();
}
/**
* Get the size of the stream if known.
*
* @return int|null
*/
public function getSize()
{
return $this->response->getBody()->getSize();
}
/**
* Returns the current position of the file read/write pointer.
*
* @return int
*/
public function tell()
{
return $this->response->getBody()->tell();
}
/**
* Returns true if the stream is at the end of the stream.
*
* @return bool
*/
public function eof()
{
return $this->response->getBody()->eof();
}
/**
* Returns whether or not the stream is seekable.
*
* @return bool
*/
public function isSeekable()
{
return $this->response->getBody()->isSeekable();
}
/**
* Seek to a position in the stream.
*
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
*
* @return mixed
*/
public function seek($offset, $whence = SEEK_SET)
{
return $this->response->getBody()->seek($offset, $whence);
}
/**
* Seek to the beginning of the stream.
*
* @return mixed
*/
public function rewind()
{
return $this->response->getBody()->rewind();
}
/**
* Returns whether or not the stream is writable.
*
* @return bool
*/
public function isWritable()
{
return $this->response->getBody()->isWritable();
}
/**
* Write data to the stream.
*
* @param string $string The string that is to be written
*
* @return mixed
*/
public function write($string)
{
return $this->response->getBody()->write($string);
}
/**
* Returns whether or not the stream is readable.
*
* @return bool
*/
public function isReadable()
{
return $this->response->getBody()->isReadable();
}
/**
* Returns the remaining contents in a string
*
* @return string
*/
public function getContents()
{
return $this->response->getBody()->getContents();
}
/**
* Get stream metadata as an associative array or retrieve a specific key.
*
* @param string $key Specific metadata to retrieve.
*
* @return array|mixed|null
*/
public function getMetadata($key = null)
{
return $this->response->getBody()->getMetadata($key);
}
}

38
classes/YoutubeStream.php Normal file
View file

@ -0,0 +1,38 @@
<?php
/**
* YoutubeStream class.
*/
namespace Alltube;
use GuzzleHttp\Psr7\AppendStream;
/**
* Stream that downloads a video in chunks.
* This is required because Youtube throttles the download speed on chunks larger than 10M.
*/
class YoutubeStream extends AppendStream
{
/**
* YoutubeStream constructor.
*
* @param Video $video Video to stream
*/
public function __construct(Video $video)
{
parent::__construct();
$stream = $video->getHttpResponse();
$fileSize = $stream->getHeader('Content-Length');
$curSize = 0;
while ($curSize < $fileSize[0]) {
$newSize = $curSize + $video->downloader_options->http_chunk_size;
if ($newSize > $fileSize[0]) {
$newSize = $fileSize[0] - 1;
}
$response = $video->getHttpResponse(['Range' => 'bytes='.$curSize.'-'.$newSize]);
$this->addStream(new YoutubeChunkStream($response));
$curSize = $newSize + 1;
}
}
}

View file

@ -10,6 +10,7 @@ use Alltube\EmptyUrlException;
use Alltube\PasswordException;
use Alltube\PlaylistArchiveStream;
use Alltube\Video;
use Alltube\YoutubeStream;
use Exception;
use Slim\Http\Request;
use Slim\Http\Response;
@ -172,7 +173,13 @@ class DownloadController extends BaseController
if ($stream->getStatusCode() == 206) {
$response = $response->withStatus(206);
}
$body = $stream->getBody();
if (isset($this->video->downloader_options->http_chunk_size)) {
// Workaround for Youtube throttling the download speed.
$body = new YoutubeStream($this->video);
} else {
$body = $stream->getBody();
}
}
if ($request->isGet()) {
$response = $response->withBody($body);