feat(video-clips): add dimensions for portrait and squared formats

This commit is contained in:
Yassine Doghri 2021-12-03 17:00:15 +00:00
parent 35aa7ea5d9
commit 3af404da3d
7 changed files with 163 additions and 34 deletions

View File

@ -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,
],
],
];
}

View File

@ -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;

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -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();

View File

@ -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>