From e6bbe54474b054405199830fea8dd60da6b655b2 Mon Sep 17 00:00:00 2001 From: Pierre Rudloff Date: Tue, 25 Apr 2017 00:40:24 +0200 Subject: [PATCH] New remux feature (fixes #103) --- classes/Config.php | 7 ++++ classes/VideoDownload.php | 64 +++++++++++++++++++++++++++------ controllers/FrontController.php | 29 ++++++++++++--- templates/video.tpl | 5 +++ tests/FrontControllerTest.php | 39 ++++++++++++++++++++ tests/VideoDownloadTest.php | 41 +++++++++++++++++++-- 6 files changed, 169 insertions(+), 16 deletions(-) diff --git a/classes/Config.php b/classes/Config.php index d9ffcf0..4ee07c1 100644 --- a/classes/Config.php +++ b/classes/Config.php @@ -75,6 +75,13 @@ class Config */ public $stream = false; + /** + * Allow to remux video + audio? + * + * @var bool + */ + public $remux = false; + /** * YAML config file path. * diff --git a/classes/VideoDownload.php b/classes/VideoDownload.php index 41b0cc0..a6b8873 100644 --- a/classes/VideoDownload.php +++ b/classes/VideoDownload.php @@ -119,15 +119,19 @@ class VideoDownload /** * Get URL of video from URL of page. * + * It generally returns only one URL. + * But it can return two URLs when multiple formats are specified + * (eg. bestvideo+bestaudio). + * * @param string $url URL of page * @param string $format Format to use for the video * @param string $password Video password * - * @return string URL of video + * @return array URLs of video * */ public function getURL($url, $format = null, $password = null) { - return $this->getProp($url, $format, 'get-url', $password); + return explode(PHP_EOL, $this->getProp($url, $format, 'get-url', $password)); } /** @@ -144,6 +148,28 @@ class VideoDownload return trim($this->getProp($url, $format, 'get-filename', $password)); } + /** + * Get filename of video with the specified extension + * + * @param string $extension New file extension + * @param string $url URL of page + * @param string $format Format to use for the video + * @param string $password Video password + * + * @return string Filename of extracted video with specified extension + */ + public function getFileNameWithExtension($extension, $url, $format = null, $password = null) + { + return html_entity_decode( + pathinfo( + $this->getFilename($url, $format, $password), + PATHINFO_FILENAME + ).'.'.$extension, + ENT_COMPAT, + 'ISO-8859-1' + ); + } + /** * Get filename of audio from URL of page. * @@ -155,14 +181,7 @@ class VideoDownload * */ public function getAudioFilename($url, $format = null, $password = null) { - return html_entity_decode( - pathinfo( - $this->getFilename($url, $format, $password), - PATHINFO_FILENAME - ).'.mp3', - ENT_COMPAT, - 'ISO-8859-1' - ); + return $this->getFileNameWithExtension('mp3', $url, $format, $password); } /** @@ -307,6 +326,31 @@ class VideoDownload return popen($procBuilder->getProcess()->getCommandLine(), 'r'); } + /** + * Get an avconv stream to remux audio and video. + * + * @param array $urls URLs of the video ($urls[0]) and audio ($urls[1]) files + * + * @return resource popen stream + */ + public function getRemuxStream(array $urls) + { + $procBuilder = ProcessBuilder::create( + [ + $this->config->avconv, + '-v', 'quiet', + '-i', $urls[0], + '-i', $urls[1], + '-c', 'copy', + '-map', '0:v:0 ', + '-map', '1:a:0', + '-f', 'matroska', + 'pipe:1', + ] + ); + return popen($procBuilder->getProcess()->getCommandLine(), 'r'); + } + /** * Get video stream from an RTMP video. * diff --git a/controllers/FrontController.php b/controllers/FrontController.php index 55011be..98d6937 100644 --- a/controllers/FrontController.php +++ b/controllers/FrontController.php @@ -177,9 +177,9 @@ class FrontController if ($this->config->stream) { return $this->getStream($params['url'], 'mp3', $response, $request, $password); } else { - $url = $this->download->getURL($params['url'], 'mp3[protocol^=http]', $password); + $urls = $this->download->getURL($params['url'], 'mp3[protocol^=http]', $password); - return $response->withRedirect($url); + return $response->withRedirect($urls[0]); } } catch (PasswordException $e) { return $this->password($request, $response); @@ -234,6 +234,7 @@ class FrontController 'config' => $this->config, 'canonical' => $this->getCanonicalUrl($request), 'uglyUrls' => $this->config->uglyUrls, + 'remux' => $this->config->remux, ] ); @@ -358,13 +359,33 @@ class FrontController $this->sessionSegment->getFlash($params['url']) ); } else { - $url = $this->download->getURL( + $urls = $this->download->getURL( $params['url'], $format, $this->sessionSegment->getFlash($params['url']) ); + if (count($urls) > 1) { + if (!$this->config->remux) { + throw new \Exception('You need to enable remux mode to merge two formats.'); + } + $stream = $this->download->getRemuxStream($urls); + $response = $response->withHeader('Content-Type', 'video/x-matroska'); + if ($request->isGet()) { + $response = $response->withBody(new Stream($stream)); + } - return $response->withRedirect($url); + return $response->withHeader('Content-Disposition', 'attachment; filename="'.pathinfo( + $this->download->getFileNameWithExtension( + 'mkv', + $params['url'], + $format, + $this->sessionSegment->getFlash($params['url']) + ), + PATHINFO_FILENAME + ).'.mkv"'); + } else { + return $response->withRedirect($urls[0]); + } } } catch (PasswordException $e) { return $response->withRedirect( diff --git a/templates/video.tpl b/templates/video.tpl index 8872b1e..64ca555 100644 --- a/templates/video.tpl +++ b/templates/video.tpl @@ -34,6 +34,11 @@ Best ({$video->ext}) {/strip} + {if $remux} + + {/if} diff --git a/tests/FrontControllerTest.php b/tests/FrontControllerTest.php index 2cc3a82..c964463 100644 --- a/tests/FrontControllerTest.php +++ b/tests/FrontControllerTest.php @@ -361,6 +361,45 @@ class FrontControllerTest extends \PHPUnit_Framework_TestCase $this->assertTrue($result->isOk()); } + /** + * Test the redirect() function with a remuxed video. + * + * @return void + */ + public function testRedirectWithRemux() + { + $controller = new FrontController($this->container, new Config(['remux'=>true])); + $result = $controller->redirect( + $this->request->withQueryParams( + [ + 'url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU', + 'format'=>'bestvideo+bestaudio' + ] + ), + $this->response + ); + $this->assertTrue($result->isOk()); + } + + /** + * Test the redirect() function with a remuxed video but remux disabled. + * + * @return void + */ + public function testRedirectWithRemuxDisabled() + { + $result = $this->controller->redirect( + $this->request->withQueryParams( + [ + 'url'=>'https://www.youtube.com/watch?v=M7IpKCZ47pU', + 'format'=>'bestvideo+bestaudio' + ] + ), + $this->response + ); + $this->assertTrue($result->isServerError()); + } + /** * Test the redirect() function with a missing password. * diff --git a/tests/VideoDownloadTest.php b/tests/VideoDownloadTest.php index bf15cfe..eeb85c2 100644 --- a/tests/VideoDownloadTest.php +++ b/tests/VideoDownloadTest.php @@ -88,7 +88,7 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase public function testGetURL($url, $format, $filename, $extension, $domain) { $videoURL = $this->download->getURL($url, $format); - $this->assertContains($domain, $videoURL); + $this->assertContains($domain, $videoURL[0]); } /** @@ -98,7 +98,8 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase */ public function testGetURLWithPassword() { - $this->assertContains('vimeocdn.com', $this->download->getURL('http://vimeo.com/68375962', null, 'youtube-dl')); + $videoURL = $this->download->getURL('http://vimeo.com/68375962', null, 'youtube-dl'); + $this->assertContains('vimeocdn.com', $videoURL[0]); } /** @@ -184,6 +185,23 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase * * @return array[] */ + public function remuxUrlProvider() + { + return [ + [ + 'https://www.youtube.com/watch?v=M7IpKCZ47pU', 'bestvideo+bestaudio', + "It's Not Me, It's You - Hearts Under Fire-M7IpKCZ47pU", + 'mp4', + 'googlevideo.com', + ], + ]; + } + + /** + * Provides URLs for remux tests. + * + * @return array[] + */ public function m3uUrlProvider() { return [ @@ -390,6 +408,25 @@ class VideoDownloadTest extends \PHPUnit_Framework_TestCase $this->assertFalse(feof($stream)); } + /** + * Test getRemuxStream function. + * + * @param string $url URL + * @param string $format Format + * + * @return void + * @dataProvider remuxUrlProvider + */ + public function testGetRemuxStream($url, $format) + { + $urls = $this->download->getURL($url, $format); + if (count($urls) > 1) { + $stream = $this->download->getRemuxStream($urls); + $this->assertInternalType('resource', $stream); + $this->assertFalse(feof($stream)); + } + } + /** * Test getRtmpStream function. *