mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(video-clip): add video-clip page with video preview + logs
This commit is contained in:
parent
2065ebbee5
commit
42538dd757
@ -12,6 +12,7 @@ namespace App\Entities\Clip;
|
||||
|
||||
use App\Entities\Episode;
|
||||
use App\Entities\Media\Audio;
|
||||
use App\Entities\Media\BaseMedia;
|
||||
use App\Entities\Media\Video;
|
||||
use App\Entities\Podcast;
|
||||
use App\Models\EpisodeModel;
|
||||
@ -44,6 +45,11 @@ use Modules\Auth\Entities\User;
|
||||
*/
|
||||
class BaseClip extends Entity
|
||||
{
|
||||
/**
|
||||
* @var BaseMedia
|
||||
*/
|
||||
protected $media = null;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
@ -122,12 +128,16 @@ class BaseClip extends Entity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMedia(): Audio | Video
|
||||
/**
|
||||
* @noRector ReturnTypeDeclarationRector
|
||||
*/
|
||||
public function getMedia(): Audio | Video | null
|
||||
{
|
||||
if ($this->media_id !== null && $this->media === null) {
|
||||
$this->media = (new MediaModel($this->type))->getMediaById($this->media_id);
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return $this->media;
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,8 @@ class VideoClip extends BaseClip
|
||||
return $this;
|
||||
}
|
||||
|
||||
$file = new File($filePath);
|
||||
helper('media');
|
||||
$file = new File(media_path($filePath));
|
||||
|
||||
$video = new Video([
|
||||
'file_path' => $filePath,
|
||||
|
@ -35,7 +35,9 @@ class VideoClipper
|
||||
|
||||
public bool $error = false;
|
||||
|
||||
public string $videoClipOutput;
|
||||
public string $videoClipFilePath;
|
||||
|
||||
protected string $videoClipOutput;
|
||||
|
||||
protected float $duration;
|
||||
|
||||
@ -95,6 +97,7 @@ class VideoClipper
|
||||
$this->subtitlesClipOutput = $podcastFolder . "/{$this->episode->slug}-subtitles-clip-{$this->start}-to-{$this->end}.srt";
|
||||
$this->videoClipBgOutput = $podcastFolder . "/{$this->episode->slug}-clip-bg-{$this->format}-{$this->theme}.png";
|
||||
$this->videoClipOutput = $podcastFolder . "/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4";
|
||||
$this->videoClipFilePath = "podcasts/{$this->episode->podcast->handle}/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4";
|
||||
}
|
||||
|
||||
public function soundbite(): void
|
||||
@ -152,6 +155,7 @@ class VideoClipper
|
||||
"color=0x{$this->colors['watermarkBg']}:{$this->dimensions['watermark']['width']}x{$this->dimensions['watermark']['height']}[over]",
|
||||
'[over][watermark]overlay=x=0:y=0:shortest=1[watermark_box]',
|
||||
"[outv][watermark_box]overlay=x={$this->dimensions['watermark']['x']}:y={$this->dimensions['watermark']['y']}:shortest=1[watermarked]",
|
||||
'[watermarked]scale=w=-1:h=-1:out_color_matrix=bt709[outfinal]',
|
||||
];
|
||||
|
||||
$watermark = config('MediaClipper')
|
||||
@ -167,10 +171,10 @@ class VideoClipper
|
||||
"-f lavfi -i color=white:{$this->dimensions['width']}x{$this->dimensions['height']}",
|
||||
"-loop 1 -framerate 1 -i {$watermark}",
|
||||
'-filter_complex "' . implode(';', $filters) . '"',
|
||||
'-map "[watermarked]"',
|
||||
'-map "[outfinal]"',
|
||||
'-map 0:a',
|
||||
'-acodec copy',
|
||||
'-vcodec libx264rgb',
|
||||
'-vcodec libx264 -pix_fmt yuv420p',
|
||||
"{$this->videoClipOutput}",
|
||||
];
|
||||
|
||||
|
@ -114,6 +114,26 @@ class ClipModel extends Model
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getVideoClipById(int $videoClipId): ?VideoClip
|
||||
{
|
||||
$cacheName = "video-clip#{$videoClipId}";
|
||||
if (! ($found = cache($cacheName))) {
|
||||
$clip = $this->find($videoClipId);
|
||||
|
||||
if ($clip === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$found = new VideoClip($clip->toArray());
|
||||
|
||||
cache()
|
||||
->save($cacheName, $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all video clips for an episode
|
||||
*
|
||||
|
@ -21,7 +21,7 @@ class Alert extends Component
|
||||
{
|
||||
$variantClasses = [
|
||||
'default' => 'text-gray-800 bg-gray-100 border-gray-300',
|
||||
'success' => 'text-pine-900 bg-pine-100 border-castopod-300',
|
||||
'success' => 'text-pine-900 bg-pine-100 border-pine-300',
|
||||
'danger' => 'text-red-900 bg-red-100 border-red-300',
|
||||
'warning' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||
];
|
||||
|
@ -22,7 +22,7 @@ class Pill extends Component
|
||||
$variantClasses = [
|
||||
'default' => 'text-gray-800 bg-gray-100 border-gray-300',
|
||||
'primary' => 'text-accent-contrast bg-accent-base border-accent-base',
|
||||
'success' => 'text-pine-900 bg-pine-100 border-castopod-300',
|
||||
'success' => 'text-pine-900 bg-pine-100 border-pine-300',
|
||||
'danger' => 'text-red-900 bg-red-100 border-red-300',
|
||||
'warning' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||
];
|
||||
@ -30,7 +30,7 @@ class Pill extends Component
|
||||
$icon = $this->icon ? icon($this->icon) : '';
|
||||
|
||||
return <<<HTML
|
||||
<span class="inline-flex items-center gap-x-1 px-1 font-semibold border rounded {$variantClasses[$this->variant]}">{$icon}{$this->slot}</span>
|
||||
<span class="inline-flex items-center gap-x-1 px-1 font-semibold text-sm border rounded {$variantClasses[$this->variant]}">{$icon}{$this->slot}</span>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
@ -379,6 +379,14 @@ $routes->group(
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'video-clips/(:num)',
|
||||
'VideoClipsController::view/$1/$2/$3',
|
||||
[
|
||||
'as' => 'video-clip',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'embed',
|
||||
'EpisodeController::embed/$1/$2',
|
||||
|
@ -53,7 +53,7 @@ class SchedulerController extends Controller
|
||||
|
||||
if ($exitCode === 0) {
|
||||
// success, video was generated
|
||||
$scheduledClip->setMedia($clipper->videoClipOutput);
|
||||
$scheduledClip->setMedia($clipper->videoClipFilePath);
|
||||
(new ClipModel())->update($scheduledClip->id, [
|
||||
'media_id' => $scheduledClip->media_id,
|
||||
'status' => 'passed',
|
||||
|
@ -87,6 +87,24 @@ class VideoClipsController extends BaseController
|
||||
return view('episode/video_clips_list', $data);
|
||||
}
|
||||
|
||||
public function view($videoClipId): string
|
||||
{
|
||||
$videoClip = (new ClipModel())->getVideoClipById((int) $videoClipId);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'videoClip' => $videoClip,
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
2 => $videoClip->label,
|
||||
]);
|
||||
return view('episode/video_clip', $data);
|
||||
}
|
||||
|
||||
public function generate(): string
|
||||
{
|
||||
helper('form');
|
||||
|
31
themes/cp_admin/episode/video_clip.php
Normal file
31
themes/cp_admin/episode/video_clip.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Episode.video_clips.title', [
|
||||
'videoClipLabel' => $videoClip->label,
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Episode.video_clips.title', [
|
||||
'videoClipLabel' => $videoClip->label,
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php if ($videoClip->media): ?>
|
||||
<video controls class="bg-black h-80 aspect-video">
|
||||
<source src="<?= $videoClip->media->file_url ?>" type="<?= $videoClip->media->file_mimetype ?>">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($videoClip->logs): ?>
|
||||
<details class="w-full mt-8 overflow-hidden text-white bg-black border rounded shadow-sm">
|
||||
<summary class="px-4 py-2 font-semibold text-black bg-white"><?= lang('VideoClip.logs') ?></summary>
|
||||
<pre class="p-4 text-sm whitespace-pre-wrap"><?= $videoClip->logs ?></pre>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
@ -39,17 +39,17 @@
|
||||
'header' => lang('VideoClip.list.label'),
|
||||
'cell' => function ($videoClip): string {
|
||||
$formatClass = [
|
||||
'landscape' => 'aspect-video h-4',
|
||||
'portrait' => 'aspect-[9/16] w-4',
|
||||
'squared' => 'aspect-square h-6',
|
||||
'landscape' => 'aspect-video',
|
||||
'portrait' => 'aspect-[9/16]',
|
||||
'squared' => 'aspect-square',
|
||||
];
|
||||
return '<a href="#" class="inline-flex items-center w-full hover:underline gap-x-2"><span class="block w-3 h-3 rounded-full" data-tooltip="bottom" title="' . $videoClip->theme['name'] . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><span class="flex items-center justify-center text-white bg-gray-400 rounded-sm ' . $formatClass[$videoClip->format] . '" data-tooltip="bottom" title="' . $videoClip->format . '"><Icon glyph="play"/></span>' . $videoClip->label . '</a>';
|
||||
return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center font-semibold hover:underline gap-x-2 focus:ring-accent"><div class="relative"><span class="absolute block w-3 h-3 rounded-full -bottom-1 -left-1" data-tooltip="bottom" title="' . $videoClip->theme['name'] . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . $videoClip->format . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '"><Icon glyph="play"/></span></div></div>' . $videoClip->label . '</a>';
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('VideoClip.list.clip_id'),
|
||||
'cell' => function ($videoClip): string {
|
||||
return '#' . $videoClip->id . ' by ' . $videoClip->user->username;
|
||||
return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="font-semibold hover:underline focus:ring-accent">#' . $videoClip->id . '</a><span class="ml-1 text-sm">by ' . $videoClip->user->username . '</span>';
|
||||
},
|
||||
],
|
||||
[
|
||||
|
Loading…
x
Reference in New Issue
Block a user