@thewhateverapp/tile-sdk 0.12.0 → 0.12.2
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.
|
@@ -59,15 +59,19 @@ export interface CuePoint {
|
|
|
59
59
|
data?: unknown;
|
|
60
60
|
}
|
|
61
61
|
export interface VideoPlayerProps {
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Video source URL (HLS playlist or direct video URL).
|
|
64
|
+
* If not provided, VideoPlayer will auto-fetch from the tile's metadata
|
|
65
|
+
* using the tileId from NEXT_PUBLIC_TILE_ID environment variable.
|
|
66
|
+
*/
|
|
67
|
+
src?: string;
|
|
68
|
+
/** Auto-start playback (default: true, or from tile metadata) */
|
|
65
69
|
autoplay?: boolean;
|
|
66
70
|
/** Loop video (default: false, but true in preview mode) */
|
|
67
71
|
loop?: boolean;
|
|
68
72
|
/** Start muted (default: true for autoplay compliance) */
|
|
69
73
|
muted?: boolean;
|
|
70
|
-
/** Poster image URL */
|
|
74
|
+
/** Poster image URL (auto-fetched from tile metadata if not provided) */
|
|
71
75
|
poster?: string;
|
|
72
76
|
/** Show native controls (default: false) */
|
|
73
77
|
controls?: boolean;
|
|
@@ -93,8 +97,9 @@ export interface VideoPlayerProps {
|
|
|
93
97
|
* - Time-based cue points for triggering overlays
|
|
94
98
|
* - Visibility-aware playback (plays when visible, pauses when hidden)
|
|
95
99
|
* - Auto-loops in preview mode for testing
|
|
100
|
+
* - Auto-fetches video URL from tile metadata when src is not provided
|
|
96
101
|
*/
|
|
97
|
-
export declare function VideoPlayer({ src, autoplay, loop: loopProp, muted, poster, controls, children, className, videoClassName, cuePoints, onCuePoint, onTimeUpdate, }: VideoPlayerProps): React.JSX.Element;
|
|
102
|
+
export declare function VideoPlayer({ src: srcProp, autoplay: autoplayProp, loop: loopProp, muted: mutedProp, poster: posterProp, controls, children, className, videoClassName, cuePoints, onCuePoint, onTimeUpdate, }: VideoPlayerProps): React.JSX.Element;
|
|
98
103
|
/**
|
|
99
104
|
* Hook to access video state and controls from within VideoPlayer children.
|
|
100
105
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/VideoPlayer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAOZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/VideoPlayer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React, { createContext, useContext, useEffect, useRef, useState, useCallback, } from 'react';
|
|
3
3
|
import { getTileBridge } from '../../bridge/TileBridge';
|
|
4
|
+
// Cache for video metadata to avoid repeated fetches
|
|
5
|
+
const videoMetadataCache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Get tileId from environment variable (set at build time)
|
|
8
|
+
*/
|
|
9
|
+
function getTileId() {
|
|
10
|
+
if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_TILE_ID) {
|
|
11
|
+
return process.env.NEXT_PUBLIC_TILE_ID;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Fetch video metadata for a tile from the platform API
|
|
17
|
+
*/
|
|
18
|
+
async function fetchVideoMetadata(tileId) {
|
|
19
|
+
// Check cache first
|
|
20
|
+
if (videoMetadataCache.has(tileId)) {
|
|
21
|
+
return videoMetadataCache.get(tileId);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
// Use production API URL
|
|
25
|
+
const apiBase = 'https://api.thewhatever.app';
|
|
26
|
+
const response = await fetch(`${apiBase}/platform/tiles/${tileId}/video-metadata`);
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
console.error(`[VideoPlayer] Failed to fetch video metadata: ${response.status}`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const metadata = await response.json();
|
|
32
|
+
videoMetadataCache.set(tileId, metadata);
|
|
33
|
+
return metadata;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('[VideoPlayer] Error fetching video metadata:', error);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
4
40
|
const VideoContext = createContext(null);
|
|
5
41
|
// Detect if we're in preview mode (tile-preview sets this global)
|
|
6
42
|
function isPreviewMode() {
|
|
@@ -17,14 +53,48 @@ function isPreviewMode() {
|
|
|
17
53
|
* - Time-based cue points for triggering overlays
|
|
18
54
|
* - Visibility-aware playback (plays when visible, pauses when hidden)
|
|
19
55
|
* - Auto-loops in preview mode for testing
|
|
56
|
+
* - Auto-fetches video URL from tile metadata when src is not provided
|
|
20
57
|
*/
|
|
21
|
-
export function VideoPlayer({ src, autoplay
|
|
58
|
+
export function VideoPlayer({ src: srcProp, autoplay: autoplayProp, loop: loopProp, muted: mutedProp, poster: posterProp, controls = false, children, className = '', videoClassName = '', cuePoints = [], onCuePoint, onTimeUpdate, }) {
|
|
22
59
|
const videoRef = useRef(null);
|
|
23
60
|
const hlsRef = useRef(null);
|
|
24
61
|
const triggeredCuePointsRef = useRef(new Set());
|
|
25
62
|
const lastTimeRef = useRef(0);
|
|
63
|
+
// State for auto-fetched metadata
|
|
64
|
+
const [metadata, setMetadata] = useState(null);
|
|
65
|
+
const [metadataError, setMetadataError] = useState(null);
|
|
66
|
+
// Auto-fetch video metadata if no src provided
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (srcProp) {
|
|
69
|
+
// src provided directly, no need to fetch
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const tileId = getTileId();
|
|
73
|
+
if (!tileId) {
|
|
74
|
+
setMetadataError('No video source: src prop not provided and NEXT_PUBLIC_TILE_ID not set');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
fetchVideoMetadata(tileId).then((data) => {
|
|
78
|
+
if (data) {
|
|
79
|
+
if (data.videoUrl) {
|
|
80
|
+
setMetadata(data);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
setMetadataError('No video URL found in tile metadata');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
setMetadataError('Failed to fetch video metadata');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}, [srcProp]);
|
|
91
|
+
// Resolve actual values from props or metadata
|
|
92
|
+
const src = srcProp || metadata?.videoUrl || null;
|
|
93
|
+
const autoplay = autoplayProp ?? metadata?.autoplay ?? true;
|
|
94
|
+
const muted = mutedProp ?? metadata?.muted ?? true;
|
|
95
|
+
const poster = posterProp || metadata?.thumbnail || undefined;
|
|
26
96
|
// Auto-enable loop in preview mode unless explicitly set to false
|
|
27
|
-
const loop = loopProp ?? isPreviewMode();
|
|
97
|
+
const loop = loopProp ?? metadata?.loop ?? isPreviewMode();
|
|
28
98
|
const [state, setState] = useState({
|
|
29
99
|
isPlaying: false,
|
|
30
100
|
currentTime: 0,
|
|
@@ -141,8 +211,11 @@ export function VideoPlayer({ src, autoplay = true, loop: loopProp, muted = true
|
|
|
141
211
|
const video = videoRef.current;
|
|
142
212
|
if (!video)
|
|
143
213
|
return;
|
|
144
|
-
const handlePlay = () => setState(s => ({ ...s, isPlaying: true }));
|
|
214
|
+
const handlePlay = () => setState(s => ({ ...s, isPlaying: true, isLoading: false }));
|
|
145
215
|
const handlePause = () => setState(s => ({ ...s, isPlaying: false }));
|
|
216
|
+
// Fallback for loading state - hide spinner when video can play
|
|
217
|
+
const handleCanPlay = () => setState(s => ({ ...s, isLoading: false }));
|
|
218
|
+
const handleLoadedData = () => setState(s => ({ ...s, isLoading: false }));
|
|
146
219
|
const handleTimeUpdate = () => {
|
|
147
220
|
const currentTime = video.currentTime;
|
|
148
221
|
const duration = video.duration;
|
|
@@ -190,6 +263,8 @@ export function VideoPlayer({ src, autoplay = true, loop: loopProp, muted = true
|
|
|
190
263
|
video.addEventListener('volumechange', handleVolumeChange);
|
|
191
264
|
video.addEventListener('progress', handleProgress);
|
|
192
265
|
video.addEventListener('error', handleError);
|
|
266
|
+
video.addEventListener('canplay', handleCanPlay);
|
|
267
|
+
video.addEventListener('loadeddata', handleLoadedData);
|
|
193
268
|
return () => {
|
|
194
269
|
video.removeEventListener('play', handlePlay);
|
|
195
270
|
video.removeEventListener('pause', handlePause);
|
|
@@ -198,10 +273,13 @@ export function VideoPlayer({ src, autoplay = true, loop: loopProp, muted = true
|
|
|
198
273
|
video.removeEventListener('volumechange', handleVolumeChange);
|
|
199
274
|
video.removeEventListener('progress', handleProgress);
|
|
200
275
|
video.removeEventListener('error', handleError);
|
|
276
|
+
video.removeEventListener('canplay', handleCanPlay);
|
|
277
|
+
video.removeEventListener('loadeddata', handleLoadedData);
|
|
201
278
|
};
|
|
202
279
|
}, [onTimeUpdate, onCuePoint, cuePoints]);
|
|
203
|
-
// Visibility handling - play/pause based on tile visibility
|
|
280
|
+
// Visibility handling - play/pause AND mute/unmute based on tile visibility
|
|
204
281
|
// Enables TikTok-style preloaded video tiles that only play when visible
|
|
282
|
+
// Audio is controlled here to ensure only the visible tile has sound
|
|
205
283
|
useEffect(() => {
|
|
206
284
|
const video = videoRef.current;
|
|
207
285
|
if (!video || !autoplay)
|
|
@@ -211,17 +289,25 @@ export function VideoPlayer({ src, autoplay = true, loop: loopProp, muted = true
|
|
|
211
289
|
// Handle visibility changes from parent (mobile app)
|
|
212
290
|
const unsubscribe = bridge.onVisibilityChange((visibilityState) => {
|
|
213
291
|
if (visibilityState.visible) {
|
|
214
|
-
// Tile became visible -
|
|
215
|
-
|
|
292
|
+
// Tile became visible - unmute and play
|
|
293
|
+
// Unmute first so audio starts with the video
|
|
294
|
+
video.muted = false;
|
|
295
|
+
video.play().catch(() => {
|
|
296
|
+
// If play fails (e.g., autoplay policy), keep muted and retry
|
|
297
|
+
video.muted = true;
|
|
298
|
+
video.play().catch(() => { });
|
|
299
|
+
});
|
|
216
300
|
}
|
|
217
301
|
else {
|
|
218
|
-
// Tile became hidden -
|
|
302
|
+
// Tile became hidden - mute first (immediate audio stop), then pause
|
|
303
|
+
video.muted = true;
|
|
219
304
|
video.pause();
|
|
220
305
|
}
|
|
221
306
|
});
|
|
222
307
|
// Check initial visibility state
|
|
223
308
|
if (!bridge.isVisible()) {
|
|
224
|
-
// If not visible on mount,
|
|
309
|
+
// If not visible on mount, ensure muted and paused
|
|
310
|
+
video.muted = true;
|
|
225
311
|
video.pause();
|
|
226
312
|
}
|
|
227
313
|
return () => {
|
|
@@ -230,7 +316,7 @@ export function VideoPlayer({ src, autoplay = true, loop: loopProp, muted = true
|
|
|
230
316
|
}
|
|
231
317
|
catch {
|
|
232
318
|
// Bridge not available (e.g., in SSR or outside TileProvider)
|
|
233
|
-
// Video will just use autoplay behavior
|
|
319
|
+
// Video will just use autoplay behavior (muted)
|
|
234
320
|
}
|
|
235
321
|
}, [autoplay]);
|
|
236
322
|
const contextValue = {
|
|
@@ -240,11 +326,28 @@ export function VideoPlayer({ src, autoplay = true, loop: loopProp, muted = true
|
|
|
240
326
|
};
|
|
241
327
|
return (React.createElement(VideoContext.Provider, { value: contextValue },
|
|
242
328
|
React.createElement("div", { className: `relative w-full h-full bg-black ${className}` },
|
|
243
|
-
React.createElement("video", { ref: videoRef, className: `w-full h-full object-contain ${videoClassName}`, poster: poster, loop: loop, muted: muted, controls: controls, playsInline: true
|
|
329
|
+
React.createElement("video", { ref: videoRef, className: `w-full h-full object-contain ${videoClassName}`, poster: poster, loop: loop, muted: muted, controls: controls, playsInline: true,
|
|
330
|
+
// Hide iOS Safari's native poster play button
|
|
331
|
+
style: {
|
|
332
|
+
// @ts-expect-error - WebKit-specific property
|
|
333
|
+
WebkitMediaControlsStartPlaybackButton: 'none',
|
|
334
|
+
} }),
|
|
335
|
+
React.createElement("style", null, `
|
|
336
|
+
video::-webkit-media-controls-start-playback-button {
|
|
337
|
+
display: none !important;
|
|
338
|
+
-webkit-appearance: none;
|
|
339
|
+
}
|
|
340
|
+
video::-webkit-media-controls-panel {
|
|
341
|
+
display: none !important;
|
|
342
|
+
}
|
|
343
|
+
video::-webkit-media-controls {
|
|
344
|
+
display: none !important;
|
|
345
|
+
}
|
|
346
|
+
`),
|
|
244
347
|
state.isLoading && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center" },
|
|
245
348
|
React.createElement("div", { className: "w-10 h-10 border-3 border-white/30 border-t-white rounded-full animate-spin" }))),
|
|
246
|
-
state.error && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center text-white/80" },
|
|
247
|
-
React.createElement("p", null, state.error))),
|
|
349
|
+
(state.error || metadataError) && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center text-white/80" },
|
|
350
|
+
React.createElement("p", null, state.error || metadataError))),
|
|
248
351
|
children)));
|
|
249
352
|
}
|
|
250
353
|
/**
|