From 7f7c878cb6ecf7b4a967b2af87da82bc6593081e Mon Sep 17 00:00:00 2001 From: Yassine Doghri Date: Fri, 21 Jan 2022 12:35:50 +0000 Subject: [PATCH] fix(video-clips): create unique temporary files for resources to be deleted after generation - tempfile uniqueness ensures that each process lives in its independent context - add writable/temp folder to store video clips temporary resources - add videoClipWorkers config to Admin for specifying the number of ffmpeg processes to run in parallel - update video clip preview background to better suit the end result --- .gitignore | 3 + .../MediaClipper/Config/MediaClipper.php | 14 +++- app/Libraries/MediaClipper/VideoClipper.php | 20 ++++- app/Models/ClipModel.php | 14 ++++ app/Resources/js/modules/VideoClipBuilder.ts | 7 +- .../js/modules/video-clip-previewer.ts | 2 +- modules/Admin/Config/Admin.php | 6 ++ .../Admin/Controllers/SchedulerController.php | 75 ++++++++++++------- modules/Admin/Language/en/VideoClip.php | 8 +- modules/Admin/Language/fr/VideoClip.php | 8 +- themes/cp_admin/episode/video_clips_list.php | 2 +- themes/cp_admin/episode/video_clips_new.php | 8 +- writable/temp/index.html | 9 +++ 13 files changed, 128 insertions(+), 48 deletions(-) create mode 100644 writable/temp/index.html diff --git a/.gitignore b/.gitignore index 50cd1f20..d0547825 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,9 @@ writable/logs/* writable/session/* !writable/session/index.html +writable/temp/* +!writable/temp/index.html + writable/uploads/* !writable/uploads/index.html diff --git a/app/Libraries/MediaClipper/Config/MediaClipper.php b/app/Libraries/MediaClipper/Config/MediaClipper.php index f4274d6d..a0df0da9 100644 --- a/app/Libraries/MediaClipper/Config/MediaClipper.php +++ b/app/Libraries/MediaClipper/Config/MediaClipper.php @@ -229,8 +229,10 @@ class MediaClipper extends BaseConfig */ public array $themes = [ 'pine' => [ - // Preview must be a HSL colorscheme string + // Previews must be a HSL colorscheme string 'preview' => '174 100% 29%', + 'preview-background' => '172 100% 17%', + // arrays are rgb 'background' => [0, 86, 74], 'text' => [255, 255, 255], // subtitle hex color is BGR (Blue, Green, Red), @@ -248,6 +250,8 @@ class MediaClipper extends BaseConfig 'crimson' => [ // Preview must be a HSL colorscheme string 'preview' => '350 87% 61%', + 'preview-background' => '348 75% 40%', + // arrays are rgb 'background' => [179, 31, 57], 'text' => [255, 255, 255], // subtitle hex color is BGR (Blue, Green, Red), @@ -265,6 +269,8 @@ class MediaClipper extends BaseConfig 'lake' => [ // Preview must be a HSL colorscheme string 'preview' => '194 100% 44%', + 'preview-background' => '194 100% 22%', + // arrays are rgb 'background' => [0, 86, 113], 'text' => [255, 255, 255], // subtitle hex color is BGR (Blue, Green, Red), @@ -282,6 +288,8 @@ class MediaClipper extends BaseConfig 'amber' => [ // Preview must be a HSL colorscheme string 'preview' => '17 100% 57%', + 'preview-background' => '17 100% 35%', + // arrays are rgb 'background' => [177, 50, 0], 'text' => [255, 255, 255], // subtitle hex color is BGR (Blue, Green, Red), @@ -299,6 +307,8 @@ class MediaClipper extends BaseConfig 'jacaranda' => [ // Preview must be a HSL colorscheme string 'preview' => '254 72% 52%', + 'preview-background' => '254 73% 30%', + // arrays are rgb 'background' => [47, 21, 132], 'text' => [255, 255, 255], // subtitle hex color is BGR (Blue, Green, Red), @@ -316,6 +326,8 @@ class MediaClipper extends BaseConfig 'onyx' => [ // Preview must be a HSL colorscheme string 'preview' => '240 17% 2%', + 'preview-background' => '240 17% 2%', + // arrays are rgb 'background' => [5, 5, 7], 'text' => [255, 255, 255], // subtitle hex color is BGR (Blue, Green, Red), diff --git a/app/Libraries/MediaClipper/VideoClipper.php b/app/Libraries/MediaClipper/VideoClipper.php index a8e3230b..3e7cadb8 100644 --- a/app/Libraries/MediaClipper/VideoClipper.php +++ b/app/Libraries/MediaClipper/VideoClipper.php @@ -55,6 +55,8 @@ class VideoClipper protected ?string $episodeNumbering = null; + protected string $tempFileOutput; + /** * @var array */ @@ -90,11 +92,22 @@ class VideoClipper $podcastFolder = media_path("podcasts/{$this->episode->podcast->handle}"); - $this->soundbiteOutput = $podcastFolder . "/{$this->episode->slug}-soundbite-{$this->start}-to-{$this->end}.mp3"; - $this->subtitlesClipOutput = $podcastFolder . "/{$this->episode->slug}-subtitles-clip-{$this->start}-to-{$this->end}.srt"; - $this->videoClipBgOutput = $podcastFolder . "/{$this->episode->slug}-clip-bg-{$this->format}-{$this->theme}.png"; $this->videoClipOutput = $podcastFolder . "/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4"; $this->videoClipFilePath = "podcasts/{$this->episode->podcast->handle}/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4"; + + // Temporary files to generate clip + $tempFile = tempnam(WRITEPATH . 'temp', "{$this->episode->slug}-soundbite-{$this->start}-to-{$this->end}"); + + if (! $tempFile) { + throw new Exception( + 'Could not create temporary files, check for permissions on your ' . WRITEPATH . 'temp folder.' + ); + } + + $this->tempFileOutput = $tempFile; + $this->soundbiteOutput = $tempFile . '.mp3'; + $this->subtitlesClipOutput = $tempFile . '.srt'; + $this->videoClipBgOutput = $tempFile . '.png'; } public function soundbite(): void @@ -178,6 +191,7 @@ class VideoClipper public function cleanTempFiles(): void { // delete generated video background image, soundbite & subtitlesClip + unlink($this->tempFileOutput); unlink($this->soundbiteOutput); unlink($this->subtitlesClipOutput); unlink($this->videoClipBgOutput); diff --git a/app/Models/ClipModel.php b/app/Models/ClipModel.php index 1d9172b7..446e8ded 100644 --- a/app/Models/ClipModel.php +++ b/app/Models/ClipModel.php @@ -130,6 +130,20 @@ class ClipModel extends Model return $found; } + public function getRunningVideoClipsCount(): int + { + $result = $this + ->select('COUNT(*) as `running_count`') + ->where([ + 'type' => 'video', + 'status' => 'running', + ]) + ->get() + ->getResultArray(); + + return (int) $result[0]['running_count']; + } + public function deleteVideoClip(int $podcastId, int $episodeId, int $clipId): BaseResult | bool { $this->clearVideoClipCache($clipId); diff --git a/app/Resources/js/modules/VideoClipBuilder.ts b/app/Resources/js/modules/VideoClipBuilder.ts index 432d1246..b3a0865c 100644 --- a/app/Resources/js/modules/VideoClipBuilder.ts +++ b/app/Resources/js/modules/VideoClipBuilder.ts @@ -36,15 +36,16 @@ const VideoClipBuilder = (): void => { let theme = form .querySelector('input[name="theme"]:checked') - ?.parentElement?.style.getPropertyValue("--color-accent-base"); + ?.parentElement?.style.getPropertyValue("--color-background-preview"); videoClipPreviewer.setAttribute("theme", theme || ""); const watchThemeChange = (event: Event) => { theme = ( event.target as HTMLInputElement - ).parentElement?.style.getPropertyValue("--color-accent-base") ?? - theme; + ).parentElement?.style.getPropertyValue( + "--color-background-preview" + ) ?? theme; videoClipPreviewer.setAttribute("theme", theme || ""); }; for (let i = 0; i < themeOptions.length; i++) { diff --git a/app/Resources/js/modules/video-clip-previewer.ts b/app/Resources/js/modules/video-clip-previewer.ts index faa138af..a28c2f2a 100644 --- a/app/Resources/js/modules/video-clip-previewer.ts +++ b/app/Resources/js/modules/video-clip-previewer.ts @@ -31,7 +31,7 @@ export class VideoClipPreviewer extends LitElement { format: VideoFormats = VideoFormats.Portrait; @property() - theme = "173 44% 96%"; + theme = "172 100% 17%"; @property({ type: Number }) duration!: number; diff --git a/modules/Admin/Config/Admin.php b/modules/Admin/Config/Admin.php index d71f39b7..4c289d9c 100644 --- a/modules/Admin/Config/Admin.php +++ b/modules/Admin/Config/Admin.php @@ -15,4 +15,10 @@ class Admin extends BaseConfig * Defines a base route for all admin pages */ public string $gateway = 'cp-admin'; + + /** + * Number of maximum ffmpeg processes to spawn in parallel when generating video clips. Processes are instance wide, + * meaning that they are shared across all podcasts and episodes. + */ + public int $videoClipWorkers = 2; } diff --git a/modules/Admin/Controllers/SchedulerController.php b/modules/Admin/Controllers/SchedulerController.php index a5f0239d..2a00204d 100644 --- a/modules/Admin/Controllers/SchedulerController.php +++ b/modules/Admin/Controllers/SchedulerController.php @@ -13,12 +13,20 @@ namespace Modules\Admin\Controllers; use App\Models\ClipModel; use CodeIgniter\Controller; use CodeIgniter\I18n\Time; +use Exception; use MediaClipper\VideoClipper; class SchedulerController extends Controller { public function generateVideoClips(): bool { + // get number of running clips to prevent from having too much running in parallel + // TODO: get the number of running ffmpeg processes directly from the machine? + $runningVideoClips = (new ClipModel())->getRunningVideoClipsCount(); + if ($runningVideoClips >= config('Admin')->videoClipWorkers) { + return true; + } + // get all clips that haven't been processed yet $scheduledClips = (new ClipModel())->getScheduledVideoClips(); @@ -38,40 +46,49 @@ class SchedulerController extends Controller // Loop through clips to generate them foreach ($scheduledClips as $scheduledClip) { - // set clip to pending - (new ClipModel()) - ->update($scheduledClip->id, [ - 'status' => 'running', - 'job_started_at' => Time::now(), - ]); - $clipper = new VideoClipper( - $scheduledClip->episode, - $scheduledClip->start_time, - $scheduledClip->end_time, - $scheduledClip->format, - $scheduledClip->theme['name'], - ); - $exitCode = $clipper->generate(); + try { - $clipModel = new ClipModel(); - if ($exitCode === 0) { - // success, video was generated - $scheduledClip->setMedia($clipper->videoClipFilePath); - $clipModel->update($scheduledClip->id, [ - 'media_id' => $scheduledClip->media_id, - 'status' => 'passed', - 'logs' => $clipper->logs, - 'job_ended_at' => Time::now(), - ]); - } else { - // error - $clipModel->update($scheduledClip->id, [ + // set clip to pending + (new ClipModel()) + ->update($scheduledClip->id, [ + 'status' => 'running', + 'job_started_at' => Time::now(), + ]); + $clipper = new VideoClipper( + $scheduledClip->episode, + $scheduledClip->start_time, + $scheduledClip->end_time, + $scheduledClip->format, + $scheduledClip->theme['name'], + ); + $exitCode = $clipper->generate(); + + $clipModel = new ClipModel(); + if ($exitCode === 0) { + // success, video was generated + $scheduledClip->setMedia($clipper->videoClipFilePath); + $clipModel->update($scheduledClip->id, [ + 'media_id' => $scheduledClip->media_id, + 'status' => 'passed', + 'logs' => $clipper->logs, + 'job_ended_at' => Time::now(), + ]); + } else { + // error + $clipModel->update($scheduledClip->id, [ + 'status' => 'failed', + 'logs' => $clipper->logs, + 'job_ended_at' => Time::now(), + ]); + } + $clipModel->clearVideoClipCache($scheduledClip->id); + } catch (Exception $exception) { + (new ClipModel())->update($scheduledClip->id, [ 'status' => 'failed', - 'logs' => $clipper->logs, + 'logs' => $exception, 'job_ended_at' => Time::now(), ]); } - $clipModel->clearVideoClipCache($scheduledClip->id); } return true; diff --git a/modules/Admin/Language/en/VideoClip.php b/modules/Admin/Language/en/VideoClip.php index 8e804f22..84dfe89c 100644 --- a/modules/Admin/Language/en/VideoClip.php +++ b/modules/Admin/Language/en/VideoClip.php @@ -38,17 +38,19 @@ return [ 'createSuccess' => 'Video clip has been successfully created!', 'deleteSuccess' => 'Video clip has been successfully removed!', ], + 'format' => [ + 'landscape' => 'Landscape', + 'portrait' => 'Portrait', + 'squared' => 'Squared', + ], 'form' => [ 'title' => 'New video clip', 'params_section_title' => 'Video clip parameters', 'clip_title' => 'Clip title', 'format' => [ 'label' => 'Choose a format', - 'landscape' => 'Landscape', 'landscape_hint' => 'With a 16:9 ratio, landscape videos are great for PeerTube, Youtube and Vimeo.', - 'portrait' => 'Portrait', 'portrait_hint' => 'With a 9:16 ratio, portrait videos are great for TikTok, Youtube shorts and Instagram stories.', - 'squared' => 'Squared', 'squared_hint' => 'With a 1:1 ratio, squared videos are great for Mastodon, Facebook, Twitter and LinkedIn.', ], 'theme' => 'Select a theme', diff --git a/modules/Admin/Language/fr/VideoClip.php b/modules/Admin/Language/fr/VideoClip.php index a0646869..c81eee5e 100644 --- a/modules/Admin/Language/fr/VideoClip.php +++ b/modules/Admin/Language/fr/VideoClip.php @@ -38,17 +38,19 @@ return [ 'createSuccess' => 'L’extrait vidéo a été créé avec succès !', 'deleteSuccess' => 'L’extrait vidéo a bien été supprimé !', ], + 'format' => [ + 'landscape' => 'Paysage', + 'portrait' => 'Portrait', + 'squared' => 'Carré', + ], 'form' => [ 'title' => 'Nouvel extrait vidéo', 'params_section_title' => 'Paramètres de l’extrait vidéo', 'clip_title' => 'Titre de l’extrait', 'format' => [ 'label' => 'Choisissez un format', - 'landscape' => 'Paysage', 'landscape_hint' => 'Avec un ratio de 16/9, les vidéos en paysage sont adaptées pour PeerTube, Youtube et Vimeo.', - 'portrait' => 'Portrait', 'portrait_hint' => 'Avec un ratio de 9/16, les vidéos en portrait sont adaptées pour TikTok, les Youtube shorts and les stories Instagram.', - 'squared' => 'Carré', 'squared_hint' => 'Avec un ratio de 1/1, les vidéos carrées sont adaptées pour Mastodon, Facebook, Twitter et LinkedIn.', ], 'theme' => 'Sélectionnez un thème', diff --git a/themes/cp_admin/episode/video_clips_list.php b/themes/cp_admin/episode/video_clips_list.php index 6da9bc9d..5af00a75 100644 --- a/themes/cp_admin/episode/video_clips_list.php +++ b/themes/cp_admin/episode/video_clips_list.php @@ -62,7 +62,7 @@ use CodeIgniter\I18n\Time; 'portrait' => 'aspect-[9/16]', 'squared' => 'aspect-square', ]; - return '
#' . $videoClip->id . ' – ' . $videoClip->title . 'by ' . $videoClip->user->username . '
' . format_duration((int) $videoClip->duration) . '
'; + return '
#' . $videoClip->id . ' – ' . $videoClip->title . 'by ' . $videoClip->user->username . '
' . format_duration((int) $videoClip->duration) . '
'; }, ], [ diff --git a/themes/cp_admin/episode/video_clips_new.php b/themes/cp_admin/episode/video_clips_new.php index 1c9b3796..b8683f6e 100644 --- a/themes/cp_admin/episode/video_clips_new.php +++ b/themes/cp_admin/episode/video_clips_new.php @@ -39,17 +39,17 @@ name="format" isChecked="true" required="true" - hint=""> + hint=""> + hint=""> + hint="">
@@ -61,7 +61,7 @@ name="theme" required="true" isChecked="" - style="--color-accent-base: "> + style="--color-accent-base: ; --color-background-preview: ">
diff --git a/writable/temp/index.html b/writable/temp/index.html new file mode 100644 index 00000000..eebf8ecb --- /dev/null +++ b/writable/temp/index.html @@ -0,0 +1,9 @@ + + + + 403 Forbidden + + +

Directory access is forbidden.

+ +