Refactor bumper preview functionality: update API endpoints to support rendering previews for all bumper sub-blocks, modify related query and handler to generate individual asset previews, and enhance frontend components for improved user experience with variant-specific previews. Update translations for consistency in terminology.
This commit is contained in:
@@ -47,7 +47,7 @@ import {
|
||||
removeBumperVariant,
|
||||
removeChannelAd,
|
||||
removeChannelShow,
|
||||
renderBumperPreview,
|
||||
renderBumperPreviews,
|
||||
setBumperTemplateBackground,
|
||||
updateBumperTemplate,
|
||||
updateBumperVariant,
|
||||
@@ -694,7 +694,12 @@ function BumperTemplateEditor({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
<BumperPreviewPlayer channelId={channelId} templateId={template.id} onError={onError} />
|
||||
<BumperPreviewPlayer
|
||||
channelId={channelId}
|
||||
templateId={template.id}
|
||||
variants={template.variants}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
@@ -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<HTMLVideoElement>(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 (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -933,17 +915,56 @@ function BumperPreviewPlayer({
|
||||
</span>
|
||||
</div>
|
||||
{ready && (
|
||||
<video
|
||||
ref={videoRef}
|
||||
controls
|
||||
playsInline
|
||||
className="aspect-video w-full max-w-sm rounded-md border border-border bg-black"
|
||||
/>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{[...variants]
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((v) => (
|
||||
<div key={v.id} className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">{v.name}</span>
|
||||
<PreviewVideo
|
||||
src={`${bumperPreviewPlaylistUrl(channelId, templateId, v.id)}?t=${bust}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** Мини-плеер одного превью: грузит HLS через hls.js с Bearer-токеном (admin-роут под JWT). */
|
||||
function PreviewVideo({ src }: { src: string }) {
|
||||
const videoRef = useRef<HTMLVideoElement>(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 (
|
||||
<video
|
||||
ref={videoRef}
|
||||
controls
|
||||
playsInline
|
||||
className="aspect-video w-full rounded-md border border-border bg-black"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BumperBackgroundField({
|
||||
channelId,
|
||||
templateId,
|
||||
|
||||
@@ -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<void>(`/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 = {
|
||||
|
||||
Reference in New Issue
Block a user