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 $entries List of videos (if the object contains information about a playlist)
* @property-read array $rtmp_conn * @property-read array $rtmp_conn
* @property-read string|null $_type Object type (usually "playlist" or null) * @property-read string|null $_type Object type (usually "playlist" or null)
* @property-read stdClass $downloader_options
*/ */
class Video class Video
{ {
@ -62,6 +63,12 @@ class Video
*/ */
private $json; private $json;
/**
* URLs of the video files.
* @var array
*/
private $urls;
/** /**
* VideoDownload constructor. * VideoDownload constructor.
* *
@ -219,13 +226,16 @@ class Video
* */ * */
public function getUrl() 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])) { if (empty($urls[0])) {
throw new EmptyUrlException(_('youtube-dl returned an empty URL.')); 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\PasswordException;
use Alltube\PlaylistArchiveStream; use Alltube\PlaylistArchiveStream;
use Alltube\Video; use Alltube\Video;
use Alltube\YoutubeStream;
use Exception; use Exception;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
@ -172,8 +173,14 @@ class DownloadController extends BaseController
if ($stream->getStatusCode() == 206) { if ($stream->getStatusCode() == 206) {
$response = $response->withStatus(206); $response = $response->withStatus(206);
} }
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(); $body = $stream->getBody();
} }
}
if ($request->isGet()) { if ($request->isGet()) {
$response = $response->withBody($body); $response = $response->withBody($body);
} }