mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(video-clips): replace hardcoded colors with config's theme colors
This commit is contained in:
parent
827ca03f61
commit
e462abf6d6
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -44,32 +44,46 @@ class VideoClip
|
||||
|
||||
protected ?string $episodeNumbering = null;
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $dimensions = [];
|
||||
|
||||
/**
|
||||
* @var 'landscape'|'portrait'|'squared'
|
||||
*/
|
||||
protected string $format = 'landscape';
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected array $dimensions = [];
|
||||
|
||||
/**
|
||||
* @var 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx'
|
||||
*/
|
||||
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);
|
||||
@ -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;
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.8 KiB |
6
app/Resources/icons/clapperboard.svg
Normal file
6
app/Resources/icons/clapperboard.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<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>
|
After Width: | Height: | Size: 345 B |
@ -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',
|
||||
|
@ -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')
|
||||
);
|
@ -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',
|
||||
];
|
||||
|
@ -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',
|
||||
];
|
||||
|
@ -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'],
|
||||
],
|
||||
]; ?>
|
||||
|
||||
|
13
themes/cp_admin/episode/video_clips_list.php
Normal file
13
themes/cp_admin/episode/video_clips_list.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Episode.video_clips.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Episode.video_clips.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
@ -28,23 +28,31 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="grid gap-4 grid-cols-colorButtons">
|
||||
<?php foreach (config('Colors')->themes as $themeName => $color): ?>
|
||||
<Forms.ColorRadioButton
|
||||
class="theme-<?= $themeName ?> mx-auto"
|
||||
value="<?= $themeName ?>"
|
||||
name="theme"
|
||||
isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<Forms.Field
|
||||
type="number"
|
||||
name="start_time"
|
||||
label="START"
|
||||
required="true"
|
||||
value="0"
|
||||
value="5"
|
||||
/>
|
||||
<Forms.Field
|
||||
type="number"
|
||||
name="end_time"
|
||||
label="END"
|
||||
required="true"
|
||||
value="15"
|
||||
value="10"
|
||||
/>
|
||||
|
||||
<audio></audio>
|
||||
|
||||
<Button variant="primary" type="submit"><?= lang('Episode.video_clips.submit') ?></Button>
|
||||
|
||||
</form>
|
Loading…
x
Reference in New Issue
Block a user