@@ -871,19 +876,20 @@ function BumperVariantEditor({
function BumperPreviewPlayer({
channelId,
templateId,
+ variants,
onError,
}: {
channelId: string
templateId: string
+ variants: BumperTextVariantDto[]
onError: (e: unknown) => void
}) {
const { t } = useTranslation()
- const videoRef = useRef
(null)
const [ready, setReady] = useState(false)
const [bust, setBust] = useState(0)
const render = useMutation({
- mutationFn: () => renderBumperPreview(channelId, templateId),
+ mutationFn: () => renderBumperPreviews(channelId, templateId),
onSuccess: () => {
setBust(Date.now())
setReady(true)
@@ -891,30 +897,6 @@ function BumperPreviewPlayer({
onError,
})
- // Грузим отрендеренный превью-плейлист через hls.js, добавляя Bearer-токен (admin-роут под JWT).
- useEffect(() => {
- if (!ready) return
- const video = videoRef.current
- if (!video) return
- const src = `${bumperPreviewPlaylistUrl(channelId, templateId)}?t=${bust}`
- let hls: Hls | null = null
- if (Hls.isSupported()) {
- hls = new Hls({
- xhrSetup: (xhr) => {
- const token = getAccessToken()
- if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`)
- },
- })
- hls.loadSource(src)
- hls.attachMedia(video)
- } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
- video.src = src
- }
- return () => {
- hls?.destroy()
- }
- }, [ready, bust, channelId, templateId])
-
return (
<>
@@ -933,17 +915,56 @@ function BumperPreviewPlayer({
{ready && (
-
+
+ {[...variants]
+ .sort((a, b) => a.position - b.position)
+ .map((v) => (
+
+ ))}
+
)}
>
)
}
+/** Мини-плеер одного превью: грузит HLS через hls.js с Bearer-токеном (admin-роут под JWT). */
+function PreviewVideo({ src }: { src: string }) {
+ const videoRef = useRef(null)
+ useEffect(() => {
+ const video = videoRef.current
+ if (!video) return
+ let hls: Hls | null = null
+ if (Hls.isSupported()) {
+ hls = new Hls({
+ xhrSetup: (xhr) => {
+ const token = getAccessToken()
+ if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`)
+ },
+ })
+ hls.loadSource(src)
+ hls.attachMedia(video)
+ } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
+ video.src = src
+ }
+ return () => {
+ hls?.destroy()
+ }
+ }, [src])
+ return (
+
+ )
+}
+
function BumperBackgroundField({
channelId,
templateId,
diff --git a/frontend/src/features/admin/channels/api.ts b/frontend/src/features/admin/channels/api.ts
index e6a74b5..bd58017 100644
--- a/frontend/src/features/admin/channels/api.ts
+++ b/frontend/src/features/admin/channels/api.ts
@@ -195,15 +195,15 @@ export function clearBumperTemplateBackground(id: string, templateId: string) {
})
}
-/** Синхронно рендерит пример заставки блока (сервер собирает ffmpeg-клип). */
-export function renderBumperPreview(id: string, templateId: string) {
+/** Синхронно рендерит примеры всех подблоков блока (сервер собирает ffmpeg-клипы). */
+export function renderBumperPreviews(id: string, templateId: string) {
return apiRequest(`/admin/channels/${id}/bumper/templates/${templateId}/preview`, {
method: 'POST',
})
}
-export function bumperPreviewPlaylistUrl(id: string, templateId: string) {
- return `/api/admin/channels/${id}/bumper/templates/${templateId}/preview/index.m3u8`
+export function bumperPreviewPlaylistUrl(id: string, templateId: string, variantId: string) {
+ return `/api/admin/channels/${id}/bumper/templates/${templateId}/preview/${variantId}/index.m3u8`
}
export type OverrideBody = {
diff --git a/frontend/src/shared/lib/i18n.ts b/frontend/src/shared/lib/i18n.ts
index 2b7734e..53a6d20 100644
--- a/frontend/src/shared/lib/i18n.ts
+++ b/frontend/src/shared/lib/i18n.ts
@@ -239,9 +239,9 @@ const resources = {
bumperDefaultDuration: '≈8 с (джингл)',
bumperAudio: 'Звук',
bumperAudioHint: 'Звук заставки; иначе — синтезированный джингл',
- bumperPreview: 'Отрендерить пример',
+ bumperPreview: 'Отрендерить примеры',
bumperPreviewRendering: 'Рендерим…',
- bumperPreviewHint: 'Пример со звуком и анимацией (примерные названия шоу). Использует сохранённые настройки.',
+ bumperPreviewHint: 'Примеры всех подблоков со звуком и анимацией (примерные названия шоу). Использует сохранённые настройки.',
bumperBackground: 'Фон-картинка',
bumperBackgroundHint: 'Картинка фона; иначе — постер шоу или градиент',
bumperBackgroundPick: 'Выбрать из галереи',
@@ -559,9 +559,9 @@ const resources = {
bumperDefaultDuration: '≈8 s (jingle)',
bumperAudio: 'Sound',
bumperAudioHint: 'Bumper sound; otherwise a synthesized jingle',
- bumperPreview: 'Render sample',
+ bumperPreview: 'Render samples',
bumperPreviewRendering: 'Rendering…',
- bumperPreviewHint: 'Sample with sound and animation (example show names). Uses saved settings.',
+ bumperPreviewHint: 'Samples of all sub-blocks with sound and animation (example show names). Uses saved settings.',
bumperBackground: 'Background image',
bumperBackgroundHint: 'Background image; otherwise the show poster or a gradient',
bumperBackgroundPick: 'Pick from gallery',