@thewhateverapp/tile-sdk 0.11.2 → 0.12.1

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.
@@ -50,12 +50,20 @@ export interface VideoContextValue {
50
50
  controls: VideoControls;
51
51
  videoRef: React.RefObject<HTMLVideoElement>;
52
52
  }
53
+ export interface CuePoint {
54
+ /** Time in seconds when the cue should trigger */
55
+ time: number;
56
+ /** Unique identifier for the cue point */
57
+ id: string;
58
+ /** Optional data to pass to the callback */
59
+ data?: unknown;
60
+ }
53
61
  export interface VideoPlayerProps {
54
62
  /** HLS playlist URL (m3u8) */
55
63
  src: string;
56
64
  /** Auto-start playback (default: true) */
57
65
  autoplay?: boolean;
58
- /** Loop video (default: false) */
66
+ /** Loop video (default: false, but true in preview mode) */
59
67
  loop?: boolean;
60
68
  /** Start muted (default: true for autoplay compliance) */
61
69
  muted?: boolean;
@@ -69,15 +77,103 @@ export interface VideoPlayerProps {
69
77
  className?: string;
70
78
  /** Video wrapper class names */
71
79
  videoClassName?: string;
80
+ /** Cue points for time-based triggers */
81
+ cuePoints?: CuePoint[];
82
+ /** Callback when a cue point is reached */
83
+ onCuePoint?: (cuePoint: CuePoint) => void;
84
+ /** Callback on time update (fires ~4 times per second) */
85
+ onTimeUpdate?: (currentTime: number, duration: number) => void;
72
86
  }
73
87
  /**
74
88
  * VideoPlayer component with HLS streaming support.
75
89
  * Provides video state and controls to child overlays via context.
90
+ *
91
+ * Features:
92
+ * - HLS streaming with automatic quality adaptation
93
+ * - Time-based cue points for triggering overlays
94
+ * - Visibility-aware playback (plays when visible, pauses when hidden)
95
+ * - Auto-loops in preview mode for testing
76
96
  */
77
- export declare function VideoPlayer({ src, autoplay, loop, muted, poster, controls, children, className, videoClassName, }: VideoPlayerProps): React.JSX.Element;
97
+ export declare function VideoPlayer({ src, autoplay, loop: loopProp, muted, poster, controls, children, className, videoClassName, cuePoints, onCuePoint, onTimeUpdate, }: VideoPlayerProps): React.JSX.Element;
78
98
  /**
79
99
  * Hook to access video state and controls from within VideoPlayer children.
80
100
  */
81
101
  export declare function useVideoState(): VideoContextValue;
102
+ /**
103
+ * Hook to trigger an action when a specific time is reached.
104
+ * Returns true when the video has reached or passed the specified time.
105
+ *
106
+ * @param triggerTime - Time in seconds when to trigger
107
+ * @param options - Configuration options
108
+ * @returns boolean indicating if the trigger time has been reached
109
+ *
110
+ * @example
111
+ * ```tsx
112
+ * function PollOverlay() {
113
+ * const showPoll = useCuePoint(5); // Show after 5 seconds
114
+ * return showPoll ? <Poll /> : null;
115
+ * }
116
+ * ```
117
+ */
118
+ export declare function useCuePoint(triggerTime: number, options?: {
119
+ /** Reset trigger when video loops (default: true) */
120
+ resetOnLoop?: boolean;
121
+ /** Trigger once or every time the time is crossed (default: 'once') */
122
+ mode?: 'once' | 'every-loop';
123
+ }): boolean;
124
+ /**
125
+ * Hook to manage multiple cue points with callbacks.
126
+ * More flexible than useCuePoint for complex time-based interactions.
127
+ *
128
+ * @param cuePoints - Array of cue points with times and callbacks
129
+ * @param options - Configuration options
130
+ *
131
+ * @example
132
+ * ```tsx
133
+ * function InteractiveOverlay() {
134
+ * const [showPoll, setShowPoll] = useState(false);
135
+ * const [showCTA, setShowCTA] = useState(false);
136
+ *
137
+ * useCuePoints([
138
+ * { time: 5, onTrigger: () => setShowPoll(true) },
139
+ * { time: 10, onTrigger: () => setShowCTA(true) },
140
+ * { time: 15, onTrigger: () => { setShowPoll(false); setShowCTA(false); } },
141
+ * ]);
142
+ *
143
+ * return (
144
+ * <>
145
+ * {showPoll && <Poll />}
146
+ * {showCTA && <CTAButton />}
147
+ * </>
148
+ * );
149
+ * }
150
+ * ```
151
+ */
152
+ export declare function useCuePoints(cuePoints: Array<{
153
+ /** Time in seconds when to trigger */
154
+ time: number;
155
+ /** Callback to execute when time is reached */
156
+ onTrigger: () => void;
157
+ /** Optional unique ID (defaults to index) */
158
+ id?: string;
159
+ }>, options?: {
160
+ /** Reset triggers when video loops (default: true) */
161
+ resetOnLoop?: boolean;
162
+ }): void;
163
+ /**
164
+ * Hook to get the current video progress as a percentage (0-100).
165
+ * Useful for progress bars or time-based animations.
166
+ *
167
+ * @returns Progress percentage (0-100)
168
+ *
169
+ * @example
170
+ * ```tsx
171
+ * function ProgressBar() {
172
+ * const progress = useVideoProgress();
173
+ * return <div style={{ width: `${progress}%` }} className="h-1 bg-white" />;
174
+ * }
175
+ * ```
176
+ */
177
+ export declare function useVideoProgress(): number;
82
178
  export {};
83
179
  //# sourceMappingURL=VideoPlayer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/VideoPlayer.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/VideoPlayer.tsx"],"names":[],"mappings":"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"}
@@ -2,13 +2,29 @@
2
2
  import React, { createContext, useContext, useEffect, useRef, useState, useCallback, } from 'react';
3
3
  import { getTileBridge } from '../../bridge/TileBridge';
4
4
  const VideoContext = createContext(null);
5
+ // Detect if we're in preview mode (tile-preview sets this global)
6
+ function isPreviewMode() {
7
+ if (typeof window === 'undefined')
8
+ return false;
9
+ return !!window.__PREVIEW_SESSION_ID__;
10
+ }
5
11
  /**
6
12
  * VideoPlayer component with HLS streaming support.
7
13
  * Provides video state and controls to child overlays via context.
14
+ *
15
+ * Features:
16
+ * - HLS streaming with automatic quality adaptation
17
+ * - Time-based cue points for triggering overlays
18
+ * - Visibility-aware playback (plays when visible, pauses when hidden)
19
+ * - Auto-loops in preview mode for testing
8
20
  */
9
- export function VideoPlayer({ src, autoplay = true, loop = false, muted = true, poster, controls = false, children, className = '', videoClassName = '', }) {
21
+ export function VideoPlayer({ src, autoplay = true, loop: loopProp, muted = true, poster, controls = false, children, className = '', videoClassName = '', cuePoints = [], onCuePoint, onTimeUpdate, }) {
10
22
  const videoRef = useRef(null);
11
23
  const hlsRef = useRef(null);
24
+ const triggeredCuePointsRef = useRef(new Set());
25
+ const lastTimeRef = useRef(0);
26
+ // Auto-enable loop in preview mode unless explicitly set to false
27
+ const loop = loopProp ?? isPreviewMode();
12
28
  const [state, setState] = useState({
13
29
  isPlaying: false,
14
30
  currentTime: 0,
@@ -125,9 +141,40 @@ export function VideoPlayer({ src, autoplay = true, loop = false, muted = true,
125
141
  const video = videoRef.current;
126
142
  if (!video)
127
143
  return;
128
- const handlePlay = () => setState(s => ({ ...s, isPlaying: true }));
144
+ const handlePlay = () => setState(s => ({ ...s, isPlaying: true, isLoading: false }));
129
145
  const handlePause = () => setState(s => ({ ...s, isPlaying: false }));
130
- const handleTimeUpdate = () => setState(s => ({ ...s, currentTime: video.currentTime }));
146
+ // Fallback for loading state - hide spinner when video can play
147
+ const handleCanPlay = () => setState(s => ({ ...s, isLoading: false }));
148
+ const handleLoadedData = () => setState(s => ({ ...s, isLoading: false }));
149
+ const handleTimeUpdate = () => {
150
+ const currentTime = video.currentTime;
151
+ const duration = video.duration;
152
+ setState(s => ({ ...s, currentTime }));
153
+ // Call onTimeUpdate callback
154
+ if (onTimeUpdate && !isNaN(duration)) {
155
+ onTimeUpdate(currentTime, duration);
156
+ }
157
+ // Check cue points - trigger if we crossed the cue time since last update
158
+ if (onCuePoint && cuePoints.length > 0) {
159
+ const lastTime = lastTimeRef.current;
160
+ for (const cuePoint of cuePoints) {
161
+ const cueKey = `${cuePoint.id}-${Math.floor(currentTime / (duration || 1))}`;
162
+ // Trigger if we crossed the cue point time (handles both forward and loop)
163
+ // For looping: reset triggered cues when video loops back
164
+ if (currentTime < lastTime && lastTime > duration * 0.9) {
165
+ // Video looped - clear triggered cues
166
+ triggeredCuePointsRef.current.clear();
167
+ }
168
+ const alreadyTriggered = triggeredCuePointsRef.current.has(cueKey);
169
+ const crossedCuePoint = lastTime < cuePoint.time && currentTime >= cuePoint.time;
170
+ if (!alreadyTriggered && crossedCuePoint) {
171
+ triggeredCuePointsRef.current.add(cueKey);
172
+ onCuePoint(cuePoint);
173
+ }
174
+ }
175
+ }
176
+ lastTimeRef.current = currentTime;
177
+ };
131
178
  const handleDurationChange = () => setState(s => ({ ...s, duration: video.duration }));
132
179
  const handleVolumeChange = () => setState(s => ({ ...s, volume: video.volume, muted: video.muted }));
133
180
  const handleProgress = () => {
@@ -146,6 +193,8 @@ export function VideoPlayer({ src, autoplay = true, loop = false, muted = true,
146
193
  video.addEventListener('volumechange', handleVolumeChange);
147
194
  video.addEventListener('progress', handleProgress);
148
195
  video.addEventListener('error', handleError);
196
+ video.addEventListener('canplay', handleCanPlay);
197
+ video.addEventListener('loadeddata', handleLoadedData);
149
198
  return () => {
150
199
  video.removeEventListener('play', handlePlay);
151
200
  video.removeEventListener('pause', handlePause);
@@ -154,8 +203,10 @@ export function VideoPlayer({ src, autoplay = true, loop = false, muted = true,
154
203
  video.removeEventListener('volumechange', handleVolumeChange);
155
204
  video.removeEventListener('progress', handleProgress);
156
205
  video.removeEventListener('error', handleError);
206
+ video.removeEventListener('canplay', handleCanPlay);
207
+ video.removeEventListener('loadeddata', handleLoadedData);
157
208
  };
158
- }, []);
209
+ }, [onTimeUpdate, onCuePoint, cuePoints]);
159
210
  // Visibility handling - play/pause based on tile visibility
160
211
  // Enables TikTok-style preloaded video tiles that only play when visible
161
212
  useEffect(() => {
@@ -196,7 +247,24 @@ export function VideoPlayer({ src, autoplay = true, loop = false, muted = true,
196
247
  };
197
248
  return (React.createElement(VideoContext.Provider, { value: contextValue },
198
249
  React.createElement("div", { className: `relative w-full h-full bg-black ${className}` },
199
- React.createElement("video", { ref: videoRef, className: `w-full h-full object-contain ${videoClassName}`, poster: poster, loop: loop, muted: muted, controls: controls, playsInline: true }),
250
+ React.createElement("video", { ref: videoRef, className: `w-full h-full object-contain ${videoClassName}`, poster: poster, loop: loop, muted: muted, controls: controls, playsInline: true,
251
+ // Hide iOS Safari's native poster play button
252
+ style: {
253
+ // @ts-expect-error - WebKit-specific property
254
+ WebkitMediaControlsStartPlaybackButton: 'none',
255
+ } }),
256
+ React.createElement("style", null, `
257
+ video::-webkit-media-controls-start-playback-button {
258
+ display: none !important;
259
+ -webkit-appearance: none;
260
+ }
261
+ video::-webkit-media-controls-panel {
262
+ display: none !important;
263
+ }
264
+ video::-webkit-media-controls {
265
+ display: none !important;
266
+ }
267
+ `),
200
268
  state.isLoading && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center" },
201
269
  React.createElement("div", { className: "w-10 h-10 border-3 border-white/30 border-t-white rounded-full animate-spin" }))),
202
270
  state.error && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center text-white/80" },
@@ -213,3 +281,116 @@ export function useVideoState() {
213
281
  }
214
282
  return context;
215
283
  }
284
+ /**
285
+ * Hook to trigger an action when a specific time is reached.
286
+ * Returns true when the video has reached or passed the specified time.
287
+ *
288
+ * @param triggerTime - Time in seconds when to trigger
289
+ * @param options - Configuration options
290
+ * @returns boolean indicating if the trigger time has been reached
291
+ *
292
+ * @example
293
+ * ```tsx
294
+ * function PollOverlay() {
295
+ * const showPoll = useCuePoint(5); // Show after 5 seconds
296
+ * return showPoll ? <Poll /> : null;
297
+ * }
298
+ * ```
299
+ */
300
+ export function useCuePoint(triggerTime, options = {}) {
301
+ const { resetOnLoop = true, mode = 'once' } = options;
302
+ const { state } = useVideoState();
303
+ const [triggered, setTriggered] = useState(false);
304
+ const lastTimeRef = useRef(0);
305
+ useEffect(() => {
306
+ const { currentTime, duration } = state;
307
+ // Detect video loop (time jumped backwards significantly)
308
+ if (currentTime < lastTimeRef.current && lastTimeRef.current > duration * 0.9) {
309
+ if (resetOnLoop && mode === 'every-loop') {
310
+ setTriggered(false);
311
+ }
312
+ }
313
+ // Check if we crossed the trigger time
314
+ if (!triggered && currentTime >= triggerTime) {
315
+ setTriggered(true);
316
+ }
317
+ lastTimeRef.current = currentTime;
318
+ }, [state.currentTime, state.duration, triggerTime, triggered, resetOnLoop, mode]);
319
+ return triggered;
320
+ }
321
+ /**
322
+ * Hook to manage multiple cue points with callbacks.
323
+ * More flexible than useCuePoint for complex time-based interactions.
324
+ *
325
+ * @param cuePoints - Array of cue points with times and callbacks
326
+ * @param options - Configuration options
327
+ *
328
+ * @example
329
+ * ```tsx
330
+ * function InteractiveOverlay() {
331
+ * const [showPoll, setShowPoll] = useState(false);
332
+ * const [showCTA, setShowCTA] = useState(false);
333
+ *
334
+ * useCuePoints([
335
+ * { time: 5, onTrigger: () => setShowPoll(true) },
336
+ * { time: 10, onTrigger: () => setShowCTA(true) },
337
+ * { time: 15, onTrigger: () => { setShowPoll(false); setShowCTA(false); } },
338
+ * ]);
339
+ *
340
+ * return (
341
+ * <>
342
+ * {showPoll && <Poll />}
343
+ * {showCTA && <CTAButton />}
344
+ * </>
345
+ * );
346
+ * }
347
+ * ```
348
+ */
349
+ export function useCuePoints(cuePoints, options = {}) {
350
+ const { resetOnLoop = true } = options;
351
+ const { state } = useVideoState();
352
+ const triggeredRef = useRef(new Set());
353
+ const lastTimeRef = useRef(0);
354
+ useEffect(() => {
355
+ const { currentTime, duration } = state;
356
+ // Detect video loop
357
+ if (currentTime < lastTimeRef.current && lastTimeRef.current > duration * 0.9) {
358
+ if (resetOnLoop) {
359
+ triggeredRef.current.clear();
360
+ }
361
+ }
362
+ // Check each cue point
363
+ for (let i = 0; i < cuePoints.length; i++) {
364
+ const cue = cuePoints[i];
365
+ const cueId = cue.id ?? `cue-${i}`;
366
+ const alreadyTriggered = triggeredRef.current.has(cueId);
367
+ const crossedTime = lastTimeRef.current < cue.time && currentTime >= cue.time;
368
+ if (!alreadyTriggered && crossedTime) {
369
+ triggeredRef.current.add(cueId);
370
+ cue.onTrigger();
371
+ }
372
+ }
373
+ lastTimeRef.current = currentTime;
374
+ }, [state.currentTime, state.duration, cuePoints, resetOnLoop]);
375
+ }
376
+ /**
377
+ * Hook to get the current video progress as a percentage (0-100).
378
+ * Useful for progress bars or time-based animations.
379
+ *
380
+ * @returns Progress percentage (0-100)
381
+ *
382
+ * @example
383
+ * ```tsx
384
+ * function ProgressBar() {
385
+ * const progress = useVideoProgress();
386
+ * return <div style={{ width: `${progress}%` }} className="h-1 bg-white" />;
387
+ * }
388
+ * ```
389
+ */
390
+ export function useVideoProgress() {
391
+ const { state } = useVideoState();
392
+ const { currentTime, duration } = state;
393
+ if (!duration || duration === 0)
394
+ return 0;
395
+ return Math.min(100, (currentTime / duration) * 100);
396
+ }
@@ -1,5 +1,5 @@
1
- export { VideoPlayer, useVideoState, } from './VideoPlayer';
2
- export type { VideoState, VideoControls, VideoContextValue, VideoPlayerProps, } from './VideoPlayer';
1
+ export { VideoPlayer, useVideoState, useCuePoint, useCuePoints, useVideoProgress, } from './VideoPlayer';
2
+ export type { VideoState, VideoControls, VideoContextValue, VideoPlayerProps, CuePoint, } from './VideoPlayer';
3
3
  export { Slideshow, useSlideshowState, } from './Slideshow';
4
4
  export type { SlideImage, SlideshowState, SlideshowControls, SlideshowContextValue, SlideshowProps, } from './Slideshow';
5
5
  export { OverlaySlot, FullOverlay, GradientOverlay, } from './OverlaySlot';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,SAAS,EACT,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,GACf,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,aAAa,EACb,WAAW,EACX,YAAY,EACZ,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,GACT,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,SAAS,EACT,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,GACf,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,eAAe,CAAC"}
@@ -1,5 +1,5 @@
1
1
  // Video player with HLS streaming support
2
- export { VideoPlayer, useVideoState, } from './VideoPlayer';
2
+ export { VideoPlayer, useVideoState, useCuePoint, useCuePoints, useVideoProgress, } from './VideoPlayer';
3
3
  // Image slideshow component
4
4
  export { Slideshow, useSlideshowState, } from './Slideshow';
5
5
  // Overlay positioning components
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thewhateverapp/tile-sdk",
3
- "version": "0.11.2",
3
+ "version": "0.12.1",
4
4
  "description": "SDK for building interactive tiles on The Whatever App platform",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",