mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(video-clips): add dimensions for portrait and squared formats
This commit is contained in:
parent
35aa7ea5d9
commit
3af404da3d
@ -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,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -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;
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
app/Libraries/MediaClipper/soundwaves-mask-portrait.png
Normal file
BIN
app/Libraries/MediaClipper/soundwaves-mask-portrait.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
app/Libraries/MediaClipper/soundwaves-mask-squared.png
Normal file
BIN
app/Libraries/MediaClipper/soundwaves-mask-squared.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user