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');
+ }
+ }
}