Loading app/Libraries/MediaClipper/Config/MediaClipper.php +20 −0 Original line number Diff line number Diff line Loading @@ -203,4 +203,24 @@ class MediaClipper extends BaseConfig ], ], ]; /** * @var array<string, array<string, string|int[]>> */ 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', ], ]; } app/Libraries/MediaClipper/VideoClip.php +43 −18 Original line number Diff line number Diff line Loading @@ -44,32 +44,46 @@ class VideoClip protected ?string $episodeNumbering = null; /** * @var 'landscape'|'portrait'|'squared' */ protected string $format = 'landscape'; /** * @var array<string, mixed> */ protected array $dimensions = []; /** * @var 'landscape'|'portrait'|'squared' * @var 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx' */ protected string $format = 'landscape'; protected string $theme = 'pine'; /** * @var array<string, mixed> */ 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); Loading Loading @@ -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]', Loading @@ -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 = [ Loading @@ -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}", ]; Loading Loading @@ -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; Loading Loading @@ -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'], ); Loading @@ -289,6 +306,8 @@ class VideoClip return false; } imagefilter($quotes, IMG_FILTER_COLORIZE, ...$this->colors['quotes']); $scaledQuotes = $this->scaleImage( $quotes, $this->dimensions['quotes']['width'], Loading Loading @@ -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); Loading @@ -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; Loading Loading @@ -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; } Loading @@ -470,7 +489,7 @@ class VideoClip 0, $x + ($paragraphIndent * ($i === 0 ? 1 : 0)), $y + $fontsize + ($leading * $i), $white, $textColor, $fontPath, $line ); Loading Loading @@ -510,6 +529,10 @@ class VideoClip ]; } /** * @param int[] $boxTextColor * @param int[] $boxBgColor */ private function addTextWithBox( GdImage $image, int $x, Loading @@ -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; } Loading @@ -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; } Loading app/Libraries/MediaClipper/quotes.png −1.39 KiB (2.76 KiB) Loading image diff... app/Resources/icons/clapperboard.svg 0 → 100644 +6 −0 Original line number Diff line number Diff line <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <g> <path fill="none" d="M0 0h24v24H0z"/> <path d="M17.998 7l2.31-4h.7c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h3.006l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31z"/> </g> </svg> modules/Admin/Config/Routes.php +12 −4 Original line number Diff line number Diff line Loading @@ -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', Loading Loading
app/Libraries/MediaClipper/Config/MediaClipper.php +20 −0 Original line number Diff line number Diff line Loading @@ -203,4 +203,24 @@ class MediaClipper extends BaseConfig ], ], ]; /** * @var array<string, array<string, string|int[]>> */ 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', ], ]; }
app/Libraries/MediaClipper/VideoClip.php +43 −18 Original line number Diff line number Diff line Loading @@ -44,32 +44,46 @@ class VideoClip protected ?string $episodeNumbering = null; /** * @var 'landscape'|'portrait'|'squared' */ protected string $format = 'landscape'; /** * @var array<string, mixed> */ protected array $dimensions = []; /** * @var 'landscape'|'portrait'|'squared' * @var 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx' */ protected string $format = 'landscape'; protected string $theme = 'pine'; /** * @var array<string, mixed> */ 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); Loading Loading @@ -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]', Loading @@ -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 = [ Loading @@ -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}", ]; Loading Loading @@ -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; Loading Loading @@ -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'], ); Loading @@ -289,6 +306,8 @@ class VideoClip return false; } imagefilter($quotes, IMG_FILTER_COLORIZE, ...$this->colors['quotes']); $scaledQuotes = $this->scaleImage( $quotes, $this->dimensions['quotes']['width'], Loading Loading @@ -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); Loading @@ -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; Loading Loading @@ -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; } Loading @@ -470,7 +489,7 @@ class VideoClip 0, $x + ($paragraphIndent * ($i === 0 ? 1 : 0)), $y + $fontsize + ($leading * $i), $white, $textColor, $fontPath, $line ); Loading Loading @@ -510,6 +529,10 @@ class VideoClip ]; } /** * @param int[] $boxTextColor * @param int[] $boxBgColor */ private function addTextWithBox( GdImage $image, int $x, Loading @@ -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; } Loading @@ -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; } Loading
app/Resources/icons/clapperboard.svg 0 → 100644 +6 −0 Original line number Diff line number Diff line <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <g> <path fill="none" d="M0 0h24v24H0z"/> <path d="M17.998 7l2.31-4h.7c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h3.006l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31z"/> </g> </svg>
modules/Admin/Config/Routes.php +12 −4 Original line number Diff line number Diff line Loading @@ -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', Loading