diff --git a/app/Libraries/MediaClipper/Config/MediaClipper.php b/app/Libraries/MediaClipper/Config/MediaClipper.php index 52702da6..c4571851 100644 --- a/app/Libraries/MediaClipper/Config/MediaClipper.php +++ b/app/Libraries/MediaClipper/Config/MediaClipper.php @@ -203,4 +203,24 @@ class MediaClipper extends BaseConfig ], ], ]; + + /** + * @var array> + */ + public array $themes = [ + 'pine' => [ + 'background' => [0, 86, 74], + 'text' => [255, 255, 255], + // subtitle hex color is BGR (Blue, Green, Red), + 'subtitles' => 'FFFFFF', + // quotes image MUST BE black + 'quotes' => [0, 148, 134], + 'episodeNumberingBg' => [0, 61, 11], + 'episodeNumberingText' => [255, 255, 255], + 'progressbar' => '009486', + 'timestampBg' => '00564A', + 'timestampText' => 'FFFFFF', + 'soundwaves' => 'F2FAF9', + ], + ]; } diff --git a/app/Libraries/MediaClipper/VideoClip.php b/app/Libraries/MediaClipper/VideoClip.php index 9be8e42f..909bd13e 100644 --- a/app/Libraries/MediaClipper/VideoClip.php +++ b/app/Libraries/MediaClipper/VideoClip.php @@ -44,32 +44,46 @@ class VideoClip protected ?string $episodeNumbering = null; - /** - * @var array - */ - protected array $dimensions = []; - /** * @var 'landscape'|'portrait'|'squared' */ protected string $format = 'landscape'; + /** + * @var array + */ + protected array $dimensions = []; + + /** + * @var 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx' + */ + protected string $theme = 'pine'; + + /** + * @var array + */ + protected array $colors = []; + /** * @param 'landscape'|'portrait'|'squared' $format + * @param 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx' $theme */ public function __construct( protected Episode $episode, protected float $start, protected float $end, string $format, + string $theme, ) { $this->duration = $end - $start; $this->format = $format; $this->episodeNumbering = $this->episodeNumbering($this->episode->number, $this->episode->season_number); $this->dimensions = config('MediaClipper') ->formats[$format]; + $this->colors = config('MediaClipper') + ->themes[$theme]; - helper('media'); + helper(['media']); $this->audioInput = media_path($this->episode->audio_file_path); $this->episodeCoverPath = media_path($this->episode->cover->path); @@ -118,7 +132,7 @@ class VideoClip { // @phpstan-ignore $filters = [ - "[0:a]aformat=channel_layouts=mono,showwaves=s={$this->dimensions['soundwaves']['width']}x{$this->dimensions['soundwaves']['height']}:mode=cline:rate=10:colors=white,format=yuva420p[waves]", + "[0:a]aformat=channel_layouts=mono,showwaves=s={$this->dimensions['soundwaves']['width']}x{$this->dimensions['soundwaves']['height']}:mode=cline:rate=10:colors=0xFFFFFF,format=yuva420p[waves]", "[waves]scale={$this->dimensions['width']}:{$this->dimensions['height']}:flags=neighbor[resizedwaves]", '[resizedwaves][3:v][4:v][5:v]threshold[cleanwaves]', '[cleanwaves][2:v]alphamerge[waves_t]', @@ -128,11 +142,11 @@ class VideoClip "[waves_t3]scale={$this->dimensions['soundwaves']['rescaleWidth']}:{$this->dimensions['soundwaves']['rescaleHeight']}[waves_final]", "[1:v][waves_final]overlay=x={$this->dimensions['soundwaves']['x']}:y={$this->dimensions['soundwaves']['y']}:shortest=1,drawtext=fontfile=" . $this->getFont( 'timestamp' - ) . ":text='%{pts\:gmtime\:{$this->start}\:%H\\\\\\\\\\:%M\\\\\\\\\\:%S\}':x={$this->dimensions['timestamp']['x']}:y={$this->dimensions['timestamp']['y']}:fontsize={$this->dimensions['timestamp']['fontsize']}:fontcolor=white:box=1:boxcolor=0x00564A:boxborderw={$this->dimensions['timestamp']['padding']}[v3]", - "color=c=0x009486:s={$this->dimensions['width']}x{$this->dimensions['progressbar']['height']}[progressbar]", + ) . ":text='%{pts\:gmtime\:{$this->start}\:%H\\\\\\\\\\:%M\\\\\\\\\\:%S\}':x={$this->dimensions['timestamp']['x']}:y={$this->dimensions['timestamp']['y']}:fontsize={$this->dimensions['timestamp']['fontsize']}:fontcolor=0x{$this->colors['timestampText']}:box=1:boxcolor=0x{$this->colors['timestampBg']}:boxborderw={$this->dimensions['timestamp']['padding']},format=yuv420p,colormatrix=bt601:bt2020[v3]", + "color=c=0x{$this->colors['progressbar']}:s={$this->dimensions['width']}x{$this->dimensions['progressbar']['height']}[progressbar]", "[v3][progressbar]overlay=-w+(w/{$this->duration})*t:0:shortest=1:format=rgb,subtitles={$this->subtitlesClipOutput}:fontsdir=" . config( 'MediaClipper' - )->fontsFolder . ":force_style='Fontname=" . self::FONTS['subtitles'] . ",Alignment=5,Fontsize={$this->dimensions['subtitles']['fontsize']},BorderStyle=1,Outline=0,Shadow=0,MarginL={$this->dimensions['subtitles']['marginL']},MarginR={$this->dimensions['subtitles']['marginR']},MarginV={$this->dimensions['subtitles']['marginV']}'[outv]", + )->fontsFolder . ":force_style='Fontname=" . self::FONTS['subtitles'] . ",Alignment=5,Fontsize={$this->dimensions['subtitles']['fontsize']},PrimaryColour=&H{$this->colors['subtitles']}&,BorderStyle=1,Outline=0,Shadow=0,MarginL={$this->dimensions['subtitles']['marginL']},MarginR={$this->dimensions['subtitles']['marginR']},MarginV={$this->dimensions['subtitles']['marginV']}',format=yuv420p,colormatrix=bt601:bt2020[outv]", ]; $videoClipCmd = [ @@ -142,12 +156,13 @@ class VideoClip "-loop 1 -framerate 30 -i {$this->dimensions['soundwaves']['mask']}", "-f lavfi -i color=gray:{$this->dimensions['width']}x{$this->dimensions['height']}", "-f lavfi -i color=black:{$this->dimensions['width']}x{$this->dimensions['height']}", - "-f lavfi -i color=white:{$this->dimensions['width']}x{$this->dimensions['height']}", + "-f lavfi -i color=0x{$this->colors['soundwaves']}:{$this->dimensions['width']}x{$this->dimensions['height']}", '-filter_complex "' . implode(';', $filters) . '"', '-map "[outv]"', '-map 0:a', '-acodec copy', '-vcodec libx264', + '-pix_fmt yuv420p', "{$this->videoClipOutput}", ]; @@ -184,7 +199,7 @@ class VideoClip private function generateVideoClipBg(): bool { - $background = $this->generateColouredBg($this->dimensions['width'], $this->dimensions['height']); + $background = $this->generateBackground($this->dimensions['width'], $this->dimensions['height']); if ($background === null) { return false; @@ -265,6 +280,8 @@ class VideoClip $this->episodeNumbering, $this->getFont('episodeNumbering'), $this->dimensions['episodeNumbering']['fontsize'], + $this->colors['episodeNumberingText'], + $this->colors['episodeNumberingBg'], $this->dimensions['episodeNumbering']['paddingX'], $this->dimensions['episodeNumbering']['paddingY'], ); @@ -289,6 +306,8 @@ class VideoClip return false; } + imagefilter($quotes, IMG_FILTER_COLORIZE, ...$this->colors['quotes']); + $scaledQuotes = $this->scaleImage( $quotes, $this->dimensions['quotes']['width'], @@ -319,7 +338,7 @@ class VideoClip return config('MediaClipper')->fontsFolder . self::FONTS[$name]; } - private function generateColouredBg(int $width, int $height): ?GdImage + private function generateBackground(int $width, int $height): ?GdImage { $background = imagecreatetruecolor($width, $height); @@ -327,7 +346,7 @@ class VideoClip return null; } - $coloredBackground = imagecolorallocate($background, 0, 86, 74); + $coloredBackground = imagecolorallocate($background, ...$this->colors['background']); if ($coloredBackground === false) { return null; @@ -450,9 +469,9 @@ class VideoClip int $paragraphIndent = 0, ): bool { // Allocate A Color For The Text - $white = imagecolorallocate($image, 255, 255, 255); + $textColor = imagecolorallocate($image, ...$this->colors['text']); - if ($white === false) { + if ($textColor === false) { return false; } @@ -470,7 +489,7 @@ class VideoClip 0, $x + ($paragraphIndent * ($i === 0 ? 1 : 0)), $y + $fontsize + ($leading * $i), - $white, + $textColor, $fontPath, $line ); @@ -510,6 +529,10 @@ class VideoClip ]; } + /** + * @param int[] $boxTextColor + * @param int[] $boxBgColor + */ private function addTextWithBox( GdImage $image, int $x, @@ -517,14 +540,16 @@ class VideoClip string $text, string $fontPath, int $fontsize, + array $boxTextColor, + array $boxBgColor, int $paddingX = 0, int $paddingY = 0, ): bool { // Create some colors - $white = imagecolorallocate($image, 255, 255, 255); - $bgColor = imagecolorallocate($image, 0, 61, 11); + $textColor = imagecolorallocate($image, ...$boxTextColor); + $bgColor = imagecolorallocate($image, ...$boxBgColor); - if ($white === false || $bgColor === false) { + if ($textColor === false || $bgColor === false) { return false; } @@ -540,7 +565,7 @@ class VideoClip $y2 = $y + $bbox['height'] + ($paddingY * 2); imagefilledrectangle($image, $x, $y, $x2, $y2, $bgColor); - imagettftext($image, $fontsize, 0, $x1, $y1, $white, $fontPath, $text); + imagettftext($image, $fontsize, 0, $x1, $y1, $textColor, $fontPath, $text); return true; } diff --git a/app/Libraries/MediaClipper/quotes.png b/app/Libraries/MediaClipper/quotes.png index 2c652428..41f22cac 100644 Binary files a/app/Libraries/MediaClipper/quotes.png and b/app/Libraries/MediaClipper/quotes.png differ diff --git a/app/Resources/icons/clapperboard.svg b/app/Resources/icons/clapperboard.svg new file mode 100644 index 00000000..c5d7d121 --- /dev/null +++ b/app/Resources/icons/clapperboard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/modules/Admin/Config/Routes.php b/modules/Admin/Config/Routes.php index 469859db..59233f61 100644 --- a/modules/Admin/Config/Routes.php +++ b/modules/Admin/Config/Routes.php @@ -352,15 +352,23 @@ $routes->group( ); $routes->get( 'video-clips', - 'ClipsController::videoClips/$1/$2', + 'VideoClipsController::list/$1/$2', [ - 'as' => 'video-clips', + 'as' => 'video-clips-list', + 'filter' => 'permission:podcast_episodes-edit', + ], + ); + $routes->get( + 'video-clips/new', + 'VideoClipsController::generate/$1/$2', + [ + 'as' => 'video-clips-generate', 'filter' => 'permission:podcast_episodes-edit', ], ); $routes->post( - 'video-clips', - 'ClipsController::generateVideoClip/$1/$2', + 'video-clips/new', + 'VideoClipsController::attemptGenerate/$1/$2', [ 'as' => 'video-clips-generate', 'filter' => 'permission:podcast_episodes-edit', diff --git a/modules/Admin/Controllers/ClipsController.php b/modules/Admin/Controllers/VideoClipsController.php similarity index 71% rename from modules/Admin/Controllers/ClipsController.php rename to modules/Admin/Controllers/VideoClipsController.php index cfb9eb4d..3520e286 100644 --- a/modules/Admin/Controllers/ClipsController.php +++ b/modules/Admin/Controllers/VideoClipsController.php @@ -18,7 +18,7 @@ use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\RedirectResponse; use MediaClipper\VideoClip; -class ClipsController extends BaseController +class VideoClipsController extends BaseController { protected Podcast $podcast; @@ -55,7 +55,21 @@ class ClipsController extends BaseController return $this->{$method}(...$params); } - public function videoClips(): string + public function list(): string + { + $data = [ + 'podcast' => $this->podcast, + 'episode' => $this->episode, + ]; + + replace_breadcrumb_params([ + 0 => $this->podcast->title, + 1 => $this->episode->title, + ]); + return view('episode/video_clips_list', $data); + } + + public function generate(): string { helper('form'); @@ -66,18 +80,19 @@ class ClipsController extends BaseController replace_breadcrumb_params([ 0 => $this->podcast->title, - 1 => $this->episode->slug, + 1 => $this->episode->title, ]); - return view('episode/video_clips', $data); + return view('episode/video_clips_new', $data); } - public function generateVideoClip(): RedirectResponse + public function attemptGenerate(): RedirectResponse { // TODO: add end_time greater than start_time, with minimum ? $rules = [ - 'format' => 'required|in_list[landscape,portrait,squared]', 'start_time' => 'required|numeric', 'end_time' => 'required|numeric|differs[start_time]', + 'format' => 'required|in_list[' . implode(',', array_keys(config('MediaClipper')->formats)) . ']', + 'theme' => 'required|in_list[' . implode(',', array_keys(config('Colors')->themes)) . ']', ]; if (! $this->validate($rules)) { @@ -92,10 +107,11 @@ class ClipsController extends BaseController (float) $this->request->getPost('start_time'), (float) $this->request->getPost('end_time',), $this->request->getPost('format'), + $this->request->getPost('theme'), ); $clipper->generate(); - return redirect()->route('video-clips', [$this->podcast->id, $this->episode->id])->with( + return redirect()->route('video-clips-generate', [$this->podcast->id, $this->episode->id])->with( 'message', lang('Settings.images.regenerationSuccess') ); diff --git a/modules/Admin/Language/en/EpisodeNavigation.php b/modules/Admin/Language/en/EpisodeNavigation.php index c7f7dba3..8e2df5be 100644 --- a/modules/Admin/Language/en/EpisodeNavigation.php +++ b/modules/Admin/Language/en/EpisodeNavigation.php @@ -15,6 +15,8 @@ return [ 'episode-edit' => 'Edit episode', 'episode-persons-manage' => 'Manage persons', 'embed-add' => 'Embeddable player', + 'clips' => 'Clips', 'soundbites-edit' => 'Soundbites', - 'video-clips' => 'Video clips', + 'video-clips-list' => 'Video clips', + 'video-clips-generate' => 'New video clip', ]; diff --git a/modules/Admin/Language/fr/EpisodeNavigation.php b/modules/Admin/Language/fr/EpisodeNavigation.php index d85c74d2..288b9203 100644 --- a/modules/Admin/Language/fr/EpisodeNavigation.php +++ b/modules/Admin/Language/fr/EpisodeNavigation.php @@ -15,5 +15,8 @@ return [ 'episode-edit' => 'Modifier l’épisode', 'episode-persons-manage' => 'Gestion des intervenants', 'embed' => 'Lecteur intégré', + 'clips' => 'Extraits', 'soundbites-edit' => 'Extraits sonores', + 'video-clips-list' => 'Extraits video', + 'video-clips-generate' => 'Nouvel extrait video', ]; diff --git a/themes/cp_admin/episode/_sidebar.php b/themes/cp_admin/episode/_sidebar.php index 32332db0..9fbcf493 100644 --- a/themes/cp_admin/episode/_sidebar.php +++ b/themes/cp_admin/episode/_sidebar.php @@ -3,7 +3,11 @@ $podcastNavigation = [ 'dashboard' => [ 'icon' => 'dashboard', - 'items' => ['episode-view', 'episode-edit', 'episode-persons-manage', 'embed-add', 'soundbites-edit', 'video-clips'], + 'items' => ['episode-view', 'episode-edit', 'episode-persons-manage', 'embed-add'], + ], + 'clips' => [ + 'icon' => 'clapperboard', + 'items' => ['video-clips-list', 'video-clips-generate', 'soundbites-edit'], ], ]; ?> diff --git a/themes/cp_admin/episode/video_clips_list.php b/themes/cp_admin/episode/video_clips_list.php new file mode 100644 index 00000000..f357a622 --- /dev/null +++ b/themes/cp_admin/episode/video_clips_list.php @@ -0,0 +1,13 @@ +extend('_layout') ?> + +section('title') ?> + +endSection() ?> + +section('pageTitle') ?> + +endSection() ?> + +section('content') ?> + +endSection() ?> diff --git a/themes/cp_admin/episode/video_clips.php b/themes/cp_admin/episode/video_clips_new.php similarity index 73% rename from themes/cp_admin/episode/video_clips.php rename to themes/cp_admin/episode/video_clips_new.php index 314b060c..abcb48b2 100644 --- a/themes/cp_admin/episode/video_clips.php +++ b/themes/cp_admin/episode/video_clips_new.php @@ -28,23 +28,31 @@ +
+ themes as $themeName => $color): ?> + + +
+ - -