diff --git a/app/Libraries/MediaClipper/Config/MediaClipper.php b/app/Libraries/MediaClipper/Config/MediaClipper.php index 536e4a5e4b3292fd3adc20b102c7232d79f66759..5f5420d6ef23bdeb560cd433d1df6ead9e3695f9 100644 --- a/app/Libraries/MediaClipper/Config/MediaClipper.php +++ b/app/Libraries/MediaClipper/Config/MediaClipper.php @@ -37,7 +37,7 @@ class MediaClipper extends BaseConfig 'episodeTitle' => [ 'fontsize' => 32, 'x' => 150, - 'y' => 690, + 'y' => 660, 'lines' => 3, 'lineWidth' => 28, 'leading' => 20, @@ -45,20 +45,20 @@ class MediaClipper extends BaseConfig 'podcastTitle' => [ 'fontsize' => 20, 'x' => 150, - 'y' => 640, + 'y' => 620, ], 'episodeNumbering' => [ 'fontsize' => 18, 'paddingX' => 10, 'paddingY' => 5, - 'x' => 180 + 10, + 'x' => 180, 'y' => 540, ], 'timestamp' => [ 'fontsize' => 32, 'padding' => 10, - 'x' => 1678, - 'y' => 986, + 'x' => 1680, + 'y' => 985, ], 'progressbar' => [ 'height' => 10, @@ -70,7 +70,7 @@ class MediaClipper extends BaseConfig 'rescaleHeight' => 540, 'x' => 0, 'y' => 810, - 'mask' => APPPATH . 'Libraries/MediaClipper/waves-mask.png', + 'mask' => APPPATH . 'Libraries/MediaClipper/soundwaves-mask-landscape.png', ], 'subtitles' => [ 'fontsize' => 18, @@ -82,10 +82,124 @@ class MediaClipper extends BaseConfig 'portrait' => [ 'width' => 1080, 'height' => 1920, + 'cover' => [ + 'width' => 280, + 'height' => 280, + 'radius' => 16, + 'x' => 50, + 'y' => 50, + ], + 'quotes' => [ + 'width' => 256, + 'height' => 256, + 'x' => 75, + 'y' => 520, + ], + 'episodeTitle' => [ + 'fontsize' => 42, + 'x' => 360, + 'y' => 110, + 'lines' => 3, + 'lineWidth' => 32, + 'leading' => 20, + ], + 'podcastTitle' => [ + 'fontsize' => 32, + 'x' => 360, + 'y' => 55, + ], + 'episodeNumbering' => [ + 'fontsize' => 28, + 'paddingX' => 0, + 'paddingY' => 10, + 'x' => 50, + 'y' => 330, + ], + 'timestamp' => [ + 'fontsize' => 48, + 'padding' => 10, + 'x' => 735, + 'y' => 1800, + ], + 'progressbar' => [ + 'height' => 10, + ], + 'soundwaves' => [ + 'width' => 54, + 'height' => 96, + 'rescaleWidth' => 1080, + 'rescaleHeight' => 1920, + 'x' => 0, + 'y' => 960, + 'mask' => APPPATH . 'Libraries/MediaClipper/soundwaves-mask-portrait.png', + ], + 'subtitles' => [ + 'fontsize' => 18, + 'marginL' => 60, + 'marginR' => 20, + 'marginV' => 97, + ], ], 'squared' => [ 'width' => 1200, 'height' => 1200, + 'cover' => [ + 'width' => 200, + 'height' => 200, + 'radius' => 16, + 'x' => 40, + 'y' => 40, + ], + 'quotes' => [ + 'width' => 200, + 'height' => 200, + 'x' => 85, + 'y' => 320, + ], + 'episodeTitle' => [ + 'fontsize' => 36, + 'x' => 260, + 'y' => 90, + 'lines' => 2, + 'lineWidth' => 38, + 'leading' => 20, + ], + 'podcastTitle' => [ + 'fontsize' => 28, + 'x' => 260, + 'y' => 50, + ], + 'episodeNumbering' => [ + 'fontsize' => 20, + 'paddingX' => 0, + 'paddingY' => 10, + 'x' => 40, + 'y' => 240, + ], + 'timestamp' => [ + 'fontsize' => 48, + 'padding' => 10, + 'x' => 855, + 'y' => 1070, + ], + 'progressbar' => [ + 'height' => 10, + ], + 'soundwaves' => [ + 'width' => 60, + 'height' => 60, + 'rescaleWidth' => 1200, + 'rescaleHeight' => 1200, + 'x' => 0, + 'y' => 600, + 'mask' => APPPATH . 'Libraries/MediaClipper/soundwaves-mask-squared.png', + ], + 'subtitles' => [ + 'fontsize' => 20, + 'marginL' => 60, + 'marginR' => 20, + 'marginV' => 98, + ], ], ]; } diff --git a/app/Libraries/MediaClipper/VideoClip.php b/app/Libraries/MediaClipper/VideoClip.php index 257522df20a46d504c0af0e8832de30fd74e37fa..e2f7ad6e218662caecc11a6ff3ac4d6b30bb9329 100644 --- a/app/Libraries/MediaClipper/VideoClip.php +++ b/app/Libraries/MediaClipper/VideoClip.php @@ -28,6 +28,12 @@ class VideoClip protected float $duration; + protected string $audioInput; + + protected string $episodeCoverPath; + + protected ?string $subtitlesInput = null; + protected string $soundbiteOutput; protected string $subtitlesClipOutput; @@ -65,27 +71,30 @@ class VideoClip helper('media'); + $this->audioInput = media_path($this->episode->audio_file_path); + $this->episodeCoverPath = media_path($this->episode->cover->path); + if ($this->episode->transcript_file_path !== null) { + $this->subtitlesInput = media_path($this->episode->transcript_file_path); + } + $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}.png"; - $this->videoClipOutput = $podcastFolder . "/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}.mp4"; + $this->videoClipOutput = $podcastFolder . "/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}.mp4"; } public function soundbite(): void { - $audioInput = media_path($this->episode->audio_file_path); - $soundbiteCmd = "ffmpeg -y -ss {$this->start} -t {$this->duration} -i {$audioInput} {$this->soundbiteOutput}"; + $soundbiteCmd = "ffmpeg -y -ss {$this->start} -t {$this->duration} -i {$this->audioInput} {$this->soundbiteOutput}"; exec($soundbiteCmd); } public function subtitlesClip(): void { - if ($this->episode->transcript_file_path !== null) { - $srtFileInput = media_path($this->episode->transcript_file_path); - - $subtitleClipCmd = "ffmpeg -y -i {$srtFileInput} -ss {$this->start} -t {$this->duration} {$this->subtitlesClipOutput}"; + if ($this->subtitlesInput) { + $subtitleClipCmd = "ffmpeg -y -i {$this->subtitlesInput} -ss {$this->start} -t {$this->duration} {$this->subtitlesClipOutput}"; exec($subtitleClipCmd); } } @@ -181,7 +190,7 @@ class VideoClip return false; } - $episodeCover = imagecreatefromjpeg(media_path($this->episode->cover->path)); + $episodeCover = imagecreatefromjpeg($this->episodeCoverPath); if (! $episodeCover) { return false; } @@ -431,11 +440,20 @@ class VideoClip $lines = explode(PHP_EOL, $text); foreach ($lines as $i => $line) { // Print line On Image - imagettftext($image, $fontsize, 0, $x, $y + (($fontsize + $leading) * $i), $white, $fontPath, $line); + imagettftext( + $image, + $fontsize, + 0, + $x, + $y + $fontsize + (($fontsize + $leading) * $i), + $white, + $fontPath, + $line + ); } } else { // Print Text On Image - imagettftext($image, $fontsize, 0, $x, $y, $white, $fontPath, $text); + imagettftext($image, $fontsize, 0, $x, $y + $fontsize, $white, $fontPath, $text); } return true; @@ -465,12 +483,12 @@ class VideoClip return false; } - $x1 = $x + $bbox['left']; - $y1 = $y + $bbox['top']; - $x2 = $x + $bbox['width'] + $paddingX; - $y2 = $y + $bbox['height'] + $paddingY; + $x1 = $x + $bbox['left'] + $paddingX; + $y1 = $y + $bbox['top'] + $paddingY; + $x2 = $x + $bbox['width'] + ($paddingX * 2); + $y2 = $y + $bbox['height'] + ($paddingY * 2); - imagefilledrectangle($image, $x - $paddingX, $y - $paddingY, $x2, $y2, $bgColor); + imagefilledrectangle($image, $x, $y, $x2, $y2, $bgColor); imagettftext($image, $fontsize, 0, $x1, $y1, $white, $fontPath, $text); return true; diff --git a/app/Libraries/MediaClipper/waves-mask.png b/app/Libraries/MediaClipper/soundwaves-mask-landscape.png similarity index 100% rename from app/Libraries/MediaClipper/waves-mask.png rename to app/Libraries/MediaClipper/soundwaves-mask-landscape.png diff --git a/app/Libraries/MediaClipper/soundwaves-mask-portrait.png b/app/Libraries/MediaClipper/soundwaves-mask-portrait.png new file mode 100644 index 0000000000000000000000000000000000000000..67a2bfb0ad8b4ceab5fe8bbfcc01523c17422e33 Binary files /dev/null and b/app/Libraries/MediaClipper/soundwaves-mask-portrait.png differ diff --git a/app/Libraries/MediaClipper/soundwaves-mask-squared.png b/app/Libraries/MediaClipper/soundwaves-mask-squared.png new file mode 100644 index 0000000000000000000000000000000000000000..4c64e69a452b1b710b0feaf2dfe56b75351c4fa3 Binary files /dev/null and b/app/Libraries/MediaClipper/soundwaves-mask-squared.png differ diff --git a/modules/Admin/Controllers/ClipsController.php b/modules/Admin/Controllers/ClipsController.php index f93ffead1290b0b3a58e303835ec5ace5e7eac4e..cfb9eb4d2d88f0cc9ed73e1a6f0fa9554e069207 100644 --- a/modules/Admin/Controllers/ClipsController.php +++ b/modules/Admin/Controllers/ClipsController.php @@ -73,10 +73,11 @@ class ClipsController extends BaseController public function generateVideoClip(): RedirectResponse { + // TODO: add end_time greater than start_time, with minimum ? $rules = [ - 'format' => 'required', - 'start_time' => 'required', - 'end_time' => 'required', + 'format' => 'required|in_list[landscape,portrait,squared]', + 'start_time' => 'required|numeric', + 'end_time' => 'required|numeric|differs[start_time]', ]; if (! $this->validate($rules)) { @@ -86,15 +87,11 @@ class ClipsController extends BaseController ->with('errors', $this->validator->getErrors()); } - // TODO: start and end - - helper('media'); - $clipper = new VideoClip( $this->episode, (float) $this->request->getPost('start_time'), (float) $this->request->getPost('end_time',), - 'landscape' + $this->request->getPost('format'), ); $clipper->generate(); diff --git a/themes/cp_admin/episode/video_clips.php b/themes/cp_admin/episode/video_clips.php index e64144c9cd7131fbb9bb0767d90fcc13b42c4bdc..314b060cbed9bad86c9ca750181ee69573d6ef56 100644 --- a/themes/cp_admin/episode/video_clips.php +++ b/themes/cp_admin/episode/video_clips.php @@ -15,16 +15,16 @@ <fieldset> <legend>Format</legend> <div class="mx-auto"> - <input type="radio" name="format" value="16:9" id="landscape"/> + <input type="radio" name="format" value="landscape" id="landscape" checked="checked"/> <label for="landscape">Landscape - 16:9</label> </div> <div class="mx-auto"> - <input type="radio" name="format" value="1:1" id="square" checked="checked"/> - <label for="square">Square - 1:1</label> + <input type="radio" name="format" value="portrait" id="portrait"/> + <label for="portrait">Portrait - 9:16</label> </div> <div class="mx-auto"> - <input type="radio" name="format" value="9:16" id="portrait"/> - <label for="portrait">Portrait - 9:16</label> + <input type="radio" name="format" value="squared" id="square"/> + <label for="square">Square - 1:1</label> </div> </fieldset>