@flamingo-stack/openframe-frontend-core 0.0.178 → 0.0.179-snapshot.20260514181702
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-AAX27BCR.js → chunk-DV2GT7RI.js} +3703 -4168
- package/dist/chunk-DV2GT7RI.js.map +1 -0
- package/dist/{chunk-L4T24AN4.cjs → chunk-JFGORTXV.cjs} +868 -1333
- package/dist/chunk-JFGORTXV.cjs.map +1 -0
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/features/entity-video-section.d.ts +54 -0
- package/dist/components/features/entity-video-section.d.ts.map +1 -0
- package/dist/components/features/index.cjs +18 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +4 -2
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +21 -5
- package/dist/components/features/video-bites-display.d.ts +38 -0
- package/dist/components/features/video-bites-display.d.ts.map +1 -0
- package/dist/components/features/video-ratio-tabs.d.ts +62 -0
- package/dist/components/features/video-ratio-tabs.d.ts.map +1 -0
- package/dist/components/features/video.d.ts +94 -0
- package/dist/components/features/video.d.ts.map +1 -0
- package/dist/components/index.cjs +18 -2
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +21 -5
- package/dist/components/media-carousel.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +18 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +21 -5
- package/package.json +2 -2
- package/src/components/chat/chat-message-list.tsx +62 -18
- package/src/components/features/entity-video-section.tsx +175 -0
- package/src/components/features/index.ts +9 -2
- package/src/components/features/video-bites-display.tsx +216 -0
- package/src/components/features/video-ratio-tabs.tsx +174 -0
- package/src/components/features/video.tsx +474 -0
- package/src/components/media-carousel.tsx +43 -236
- package/src/components/shared/product-release/release-detail-page.tsx +26 -19
- package/dist/chunk-AAX27BCR.js.map +0 -1
- package/dist/chunk-L4T24AN4.cjs.map +0 -1
- package/dist/components/features/video-player.d.ts +0 -44
- package/dist/components/features/video-player.d.ts.map +0 -1
- package/dist/components/features/youtube-embed.d.ts +0 -31
- package/dist/components/features/youtube-embed.d.ts.map +0 -1
- package/dist/utils/lite-youtube-embed-stub.d.ts +0 -8
- package/dist/utils/lite-youtube-embed-stub.d.ts.map +0 -1
- package/dist/utils/lite-youtube-embed.d.ts +0 -9
- package/dist/utils/lite-youtube-embed.d.ts.map +0 -1
- package/src/components/features/.video-player.md +0 -44
- package/src/components/features/.youtube-embed.md +0 -40
- package/src/components/features/video-player.tsx +0 -893
- package/src/components/features/youtube-embed.tsx +0 -158
- package/src/utils/lite-youtube-embed-stub.tsx +0 -21
- package/src/utils/lite-youtube-embed.tsx +0 -46
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useRef, useEffect, memo, useCallback } from 'react';
|
|
4
4
|
import { cn } from "../utils/cn";
|
|
5
5
|
import { MediaItem } from '../utils/media-carousel-utils-stub';
|
|
6
|
-
import {
|
|
6
|
+
import { Video, extractYouTubeId } from './features/video';
|
|
7
7
|
|
|
8
8
|
// Navigation icons
|
|
9
9
|
const ChevronLeftIcon = () => (
|
|
@@ -18,17 +18,6 @@ const ChevronRightIcon = () => (
|
|
|
18
18
|
</svg>
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
const PlayIcon = () => (
|
|
22
|
-
<svg width="24" height="24" fill="white" viewBox="0 0 24 24">
|
|
23
|
-
<path d="M8 5v14l11-7z"/>
|
|
24
|
-
</svg>
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
function getYouTubeVideoId(url: string): string | null {
|
|
28
|
-
const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\n?#]+)/);
|
|
29
|
-
return match ? match[1] : null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
21
|
interface MediaCarouselProps {
|
|
33
22
|
media: MediaItem[];
|
|
34
23
|
className?: string;
|
|
@@ -40,71 +29,6 @@ interface MediaCarouselProps {
|
|
|
40
29
|
objectFit?: 'contain' | 'cover';
|
|
41
30
|
}
|
|
42
31
|
|
|
43
|
-
// Carousel-specific YouTube embed component
|
|
44
|
-
const CarouselYouTubeEmbed = ({ videoId, title }: { videoId: string; title: string }) => {
|
|
45
|
-
const [mounted, setMounted] = useState(false);
|
|
46
|
-
const [isActivated, setIsActivated] = useState(false);
|
|
47
|
-
const [isLoaded, setIsLoaded] = useState(false);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
setMounted(true);
|
|
51
|
-
}, []);
|
|
52
|
-
|
|
53
|
-
if (!mounted) {
|
|
54
|
-
return (
|
|
55
|
-
<div className="absolute inset-0 bg-black rounded-lg overflow-hidden">
|
|
56
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
57
|
-
<div className="text-white text-sm">Loading video...</div>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const thumbnailUrl = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`;
|
|
64
|
-
const embedUrl = `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&playsinline=1&rel=0&modestbranding=1`;
|
|
65
|
-
|
|
66
|
-
const handleActivate = () => {
|
|
67
|
-
setIsActivated(true);
|
|
68
|
-
setTimeout(() => setIsLoaded(true), 100);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div className="absolute inset-0 bg-black">
|
|
73
|
-
{!isActivated ? (
|
|
74
|
-
<button
|
|
75
|
-
onClick={handleActivate}
|
|
76
|
-
className="absolute inset-0 cursor-pointer group"
|
|
77
|
-
aria-label={`Play video: ${title}`}
|
|
78
|
-
>
|
|
79
|
-
<img
|
|
80
|
-
src={thumbnailUrl}
|
|
81
|
-
alt={title}
|
|
82
|
-
className="w-full h-full object-cover transition-opacity group-hover:opacity-90"
|
|
83
|
-
loading="lazy"
|
|
84
|
-
/>
|
|
85
|
-
<div className="absolute inset-0 flex items-center justify-center bg-black/20 group-hover:bg-black/30 transition-colors">
|
|
86
|
-
<div className="bg-red-600 rounded-full p-4 group-hover:scale-110 transition-transform">
|
|
87
|
-
<PlayIcon />
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
</button>
|
|
91
|
-
) : (
|
|
92
|
-
<div className="absolute inset-0">
|
|
93
|
-
{isLoaded && (
|
|
94
|
-
<iframe
|
|
95
|
-
src={embedUrl}
|
|
96
|
-
title={title}
|
|
97
|
-
className="w-full h-full"
|
|
98
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
99
|
-
allowFullScreen
|
|
100
|
-
/>
|
|
101
|
-
)}
|
|
102
|
-
</div>
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
32
|
export const MediaCarousel = memo(function MediaCarousel({
|
|
109
33
|
media,
|
|
110
34
|
className,
|
|
@@ -116,10 +40,21 @@ export const MediaCarousel = memo(function MediaCarousel({
|
|
|
116
40
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
117
41
|
const [touchStart, setTouchStart] = useState<number | null>(null);
|
|
118
42
|
const [touchEnd, setTouchEnd] = useState<number | null>(null);
|
|
119
|
-
const [playingVideos, setPlayingVideos] = useState<Set<number>>(new Set());
|
|
120
43
|
const carouselRef = useRef<HTMLDivElement>(null);
|
|
121
44
|
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
|
122
45
|
|
|
46
|
+
// Clamp `currentIndex` whenever `media` shrinks (e.g. an admin removes
|
|
47
|
+
// a slide while the user is on the last one) so the thumbnail active
|
|
48
|
+
// state + `currentItem` lookup stay in sync. Without this, the
|
|
49
|
+
// `media[currentIndex] || media[0]` fallback renders the first slide
|
|
50
|
+
// but every thumbnail's `isActive = index === currentIndex` reads
|
|
51
|
+
// false — visually nothing is selected.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (currentIndex >= media.length && media.length > 0) {
|
|
54
|
+
setCurrentIndex(media.length - 1);
|
|
55
|
+
}
|
|
56
|
+
}, [media.length, currentIndex]);
|
|
57
|
+
|
|
123
58
|
// Early return if no media provided
|
|
124
59
|
if (!media || media.length === 0) {
|
|
125
60
|
return null;
|
|
@@ -132,84 +67,19 @@ export const MediaCarousel = memo(function MediaCarousel({
|
|
|
132
67
|
return null;
|
|
133
68
|
}
|
|
134
69
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (item.type !== 'video') return;
|
|
139
|
-
|
|
140
|
-
// Find the video element
|
|
141
|
-
const videoElements = document.querySelectorAll(`video[data-video-index="${index}"]`);
|
|
142
|
-
const video = videoElements[0] as HTMLVideoElement;
|
|
143
|
-
|
|
144
|
-
if (!video) {
|
|
145
|
-
console.log('❌ Video element not found for index:', index);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (video.paused) {
|
|
150
|
-
const playPromise = video.play();
|
|
151
|
-
|
|
152
|
-
if (playPromise !== undefined) {
|
|
153
|
-
playPromise
|
|
154
|
-
.then(() => {
|
|
155
|
-
const playButton = video.parentElement?.querySelector('.video-play-button');
|
|
156
|
-
if (playButton) {
|
|
157
|
-
(playButton as HTMLElement).style.display = 'none';
|
|
158
|
-
}
|
|
159
|
-
})
|
|
160
|
-
.catch((error) => {
|
|
161
|
-
if (error.name === 'NotSupportedError' || error.name === 'MediaElementError') {
|
|
162
|
-
const fallbackDiv = document.createElement('div');
|
|
163
|
-
fallbackDiv.className = 'absolute inset-0 flex items-center justify-center bg-ods-card text-center p-4';
|
|
164
|
-
fallbackDiv.innerHTML = `
|
|
165
|
-
<div>
|
|
166
|
-
<p class="text-ods-text-primary text-sm mb-2">Video format not supported</p>
|
|
167
|
-
<a href="${item.src}" target="_blank" rel="noopener noreferrer"
|
|
168
|
-
class="text-ods-accent hover:text-[#FFD700] text-sm">
|
|
169
|
-
Open Video Directly
|
|
170
|
-
</a>
|
|
171
|
-
</div>
|
|
172
|
-
`;
|
|
173
|
-
video.parentElement?.appendChild(fallbackDiv);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
video.pause();
|
|
179
|
-
const playButton = video.parentElement?.querySelector('.video-play-button');
|
|
180
|
-
if (playButton) {
|
|
181
|
-
(playButton as HTMLElement).style.display = 'flex';
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}, [media]);
|
|
185
|
-
|
|
186
|
-
// Navigation functions
|
|
70
|
+
// Navigation functions — `<Video>` (MuxPlayer/YT facade) owns its own
|
|
71
|
+
// play/pause lifecycle, so the previous bare-`<video>`-element pause
|
|
72
|
+
// logic in nextSlide/prevSlide/selectSlide is no longer needed.
|
|
187
73
|
const nextSlide = useCallback(() => {
|
|
188
|
-
const currentVideo = document.querySelector(`[data-video-index="${currentIndex}"]`) as HTMLVideoElement;
|
|
189
|
-
if (currentVideo && !currentVideo.paused) {
|
|
190
|
-
currentVideo.pause();
|
|
191
|
-
}
|
|
192
|
-
setPlayingVideos(new Set());
|
|
193
74
|
setCurrentIndex((prev) => (prev + 1) % media.length);
|
|
194
|
-
}, [
|
|
75
|
+
}, [media.length]);
|
|
195
76
|
|
|
196
77
|
const prevSlide = useCallback(() => {
|
|
197
|
-
const currentVideo = document.querySelector(`[data-video-index="${currentIndex}"]`) as HTMLVideoElement;
|
|
198
|
-
if (currentVideo && !currentVideo.paused) {
|
|
199
|
-
currentVideo.pause();
|
|
200
|
-
}
|
|
201
|
-
setPlayingVideos(new Set());
|
|
202
78
|
setCurrentIndex((prev) => (prev - 1 + media.length) % media.length);
|
|
203
|
-
}, [
|
|
79
|
+
}, [media.length]);
|
|
204
80
|
|
|
205
81
|
const selectSlide = useCallback((index: number) => {
|
|
206
82
|
if (index === currentIndex) return;
|
|
207
|
-
|
|
208
|
-
const currentVideo = document.querySelector(`[data-video-index="${currentIndex}"]`) as HTMLVideoElement;
|
|
209
|
-
if (currentVideo && !currentVideo.paused) {
|
|
210
|
-
currentVideo.pause();
|
|
211
|
-
}
|
|
212
|
-
setPlayingVideos(new Set());
|
|
213
83
|
setCurrentIndex(index);
|
|
214
84
|
}, [currentIndex]);
|
|
215
85
|
|
|
@@ -253,92 +123,29 @@ export const MediaCarousel = memo(function MediaCarousel({
|
|
|
253
123
|
}
|
|
254
124
|
};
|
|
255
125
|
|
|
256
|
-
// Render YouTube embed
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return <CarouselYouTubeEmbed videoId={videoId} title={item.alt || `Video ${index + 1}`} />;
|
|
273
|
-
};
|
|
126
|
+
// Render YouTube embed via the SSoT `<Video>` — same lite-youtube
|
|
127
|
+
// facade everywhere, no carousel-local fork.
|
|
128
|
+
const renderYouTubeEmbed = (item: MediaItem, index: number) => (
|
|
129
|
+
<Video
|
|
130
|
+
kind="youtube"
|
|
131
|
+
url={item.src}
|
|
132
|
+
title={item.alt || `Video ${index + 1}`}
|
|
133
|
+
layout="fill"
|
|
134
|
+
priority={index === currentIndex}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
274
137
|
|
|
275
|
-
// Render video
|
|
138
|
+
// Render video via the SSoT `<Video>` — MuxPlayer handles both HLS
|
|
139
|
+
// and plain MP4, so the carousel no longer needs its own bare
|
|
140
|
+
// `<video>` element + custom play overlay + 60 LOC of error handling.
|
|
276
141
|
const renderVideo = (item: MediaItem, index: number) => (
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
muted
|
|
285
|
-
data-video-index={index}
|
|
286
|
-
onClick={() => handleVideoClick(index)}
|
|
287
|
-
onPlay={() => {
|
|
288
|
-
const playButton = document.querySelector(`[data-video-index="${index}"]`)?.parentElement?.querySelector('.video-play-button');
|
|
289
|
-
if (playButton) {
|
|
290
|
-
(playButton as HTMLElement).style.display = 'none';
|
|
291
|
-
}
|
|
292
|
-
}}
|
|
293
|
-
onPause={() => {
|
|
294
|
-
const playButton = document.querySelector(`[data-video-index="${index}"]`)?.parentElement?.querySelector('.video-play-button');
|
|
295
|
-
if (playButton) {
|
|
296
|
-
(playButton as HTMLElement).style.display = 'flex';
|
|
297
|
-
}
|
|
298
|
-
}}
|
|
299
|
-
onLoadedMetadata={(e) => {
|
|
300
|
-
const video = e.target as HTMLVideoElement;
|
|
301
|
-
video.currentTime = 1;
|
|
302
|
-
}}
|
|
303
|
-
onError={(e) => {
|
|
304
|
-
const target = e.target as HTMLVideoElement;
|
|
305
|
-
|
|
306
|
-
if (target.crossOrigin) {
|
|
307
|
-
target.crossOrigin = '';
|
|
308
|
-
target.load();
|
|
309
|
-
} else {
|
|
310
|
-
target.style.display = 'none';
|
|
311
|
-
const fallbackDiv = document.createElement('div');
|
|
312
|
-
fallbackDiv.className = 'absolute inset-0 flex items-center justify-center bg-ods-card text-center p-4';
|
|
313
|
-
fallbackDiv.innerHTML = `
|
|
314
|
-
<div>
|
|
315
|
-
<p class="text-ods-text-primary text-sm mb-2">Video could not be loaded</p>
|
|
316
|
-
<a href="${item.src}" target="_blank" rel="noopener noreferrer"
|
|
317
|
-
class="text-ods-accent hover:text-[#FFD700] text-sm">
|
|
318
|
-
Open Video Directly
|
|
319
|
-
</a>
|
|
320
|
-
</div>
|
|
321
|
-
`;
|
|
322
|
-
target.parentElement?.appendChild(fallbackDiv);
|
|
323
|
-
}
|
|
324
|
-
}}
|
|
325
|
-
>
|
|
326
|
-
<source src={item.src} type="video/mp4" />
|
|
327
|
-
<source src={item.src} type="video/webm" />
|
|
328
|
-
<source src={item.src} type="video/ogg" />
|
|
329
|
-
Your browser does not support the video tag.
|
|
330
|
-
</video>
|
|
331
|
-
|
|
332
|
-
{/* Play Button Overlay */}
|
|
333
|
-
<div
|
|
334
|
-
className="video-play-button absolute inset-0 flex items-center justify-center bg-black bg-opacity-20 cursor-pointer transition-opacity hover:bg-opacity-30"
|
|
335
|
-
onClick={() => handleVideoClick(index)}
|
|
336
|
-
>
|
|
337
|
-
<div className="w-16 h-16 bg-black bg-opacity-60 rounded-full flex items-center justify-center">
|
|
338
|
-
<PlayIcon />
|
|
339
|
-
</div>
|
|
340
|
-
</div>
|
|
341
|
-
</div>
|
|
142
|
+
<Video
|
|
143
|
+
url={item.src}
|
|
144
|
+
poster={item.poster}
|
|
145
|
+
muted
|
|
146
|
+
layout="fill"
|
|
147
|
+
priority={index === currentIndex}
|
|
148
|
+
/>
|
|
342
149
|
);
|
|
343
150
|
|
|
344
151
|
// Render image
|
|
@@ -350,7 +157,6 @@ export const MediaCarousel = memo(function MediaCarousel({
|
|
|
350
157
|
className={`w-full h-full object-${objectFit}`}
|
|
351
158
|
loading="lazy"
|
|
352
159
|
onError={(e) => {
|
|
353
|
-
console.log('❌ Image failed to load:', item.src);
|
|
354
160
|
const target = e.target as HTMLImageElement;
|
|
355
161
|
target.style.display = 'none';
|
|
356
162
|
}}
|
|
@@ -360,8 +166,6 @@ export const MediaCarousel = memo(function MediaCarousel({
|
|
|
360
166
|
|
|
361
167
|
// Render main media item
|
|
362
168
|
const renderMainMedia = (item: MediaItem, index: number) => {
|
|
363
|
-
console.log('🎬 Rendering media item:', { type: item.type, src: item.src.substring(0, 100) + '...' });
|
|
364
|
-
|
|
365
169
|
switch (item.type) {
|
|
366
170
|
case 'youtube':
|
|
367
171
|
return renderYouTubeEmbed(item, index);
|
|
@@ -379,7 +183,10 @@ export const MediaCarousel = memo(function MediaCarousel({
|
|
|
379
183
|
|
|
380
184
|
let thumbnailSrc = item.src;
|
|
381
185
|
if (item.type === 'youtube') {
|
|
382
|
-
|
|
186
|
+
// Use the SSoT `extractYouTubeId` (strict URL parsing, ReDoS-safe)
|
|
187
|
+
// rather than a local regex so YouTube id extraction has a single
|
|
188
|
+
// implementation across the lib.
|
|
189
|
+
const videoId = extractYouTubeId(item.src);
|
|
383
190
|
thumbnailSrc = videoId ? `https://img.youtube.com/vi/${videoId}/mqdefault.jpg` : item.src;
|
|
384
191
|
} else if (item.type === 'video' && item.poster) {
|
|
385
192
|
thumbnailSrc = item.poster;
|
|
@@ -11,8 +11,7 @@ import { ImageGalleryModal } from '../../ui/image-gallery-modal';
|
|
|
11
11
|
import { GitHubIcon } from '../../icons/github-icon';
|
|
12
12
|
import { AlertTriangle, ExternalLink, BookMarked } from 'lucide-react';
|
|
13
13
|
import { formatReleaseDate } from '../../../utils/date-formatters';
|
|
14
|
-
import {
|
|
15
|
-
import { VideoPlayer } from '../../features/video-player';
|
|
14
|
+
import { Video } from '../../features/video';
|
|
16
15
|
import { DetailPageSkeleton } from '../detail-page-skeleton';
|
|
17
16
|
import type { ChangelogEntry } from '../../../types/product-release';
|
|
18
17
|
import type { VideoTeaser } from '../../../types/video-processing';
|
|
@@ -308,7 +307,7 @@ export function ReleaseDetailPage({
|
|
|
308
307
|
}}
|
|
309
308
|
>
|
|
310
309
|
{mediaItem.media_type === 'video' || mediaItem.media_type === 'demo' ? (
|
|
311
|
-
<
|
|
310
|
+
<Video url={mediaItem.media_url} layout="native" />
|
|
312
311
|
) : (
|
|
313
312
|
<img src={mediaItem.media_url} alt={mediaItem.title || `Media ${index + 1}`} className="w-full h-full object-cover" />
|
|
314
313
|
)}
|
|
@@ -340,25 +339,33 @@ export function ReleaseDetailPage({
|
|
|
340
339
|
/>
|
|
341
340
|
) : (
|
|
342
341
|
<>
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
342
|
+
{/*
|
|
343
|
+
Fallback when no `VideoDisplaySection` is injected. `<Video>` is the
|
|
344
|
+
SSoT for every video surface — single source of truth across YouTube,
|
|
345
|
+
HLS, and MP4 paths.
|
|
346
|
+
*/}
|
|
347
|
+
{youtubeUrl && (
|
|
348
|
+
<Video
|
|
349
|
+
kind="youtube"
|
|
350
|
+
url={youtubeUrl}
|
|
351
|
+
title={`${releaseTitle} - Video`}
|
|
352
|
+
layout="native"
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
349
355
|
{!youtubeUrl && mainVideoUrl && (
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
356
|
+
<Video
|
|
357
|
+
url={mainVideoUrl}
|
|
358
|
+
srtContent={release?.srt_content as string | undefined}
|
|
359
|
+
captionsUrl={release?.captionsUrl as string | undefined}
|
|
360
|
+
layout="centered"
|
|
361
|
+
/>
|
|
355
362
|
)}
|
|
356
363
|
{highlightVideoUrl && (
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
364
|
+
<Video
|
|
365
|
+
url={highlightVideoUrl}
|
|
366
|
+
poster={highlightVideoThumbnail}
|
|
367
|
+
layout="centered"
|
|
368
|
+
/>
|
|
362
369
|
)}
|
|
363
370
|
</>
|
|
364
371
|
)}
|