From ec4db393b3cb451643b53187cf7618860ff1229e Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 9 Mar 2023 12:40:02 -0800 Subject: [PATCH] vod: switch to new config pattern --- lib/Command/VideoSetup.php | 279 ++++++++++++++++++++++++----- lib/Controller/PageController.php | 2 +- lib/Controller/VideoController.php | 208 +++++++++++++-------- 3 files changed, 370 insertions(+), 119 deletions(-) diff --git a/lib/Command/VideoSetup.php b/lib/Command/VideoSetup.php index 777f1f1b..32802168 100644 --- a/lib/Command/VideoSetup.php +++ b/lib/Command/VideoSetup.php @@ -32,6 +32,8 @@ class VideoSetup extends Command { protected IConfig $config; protected OutputInterface $output; + protected string $sampleFile; + protected string $logFile; public function __construct( IConfig $config @@ -50,16 +52,18 @@ class VideoSetup extends Command protected function execute(InputInterface $input, OutputInterface $output): int { + $this->output = $output; + // Preset executables - $ffmpegPath = $this->config->getSystemValue('memories.ffmpeg_path', 'ffmpeg'); + $ffmpegPath = $this->config->getSystemValue('memories.vod.ffmpeg', 'ffmpeg'); if ('ffmpeg' === $ffmpegPath) { $ffmpegPath = trim(shell_exec('which ffmpeg') ?: 'ffmpeg'); - $this->config->setSystemValue('memories.ffmpeg_path', $ffmpegPath); + $this->config->setSystemValue('memories.vod.ffmpeg', $ffmpegPath); } - $ffprobePath = $this->config->getSystemValue('memories.ffprobe_path', 'ffprobe'); + $ffprobePath = $this->config->getSystemValue('memories.vod.ffprobe', 'ffprobe'); if ('ffprobe' === $ffprobePath) { $ffprobePath = trim(shell_exec('which ffprobe') ?: 'ffprobe'); - $this->config->setSystemValue('memories.ffprobe_path', $ffprobePath); + $this->config->setSystemValue('memories.vod.ffprobe', $ffprobePath); } // Get ffmpeg version @@ -83,12 +87,12 @@ class VideoSetup extends Command if (null === $ffmpeg || null === $ffprobe) { $output->writeln('ffmpeg and ffprobe are required for video transcoding'); - return $this->suggestDisable($output); + return $this->suggestDisable(); } // Check go-vod binary $output->writeln('Checking for go-vod binary'); - $goVodPath = $this->config->getSystemValue('memories.transcoder', false); + $goVodPath = $this->config->getSystemValue('memories.vod.path', false); if (!\is_string($goVodPath) || !file_exists($goVodPath)) { // Detect architecture @@ -97,9 +101,9 @@ class VideoSetup extends Command if (!$goVodPath) { $output->writeln('Compatible go-vod binary not found'); - $this->suggestGoVod($output); + $this->suggestGoVod(); - return $this->suggestDisable($output); + return $this->suggestDisable(); } } @@ -109,9 +113,9 @@ class VideoSetup extends Command $goVod = shell_exec($goVodPath.' test'); if (!$goVod || false === strpos($goVod, 'test successful')) { $output->writeln('go-vod could not be run'); - $this->suggestGoVod($output); + $this->suggestGoVod(); - return $this->suggestDisable($output); + return $this->suggestDisable(); } // Go transcode is working. Yay! @@ -127,70 +131,251 @@ class VideoSetup extends Command $output->writeln('Do you want to enable transcoding and HLS? [Y/n]'); if ('n' === trim(fgets(fopen('php://stdin', 'r')))) { - $this->config->setSystemValue('memories.no_transcode', true); + $this->config->setSystemValue('memories.vod.disable', true); $output->writeln('Transcoding and HLS are now disabled'); - $this->killGoVod($output, $goVodPath); + $this->killGoVod($goVodPath); return 0; } - $this->config->setSystemValue('memories.transcoder', $goVodPath); - $this->config->setSystemValue('memories.no_transcode', false); - $output->writeln('Transcoding and HLS are now enabled! Monitor the output at /tmp/go-vod.log for any errors'); - $output->writeln('You should restart the server for changes to take effect'); + $this->config->setSystemValue('memories.vod.path', $goVodPath); + $this->config->setSystemValue('memories.vod.disable', false); - // Check for VAAPI - $output->writeln("\nChecking for VAAPI (/dev/dri/renderD128)"); - if (file_exists('/dev/dri/renderD128')) { - $output->writeln('VAAPI is available. Do you want to enable it? [Y/n]'); + // Feature detection + $this->detectFeatures(); - if ('n' === trim(fgets(fopen('php://stdin', 'r')))) { - $this->config->setSystemValue('memories.qsv', false); - $output->writeln('VAAPI is now disabled'); - } else { - $output->writeln("\nVAAPI is now enabled. You may still need to install the Intel Media Driver"); - $output->writeln('and ensure proper permissions for /dev/dri/renderD128.'); - $output->writeln('See the documentation for more details.'); - $this->config->setSystemValue('memories.qsv', true); - } - } else { - $output->writeln('VAAPI is not available'); - $this->config->setSystemValue('memories.qsv', false); - } + // Success + $output->writeln("\nTranscoding and HLS are now enabled! Monitor the log file for any errors"); + $output->writeln('You should restart the server for changes to take effect'); - $this->killGoVod($output, $goVodPath); + $this->killGoVod(); return 0; } - protected function suggestGoVod(OutputInterface $output): void + protected function suggestGoVod(): void { - $output->writeln('You may build go-vod from source'); - $output->writeln('It can be downloaded from https://github.com/pulsejet/go-vod'); - $output->writeln('Once built, point the path to the binary in the config for `memories.transcoder`'); + $this->output->writeln('You may build go-vod from source'); + $this->output->writeln('It can be downloaded from https://github.com/pulsejet/go-vod'); + $this->output->writeln('Once built, point the path to the binary in the config for `memories.vod.path`'); } - protected function suggestDisable(OutputInterface $output) + protected function suggestDisable() { - $output->writeln('Without transcoding, video playback may be slow and limited'); - $output->writeln('Do you want to disable transcoding and HLS streaming? [y/N]'); + $this->output->writeln('Without transcoding, video playback may be slow and limited'); + $this->output->writeln('Do you want to disable transcoding and HLS streaming? [y/N]'); if ('y' !== trim(fgets(fopen('php://stdin', 'r')))) { - $output->writeln('Aborting'); + $this->output->writeln('Aborting'); return 1; } - $this->config->setSystemValue('memories.no_transcode', true); - $output->writeln('Transcoding and HLS are now disabled'); - $output->writeln('You should restart the server for changes to take effect'); + $this->config->setSystemValue('memories.vod.disable', true); + $this->output->writeln('Transcoding and HLS are now disabled'); + $this->output->writeln('You should restart the server for changes to take effect'); return 0; } - protected function killGoVod(OutputInterface $output, string $path): void + protected function detectFeatures() { - $output->writeln("\nKilling any existing go-vod processes"); + $this->output->writeln("\nStarting ffmpeg feature detection"); + $this->output->writeln('This may take a while. Please be patient'); + + try { + // Download test file + $this->output->write("\nDownloading test video file ... "); + $this->sampleFile = $this->downloadSampleFile(); + if (!file_exists($this->sampleFile)) { + $this->output->writeln('FAIL'); + $this->output->writeln('Could not download sample file'); + $this->output->writeln('Failed to perform feature detection'); + + return; + } + $this->output->writeln('OK'); + + // Start go-vod + if (!$this->startGoVod()) { + return; + } + + $this->checkCPU(); + $this->checkVAAPI(); + } finally { + if (file_exists($this->sampleFile)) { + unlink($this->sampleFile); + } + } + + $this->output->writeln("\nFeature detection completed"); + } + + protected function checkCPU() + { + $this->output->writeln(''); + $this->testResult('CPU'); + } + + protected function checkVAAPI() + { + // Check for VAAPI + $this->output->write("\nChecking for VAAPI acceleration (/dev/dri/renderD128) ... "); + if (!file_exists('/dev/dri/renderD128')) { + $this->output->writeln('NOT FOUND'); + $this->config->setSystemValue('memories.vod.vaapi', false); + + return; + } + $this->output->writeln('OK'); + + // Check permissions + $this->output->write('Checking for permissions on /dev/dri/renderD128 ... '); + if (!is_readable('/dev/dri/renderD128')) { + $this->output->writeln('NO'); + $this->output->writeln('Current user does not have read permissions on /dev/dri/renderD128'); + $this->output->writeln('VAAPI will not work. You may need to add your user to the video/render groups'); + $this->config->setSystemValue('memories.vod.vaapi', false); + + return; + } + $this->output->writeln('OK'); + + // Try enabling VAAPI + $this->config->setSystemValue('memories.vod.vaapi', true); + $basic = $this->testResult('VAAPI'); + + // Try with low_power + $this->config->setSystemValue('memories.vod.vaapi.low_power', true); + $lowPower = $this->testResult('VAAPI (low_power)'); + if (!$lowPower) { + $this->config->deleteSystemValue('memories.vod.vaapi.low_power'); + } + + // Check if passed any test + if (!$basic && !$lowPower) { + $this->config->setSystemValue('memories.vod.vaapi', false); + + return; + } + + // Everything is good + $this->output->writeln('Do you want to enable VAAPI acceleration? [Y/n]'); + if ('n' === trim(fgets(fopen('php://stdin', 'r')))) { + $this->config->setSystemValue('memories.vod.vaapi', false); + $this->output->writeln('VAAPI is now disabled'); + } else { + $this->output->writeln("\nVAAPI is now enabled. You may still need to install the Intel Media Driver"); + $this->output->writeln('and ensure proper permissions for /dev/dri/renderD128.'); + $this->output->writeln('See the documentation for more details.'); + $this->config->setSystemValue('memories.vod.vaapi', true); + } + } + + protected function test(): void + { + $url = \OCA\Memories\Controller\VideoController::getGoVodUrl('test', $this->sampleFile, '360p-000001.ts'); + + // Make a GET request + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + // Check for errors + if (curl_errno($ch)) { + throw new \Exception('Curl: '.curl_error($ch)); + } + + // Check for 200 + if (200 !== $httpCode) { + throw new \Exception('HTTP status: '.$httpCode); + } + + // Check response size is greater than 10kb + if (\strlen($response) < 10240) { + throw new \Exception('Response size is too small'); + } + } + + private function testResult(string $name): bool + { + $this->output->write("Testing transcoding with {$name} ... "); + + try { + $this->restartGoVod($this->output); + $this->test(); + $this->output->writeln('OK'); + + return true; + } catch (\Throwable $e) { + $msg = $e->getMessage(); + $logFile = $this->logFile; + $this->output->writeln('FAIL'); + $this->output->writeln("{$name} transcoding failed with error {$msg}"); + $this->output->writeln("Check the log file of go-vod for more details ({$logFile})"); + + return false; + } + } + + private function startGoVod(bool $suppress = false): bool + { + if (!$suppress) { + $this->output->write("\nAttempting to start go-vod ... "); + } + + try { + $this->logFile = $logFile = \OCA\Memories\Controller\VideoController::startGoVod(); + if (!$suppress) { + $this->output->writeln('OK'); + $this->output->writeln("go-vod logs will be stored at: {$logFile}"); + } + + return true; + } catch (\Exception $e) { + if (!$suppress) { + $this->output->writeln('FAIL'); + } else { + $this->output->writeln('Failed to (re-)start go-vod'); + } + $this->output->writeln($e->getMessage()); + + return false; + } + } + + private function killGoVod(string $path = ''): void + { + if ('' === $path) { + $path = $this->config->getSystemValue('memories.vod.path'); + } + \OCA\Memories\Util::pkill($path); } + + private function restartGoVod(): void + { + $this->killGoVod(); + sleep(1); + $this->startGoVod(true); + } + + private function downloadSampleFile(): string + { + $sampleFile = tempnam(sys_get_temp_dir(), 'sample.mp4'); + $fp = fopen($sampleFile, 'w+'); + $ch = curl_init('https://github.com/pulsejet/memories-assets/raw/main/sample.mp4'); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_exec($ch); + curl_close($ch); + fclose($fp); + + return $sampleFile; + } } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index ae8cad0b..a79f9f76 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -151,7 +151,7 @@ class PageController extends Controller $initialState->provideInitialState('version', $appManager->getAppInfo('memories')['version']); // Video configuration - $initialState->provideInitialState('notranscode', $config->getSystemValue('memories.no_transcode', 'UNSET')); + $initialState->provideInitialState('notranscode', $config->getSystemValue('memories.vod.disable', 'UNSET')); $initialState->provideInitialState('video_default_quality', $config->getSystemValue('memories.video_default_quality', '0')); // Geo configuration diff --git a/lib/Controller/VideoController.php b/lib/Controller/VideoController.php index b18f46ce..b284cafb 100644 --- a/lib/Controller/VideoController.php +++ b/lib/Controller/VideoController.php @@ -43,7 +43,7 @@ class VideoController extends ApiBase public function transcode(string $client, int $fileid, string $profile): Http\Response { // Make sure not running in read-only mode - if (false !== $this->config->getSystemValue('memories.no_transcode', 'UNSET')) { + if (false !== $this->config->getSystemValue('memories.vod.disable', 'UNSET')) { return new JSONResponse(['message' => 'Transcoding disabled'], Http::STATUS_FORBIDDEN); } @@ -195,7 +195,7 @@ class VideoController extends ApiBase } // Transcode video if allowed - if ($transcode && !$this->config->getSystemValue('memories.no_transcode', true)) { + if ($transcode && !$this->config->getSystemValue('memories.vod.disable', true)) { // If video path not given, write to temp file if (!$liveVideoPath) { $liveVideoPath = tempnam(sys_get_temp_dir(), 'livevideo'); @@ -223,18 +223,17 @@ class VideoController extends ApiBase return $response; } - private function getUpstream($client, $path, $profile) + /** + * Start the transcoder. + * + * @return string Path to log file + */ + public static function startGoVod() { - $returnCode = $this->getUpstreamInternal($client, $path, $profile); - - // If status code was 0, it's likely the server is down - // Make one attempt to start after killing whatever is there - if (0 !== $returnCode) { - return $returnCode; - } + $config = \OC::$server->get(\OCP\IConfig::class); // Get transcoder path - $transcoder = $this->config->getSystemValue('memories.transcoder', false); + $transcoder = $config->getSystemValue('memories.vod.path', false); if (!$transcoder) { throw new \Exception('Transcoder not configured'); } @@ -255,65 +254,41 @@ class VideoController extends ApiBase // Kill the transcoder in case it's running \OCA\Memories\Util::pkill($transcoder); - // Check for environment variables - $env = []; - - // QSV with VAAPI - if ($this->config->getSystemValue('memories.qsv', false)) { - $env[] = 'VAAPI=1'; - } - - // NVENC - if ($this->config->getSystemValue('memories.nvenc', false)) { - $env[] = 'NVENC=1'; - } - - // Bind address / port - $port = $this->config->getSystemValue('memories.govod_port', 47788); - $env[] = "GOVOD_BIND='127.0.0.1:{$port}'"; - - // Paths - $ffmpegPath = $this->config->getSystemValue('memories.ffmpeg_path', 'ffmpeg'); - $ffprobePath = $this->config->getSystemValue('memories.ffprobe_path', 'ffprobe'); - $env[] = "FFMPEG='{$ffmpegPath}'"; - $env[] = "FFPROBE='{$ffprobePath}'"; - - // Get temp directory - $defaultTmp = sys_get_temp_dir().'/go-vod/'; - $tmpPath = $this->config->getSystemValue('memories.tmp_path', $defaultTmp); - - // Make sure path ends with slash - if ('/' !== substr($tmpPath, -1)) { - $tmpPath .= '/'; - } - - // Add instance ID to path - $tmpPath .= $this->config->getSystemValue('instanceid', 'default'); - - // (Re-)create temp dir - shell_exec("rm -rf '{$tmpPath}' && mkdir -p '{$tmpPath}' && chmod 755 '{$tmpPath}'"); - - // Check temp directory exists - if (!is_dir($tmpPath)) { - throw new \Exception("Temp directory could not be created ({$tmpPath})"); - } - - // Check temp directory is writable - if (!is_writable($tmpPath)) { - throw new \Exception("Temp directory is not writable ({$tmpPath})"); - } - - // Set temp dir - $env[] = "GOVOD_TEMPDIR='{$tmpPath}'"; - // Start transcoder - $env = implode(' ', $env); - $logFile = $tmpPath.'.log'; - shell_exec("{$env} nohup {$transcoder} > '{$logFile}' 2>&1 & > /dev/null"); + [$configFile, $logFile] = self::makeGoVodConfig($config); + shell_exec("nohup {$transcoder} {$configFile} >> '{$logFile}' 2>&1 & > /dev/null"); - // wait for 1s and try again + // wait for 1s sleep(1); + return $logFile; + } + + /** + * Get the upstream URL for a video. + */ + public static function getGoVodUrl(string $client, string $path, string $profile): string + { + $config = \OC::$server->get(\OCP\IConfig::class); + $path = rawurlencode($path); + $port = $config->getSystemValue('memories.govod_port', 47788); + + return "http://127.0.0.1:{$port}/{$client}{$path}/{$profile}"; + } + + private function getUpstream(string $client, string $path, string $profile) + { + $returnCode = $this->getUpstreamInternal($client, $path, $profile); + + // If status code was 0, it's likely the server is down + // Make one attempt to start after killing whatever is there + if (0 !== $returnCode) { + return $returnCode; + } + + // Start goVod and get log file + $logFile = self::startGoVod(); + $returnCode = $this->getUpstreamInternal($client, $path, $profile); if (0 === $returnCode) { throw new \Exception("Transcoder could not be started, check {$logFile}"); @@ -322,14 +297,11 @@ class VideoController extends ApiBase return $returnCode; } - private function getUpstreamInternal($client, $path, $profile) + private function getUpstreamInternal(string $client, string $path, string $profile) { - $path = rawurlencode($path); - // Make sure query params are repeated // For example, in folder sharing, we need the params on every request - $port = $this->config->getSystemValue('memories.govod_port', 47788); - $url = "http://127.0.0.1:{$port}/{$client}{$path}/{$profile}"; + $url = self::getGoVodUrl($client, $path, $profile); if ($params = $_SERVER['QUERY_STRING']) { $url .= "?{$params}"; } @@ -383,4 +355,98 @@ class VideoController extends ApiBase return $returnCode; } + + /** + * Construct the goVod config JSON. + * + * @return array [config file, log file] + */ + private static function makeGoVodConfig(\OCP\IConfig $config): array + { + // Migrate legacy config: remove in 2024 + self::migrateLegacyConfig($config); + + // Get temp directory + $defaultTmp = sys_get_temp_dir().'/go-vod/'; + $tmpPath = $config->getSystemValue('memories.vod.tempdir', $defaultTmp); + + // Make sure path ends with slash + if ('/' !== substr($tmpPath, -1)) { + $tmpPath .= '/'; + } + + // Add instance ID to path + $tmpPath .= $config->getSystemValue('instanceid', 'default'); + + // (Re-)create temp dir + shell_exec("rm -rf '{$tmpPath}' && mkdir -p '{$tmpPath}' && chmod 755 '{$tmpPath}'"); + + // Check temp directory exists + if (!is_dir($tmpPath)) { + throw new \Exception("Temp directory could not be created ({$tmpPath})"); + } + + // Check temp directory is writable + if (!is_writable($tmpPath)) { + throw new \Exception("Temp directory is not writable ({$tmpPath})"); + } + + // Get config from system values + $env = [ + 'bind' => $config->getSystemValue('memories.vod.bind', '127.0.0.1:47788'), + 'ffmpeg' => $config->getSystemValue('memories.vod.ffmpeg', 'ffmpeg'), + 'ffprobe' => $config->getSystemValue('memories.vod.ffprobe', 'ffprobe'), + 'tempdir' => $tmpPath, + + 'vaapi' => $config->getSystemValue('memories.vod.vaapi', false), + 'vaapiLowPower' => $config->getSystemValue('memories.vod.vaapi.low_power', false), + + 'nvenc' => $config->getSystemValue('memories.vod.nvenc', false), + ]; + + // Write config to file + $logFile = $tmpPath.'.log'; + $configFile = $tmpPath.'.json'; + file_put_contents($configFile, json_encode($env, JSON_PRETTY_PRINT)); + + // Log file is not in config + // go-vod just writes to stdout/stderr + return [$configFile, $logFile]; + } + + /** + * Migrate legacy config to new. + * + * Remove in year 2024 + */ + private static function migrateLegacyConfig(\OCP\IConfig $config) + { + if (null === $config->getSystemValue('memories.no_transcode', null)) { + return; + } + + // Mapping + $legacyConfig = [ + 'memories.no_transcode' => 'memories.vod.disable', + 'memories.transcoder' => 'memories.vod.path', + 'memories.ffmpeg_path' => 'memories.vod.ffmpeg', + 'memories.ffprobe_path' => 'memories.vod.ffprobe', + 'memories.qsv' => 'memories.vod.vaapi', + 'memories.nvenc' => 'memories.vod.nvenc', + 'memories.tmp_path' => 'memories.vod.tempdir', + ]; + + foreach ($legacyConfig as $old => $new) { + if (null !== $config->getSystemValue($old, null)) { + $config->setSystemValue($new, $config->getSystemValue($old)); + $config->deleteSystemValue($old); + } + } + + // Migrate bind address + if ($port = null !== $config->getSystemValue('memories.govod_port', null)) { + $config->setSystemValue('memories.vod.bind', "127.0.0.1:{$port}"); + $config->deleteSystemValue('memories.govod_port'); + } + } }