@thewhateverapp/tile-sdk 0.12.21 → 0.13.0
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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/react/overlay/VideoPlayer.d.ts +61 -80
- package/dist/react/overlay/VideoPlayer.d.ts.map +1 -1
- package/dist/react/overlay/VideoPlayer.js +353 -430
- package/dist/react/overlay/index.d.ts +2 -1
- package/dist/react/overlay/index.d.ts.map +1 -1
- package/dist/react/overlay/index.js +3 -2
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +4 -3
- package/dist/templates/slideshow/index.d.ts +2 -2
- package/dist/templates/slideshow/index.d.ts.map +1 -1
- package/dist/templates/slideshow/index.js +3 -2
- package/dist/templates/video/index.d.ts +3 -3
- package/dist/templates/video/index.d.ts.map +1 -1
- package/dist/templates/video/index.js +4 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export { useTileNavigation } from './react/useTileNavigation';
|
|
|
4
4
|
export { useKeyboard } from './react/useKeyboard';
|
|
5
5
|
export { TileContainer } from './react/TileContainer';
|
|
6
6
|
export { withTile } from './react/withTile';
|
|
7
|
-
export { VideoPlayer, useVideoState,
|
|
7
|
+
export { VideoPlayer, useVideoState, useVideo, // Alias for useVideoState
|
|
8
|
+
Slideshow, useSlideshowState, OverlaySlot, FullOverlay, GradientOverlay, } from './react/overlay';
|
|
8
9
|
export type { VideoState, VideoControls, VideoContextValue, VideoPlayerProps, SlideImage, SlideshowState, SlideshowControls, SlideshowContextValue, SlideshowProps, SlotPosition, OverlaySlotProps, FullOverlayProps, GradientOverlayProps, } from './react/overlay';
|
|
9
10
|
export { getTileBridge, TileBridge } from './bridge/TileBridge';
|
|
10
11
|
export type { TileMessage, TileConfig, TileTokenData, KeyboardState, VisibilityState } from './bridge/TileBridge';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,EAEL,WAAW,EACX,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,OAAO,EAEL,WAAW,EACX,aAAa,EACb,QAAQ,EAAE,0BAA0B;AAEpC,SAAS,EACT,iBAAiB,EAEjB,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAEV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAEhB,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EAEd,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAChE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGlH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnE,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ export { TileContainer } from './react/TileContainer';
|
|
|
7
7
|
export { withTile } from './react/withTile';
|
|
8
8
|
// Overlay components for hybrid tiles (video/image with interactive overlays)
|
|
9
9
|
export {
|
|
10
|
-
// Video player
|
|
11
|
-
VideoPlayer, useVideoState,
|
|
10
|
+
// Video player (with route persistence)
|
|
11
|
+
VideoPlayer, useVideoState, useVideo, // Alias for useVideoState
|
|
12
12
|
// Slideshow
|
|
13
13
|
Slideshow, useSlideshowState,
|
|
14
14
|
// Positioning components
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import React, { type ReactNode } from 'react';
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
__videoElement?: HTMLVideoElement;
|
|
5
|
+
__videoHls?: HlsInstance;
|
|
6
|
+
__videoSingleton?: VideoSingletonClass;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
2
9
|
interface HlsInstance {
|
|
3
10
|
loadSource: (url: string) => void;
|
|
4
11
|
attachMedia: (video: HTMLVideoElement) => void;
|
|
@@ -37,6 +44,55 @@ export interface VideoState {
|
|
|
37
44
|
isLoading: boolean;
|
|
38
45
|
error: string | null;
|
|
39
46
|
}
|
|
47
|
+
type StateCallback = (state: VideoState) => void;
|
|
48
|
+
/**
|
|
49
|
+
* VideoSingleton - Manages a persistent video element across route changes.
|
|
50
|
+
*
|
|
51
|
+
* The video element is stored on `window.__videoElement` and survives
|
|
52
|
+
* Next.js soft navigations. When navigating from /tile to /page,
|
|
53
|
+
* the video keeps playing seamlessly.
|
|
54
|
+
*/
|
|
55
|
+
declare class VideoSingletonClass {
|
|
56
|
+
private stateCallbacks;
|
|
57
|
+
private currentState;
|
|
58
|
+
private currentSrc;
|
|
59
|
+
private hlsScriptLoaded;
|
|
60
|
+
private visibilitySetup;
|
|
61
|
+
/**
|
|
62
|
+
* Get or create the persistent video element
|
|
63
|
+
*/
|
|
64
|
+
getVideo(): HTMLVideoElement;
|
|
65
|
+
/**
|
|
66
|
+
* Load a video source (supports HLS and regular video)
|
|
67
|
+
*/
|
|
68
|
+
loadSource(src: string, poster?: string): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Load HLS stream using hls.js
|
|
71
|
+
*/
|
|
72
|
+
private loadHls;
|
|
73
|
+
/**
|
|
74
|
+
* Set up visibility handling via TileBridge
|
|
75
|
+
*/
|
|
76
|
+
setupVisibilityHandling(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Attach video element to a container (moves existing element)
|
|
79
|
+
*/
|
|
80
|
+
attachTo(container: HTMLElement): void;
|
|
81
|
+
/**
|
|
82
|
+
* Get video element reference (for external use)
|
|
83
|
+
*/
|
|
84
|
+
getVideoRef(): HTMLVideoElement;
|
|
85
|
+
play(): void;
|
|
86
|
+
pause(): void;
|
|
87
|
+
toggle(): void;
|
|
88
|
+
seek(time: number): void;
|
|
89
|
+
setMuted(muted: boolean): void;
|
|
90
|
+
setVolume(volume: number): void;
|
|
91
|
+
setLoop(loop: boolean): void;
|
|
92
|
+
getState(): VideoState;
|
|
93
|
+
onStateChange(callback: StateCallback): () => void;
|
|
94
|
+
private updateState;
|
|
95
|
+
}
|
|
40
96
|
export interface VideoControls {
|
|
41
97
|
play: () => void;
|
|
42
98
|
pause: () => void;
|
|
@@ -51,46 +107,24 @@ export interface VideoContextValue {
|
|
|
51
107
|
videoRef: React.RefObject<HTMLVideoElement>;
|
|
52
108
|
}
|
|
53
109
|
export interface CuePoint {
|
|
54
|
-
/** Time in seconds when the cue should trigger */
|
|
55
110
|
time: number;
|
|
56
|
-
/** Unique identifier for the cue point */
|
|
57
111
|
id: string;
|
|
58
|
-
/** Optional data to pass to the callback */
|
|
59
112
|
data?: unknown;
|
|
60
113
|
}
|
|
61
114
|
export interface VideoPlayerProps {
|
|
62
|
-
/** Children rendered as overlay */
|
|
63
115
|
children?: ReactNode;
|
|
64
|
-
/** Additional class names */
|
|
65
116
|
className?: string;
|
|
66
|
-
/** Video wrapper class names */
|
|
67
117
|
videoClassName?: string;
|
|
68
|
-
/** Show native controls (default: false) */
|
|
69
118
|
controls?: boolean;
|
|
70
|
-
/** Cue points for time-based triggers */
|
|
71
119
|
cuePoints?: CuePoint[];
|
|
72
|
-
/** Callback when a cue point is reached */
|
|
73
120
|
onCuePoint?: (cuePoint: CuePoint) => void;
|
|
74
|
-
/** Callback on time update (fires ~4 times per second) */
|
|
75
121
|
onTimeUpdate?: (currentTime: number, duration: number) => void;
|
|
76
122
|
}
|
|
77
123
|
/**
|
|
78
|
-
* VideoPlayer component with HLS streaming support.
|
|
79
|
-
* Provides video state and controls to child overlays via context.
|
|
80
|
-
*
|
|
81
|
-
* Features:
|
|
82
|
-
* - Video URL is injected at build time via NEXT_PUBLIC env vars (no API call!)
|
|
83
|
-
* - HLS streaming with automatic quality adaptation
|
|
84
|
-
* - Falls back to original video URL if transcoding not complete
|
|
85
|
-
* - Time-based cue points for triggering overlays
|
|
86
|
-
* - Visibility-aware playback (plays when visible, pauses when hidden)
|
|
87
|
-
* - Auto-loops by default (can be disabled via tile metadata)
|
|
124
|
+
* VideoPlayer component with HLS streaming support and route persistence.
|
|
88
125
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* - NEXT_PUBLIC_VIDEO_ORIGINAL_URL - Original video URL (fallback)
|
|
92
|
-
* - NEXT_PUBLIC_VIDEO_THUMBNAIL - Poster image
|
|
93
|
-
* - NEXT_PUBLIC_VIDEO_AUTOPLAY/LOOP/MUTED - Playback settings
|
|
126
|
+
* The video element persists across Next.js route changes (tile ↔ page).
|
|
127
|
+
* Video URL is read from NEXT_PUBLIC env vars (set at build time).
|
|
94
128
|
*
|
|
95
129
|
* Usage:
|
|
96
130
|
* ```tsx
|
|
@@ -102,82 +136,29 @@ export interface VideoPlayerProps {
|
|
|
102
136
|
export declare function VideoPlayer({ controls, children, className, videoClassName, cuePoints, onCuePoint, onTimeUpdate, }: VideoPlayerProps): React.JSX.Element;
|
|
103
137
|
/**
|
|
104
138
|
* Hook to access video state and controls from within VideoPlayer children.
|
|
139
|
+
* Also aliased as useVideo for compatibility.
|
|
105
140
|
*/
|
|
106
141
|
export declare function useVideoState(): VideoContextValue;
|
|
142
|
+
export declare const useVideo: typeof useVideoState;
|
|
107
143
|
/**
|
|
108
144
|
* Hook to trigger an action when a specific time is reached.
|
|
109
|
-
* Returns true when the video has reached or passed the specified time.
|
|
110
|
-
*
|
|
111
|
-
* @param triggerTime - Time in seconds when to trigger
|
|
112
|
-
* @param options - Configuration options
|
|
113
|
-
* @returns boolean indicating if the trigger time has been reached
|
|
114
|
-
*
|
|
115
|
-
* @example
|
|
116
|
-
* ```tsx
|
|
117
|
-
* function PollOverlay() {
|
|
118
|
-
* const showPoll = useCuePoint(5); // Show after 5 seconds
|
|
119
|
-
* return showPoll ? <Poll /> : null;
|
|
120
|
-
* }
|
|
121
|
-
* ```
|
|
122
145
|
*/
|
|
123
146
|
export declare function useCuePoint(triggerTime: number, options?: {
|
|
124
|
-
/** Reset trigger when video loops (default: true) */
|
|
125
147
|
resetOnLoop?: boolean;
|
|
126
|
-
/** Trigger once or every time the time is crossed (default: 'once') */
|
|
127
148
|
mode?: 'once' | 'every-loop';
|
|
128
149
|
}): boolean;
|
|
129
150
|
/**
|
|
130
151
|
* Hook to manage multiple cue points with callbacks.
|
|
131
|
-
* More flexible than useCuePoint for complex time-based interactions.
|
|
132
|
-
*
|
|
133
|
-
* @param cuePoints - Array of cue points with times and callbacks
|
|
134
|
-
* @param options - Configuration options
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* ```tsx
|
|
138
|
-
* function InteractiveOverlay() {
|
|
139
|
-
* const [showPoll, setShowPoll] = useState(false);
|
|
140
|
-
* const [showCTA, setShowCTA] = useState(false);
|
|
141
|
-
*
|
|
142
|
-
* useCuePoints([
|
|
143
|
-
* { time: 5, onTrigger: () => setShowPoll(true) },
|
|
144
|
-
* { time: 10, onTrigger: () => setShowCTA(true) },
|
|
145
|
-
* { time: 15, onTrigger: () => { setShowPoll(false); setShowCTA(false); } },
|
|
146
|
-
* ]);
|
|
147
|
-
*
|
|
148
|
-
* return (
|
|
149
|
-
* <>
|
|
150
|
-
* {showPoll && <Poll />}
|
|
151
|
-
* {showCTA && <CTAButton />}
|
|
152
|
-
* </>
|
|
153
|
-
* );
|
|
154
|
-
* }
|
|
155
|
-
* ```
|
|
156
152
|
*/
|
|
157
153
|
export declare function useCuePoints(cuePoints: Array<{
|
|
158
|
-
/** Time in seconds when to trigger */
|
|
159
154
|
time: number;
|
|
160
|
-
/** Callback to execute when time is reached */
|
|
161
155
|
onTrigger: () => void;
|
|
162
|
-
/** Optional unique ID (defaults to index) */
|
|
163
156
|
id?: string;
|
|
164
157
|
}>, options?: {
|
|
165
|
-
/** Reset triggers when video loops (default: true) */
|
|
166
158
|
resetOnLoop?: boolean;
|
|
167
159
|
}): void;
|
|
168
160
|
/**
|
|
169
161
|
* Hook to get the current video progress as a percentage (0-100).
|
|
170
|
-
* Useful for progress bars or time-based animations.
|
|
171
|
-
*
|
|
172
|
-
* @returns Progress percentage (0-100)
|
|
173
|
-
*
|
|
174
|
-
* @example
|
|
175
|
-
* ```tsx
|
|
176
|
-
* function ProgressBar() {
|
|
177
|
-
* const progress = useVideoProgress();
|
|
178
|
-
* return <div style={{ width: `${progress}%` }} className="h-1 bg-white" />;
|
|
179
|
-
* }
|
|
180
|
-
* ```
|
|
181
162
|
*/
|
|
182
163
|
export declare function useVideoProgress(): number;
|
|
183
164
|
export {};
|
|
@@ -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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"}
|
|
@@ -2,23 +2,282 @@
|
|
|
2
2
|
import React, { createContext, useContext, useEffect, useRef, useState, useCallback, } from 'react';
|
|
3
3
|
import { getTileBridge } from '../../bridge/TileBridge';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* These are injected at build time by tile-deploy for video tiles.
|
|
5
|
+
* VideoSingleton - Manages a persistent video element across route changes.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - NEXT_PUBLIC_VIDEO_ORIGINAL_URL - Original video URL (fallback)
|
|
12
|
-
* - NEXT_PUBLIC_VIDEO_THUMBNAIL - Thumbnail image URL
|
|
13
|
-
* - NEXT_PUBLIC_VIDEO_DURATION - Video duration in seconds
|
|
14
|
-
* - NEXT_PUBLIC_VIDEO_TITLE - Video title
|
|
15
|
-
* - NEXT_PUBLIC_VIDEO_AUTOPLAY - Auto-play setting (default: true)
|
|
16
|
-
* - NEXT_PUBLIC_VIDEO_LOOP - Loop setting (default: true)
|
|
17
|
-
* - NEXT_PUBLIC_VIDEO_MUTED - Muted setting (default: true)
|
|
18
|
-
* - NEXT_PUBLIC_VIDEO_TRANSCODING_COMPLETE - Transcoding status
|
|
7
|
+
* The video element is stored on `window.__videoElement` and survives
|
|
8
|
+
* Next.js soft navigations. When navigating from /tile to /page,
|
|
9
|
+
* the video keeps playing seamlessly.
|
|
19
10
|
*/
|
|
11
|
+
class VideoSingletonClass {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.stateCallbacks = new Set();
|
|
14
|
+
this.currentState = {
|
|
15
|
+
isPlaying: false,
|
|
16
|
+
currentTime: 0,
|
|
17
|
+
duration: 0,
|
|
18
|
+
buffered: 0,
|
|
19
|
+
volume: 1,
|
|
20
|
+
muted: true,
|
|
21
|
+
isLoading: true,
|
|
22
|
+
error: null,
|
|
23
|
+
};
|
|
24
|
+
this.currentSrc = null;
|
|
25
|
+
this.hlsScriptLoaded = false;
|
|
26
|
+
this.visibilitySetup = false;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get or create the persistent video element
|
|
30
|
+
*/
|
|
31
|
+
getVideo() {
|
|
32
|
+
if (typeof window === 'undefined') {
|
|
33
|
+
throw new Error('VideoSingleton requires browser environment');
|
|
34
|
+
}
|
|
35
|
+
if (!window.__videoElement) {
|
|
36
|
+
const video = document.createElement('video');
|
|
37
|
+
video.id = 'persistent-video';
|
|
38
|
+
video.playsInline = true;
|
|
39
|
+
video.muted = true;
|
|
40
|
+
video.loop = true;
|
|
41
|
+
video.preload = 'auto';
|
|
42
|
+
video.style.width = '100%';
|
|
43
|
+
video.style.height = '100%';
|
|
44
|
+
video.style.objectFit = 'contain';
|
|
45
|
+
video.style.backgroundColor = 'black';
|
|
46
|
+
// Set up event listeners
|
|
47
|
+
video.addEventListener('play', () => this.updateState({ isPlaying: true, isLoading: false }));
|
|
48
|
+
video.addEventListener('pause', () => this.updateState({ isPlaying: false }));
|
|
49
|
+
video.addEventListener('timeupdate', () => {
|
|
50
|
+
this.updateState({ currentTime: video.currentTime });
|
|
51
|
+
});
|
|
52
|
+
video.addEventListener('durationchange', () => {
|
|
53
|
+
this.updateState({ duration: video.duration });
|
|
54
|
+
});
|
|
55
|
+
video.addEventListener('loadeddata', () => {
|
|
56
|
+
this.updateState({ isLoading: false });
|
|
57
|
+
});
|
|
58
|
+
video.addEventListener('canplay', () => {
|
|
59
|
+
this.updateState({ isLoading: false });
|
|
60
|
+
});
|
|
61
|
+
video.addEventListener('waiting', () => {
|
|
62
|
+
this.updateState({ isLoading: true });
|
|
63
|
+
});
|
|
64
|
+
video.addEventListener('playing', () => {
|
|
65
|
+
this.updateState({ isLoading: false, isPlaying: true });
|
|
66
|
+
});
|
|
67
|
+
video.addEventListener('volumechange', () => {
|
|
68
|
+
this.updateState({ muted: video.muted, volume: video.volume });
|
|
69
|
+
});
|
|
70
|
+
video.addEventListener('progress', () => {
|
|
71
|
+
if (video.buffered.length > 0) {
|
|
72
|
+
this.updateState({ buffered: video.buffered.end(video.buffered.length - 1) });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
video.addEventListener('error', () => {
|
|
76
|
+
this.updateState({ error: 'Video playback error', isLoading: false });
|
|
77
|
+
});
|
|
78
|
+
window.__videoElement = video;
|
|
79
|
+
}
|
|
80
|
+
return window.__videoElement;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Load a video source (supports HLS and regular video)
|
|
84
|
+
*/
|
|
85
|
+
async loadSource(src, poster) {
|
|
86
|
+
// Skip if same source already loaded
|
|
87
|
+
if (this.currentSrc === src) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const video = this.getVideo();
|
|
91
|
+
this.currentSrc = src;
|
|
92
|
+
this.updateState({ isLoading: true, error: null });
|
|
93
|
+
if (poster) {
|
|
94
|
+
video.poster = poster;
|
|
95
|
+
}
|
|
96
|
+
// Clean up existing HLS instance
|
|
97
|
+
if (window.__videoHls) {
|
|
98
|
+
window.__videoHls.destroy();
|
|
99
|
+
window.__videoHls = undefined;
|
|
100
|
+
}
|
|
101
|
+
// Check if this is an HLS stream
|
|
102
|
+
if (src.includes('.m3u8')) {
|
|
103
|
+
await this.loadHls(src);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Regular video file
|
|
107
|
+
video.src = src;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Load HLS stream using hls.js
|
|
112
|
+
*/
|
|
113
|
+
async loadHls(src) {
|
|
114
|
+
const video = this.getVideo();
|
|
115
|
+
// Load HLS.js from CDN if not already loaded
|
|
116
|
+
if (!this.hlsScriptLoaded && !window.Hls) {
|
|
117
|
+
await new Promise((resolve, reject) => {
|
|
118
|
+
const script = document.createElement('script');
|
|
119
|
+
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1';
|
|
120
|
+
script.onload = () => {
|
|
121
|
+
this.hlsScriptLoaded = true;
|
|
122
|
+
resolve();
|
|
123
|
+
};
|
|
124
|
+
script.onerror = () => reject(new Error('Failed to load HLS.js'));
|
|
125
|
+
document.head.appendChild(script);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const Hls = window.Hls;
|
|
129
|
+
if (Hls.isSupported()) {
|
|
130
|
+
const hls = new Hls({
|
|
131
|
+
enableWorker: true,
|
|
132
|
+
lowLatencyMode: false,
|
|
133
|
+
});
|
|
134
|
+
hls.loadSource(src);
|
|
135
|
+
hls.attachMedia(video);
|
|
136
|
+
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
137
|
+
this.updateState({ isLoading: false });
|
|
138
|
+
});
|
|
139
|
+
hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
140
|
+
const errorData = data;
|
|
141
|
+
if (errorData.fatal) {
|
|
142
|
+
switch (errorData.type) {
|
|
143
|
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
144
|
+
hls.startLoad();
|
|
145
|
+
break;
|
|
146
|
+
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
147
|
+
hls.recoverMediaError();
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
this.updateState({ error: 'HLS playback error', isLoading: false });
|
|
151
|
+
hls.destroy();
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
window.__videoHls = hls;
|
|
157
|
+
}
|
|
158
|
+
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
159
|
+
// Native HLS support (Safari)
|
|
160
|
+
video.src = src;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.updateState({ error: 'HLS not supported', isLoading: false });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Set up visibility handling via TileBridge
|
|
168
|
+
*/
|
|
169
|
+
setupVisibilityHandling() {
|
|
170
|
+
if (typeof window === 'undefined' || this.visibilitySetup)
|
|
171
|
+
return;
|
|
172
|
+
this.visibilitySetup = true;
|
|
173
|
+
const video = this.getVideo();
|
|
174
|
+
// Ensure video starts paused and muted
|
|
175
|
+
video.muted = true;
|
|
176
|
+
video.pause();
|
|
177
|
+
try {
|
|
178
|
+
const bridge = getTileBridge();
|
|
179
|
+
bridge.onVisibilityChange((visibilityState) => {
|
|
180
|
+
console.log('[VideoPlayer] Visibility changed:', visibilityState.visible);
|
|
181
|
+
if (visibilityState.visible) {
|
|
182
|
+
// Tile is visible - play video, unmute audio
|
|
183
|
+
video.muted = false;
|
|
184
|
+
video.play().catch(() => {
|
|
185
|
+
// If unmuted play fails (browser policy), retry muted
|
|
186
|
+
video.muted = true;
|
|
187
|
+
video.play().catch(() => { });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Tile is hidden - pause video, mute audio
|
|
192
|
+
video.muted = true;
|
|
193
|
+
video.pause();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Bridge not available (standalone tile, not in feed)
|
|
199
|
+
// For standalone tiles, play with audio if document is visible
|
|
200
|
+
if (!document.hidden) {
|
|
201
|
+
video.muted = false;
|
|
202
|
+
video.play().catch(() => {
|
|
203
|
+
video.muted = true;
|
|
204
|
+
video.play().catch(() => { });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Attach video element to a container (moves existing element)
|
|
211
|
+
*/
|
|
212
|
+
attachTo(container) {
|
|
213
|
+
const video = this.getVideo();
|
|
214
|
+
if (!container.contains(video)) {
|
|
215
|
+
container.appendChild(video);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get video element reference (for external use)
|
|
220
|
+
*/
|
|
221
|
+
getVideoRef() {
|
|
222
|
+
return this.getVideo();
|
|
223
|
+
}
|
|
224
|
+
// Playback controls
|
|
225
|
+
play() {
|
|
226
|
+
this.getVideo().play().catch(() => { });
|
|
227
|
+
}
|
|
228
|
+
pause() {
|
|
229
|
+
this.getVideo().pause();
|
|
230
|
+
}
|
|
231
|
+
toggle() {
|
|
232
|
+
const video = this.getVideo();
|
|
233
|
+
if (video.paused) {
|
|
234
|
+
video.play().catch(() => { });
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
video.pause();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
seek(time) {
|
|
241
|
+
this.getVideo().currentTime = time;
|
|
242
|
+
}
|
|
243
|
+
setMuted(muted) {
|
|
244
|
+
this.getVideo().muted = muted;
|
|
245
|
+
}
|
|
246
|
+
setVolume(volume) {
|
|
247
|
+
this.getVideo().volume = Math.max(0, Math.min(1, volume));
|
|
248
|
+
}
|
|
249
|
+
setLoop(loop) {
|
|
250
|
+
this.getVideo().loop = loop;
|
|
251
|
+
}
|
|
252
|
+
// State management
|
|
253
|
+
getState() {
|
|
254
|
+
return { ...this.currentState };
|
|
255
|
+
}
|
|
256
|
+
onStateChange(callback) {
|
|
257
|
+
this.stateCallbacks.add(callback);
|
|
258
|
+
// Immediately call with current state
|
|
259
|
+
callback(this.currentState);
|
|
260
|
+
return () => this.stateCallbacks.delete(callback);
|
|
261
|
+
}
|
|
262
|
+
updateState(partial) {
|
|
263
|
+
this.currentState = { ...this.currentState, ...partial };
|
|
264
|
+
this.stateCallbacks.forEach(cb => cb(this.currentState));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get the VideoSingleton instance (creates if needed)
|
|
269
|
+
*/
|
|
270
|
+
function getVideoSingleton() {
|
|
271
|
+
if (typeof window === 'undefined') {
|
|
272
|
+
// SSR - return a dummy that will be replaced on client
|
|
273
|
+
return new VideoSingletonClass();
|
|
274
|
+
}
|
|
275
|
+
if (!window.__videoSingleton) {
|
|
276
|
+
window.__videoSingleton = new VideoSingletonClass();
|
|
277
|
+
}
|
|
278
|
+
return window.__videoSingleton;
|
|
279
|
+
}
|
|
20
280
|
function getVideoMetadataFromEnv() {
|
|
21
|
-
// Check if we're in a Next.js environment with process.env
|
|
22
281
|
if (typeof process === 'undefined' || !process.env) {
|
|
23
282
|
return null;
|
|
24
283
|
}
|
|
@@ -28,11 +287,9 @@ function getVideoMetadataFromEnv() {
|
|
|
28
287
|
}
|
|
29
288
|
const hlsUrl = process.env.NEXT_PUBLIC_VIDEO_HLS_URL || null;
|
|
30
289
|
const originalUrl = process.env.NEXT_PUBLIC_VIDEO_ORIGINAL_URL || null;
|
|
31
|
-
// If neither URL is available, this isn't a video tile
|
|
32
290
|
if (!hlsUrl && !originalUrl) {
|
|
33
291
|
return null;
|
|
34
292
|
}
|
|
35
|
-
// Prefer HLS URL if available and transcoding is complete, otherwise use original
|
|
36
293
|
const transcodingComplete = process.env.NEXT_PUBLIC_VIDEO_TRANSCODING_COMPLETE === 'true';
|
|
37
294
|
const videoUrl = (hlsUrl && transcodingComplete) ? hlsUrl : (originalUrl || hlsUrl);
|
|
38
295
|
return {
|
|
@@ -46,33 +303,17 @@ function getVideoMetadataFromEnv() {
|
|
|
46
303
|
? parseFloat(process.env.NEXT_PUBLIC_VIDEO_DURATION)
|
|
47
304
|
: null,
|
|
48
305
|
title: process.env.NEXT_PUBLIC_VIDEO_TITLE || null,
|
|
49
|
-
// Default to false for autoplay - wait for visibility message from parent
|
|
50
|
-
// This prevents offscreen tiles from playing in TikTok-style feeds
|
|
51
306
|
autoplay: process.env.NEXT_PUBLIC_VIDEO_AUTOPLAY === 'true',
|
|
52
|
-
// Default to true for loop (most video tiles loop)
|
|
53
307
|
loop: process.env.NEXT_PUBLIC_VIDEO_LOOP !== 'false',
|
|
54
|
-
// Default to true for muted - start muted, unmute on visibility
|
|
55
308
|
muted: process.env.NEXT_PUBLIC_VIDEO_MUTED !== 'false',
|
|
56
309
|
};
|
|
57
310
|
}
|
|
58
311
|
const VideoContext = createContext(null);
|
|
59
312
|
/**
|
|
60
|
-
* VideoPlayer component with HLS streaming support.
|
|
61
|
-
* Provides video state and controls to child overlays via context.
|
|
62
|
-
*
|
|
63
|
-
* Features:
|
|
64
|
-
* - Video URL is injected at build time via NEXT_PUBLIC env vars (no API call!)
|
|
65
|
-
* - HLS streaming with automatic quality adaptation
|
|
66
|
-
* - Falls back to original video URL if transcoding not complete
|
|
67
|
-
* - Time-based cue points for triggering overlays
|
|
68
|
-
* - Visibility-aware playback (plays when visible, pauses when hidden)
|
|
69
|
-
* - Auto-loops by default (can be disabled via tile metadata)
|
|
313
|
+
* VideoPlayer component with HLS streaming support and route persistence.
|
|
70
314
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* - NEXT_PUBLIC_VIDEO_ORIGINAL_URL - Original video URL (fallback)
|
|
74
|
-
* - NEXT_PUBLIC_VIDEO_THUMBNAIL - Poster image
|
|
75
|
-
* - NEXT_PUBLIC_VIDEO_AUTOPLAY/LOOP/MUTED - Playback settings
|
|
315
|
+
* The video element persists across Next.js route changes (tile ↔ page).
|
|
316
|
+
* Video URL is read from NEXT_PUBLIC env vars (set at build time).
|
|
76
317
|
*
|
|
77
318
|
* Usage:
|
|
78
319
|
* ```tsx
|
|
@@ -82,379 +323,112 @@ const VideoContext = createContext(null);
|
|
|
82
323
|
* ```
|
|
83
324
|
*/
|
|
84
325
|
export function VideoPlayer({ controls = false, children, className = '', videoClassName = '', cuePoints = [], onCuePoint, onTimeUpdate, }) {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
326
|
+
const containerRef = useRef(null);
|
|
327
|
+
const videoRefProxy = useRef(null);
|
|
87
328
|
const triggeredCuePointsRef = useRef(new Set());
|
|
88
329
|
const lastTimeRef = useRef(0);
|
|
89
|
-
|
|
90
|
-
const [metadata, setMetadata] = useState(null);
|
|
330
|
+
const [state, setState] = useState(() => getVideoSingleton().getState());
|
|
91
331
|
const [metadataError, setMetadataError] = useState(null);
|
|
92
|
-
// Get video metadata from
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
const envMetadata = getVideoMetadataFromEnv();
|
|
96
|
-
if (!envMetadata) {
|
|
97
|
-
// Check if TILE_ID is set - if not, likely a dev environment issue
|
|
98
|
-
if (typeof process !== 'undefined' && !process.env?.NEXT_PUBLIC_TILE_ID) {
|
|
99
|
-
setMetadataError('NEXT_PUBLIC_TILE_ID not set');
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
setMetadataError('No video metadata found in environment variables');
|
|
103
|
-
}
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (!envMetadata.videoUrl) {
|
|
107
|
-
setMetadataError('No video URL found in environment variables');
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
setMetadata(envMetadata);
|
|
111
|
-
}, []);
|
|
112
|
-
// Get values from metadata
|
|
332
|
+
// Get video metadata from env vars
|
|
333
|
+
const metadata = getVideoMetadataFromEnv();
|
|
113
334
|
const src = metadata?.videoUrl || null;
|
|
114
|
-
// Default to false for autoplay - videos wait for visibility message from parent
|
|
115
|
-
// This prevents offscreen tiles from playing in TikTok-style feeds
|
|
116
|
-
const autoplay = metadata?.autoplay ?? false;
|
|
117
|
-
// Default to true for muted - videos start muted until visibility says otherwise
|
|
118
|
-
const muted = metadata?.muted ?? true;
|
|
119
335
|
const poster = metadata?.thumbnail || undefined;
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
duration: 0,
|
|
126
|
-
buffered: 0,
|
|
127
|
-
volume: 1,
|
|
128
|
-
muted: muted,
|
|
129
|
-
isLoading: true,
|
|
130
|
-
error: null,
|
|
131
|
-
});
|
|
132
|
-
// Control functions
|
|
133
|
-
const play = useCallback(() => {
|
|
134
|
-
videoRef.current?.play().catch(() => { });
|
|
135
|
-
}, []);
|
|
136
|
-
const pause = useCallback(() => {
|
|
137
|
-
videoRef.current?.pause();
|
|
138
|
-
}, []);
|
|
139
|
-
const toggle = useCallback(() => {
|
|
140
|
-
if (videoRef.current?.paused) {
|
|
141
|
-
play();
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
pause();
|
|
145
|
-
}
|
|
146
|
-
}, [play, pause]);
|
|
147
|
-
const seek = useCallback((time) => {
|
|
148
|
-
if (videoRef.current) {
|
|
149
|
-
videoRef.current.currentTime = time;
|
|
150
|
-
}
|
|
151
|
-
}, []);
|
|
152
|
-
const setVolume = useCallback((volume) => {
|
|
153
|
-
if (videoRef.current) {
|
|
154
|
-
videoRef.current.volume = Math.max(0, Math.min(1, volume));
|
|
155
|
-
}
|
|
336
|
+
// Subscribe to singleton state changes
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
const singleton = getVideoSingleton();
|
|
339
|
+
const unsubscribe = singleton.onStateChange(setState);
|
|
340
|
+
return unsubscribe;
|
|
156
341
|
}, []);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
342
|
+
// Attach video to container and load source
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (!containerRef.current)
|
|
345
|
+
return;
|
|
346
|
+
const singleton = getVideoSingleton();
|
|
347
|
+
// Attach the persistent video element to our container
|
|
348
|
+
singleton.attachTo(containerRef.current);
|
|
349
|
+
// Update video ref proxy
|
|
350
|
+
videoRefProxy.current = singleton.getVideoRef();
|
|
351
|
+
// Set loop preference
|
|
352
|
+
if (metadata?.loop !== undefined) {
|
|
353
|
+
singleton.setLoop(metadata.loop);
|
|
160
354
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const bridge = getTileBridge();
|
|
166
|
-
if (bridge.isVisible()) {
|
|
167
|
-
// Visible tile - try unmuted first for mobile WebViews
|
|
168
|
-
video.muted = false;
|
|
169
|
-
video.play().catch(() => {
|
|
170
|
-
// Unmuted failed (browser policy) - fall back to muted
|
|
171
|
-
video.muted = true;
|
|
172
|
-
video.play().catch(() => { });
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// Not visible - play muted
|
|
177
|
-
video.muted = true;
|
|
178
|
-
video.play().catch(() => { });
|
|
179
|
-
}
|
|
355
|
+
// Load source if available
|
|
356
|
+
if (src) {
|
|
357
|
+
singleton.loadSource(src, poster);
|
|
180
358
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
video.play().catch(() => { });
|
|
359
|
+
else if (!metadata) {
|
|
360
|
+
setMetadataError('No video metadata found in environment variables');
|
|
184
361
|
}
|
|
185
|
-
|
|
186
|
-
|
|
362
|
+
// Set up visibility handling
|
|
363
|
+
singleton.setupVisibilityHandling();
|
|
364
|
+
// Note: We don't detach on unmount - that's the whole point of persistence!
|
|
365
|
+
}, [src, poster, metadata]);
|
|
366
|
+
// Handle cue points and time updates
|
|
187
367
|
useEffect(() => {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const isHlsUrl = src.includes('.m3u8');
|
|
193
|
-
if (!isHlsUrl) {
|
|
194
|
-
// Regular video file (MP4, WebM, etc.) - just set src directly
|
|
195
|
-
const handleLoadedMetadata = () => {
|
|
196
|
-
setState(s => ({ ...s, isLoading: false }));
|
|
197
|
-
if (autoplay) {
|
|
198
|
-
startPlayback(video);
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
const handleError = () => {
|
|
202
|
-
setState(s => ({ ...s, error: 'Failed to load video', isLoading: false }));
|
|
203
|
-
};
|
|
204
|
-
video.src = src;
|
|
205
|
-
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
206
|
-
video.addEventListener('error', handleError);
|
|
207
|
-
return () => {
|
|
208
|
-
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
209
|
-
video.removeEventListener('error', handleError);
|
|
210
|
-
};
|
|
368
|
+
const { currentTime, duration } = state;
|
|
369
|
+
// Call onTimeUpdate callback
|
|
370
|
+
if (onTimeUpdate && !isNaN(duration) && duration > 0) {
|
|
371
|
+
onTimeUpdate(currentTime, duration);
|
|
211
372
|
}
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
script.onload = () => resolve();
|
|
219
|
-
script.onerror = () => reject(new Error('Failed to load HLS.js'));
|
|
220
|
-
document.head.appendChild(script);
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
const Hls = window.Hls;
|
|
224
|
-
if (Hls.isSupported()) {
|
|
225
|
-
const hls = new Hls({
|
|
226
|
-
enableWorker: true,
|
|
227
|
-
lowLatencyMode: false,
|
|
228
|
-
});
|
|
229
|
-
hlsRef.current = hls;
|
|
230
|
-
hls.loadSource(src);
|
|
231
|
-
hls.attachMedia(video);
|
|
232
|
-
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
233
|
-
setState(s => ({ ...s, isLoading: false }));
|
|
234
|
-
if (autoplay) {
|
|
235
|
-
startPlayback(video);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
239
|
-
const errorData = data;
|
|
240
|
-
if (errorData.fatal) {
|
|
241
|
-
switch (errorData.type) {
|
|
242
|
-
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
243
|
-
hls.startLoad();
|
|
244
|
-
break;
|
|
245
|
-
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
246
|
-
hls.recoverMediaError();
|
|
247
|
-
break;
|
|
248
|
-
default:
|
|
249
|
-
setState(s => ({ ...s, error: 'Failed to load video', isLoading: false }));
|
|
250
|
-
hls.destroy();
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
257
|
-
// Native HLS support (Safari)
|
|
258
|
-
video.src = src;
|
|
259
|
-
video.addEventListener('loadedmetadata', () => {
|
|
260
|
-
setState(s => ({ ...s, isLoading: false }));
|
|
261
|
-
if (autoplay) {
|
|
262
|
-
startPlayback(video);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
setState(s => ({ ...s, error: 'HLS not supported', isLoading: false }));
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
loadHls().catch(error => {
|
|
271
|
-
setState(s => ({ ...s, error: error.message, isLoading: false }));
|
|
272
|
-
});
|
|
273
|
-
return () => {
|
|
274
|
-
hlsRef.current?.destroy();
|
|
275
|
-
hlsRef.current = null;
|
|
276
|
-
};
|
|
277
|
-
}, [src, autoplay, startPlayback]);
|
|
278
|
-
// Video event listeners
|
|
279
|
-
useEffect(() => {
|
|
280
|
-
const video = videoRef.current;
|
|
281
|
-
if (!video)
|
|
282
|
-
return;
|
|
283
|
-
const handlePlay = () => setState(s => ({ ...s, isPlaying: true, isLoading: false }));
|
|
284
|
-
const handlePause = () => setState(s => ({ ...s, isPlaying: false }));
|
|
285
|
-
// Fallback for loading state - hide spinner when video can play
|
|
286
|
-
const handleCanPlay = () => setState(s => ({ ...s, isLoading: false }));
|
|
287
|
-
const handleLoadedData = () => setState(s => ({ ...s, isLoading: false }));
|
|
288
|
-
const handleTimeUpdate = () => {
|
|
289
|
-
const currentTime = video.currentTime;
|
|
290
|
-
const duration = video.duration;
|
|
291
|
-
setState(s => ({ ...s, currentTime }));
|
|
292
|
-
// Call onTimeUpdate callback
|
|
293
|
-
if (onTimeUpdate && !isNaN(duration)) {
|
|
294
|
-
onTimeUpdate(currentTime, duration);
|
|
295
|
-
}
|
|
296
|
-
// Check cue points - trigger if we crossed the cue time since last update
|
|
297
|
-
if (onCuePoint && cuePoints.length > 0) {
|
|
298
|
-
const lastTime = lastTimeRef.current;
|
|
299
|
-
for (const cuePoint of cuePoints) {
|
|
300
|
-
const cueKey = `${cuePoint.id}-${Math.floor(currentTime / (duration || 1))}`;
|
|
301
|
-
// Trigger if we crossed the cue point time (handles both forward and loop)
|
|
302
|
-
// For looping: reset triggered cues when video loops back
|
|
303
|
-
if (currentTime < lastTime && lastTime > duration * 0.9) {
|
|
304
|
-
// Video looped - clear triggered cues
|
|
305
|
-
triggeredCuePointsRef.current.clear();
|
|
306
|
-
}
|
|
307
|
-
const alreadyTriggered = triggeredCuePointsRef.current.has(cueKey);
|
|
308
|
-
const crossedCuePoint = lastTime < cuePoint.time && currentTime >= cuePoint.time;
|
|
309
|
-
if (!alreadyTriggered && crossedCuePoint) {
|
|
310
|
-
triggeredCuePointsRef.current.add(cueKey);
|
|
311
|
-
onCuePoint(cuePoint);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
lastTimeRef.current = currentTime;
|
|
316
|
-
};
|
|
317
|
-
const handleDurationChange = () => setState(s => ({ ...s, duration: video.duration }));
|
|
318
|
-
const handleVolumeChange = () => setState(s => ({ ...s, volume: video.volume, muted: video.muted }));
|
|
319
|
-
const handleProgress = () => {
|
|
320
|
-
if (video.buffered.length > 0) {
|
|
321
|
-
setState(s => ({
|
|
322
|
-
...s,
|
|
323
|
-
buffered: video.buffered.end(video.buffered.length - 1),
|
|
324
|
-
}));
|
|
373
|
+
// Check cue points
|
|
374
|
+
if (onCuePoint && cuePoints.length > 0) {
|
|
375
|
+
const lastTime = lastTimeRef.current;
|
|
376
|
+
// Detect video loop
|
|
377
|
+
if (currentTime < lastTime && lastTime > duration * 0.9) {
|
|
378
|
+
triggeredCuePointsRef.current.clear();
|
|
325
379
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
video.addEventListener('progress', handleProgress);
|
|
334
|
-
video.addEventListener('error', handleError);
|
|
335
|
-
video.addEventListener('canplay', handleCanPlay);
|
|
336
|
-
video.addEventListener('loadeddata', handleLoadedData);
|
|
337
|
-
return () => {
|
|
338
|
-
video.removeEventListener('play', handlePlay);
|
|
339
|
-
video.removeEventListener('pause', handlePause);
|
|
340
|
-
video.removeEventListener('timeupdate', handleTimeUpdate);
|
|
341
|
-
video.removeEventListener('durationchange', handleDurationChange);
|
|
342
|
-
video.removeEventListener('volumechange', handleVolumeChange);
|
|
343
|
-
video.removeEventListener('progress', handleProgress);
|
|
344
|
-
video.removeEventListener('error', handleError);
|
|
345
|
-
video.removeEventListener('canplay', handleCanPlay);
|
|
346
|
-
video.removeEventListener('loadeddata', handleLoadedData);
|
|
347
|
-
};
|
|
348
|
-
}, [onTimeUpdate, onCuePoint, cuePoints]);
|
|
349
|
-
// Visibility handling - play/pause AND mute/unmute based on tile visibility.
|
|
350
|
-
// Videos start paused and ONLY play when parent sends visible: true.
|
|
351
|
-
// This enables TikTok-style feeds where only the active tile plays.
|
|
352
|
-
// IMPORTANT: Do NOT check initial state - wait for explicit visibility message.
|
|
353
|
-
useEffect(() => {
|
|
354
|
-
const video = videoRef.current;
|
|
355
|
-
if (!video)
|
|
356
|
-
return;
|
|
357
|
-
// Ensure video starts paused and muted
|
|
358
|
-
video.muted = true;
|
|
359
|
-
video.pause();
|
|
360
|
-
try {
|
|
361
|
-
const bridge = getTileBridge();
|
|
362
|
-
// Handle visibility changes from parent (mobile app)
|
|
363
|
-
// ONLY respond to explicit messages, not initial state
|
|
364
|
-
const unsubscribe = bridge.onVisibilityChange((visibilityState) => {
|
|
365
|
-
console.log('[VideoPlayer] Visibility changed:', visibilityState.visible);
|
|
366
|
-
if (visibilityState.visible) {
|
|
367
|
-
// Tile is visible - play video, unmute audio
|
|
368
|
-
video.muted = false;
|
|
369
|
-
video.play().catch(() => {
|
|
370
|
-
// If unmuted play fails (browser policy), retry muted
|
|
371
|
-
video.muted = true;
|
|
372
|
-
video.play().catch(() => { });
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
// Tile is hidden - pause video, mute audio
|
|
377
|
-
video.muted = true;
|
|
378
|
-
video.pause();
|
|
380
|
+
for (const cuePoint of cuePoints) {
|
|
381
|
+
const cueKey = `${cuePoint.id}-${Math.floor(currentTime / (duration || 1))}`;
|
|
382
|
+
const alreadyTriggered = triggeredCuePointsRef.current.has(cueKey);
|
|
383
|
+
const crossedCuePoint = lastTime < cuePoint.time && currentTime >= cuePoint.time;
|
|
384
|
+
if (!alreadyTriggered && crossedCuePoint) {
|
|
385
|
+
triggeredCuePointsRef.current.add(cueKey);
|
|
386
|
+
onCuePoint(cuePoint);
|
|
379
387
|
}
|
|
380
|
-
});
|
|
381
|
-
return () => {
|
|
382
|
-
unsubscribe();
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
catch {
|
|
386
|
-
// Bridge not available (standalone tile, not in feed)
|
|
387
|
-
// For standalone tiles, play with audio if document is visible
|
|
388
|
-
if (!document.hidden) {
|
|
389
|
-
video.muted = false;
|
|
390
|
-
video.play().catch(() => {
|
|
391
|
-
video.muted = true;
|
|
392
|
-
video.play().catch(() => { });
|
|
393
|
-
});
|
|
394
388
|
}
|
|
395
389
|
}
|
|
396
|
-
|
|
390
|
+
lastTimeRef.current = currentTime;
|
|
391
|
+
}, [state.currentTime, state.duration, onTimeUpdate, onCuePoint, cuePoints]);
|
|
392
|
+
// Control functions
|
|
393
|
+
const singleton = getVideoSingleton();
|
|
394
|
+
const controlsObj = {
|
|
395
|
+
play: useCallback(() => singleton.play(), []),
|
|
396
|
+
pause: useCallback(() => singleton.pause(), []),
|
|
397
|
+
toggle: useCallback(() => singleton.toggle(), []),
|
|
398
|
+
seek: useCallback((time) => singleton.seek(time), []),
|
|
399
|
+
setVolume: useCallback((volume) => singleton.setVolume(volume), []),
|
|
400
|
+
setMuted: useCallback((muted) => singleton.setMuted(muted), []),
|
|
401
|
+
};
|
|
397
402
|
const contextValue = {
|
|
398
403
|
state,
|
|
399
|
-
controls:
|
|
400
|
-
videoRef:
|
|
404
|
+
controls: controlsObj,
|
|
405
|
+
videoRef: videoRefProxy,
|
|
401
406
|
};
|
|
402
407
|
return (React.createElement(VideoContext.Provider, { value: contextValue },
|
|
403
408
|
React.createElement("div", { className: `relative w-full h-full bg-black ${className}` },
|
|
404
|
-
React.createElement("
|
|
405
|
-
// Hide iOS Safari's native poster play button
|
|
406
|
-
style: {
|
|
407
|
-
// @ts-expect-error - WebKit-specific property
|
|
408
|
-
WebkitMediaControlsStartPlaybackButton: 'none',
|
|
409
|
-
} }),
|
|
409
|
+
React.createElement("div", { ref: containerRef, className: `absolute inset-0 ${videoClassName}` }),
|
|
410
410
|
React.createElement("style", null, `
|
|
411
|
-
|
|
412
|
-
video::-webkit-media-controls-start-playback-button {
|
|
411
|
+
#persistent-video::-webkit-media-controls-start-playback-button {
|
|
413
412
|
display: none !important;
|
|
414
413
|
-webkit-appearance: none;
|
|
415
414
|
opacity: 0 !important;
|
|
416
|
-
pointer-events: none !important;
|
|
417
|
-
}
|
|
418
|
-
video::-webkit-media-controls-panel {
|
|
419
|
-
display: none !important;
|
|
420
|
-
opacity: 0 !important;
|
|
421
|
-
}
|
|
422
|
-
video::-webkit-media-controls {
|
|
423
|
-
display: none !important;
|
|
424
|
-
opacity: 0 !important;
|
|
425
|
-
}
|
|
426
|
-
video::-webkit-media-controls-play-button {
|
|
427
|
-
display: none !important;
|
|
428
|
-
opacity: 0 !important;
|
|
429
|
-
}
|
|
430
|
-
video::-webkit-media-controls-overlay-play-button {
|
|
431
|
-
display: none !important;
|
|
432
|
-
opacity: 0 !important;
|
|
433
|
-
}
|
|
434
|
-
/* Firefox */
|
|
435
|
-
video::-moz-media-controls {
|
|
436
|
-
display: none !important;
|
|
437
|
-
}
|
|
438
|
-
/* Microsoft Edge / IE */
|
|
439
|
-
video::-ms-media-controls {
|
|
440
|
-
display: none !important;
|
|
441
|
-
}
|
|
442
|
-
/* General - remove default controls styling */
|
|
443
|
-
video {
|
|
444
|
-
-webkit-media-controls-start-playback-button: none;
|
|
445
|
-
}
|
|
446
|
-
video[poster] {
|
|
447
|
-
object-fit: cover;
|
|
448
415
|
}
|
|
416
|
+
#persistent-video::-webkit-media-controls-panel { display: none !important; }
|
|
417
|
+
#persistent-video::-webkit-media-controls { display: none !important; }
|
|
418
|
+
#persistent-video::-webkit-media-controls-play-button { display: none !important; }
|
|
419
|
+
#persistent-video::-webkit-media-controls-overlay-play-button { display: none !important; }
|
|
420
|
+
#persistent-video::-moz-media-controls { display: none !important; }
|
|
421
|
+
#persistent-video::-ms-media-controls { display: none !important; }
|
|
449
422
|
`),
|
|
450
|
-
(state.isLoading || (!src && !metadataError)) && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center" },
|
|
423
|
+
(state.isLoading || (!src && !metadataError)) && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center z-20" },
|
|
451
424
|
React.createElement("div", { className: "w-10 h-10 border-3 border-white/30 border-t-white rounded-full animate-spin" }))),
|
|
452
|
-
(state.error || metadataError) && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center text-white/80" },
|
|
425
|
+
(state.error || metadataError) && (React.createElement("div", { className: "absolute inset-0 flex items-center justify-center text-white/80 z-20" },
|
|
453
426
|
React.createElement("p", null, state.error || metadataError))),
|
|
454
|
-
children)));
|
|
427
|
+
React.createElement("div", { className: "absolute inset-0 z-10" }, children))));
|
|
455
428
|
}
|
|
456
429
|
/**
|
|
457
430
|
* Hook to access video state and controls from within VideoPlayer children.
|
|
431
|
+
* Also aliased as useVideo for compatibility.
|
|
458
432
|
*/
|
|
459
433
|
export function useVideoState() {
|
|
460
434
|
const context = useContext(VideoContext);
|
|
@@ -463,21 +437,10 @@ export function useVideoState() {
|
|
|
463
437
|
}
|
|
464
438
|
return context;
|
|
465
439
|
}
|
|
440
|
+
// Alias for compatibility with PersistentVideo's useVideo
|
|
441
|
+
export const useVideo = useVideoState;
|
|
466
442
|
/**
|
|
467
443
|
* Hook to trigger an action when a specific time is reached.
|
|
468
|
-
* Returns true when the video has reached or passed the specified time.
|
|
469
|
-
*
|
|
470
|
-
* @param triggerTime - Time in seconds when to trigger
|
|
471
|
-
* @param options - Configuration options
|
|
472
|
-
* @returns boolean indicating if the trigger time has been reached
|
|
473
|
-
*
|
|
474
|
-
* @example
|
|
475
|
-
* ```tsx
|
|
476
|
-
* function PollOverlay() {
|
|
477
|
-
* const showPoll = useCuePoint(5); // Show after 5 seconds
|
|
478
|
-
* return showPoll ? <Poll /> : null;
|
|
479
|
-
* }
|
|
480
|
-
* ```
|
|
481
444
|
*/
|
|
482
445
|
export function useCuePoint(triggerTime, options = {}) {
|
|
483
446
|
const { resetOnLoop = true, mode = 'once' } = options;
|
|
@@ -486,13 +449,11 @@ export function useCuePoint(triggerTime, options = {}) {
|
|
|
486
449
|
const lastTimeRef = useRef(0);
|
|
487
450
|
useEffect(() => {
|
|
488
451
|
const { currentTime, duration } = state;
|
|
489
|
-
// Detect video loop (time jumped backwards significantly)
|
|
490
452
|
if (currentTime < lastTimeRef.current && lastTimeRef.current > duration * 0.9) {
|
|
491
453
|
if (resetOnLoop && mode === 'every-loop') {
|
|
492
454
|
setTriggered(false);
|
|
493
455
|
}
|
|
494
456
|
}
|
|
495
|
-
// Check if we crossed the trigger time
|
|
496
457
|
if (!triggered && currentTime >= triggerTime) {
|
|
497
458
|
setTriggered(true);
|
|
498
459
|
}
|
|
@@ -502,31 +463,6 @@ export function useCuePoint(triggerTime, options = {}) {
|
|
|
502
463
|
}
|
|
503
464
|
/**
|
|
504
465
|
* Hook to manage multiple cue points with callbacks.
|
|
505
|
-
* More flexible than useCuePoint for complex time-based interactions.
|
|
506
|
-
*
|
|
507
|
-
* @param cuePoints - Array of cue points with times and callbacks
|
|
508
|
-
* @param options - Configuration options
|
|
509
|
-
*
|
|
510
|
-
* @example
|
|
511
|
-
* ```tsx
|
|
512
|
-
* function InteractiveOverlay() {
|
|
513
|
-
* const [showPoll, setShowPoll] = useState(false);
|
|
514
|
-
* const [showCTA, setShowCTA] = useState(false);
|
|
515
|
-
*
|
|
516
|
-
* useCuePoints([
|
|
517
|
-
* { time: 5, onTrigger: () => setShowPoll(true) },
|
|
518
|
-
* { time: 10, onTrigger: () => setShowCTA(true) },
|
|
519
|
-
* { time: 15, onTrigger: () => { setShowPoll(false); setShowCTA(false); } },
|
|
520
|
-
* ]);
|
|
521
|
-
*
|
|
522
|
-
* return (
|
|
523
|
-
* <>
|
|
524
|
-
* {showPoll && <Poll />}
|
|
525
|
-
* {showCTA && <CTAButton />}
|
|
526
|
-
* </>
|
|
527
|
-
* );
|
|
528
|
-
* }
|
|
529
|
-
* ```
|
|
530
466
|
*/
|
|
531
467
|
export function useCuePoints(cuePoints, options = {}) {
|
|
532
468
|
const { resetOnLoop = true } = options;
|
|
@@ -535,13 +471,11 @@ export function useCuePoints(cuePoints, options = {}) {
|
|
|
535
471
|
const lastTimeRef = useRef(0);
|
|
536
472
|
useEffect(() => {
|
|
537
473
|
const { currentTime, duration } = state;
|
|
538
|
-
// Detect video loop
|
|
539
474
|
if (currentTime < lastTimeRef.current && lastTimeRef.current > duration * 0.9) {
|
|
540
475
|
if (resetOnLoop) {
|
|
541
476
|
triggeredRef.current.clear();
|
|
542
477
|
}
|
|
543
478
|
}
|
|
544
|
-
// Check each cue point
|
|
545
479
|
for (let i = 0; i < cuePoints.length; i++) {
|
|
546
480
|
const cue = cuePoints[i];
|
|
547
481
|
const cueId = cue.id ?? `cue-${i}`;
|
|
@@ -557,17 +491,6 @@ export function useCuePoints(cuePoints, options = {}) {
|
|
|
557
491
|
}
|
|
558
492
|
/**
|
|
559
493
|
* Hook to get the current video progress as a percentage (0-100).
|
|
560
|
-
* Useful for progress bars or time-based animations.
|
|
561
|
-
*
|
|
562
|
-
* @returns Progress percentage (0-100)
|
|
563
|
-
*
|
|
564
|
-
* @example
|
|
565
|
-
* ```tsx
|
|
566
|
-
* function ProgressBar() {
|
|
567
|
-
* const progress = useVideoProgress();
|
|
568
|
-
* return <div style={{ width: `${progress}%` }} className="h-1 bg-white" />;
|
|
569
|
-
* }
|
|
570
|
-
* ```
|
|
571
494
|
*/
|
|
572
495
|
export function useVideoProgress() {
|
|
573
496
|
const { state } = useVideoState();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { VideoPlayer, useVideoState,
|
|
1
|
+
export { VideoPlayer, useVideoState, useVideo, // Alias for useVideoState (compatibility)
|
|
2
|
+
useCuePoint, useCuePoints, useVideoProgress, } from './VideoPlayer';
|
|
2
3
|
export type { VideoState, VideoControls, VideoContextValue, VideoPlayerProps, CuePoint, } from './VideoPlayer';
|
|
3
4
|
export { Slideshow, useSlideshowState, } from './Slideshow';
|
|
4
5
|
export type { SlideImage, SlideshowState, SlideshowControls, SlideshowContextValue, SlideshowProps, } from './Slideshow';
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/overlay/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,aAAa,EACb,QAAQ,EAAE,0CAA0C;AACpD,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,6 @@
|
|
|
1
|
-
// Video player with HLS streaming support
|
|
2
|
-
export { VideoPlayer, useVideoState,
|
|
1
|
+
// Video player with HLS streaming support and route persistence
|
|
2
|
+
export { VideoPlayer, useVideoState, useVideo, // Alias for useVideoState (compatibility)
|
|
3
|
+
useCuePoint, useCuePoints, useVideoProgress, } from './VideoPlayer';
|
|
3
4
|
// Image slideshow component
|
|
4
5
|
export { Slideshow, useSlideshowState, } from './Slideshow';
|
|
5
6
|
// Overlay positioning components
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* Pre-built templates for common tile types.
|
|
5
5
|
* These get injected as strings by tile-deploy/agent-service.
|
|
6
6
|
*/
|
|
7
|
-
export * from './video';
|
|
8
|
-
export * from './slideshow';
|
|
7
|
+
export * from './video/index.js';
|
|
8
|
+
export * from './slideshow/index.js';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
|
package/dist/templates/index.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Pre-built templates for common tile types.
|
|
5
5
|
* These get injected as strings by tile-deploy/agent-service.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
export * from './
|
|
7
|
+
// ESM requires explicit index.js imports (no directory imports)
|
|
8
|
+
export * from './video/index.js';
|
|
9
|
+
export * from './slideshow/index.js';
|
|
9
10
|
// Future templates:
|
|
10
|
-
// export * from './image';
|
|
11
|
+
// export * from './image/index.js';
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* These templates are used by tile-deploy/agent-service to generate
|
|
5
5
|
* slideshow tiles with persistent state across route navigation.
|
|
6
6
|
*/
|
|
7
|
-
export { slideshowManagerTemplate } from './SlideshowManager.ts.template';
|
|
8
|
-
export { persistentSlideshowTemplate } from './PersistentSlideshow.tsx.template';
|
|
7
|
+
export { slideshowManagerTemplate } from './SlideshowManager.ts.template.js';
|
|
8
|
+
export { persistentSlideshowTemplate } from './PersistentSlideshow.tsx.template.js';
|
|
9
9
|
export declare const slideshowTemplateFiles: {
|
|
10
10
|
'src/shared/lib/SlideshowManager.ts': string;
|
|
11
11
|
'src/shared/components/PersistentSlideshow.tsx': string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/slideshow/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/slideshow/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAC;AAGpF,eAAO,MAAM,sBAAsB;;;CAGlC,CAAC"}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* These templates are used by tile-deploy/agent-service to generate
|
|
5
5
|
* slideshow tiles with persistent state across route navigation.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
export {
|
|
7
|
+
// ESM requires explicit .js extension for imports
|
|
8
|
+
export { slideshowManagerTemplate } from './SlideshowManager.ts.template.js';
|
|
9
|
+
export { persistentSlideshowTemplate } from './PersistentSlideshow.tsx.template.js';
|
|
9
10
|
// Template file structure for slideshow tiles
|
|
10
11
|
export const slideshowTemplateFiles = {
|
|
11
12
|
'src/shared/lib/SlideshowManager.ts': 'slideshowManagerTemplate',
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* These templates are used by tile-deploy/agent-service to generate
|
|
5
5
|
* video tiles with persistent playback across route navigation.
|
|
6
6
|
*/
|
|
7
|
-
export { videoLayoutTemplate } from './layout.tsx.template';
|
|
8
|
-
export { videoManagerTemplate } from './VideoManager.ts.template';
|
|
9
|
-
export { persistentVideoTemplate } from './PersistentVideo.tsx.template';
|
|
7
|
+
export { videoLayoutTemplate } from './layout.tsx.template.js';
|
|
8
|
+
export { videoManagerTemplate } from './VideoManager.ts.template.js';
|
|
9
|
+
export { persistentVideoTemplate } from './PersistentVideo.tsx.template.js';
|
|
10
10
|
export declare const videoTemplateFiles: {
|
|
11
11
|
'src/shared/lib/VideoManager.ts': string;
|
|
12
12
|
'src/shared/components/PersistentVideo.tsx': string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/video/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/video/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAG5E,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC"}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* These templates are used by tile-deploy/agent-service to generate
|
|
5
5
|
* video tiles with persistent playback across route navigation.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
7
|
+
// ESM requires explicit .js extension for imports
|
|
8
|
+
export { videoLayoutTemplate } from './layout.tsx.template.js';
|
|
9
|
+
export { videoManagerTemplate } from './VideoManager.ts.template.js';
|
|
10
|
+
export { persistentVideoTemplate } from './PersistentVideo.tsx.template.js';
|
|
10
11
|
// Template file structure for video tiles
|
|
11
12
|
export const videoTemplateFiles = {
|
|
12
13
|
'src/shared/lib/VideoManager.ts': 'videoManagerTemplate',
|