vod: switch to new config pattern

pull/465/head
Varun Patil 2023-03-09 12:40:02 -08:00
parent 722f426d3f
commit ec4db393b3
3 changed files with 370 additions and 119 deletions

View File

@ -32,6 +32,8 @@ class VideoSetup extends Command
{ {
protected IConfig $config; protected IConfig $config;
protected OutputInterface $output; protected OutputInterface $output;
protected string $sampleFile;
protected string $logFile;
public function __construct( public function __construct(
IConfig $config IConfig $config
@ -50,16 +52,18 @@ class VideoSetup extends Command
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {
$this->output = $output;
// Preset executables // Preset executables
$ffmpegPath = $this->config->getSystemValue('memories.ffmpeg_path', 'ffmpeg'); $ffmpegPath = $this->config->getSystemValue('memories.vod.ffmpeg', 'ffmpeg');
if ('ffmpeg' === $ffmpegPath) { if ('ffmpeg' === $ffmpegPath) {
$ffmpegPath = trim(shell_exec('which ffmpeg') ?: 'ffmpeg'); $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) { if ('ffprobe' === $ffprobePath) {
$ffprobePath = trim(shell_exec('which ffprobe') ?: 'ffprobe'); $ffprobePath = trim(shell_exec('which ffprobe') ?: 'ffprobe');
$this->config->setSystemValue('memories.ffprobe_path', $ffprobePath); $this->config->setSystemValue('memories.vod.ffprobe', $ffprobePath);
} }
// Get ffmpeg version // Get ffmpeg version
@ -83,12 +87,12 @@ class VideoSetup extends Command
if (null === $ffmpeg || null === $ffprobe) { if (null === $ffmpeg || null === $ffprobe) {
$output->writeln('ffmpeg and ffprobe are required for video transcoding'); $output->writeln('ffmpeg and ffprobe are required for video transcoding');
return $this->suggestDisable($output); return $this->suggestDisable();
} }
// Check go-vod binary // Check go-vod binary
$output->writeln('Checking for 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)) { if (!\is_string($goVodPath) || !file_exists($goVodPath)) {
// Detect architecture // Detect architecture
@ -97,9 +101,9 @@ class VideoSetup extends Command
if (!$goVodPath) { if (!$goVodPath) {
$output->writeln('<error>Compatible go-vod binary not found</error>'); $output->writeln('<error>Compatible go-vod binary not found</error>');
$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'); $goVod = shell_exec($goVodPath.' test');
if (!$goVod || false === strpos($goVod, 'test successful')) { if (!$goVod || false === strpos($goVod, 'test successful')) {
$output->writeln('<error>go-vod could not be run</error>'); $output->writeln('<error>go-vod could not be run</error>');
$this->suggestGoVod($output); $this->suggestGoVod();
return $this->suggestDisable($output); return $this->suggestDisable();
} }
// Go transcode is working. Yay! // 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]'); $output->writeln('Do you want to enable transcoding and HLS? [Y/n]');
if ('n' === trim(fgets(fopen('php://stdin', 'r')))) { if ('n' === trim(fgets(fopen('php://stdin', 'r')))) {
$this->config->setSystemValue('memories.no_transcode', true); $this->config->setSystemValue('memories.vod.disable', true);
$output->writeln('<error>Transcoding and HLS are now disabled</error>'); $output->writeln('<error>Transcoding and HLS are now disabled</error>');
$this->killGoVod($output, $goVodPath); $this->killGoVod($goVodPath);
return 0; return 0;
} }
$this->config->setSystemValue('memories.transcoder', $goVodPath); $this->config->setSystemValue('memories.vod.path', $goVodPath);
$this->config->setSystemValue('memories.no_transcode', false); $this->config->setSystemValue('memories.vod.disable', 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');
// Check for VAAPI // Feature detection
$output->writeln("\nChecking for VAAPI (/dev/dri/renderD128)"); $this->detectFeatures();
if (file_exists('/dev/dri/renderD128')) {
$output->writeln('VAAPI is available. Do you want to enable it? [Y/n]');
if ('n' === trim(fgets(fopen('php://stdin', 'r')))) { // Success
$this->config->setSystemValue('memories.qsv', false); $output->writeln("\nTranscoding and HLS are now enabled! Monitor the log file for any errors");
$output->writeln('VAAPI is now disabled'); $output->writeln('<error>You should restart the server for changes to take effect</error>');
} 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);
}
$this->killGoVod($output, $goVodPath); $this->killGoVod();
return 0; return 0;
} }
protected function suggestGoVod(OutputInterface $output): void protected function suggestGoVod(): void
{ {
$output->writeln('You may build go-vod from source'); $this->output->writeln('You may build go-vod from source');
$output->writeln('It can be downloaded from https://github.com/pulsejet/go-vod'); $this->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('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'); $this->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('Do you want to disable transcoding and HLS streaming? [y/N]');
if ('y' !== trim(fgets(fopen('php://stdin', 'r')))) { if ('y' !== trim(fgets(fopen('php://stdin', 'r')))) {
$output->writeln('Aborting'); $this->output->writeln('Aborting');
return 1; return 1;
} }
$this->config->setSystemValue('memories.no_transcode', true); $this->config->setSystemValue('memories.vod.disable', true);
$output->writeln('<error>Transcoding and HLS are now disabled</error>'); $this->output->writeln('<error>Transcoding and HLS are now disabled</error>');
$output->writeln('You should restart the server for changes to take effect'); $this->output->writeln('You should restart the server for changes to take effect');
return 0; 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('<error>Could not download sample file</error>');
$this->output->writeln('<error>Failed to perform feature detection</error>');
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('<error>Current user does not have read permissions on /dev/dri/renderD128</error>');
$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("<error>{$name} transcoding failed with error {$msg}</error>");
$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('<error>Failed to (re-)start go-vod</error>');
}
$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); \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;
}
} }

View File

@ -151,7 +151,7 @@ class PageController extends Controller
$initialState->provideInitialState('version', $appManager->getAppInfo('memories')['version']); $initialState->provideInitialState('version', $appManager->getAppInfo('memories')['version']);
// Video configuration // 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')); $initialState->provideInitialState('video_default_quality', $config->getSystemValue('memories.video_default_quality', '0'));
// Geo configuration // Geo configuration

View File

@ -43,7 +43,7 @@ class VideoController extends ApiBase
public function transcode(string $client, int $fileid, string $profile): Http\Response public function transcode(string $client, int $fileid, string $profile): Http\Response
{ {
// Make sure not running in read-only mode // 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); return new JSONResponse(['message' => 'Transcoding disabled'], Http::STATUS_FORBIDDEN);
} }
@ -195,7 +195,7 @@ class VideoController extends ApiBase
} }
// Transcode video if allowed // 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 video path not given, write to temp file
if (!$liveVideoPath) { if (!$liveVideoPath) {
$liveVideoPath = tempnam(sys_get_temp_dir(), 'livevideo'); $liveVideoPath = tempnam(sys_get_temp_dir(), 'livevideo');
@ -223,18 +223,17 @@ class VideoController extends ApiBase
return $response; 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); $config = \OC::$server->get(\OCP\IConfig::class);
// 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;
}
// Get transcoder path // Get transcoder path
$transcoder = $this->config->getSystemValue('memories.transcoder', false); $transcoder = $config->getSystemValue('memories.vod.path', false);
if (!$transcoder) { if (!$transcoder) {
throw new \Exception('Transcoder not configured'); throw new \Exception('Transcoder not configured');
} }
@ -255,65 +254,41 @@ class VideoController extends ApiBase
// Kill the transcoder in case it's running // Kill the transcoder in case it's running
\OCA\Memories\Util::pkill($transcoder); \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 // Start transcoder
$env = implode(' ', $env); [$configFile, $logFile] = self::makeGoVodConfig($config);
$logFile = $tmpPath.'.log'; shell_exec("nohup {$transcoder} {$configFile} >> '{$logFile}' 2>&1 & > /dev/null");
shell_exec("{$env} nohup {$transcoder} > '{$logFile}' 2>&1 & > /dev/null");
// wait for 1s and try again // wait for 1s
sleep(1); 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); $returnCode = $this->getUpstreamInternal($client, $path, $profile);
if (0 === $returnCode) { if (0 === $returnCode) {
throw new \Exception("Transcoder could not be started, check {$logFile}"); throw new \Exception("Transcoder could not be started, check {$logFile}");
@ -322,14 +297,11 @@ class VideoController extends ApiBase
return $returnCode; 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 // Make sure query params are repeated
// For example, in folder sharing, we need the params on every request // For example, in folder sharing, we need the params on every request
$port = $this->config->getSystemValue('memories.govod_port', 47788); $url = self::getGoVodUrl($client, $path, $profile);
$url = "http://127.0.0.1:{$port}/{$client}{$path}/{$profile}";
if ($params = $_SERVER['QUERY_STRING']) { if ($params = $_SERVER['QUERY_STRING']) {
$url .= "?{$params}"; $url .= "?{$params}";
} }
@ -383,4 +355,98 @@ class VideoController extends ApiBase
return $returnCode; 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');
}
}
} }