memories/lib/Command/VideoSetup.php

470 lines
17 KiB
PHP
Raw Normal View History

2022-11-09 09:23:12 +00:00
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Varun Patil <radialapps@gmail.com>
* @author Varun Patil <radialapps@gmail.com>
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Memories\Command;
use OCP\IConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class VideoSetup extends Command
{
protected IConfig $config;
protected OutputInterface $output;
2023-03-09 20:40:02 +00:00
protected string $sampleFile;
protected string $logFile;
2022-11-09 09:23:12 +00:00
public function __construct(
IConfig $config
) {
parent::__construct();
$this->config = $config;
}
protected function configure(): void
{
$this
->setName('memories:video-setup')
->setDescription('Setup video streaming')
->addOption('print-config', null, null, 'Print the current go-vod configuration JSON')
2022-11-09 09:23:12 +00:00
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
2023-03-09 20:40:02 +00:00
$this->output = $output;
// Check if we just need the config
if ($input->getOption('print-config')) {
$conf = \OCA\Memories\Controller\VideoController::getGoVodConfig(\OC::$server->get(IConfig::class));
$output->writeln(json_encode($conf, JSON_PRETTY_PRINT));
return 0;
}
// Print some information
$output->writeln('');
$output->writeln('This command will help you setup video streaming');
$output->writeln('It will check if ffmpeg and ffprobe are installed and go-vod (the transcoder) works');
$output->writeln('If you are running go-vod externally, feature detection will not work properly');
$output->writeln('In that case you will need to manually adjust the configuration file (add --print-config)');
$output->writeln('For details on hardware support: https://github.com/pulsejet/memories/wiki/HW-Transcoding');
$output->writeln('');
$output->writeln('Press enter to continue or CTRL+C to abort');
if (false === fgets(STDIN)) {
return 1;
}
2022-11-15 10:27:20 +00:00
// Preset executables
2023-03-09 20:40:02 +00:00
$ffmpegPath = $this->config->getSystemValue('memories.vod.ffmpeg', 'ffmpeg');
2022-11-21 10:38:12 +00:00
if ('ffmpeg' === $ffmpegPath) {
$ffmpegPath = trim(shell_exec('which ffmpeg') ?: 'ffmpeg');
2023-03-09 20:40:02 +00:00
$this->config->setSystemValue('memories.vod.ffmpeg', $ffmpegPath);
}
2023-03-09 20:40:02 +00:00
$ffprobePath = $this->config->getSystemValue('memories.vod.ffprobe', 'ffprobe');
2022-11-21 10:38:12 +00:00
if ('ffprobe' === $ffprobePath) {
$ffprobePath = trim(shell_exec('which ffprobe') ?: 'ffprobe');
2023-03-09 20:40:02 +00:00
$this->config->setSystemValue('memories.vod.ffprobe', $ffprobePath);
}
2022-11-09 10:26:51 +00:00
2022-11-09 09:23:12 +00:00
// Get ffmpeg version
$ffmpeg = shell_exec("{$ffmpegPath} -version") ?: '';
2022-11-09 09:23:12 +00:00
if (false === strpos($ffmpeg, 'ffmpeg version')) {
$ffmpeg = null;
$output->writeln('<error>ffmpeg is not installed</error>');
} else {
$output->writeln('ffmpeg is installed');
}
// Get ffprobe version
$ffprobe = shell_exec("{$ffprobePath} -version") ?: '';
2022-11-09 09:23:12 +00:00
if (false === strpos($ffprobe, 'ffprobe version')) {
$ffprobe = null;
$output->writeln('<error>ffprobe is not installed</error>');
} else {
$output->writeln('ffprobe is installed');
}
if (null === $ffmpeg || null === $ffprobe) {
$output->writeln('ffmpeg and ffprobe are required for video transcoding');
2022-11-09 10:42:42 +00:00
2023-03-09 20:40:02 +00:00
return $this->suggestDisable();
2022-11-09 09:23:12 +00:00
}
2022-11-11 05:25:26 +00:00
// Check go-vod binary
$output->writeln('Checking for go-vod binary');
2023-03-09 20:40:02 +00:00
$goVodPath = $this->config->getSystemValue('memories.vod.path', false);
2022-11-09 09:23:12 +00:00
2022-11-23 09:38:36 +00:00
if (!\is_string($goVodPath) || !file_exists($goVodPath)) {
// Detect architecture
$arch = \OCA\Memories\Util::getArch();
2022-11-23 09:38:36 +00:00
$goVodPath = realpath(__DIR__."/../../exiftool-bin/go-vod-{$arch}");
2022-11-09 09:23:12 +00:00
2022-11-23 09:38:36 +00:00
if (!$goVodPath) {
$output->writeln('<error>Compatible go-vod binary not found</error>');
2023-03-09 20:40:02 +00:00
$this->suggestGoVod();
2022-11-09 10:42:42 +00:00
2023-03-09 20:40:02 +00:00
return $this->suggestDisable();
}
2022-11-09 09:23:12 +00:00
}
2022-11-11 05:25:26 +00:00
$output->writeln("Trying go-vod from {$goVodPath}");
chmod($goVodPath, 0755);
2022-11-09 09:23:12 +00:00
2022-11-11 05:25:26 +00:00
$goVod = shell_exec($goVodPath.' test');
if (!$goVod || false === strpos($goVod, 'test successful')) {
$output->writeln('<error>go-vod could not be run</error>');
2023-03-09 20:40:02 +00:00
$this->suggestGoVod();
2022-11-09 10:42:42 +00:00
2023-03-09 20:40:02 +00:00
return $this->suggestDisable();
2022-11-09 09:23:12 +00:00
}
// Go transcode is working. Yay!
2022-11-11 05:25:26 +00:00
$output->writeln('go-vod is installed!');
2022-11-09 09:23:12 +00:00
$output->writeln('');
$output->writeln('You can use transcoding and HLS streaming');
2022-11-09 10:26:51 +00:00
$output->writeln('This is recommended for better performance, but has implications if');
$output->writeln('you are using external storage or run Nextcloud on a slow system.');
$output->writeln('');
$output->writeln('Read the following documentation carefully before continuing:');
$output->writeln('https://github.com/pulsejet/memories/wiki/Configuration');
2022-11-09 09:23:12 +00:00
$output->writeln('');
$output->writeln('Do you want to enable transcoding and HLS? [Y/n]');
2022-11-09 14:21:17 +00:00
if ('n' === trim(fgets(fopen('php://stdin', 'r')))) {
2023-03-09 20:40:02 +00:00
$this->config->setSystemValue('memories.vod.disable', true);
2022-11-09 09:23:12 +00:00
$output->writeln('<error>Transcoding and HLS are now disabled</error>');
2022-11-09 10:42:42 +00:00
2023-03-09 20:40:02 +00:00
$this->killGoVod($goVodPath);
2023-02-24 17:12:23 +00:00
2022-11-09 09:23:12 +00:00
return 0;
}
2023-03-09 20:40:02 +00:00
$this->config->setSystemValue('memories.vod.path', $goVodPath);
$this->config->setSystemValue('memories.vod.disable', false);
2022-11-09 09:23:12 +00:00
2023-03-09 20:40:02 +00:00
// Feature detection
$this->detectFeatures();
2022-11-09 14:21:17 +00:00
2023-03-09 20:40:02 +00:00
// Success
$output->writeln("\nTranscoding and HLS are now enabled! Monitor the log file for any errors");
$output->writeln('<error>You should restart the server for changes to take effect</error>');
2022-11-09 14:21:17 +00:00
2023-03-09 20:40:02 +00:00
$this->killGoVod();
2023-02-24 17:12:23 +00:00
2022-11-09 09:23:12 +00:00
return 0;
}
2023-03-09 20:40:02 +00:00
protected function suggestGoVod(): void
2022-11-09 09:23:12 +00:00
{
2023-03-09 20:40:02 +00:00
$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`');
2022-11-09 09:23:12 +00:00
}
2023-03-09 20:40:02 +00:00
protected function suggestDisable()
2022-11-09 10:42:42 +00:00
{
2023-03-09 20:40:02 +00:00
$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]');
2022-11-09 14:21:17 +00:00
if ('y' !== trim(fgets(fopen('php://stdin', 'r')))) {
2023-03-09 20:40:02 +00:00
$this->output->writeln('Aborting');
2022-11-09 09:23:12 +00:00
return 1;
}
2023-03-09 20:40:02 +00:00
$this->config->setSystemValue('memories.vod.disable', true);
$this->output->writeln('<error>Transcoding and HLS are now disabled</error>');
$this->output->writeln('You should restart the server for changes to take effect');
2022-11-09 09:23:12 +00:00
return 0;
}
2023-02-24 17:12:23 +00:00
2023-03-09 20:40:02 +00:00
protected function detectFeatures()
{
// Reset the current configuration
$this->config->deleteSystemValue('memories.vod.vaapi');
$this->config->deleteSystemValue('memories.vod.nvenc');
2023-03-09 20:40:02 +00:00
$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();
// We need to turn off VAAPI before checking NVENC
$wasVaapi = $this->config->getSystemValue('memories.vod.vaapi', false);
$this->config->deleteSystemValue('memories.vod.vaapi');
$this->checkNVENC();
// Restore VAAPI configuration
if ($wasVaapi) {
$this->config->setSystemValue('memories.vod.vaapi', true);
}
2023-03-09 20:40:02 +00:00
} finally {
if (file_exists($this->sampleFile)) {
unlink($this->sampleFile);
}
}
$this->output->writeln("\nFeature detection completed");
}
protected function checkCPU()
2023-02-24 17:12:23 +00:00
{
2023-03-09 20:40:02 +00:00
$this->output->writeln('');
$this->testResult('CPU');
}
protected function checkVAAPI()
{
// Reset current configuration
$this->config->deleteSystemValue('memories.vod.vaapi');
$this->config->deleteSystemValue('memories.vod.vaapi.low_power');
2023-03-09 20:40:02 +00:00
// 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->deleteSystemValue('memories.vod.vaapi');
2023-03-09 20:40:02 +00:00
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->deleteSystemValue('memories.vod.vaapi');
2023-03-09 20:40:02 +00:00
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->deleteSystemValue('memories.vod.vaapi');
2023-03-09 20:40:02 +00:00
return;
}
// Everything is good
$this->output->write('Do you want to enable VAAPI acceleration? [Y/n] ');
2023-03-09 20:40:02 +00:00
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 checkNVENC()
{
$this->output->writeln("\nChecking for NVIDIA acceleration with NVENC");
// Reset the current configuration
$this->config->deleteSystemValue('memories.vod.nvenc.temporal_aq');
$this->config->deleteSystemValue('memories.vod.nvenc.scale');
// Basic test
$this->config->setSystemValue('memories.vod.nvenc', true);
// Different scaling methods
$this->config->setSystemValue('memories.vod.nvenc.scale', 'npp');
$withScaleNpp = $this->testResult('NVENC (scale_npp)', true);
$this->config->setSystemValue('memories.vod.nvenc.scale', 'cuda');
$withScaleCuda = $this->testResult('NVENC (scale_cuda)', true);
if (!$withScaleNpp && !$withScaleCuda) {
$this->config->deleteSystemValue('memories.vod.nvenc');
$this->config->deleteSystemValue('memories.vod.nvenc.scale');
$this->output->writeln('NVENC does not seem to be available');
return;
}
if ($withScaleNpp) {
$this->config->setSystemValue('memories.vod.nvenc.scale', 'npp');
} elseif ($withScaleCuda) {
$this->config->setSystemValue('memories.vod.nvenc.scale', 'cuda');
}
// Try with temporal-aq
$this->config->setSystemValue('memories.vod.nvenc.temporal_aq', true);
if (!$this->testResult('NVENC (temporal-aq)', true)) {
$this->config->deleteSystemValue('memories.vod.nvenc.temporal_aq');
}
// Good to go
$this->output->write('Do you want to enable NVENC acceleration? [Y/n] ');
if ('n' === trim(fgets(fopen('php://stdin', 'r')))) {
$this->config->setSystemValue('memories.vod.nvenc', false);
$this->output->writeln('NVENC is now disabled');
} else {
$this->output->writeln('NVENC transcoding is now enabled');
}
}
2023-03-09 20:40:02 +00:00
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 $minor = false): bool
2023-03-09 20:40:02 +00:00
{
$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');
if (!$minor) {
$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})");
}
2023-03-09 20:40:02 +00:00
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');
}
2023-02-24 17:12:23 +00:00
\OCA\Memories\Util::pkill($path);
}
2023-03-09 20:40:02 +00:00
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;
}
2022-11-09 09:23:12 +00:00
}