@thewhateverapp/tile-sdk 0.12.19 → 0.12.20
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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/templates/index.d.ts +8 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +10 -0
- package/dist/templates/video/PersistentVideo.tsx.template.d.ts +8 -0
- package/dist/templates/video/PersistentVideo.tsx.template.d.ts.map +1 -0
- package/dist/templates/video/PersistentVideo.tsx.template.js +109 -0
- package/dist/templates/video/VideoManager.ts.template.d.ts +8 -0
- package/dist/templates/video/VideoManager.ts.template.d.ts.map +1 -0
- package/dist/templates/video/VideoManager.ts.template.js +266 -0
- package/dist/templates/video/index.d.ts +14 -0
- package/dist/templates/video/index.d.ts.map +1 -0
- package/dist/templates/video/index.js +14 -0
- package/dist/templates/video/layout.tsx.template.d.ts +8 -0
- package/dist/templates/video/layout.tsx.template.d.ts.map +1 -0
- package/dist/templates/video/layout.tsx.template.js +26 -0
- package/package.json +9 -1
package/dist/index.d.ts
CHANGED
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,EAEb,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"}
|
|
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,EAEb,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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Template - PersistentVideo Component
|
|
3
|
+
*
|
|
4
|
+
* React component that attaches to the VideoManager singleton.
|
|
5
|
+
* The video element persists across route changes.
|
|
6
|
+
*/
|
|
7
|
+
export declare const persistentVideoTemplate = "'use client';\n\nimport { useEffect, useRef, useState, createContext, useContext, ReactNode } from 'react';\nimport { VideoManager, VideoState } from './VideoManager';\n\n// Context to share video state with overlays\ninterface VideoContextValue {\n state: VideoState;\n controls: {\n play: () => void;\n pause: () => void;\n toggle: () => void;\n seek: (time: number) => void;\n setMuted: (muted: boolean) => void;\n setVolume: (volume: number) => void;\n };\n}\n\nconst VideoContext = createContext<VideoContextValue | null>(null);\n\nexport function useVideo(): VideoContextValue {\n const context = useContext(VideoContext);\n if (!context) {\n throw new Error('useVideo must be used within PersistentVideo');\n }\n return context;\n}\n\ninterface PersistentVideoProps {\n src?: string;\n hlsUrl?: string;\n className?: string;\n children?: ReactNode;\n}\n\nexport function PersistentVideo({\n src,\n hlsUrl,\n className = '',\n children\n}: PersistentVideoProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [state, setState] = useState<VideoState>(VideoManager.getState());\n\n useEffect(() => {\n // Subscribe to state changes\n const unsubscribe = VideoManager.onStateChange(setState);\n\n // Attach video to container\n if (containerRef.current) {\n VideoManager.attachTo(containerRef.current);\n }\n\n // Load source if provided\n const videoSrc = hlsUrl || src;\n if (videoSrc) {\n VideoManager.loadSource(videoSrc);\n }\n\n return () => {\n unsubscribe();\n // Don't detach - let video persist for route changes\n };\n }, [src, hlsUrl]);\n\n const contextValue: VideoContextValue = {\n state,\n controls: {\n play: () => VideoManager.play(),\n pause: () => VideoManager.pause(),\n toggle: () => VideoManager.toggle(),\n seek: (time) => VideoManager.seek(time),\n setMuted: (muted) => VideoManager.setMuted(muted),\n setVolume: (volume) => VideoManager.setVolume(volume),\n },\n };\n\n return (\n <VideoContext.Provider value={contextValue}>\n <div className={`relative w-full h-full ${className}`}>\n {/* Container for persistent video element */}\n <div\n ref={containerRef}\n className=\"absolute inset-0\"\n style={{ backgroundColor: 'black' }}\n />\n\n {/* Hide native controls */}\n <style>{`\n #persistent-video::-webkit-media-controls { display: none !important; }\n #persistent-video::-webkit-media-controls-start-playback-button { display: none !important; }\n #persistent-video::-webkit-media-controls-overlay-play-button { display: none !important; }\n `}</style>\n\n {/* Overlay content */}\n <div className=\"absolute inset-0 z-10\">\n {children}\n </div>\n </div>\n </VideoContext.Provider>\n );\n}\n";
|
|
8
|
+
//# sourceMappingURL=PersistentVideo.tsx.template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PersistentVideo.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/video/PersistentVideo.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,+2FAsGnC,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Template - PersistentVideo Component
|
|
3
|
+
*
|
|
4
|
+
* React component that attaches to the VideoManager singleton.
|
|
5
|
+
* The video element persists across route changes.
|
|
6
|
+
*/
|
|
7
|
+
export const persistentVideoTemplate = `'use client';
|
|
8
|
+
|
|
9
|
+
import { useEffect, useRef, useState, createContext, useContext, ReactNode } from 'react';
|
|
10
|
+
import { VideoManager, VideoState } from './VideoManager';
|
|
11
|
+
|
|
12
|
+
// Context to share video state with overlays
|
|
13
|
+
interface VideoContextValue {
|
|
14
|
+
state: VideoState;
|
|
15
|
+
controls: {
|
|
16
|
+
play: () => void;
|
|
17
|
+
pause: () => void;
|
|
18
|
+
toggle: () => void;
|
|
19
|
+
seek: (time: number) => void;
|
|
20
|
+
setMuted: (muted: boolean) => void;
|
|
21
|
+
setVolume: (volume: number) => void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const VideoContext = createContext<VideoContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
export function useVideo(): VideoContextValue {
|
|
28
|
+
const context = useContext(VideoContext);
|
|
29
|
+
if (!context) {
|
|
30
|
+
throw new Error('useVideo must be used within PersistentVideo');
|
|
31
|
+
}
|
|
32
|
+
return context;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PersistentVideoProps {
|
|
36
|
+
src?: string;
|
|
37
|
+
hlsUrl?: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
children?: ReactNode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function PersistentVideo({
|
|
43
|
+
src,
|
|
44
|
+
hlsUrl,
|
|
45
|
+
className = '',
|
|
46
|
+
children
|
|
47
|
+
}: PersistentVideoProps) {
|
|
48
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
const [state, setState] = useState<VideoState>(VideoManager.getState());
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// Subscribe to state changes
|
|
53
|
+
const unsubscribe = VideoManager.onStateChange(setState);
|
|
54
|
+
|
|
55
|
+
// Attach video to container
|
|
56
|
+
if (containerRef.current) {
|
|
57
|
+
VideoManager.attachTo(containerRef.current);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Load source if provided
|
|
61
|
+
const videoSrc = hlsUrl || src;
|
|
62
|
+
if (videoSrc) {
|
|
63
|
+
VideoManager.loadSource(videoSrc);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
unsubscribe();
|
|
68
|
+
// Don't detach - let video persist for route changes
|
|
69
|
+
};
|
|
70
|
+
}, [src, hlsUrl]);
|
|
71
|
+
|
|
72
|
+
const contextValue: VideoContextValue = {
|
|
73
|
+
state,
|
|
74
|
+
controls: {
|
|
75
|
+
play: () => VideoManager.play(),
|
|
76
|
+
pause: () => VideoManager.pause(),
|
|
77
|
+
toggle: () => VideoManager.toggle(),
|
|
78
|
+
seek: (time) => VideoManager.seek(time),
|
|
79
|
+
setMuted: (muted) => VideoManager.setMuted(muted),
|
|
80
|
+
setVolume: (volume) => VideoManager.setVolume(volume),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<VideoContext.Provider value={contextValue}>
|
|
86
|
+
<div className={\`relative w-full h-full \${className}\`}>
|
|
87
|
+
{/* Container for persistent video element */}
|
|
88
|
+
<div
|
|
89
|
+
ref={containerRef}
|
|
90
|
+
className="absolute inset-0"
|
|
91
|
+
style={{ backgroundColor: 'black' }}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
{/* Hide native controls */}
|
|
95
|
+
<style>{\`
|
|
96
|
+
#persistent-video::-webkit-media-controls { display: none !important; }
|
|
97
|
+
#persistent-video::-webkit-media-controls-start-playback-button { display: none !important; }
|
|
98
|
+
#persistent-video::-webkit-media-controls-overlay-play-button { display: none !important; }
|
|
99
|
+
\`}</style>
|
|
100
|
+
|
|
101
|
+
{/* Overlay content */}
|
|
102
|
+
<div className="absolute inset-0 z-10">
|
|
103
|
+
{children}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</VideoContext.Provider>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Template - VideoManager
|
|
3
|
+
*
|
|
4
|
+
* Singleton to persist video playback across route changes.
|
|
5
|
+
* Similar pattern to DoomManager - holds the video element globally.
|
|
6
|
+
*/
|
|
7
|
+
export declare const videoManagerTemplate = "/**\n * VideoManager - Singleton to persist video across route changes\n *\n * Uses a global video element that persists across Next.js soft navigations.\n * When navigating from /tile to /page, the video keeps playing seamlessly.\n */\n\nimport { getTileBridge } from '@thewhateverapp/tile-sdk';\n\ndeclare global {\n interface Window {\n __videoElement?: HTMLVideoElement;\n __videoHls?: any; // HLS.js instance\n __videoInitialized?: boolean;\n __videoVisible?: boolean;\n }\n}\n\ntype StateCallback = (state: VideoState) => void;\n\nexport interface VideoState {\n isPlaying: boolean;\n currentTime: number;\n duration: number;\n isLoading: boolean;\n isMuted: boolean;\n volume: number;\n error: string | null;\n}\n\nclass VideoManagerClass {\n private stateCallbacks: Set<StateCallback> = new Set();\n private currentState: VideoState = {\n isPlaying: false,\n currentTime: 0,\n duration: 0,\n isLoading: true,\n isMuted: true,\n volume: 1,\n error: null,\n };\n private hlsScriptLoaded = false;\n\n /**\n * Get or create the persistent video element\n */\n getVideo(): HTMLVideoElement {\n if (typeof window === 'undefined') {\n throw new Error('VideoManager requires browser environment');\n }\n\n if (!window.__videoElement) {\n const video = document.createElement('video');\n video.id = 'persistent-video';\n video.playsInline = true;\n video.muted = true; // Start muted\n video.loop = true;\n video.preload = 'auto';\n video.style.width = '100%';\n video.style.height = '100%';\n video.style.objectFit = 'contain';\n video.style.backgroundColor = 'black';\n\n // Set up event listeners\n video.addEventListener('play', () => this.updateState({ isPlaying: true }));\n video.addEventListener('pause', () => this.updateState({ isPlaying: false }));\n video.addEventListener('timeupdate', () => {\n this.updateState({ currentTime: video.currentTime });\n });\n video.addEventListener('durationchange', () => {\n this.updateState({ duration: video.duration });\n });\n video.addEventListener('loadeddata', () => {\n this.updateState({ isLoading: false });\n });\n video.addEventListener('waiting', () => {\n this.updateState({ isLoading: true });\n });\n video.addEventListener('playing', () => {\n this.updateState({ isLoading: false, isPlaying: true });\n });\n video.addEventListener('volumechange', () => {\n this.updateState({ isMuted: video.muted, volume: video.volume });\n });\n video.addEventListener('error', () => {\n this.updateState({ error: 'Video playback error' });\n });\n\n window.__videoElement = video;\n this.setupVisibilityHandling();\n }\n\n return window.__videoElement;\n }\n\n /**\n * Load a video source (supports HLS and regular video)\n */\n async loadSource(src: string): Promise<void> {\n const video = this.getVideo();\n\n // Check if this is an HLS stream\n if (src.includes('.m3u8')) {\n await this.loadHls(src);\n } else {\n // Regular video file\n video.src = src;\n video.load();\n }\n }\n\n /**\n * Load HLS stream using hls.js\n */\n private async loadHls(src: string): Promise<void> {\n const video = this.getVideo();\n\n // Load HLS.js from CDN if not already loaded\n if (!this.hlsScriptLoaded && !(window as any).Hls) {\n await new Promise<void>((resolve, reject) => {\n const script = document.createElement('script');\n script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1';\n script.onload = () => {\n this.hlsScriptLoaded = true;\n resolve();\n };\n script.onerror = reject;\n document.head.appendChild(script);\n });\n }\n\n const Hls = (window as any).Hls;\n\n // Clean up existing HLS instance\n if (window.__videoHls) {\n window.__videoHls.destroy();\n }\n\n if (Hls.isSupported()) {\n const hls = new Hls({\n enableWorker: true,\n lowLatencyMode: true,\n });\n hls.loadSource(src);\n hls.attachMedia(video);\n hls.on(Hls.Events.MANIFEST_PARSED, () => {\n this.updateState({ isLoading: false });\n });\n hls.on(Hls.Events.ERROR, (_: any, data: any) => {\n if (data.fatal) {\n this.updateState({ error: 'HLS playback error' });\n }\n });\n window.__videoHls = hls;\n } else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n // Native HLS support (Safari)\n video.src = src;\n }\n }\n\n /**\n * Set up visibility handling via TileBridge\n */\n private setupVisibilityHandling(): void {\n if (typeof window === 'undefined') return;\n\n try {\n const bridge = getTileBridge();\n\n bridge.onVisibilityChange((state) => {\n const video = this.getVideo();\n window.__videoVisible = state.visible;\n\n if (state.visible && !state.muted) {\n video.muted = false;\n video.play().catch(() => {\n video.muted = true;\n video.play().catch(() => {});\n });\n } else {\n video.muted = true;\n if (!state.visible) {\n video.pause();\n }\n }\n });\n } catch (err) {\n console.log('[VideoManager] TileBridge not available');\n }\n }\n\n /**\n * Attach video element to a container\n */\n attachTo(container: HTMLElement): void {\n const video = this.getVideo();\n if (!container.contains(video)) {\n container.appendChild(video);\n }\n }\n\n /**\n * Detach video element (but keep it in memory)\n */\n detach(): void {\n const video = this.getVideo();\n if (video.parentElement) {\n video.parentElement.removeChild(video);\n }\n }\n\n // Playback controls\n play(): void {\n this.getVideo().play().catch(() => {});\n }\n\n pause(): void {\n this.getVideo().pause();\n }\n\n toggle(): void {\n const video = this.getVideo();\n if (video.paused) {\n video.play().catch(() => {});\n } else {\n video.pause();\n }\n }\n\n seek(time: number): void {\n this.getVideo().currentTime = time;\n }\n\n setMuted(muted: boolean): void {\n this.getVideo().muted = muted;\n }\n\n setVolume(volume: number): void {\n this.getVideo().volume = Math.max(0, Math.min(1, volume));\n }\n\n // State management\n getState(): VideoState {\n return { ...this.currentState };\n }\n\n onStateChange(callback: StateCallback): () => void {\n this.stateCallbacks.add(callback);\n callback(this.currentState);\n return () => this.stateCallbacks.delete(callback);\n }\n\n private updateState(partial: Partial<VideoState>): void {\n this.currentState = { ...this.currentState, ...partial };\n this.stateCallbacks.forEach(cb => cb(this.currentState));\n }\n}\n\nexport const VideoManager = new VideoManagerClass();\n";
|
|
8
|
+
//# sourceMappingURL=VideoManager.ts.template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoManager.ts.template.d.ts","sourceRoot":"","sources":["../../../src/templates/video/VideoManager.ts.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,83NAmQhC,CAAC"}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Template - VideoManager
|
|
3
|
+
*
|
|
4
|
+
* Singleton to persist video playback across route changes.
|
|
5
|
+
* Similar pattern to DoomManager - holds the video element globally.
|
|
6
|
+
*/
|
|
7
|
+
export const videoManagerTemplate = `/**
|
|
8
|
+
* VideoManager - Singleton to persist video across route changes
|
|
9
|
+
*
|
|
10
|
+
* Uses a global video element that persists across Next.js soft navigations.
|
|
11
|
+
* When navigating from /tile to /page, the video keeps playing seamlessly.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getTileBridge } from '@thewhateverapp/tile-sdk';
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
__videoElement?: HTMLVideoElement;
|
|
19
|
+
__videoHls?: any; // HLS.js instance
|
|
20
|
+
__videoInitialized?: boolean;
|
|
21
|
+
__videoVisible?: boolean;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type StateCallback = (state: VideoState) => void;
|
|
26
|
+
|
|
27
|
+
export interface VideoState {
|
|
28
|
+
isPlaying: boolean;
|
|
29
|
+
currentTime: number;
|
|
30
|
+
duration: number;
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
isMuted: boolean;
|
|
33
|
+
volume: number;
|
|
34
|
+
error: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class VideoManagerClass {
|
|
38
|
+
private stateCallbacks: Set<StateCallback> = new Set();
|
|
39
|
+
private currentState: VideoState = {
|
|
40
|
+
isPlaying: false,
|
|
41
|
+
currentTime: 0,
|
|
42
|
+
duration: 0,
|
|
43
|
+
isLoading: true,
|
|
44
|
+
isMuted: true,
|
|
45
|
+
volume: 1,
|
|
46
|
+
error: null,
|
|
47
|
+
};
|
|
48
|
+
private hlsScriptLoaded = false;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get or create the persistent video element
|
|
52
|
+
*/
|
|
53
|
+
getVideo(): HTMLVideoElement {
|
|
54
|
+
if (typeof window === 'undefined') {
|
|
55
|
+
throw new Error('VideoManager requires browser environment');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!window.__videoElement) {
|
|
59
|
+
const video = document.createElement('video');
|
|
60
|
+
video.id = 'persistent-video';
|
|
61
|
+
video.playsInline = true;
|
|
62
|
+
video.muted = true; // Start muted
|
|
63
|
+
video.loop = true;
|
|
64
|
+
video.preload = 'auto';
|
|
65
|
+
video.style.width = '100%';
|
|
66
|
+
video.style.height = '100%';
|
|
67
|
+
video.style.objectFit = 'contain';
|
|
68
|
+
video.style.backgroundColor = 'black';
|
|
69
|
+
|
|
70
|
+
// Set up event listeners
|
|
71
|
+
video.addEventListener('play', () => this.updateState({ isPlaying: true }));
|
|
72
|
+
video.addEventListener('pause', () => this.updateState({ isPlaying: false }));
|
|
73
|
+
video.addEventListener('timeupdate', () => {
|
|
74
|
+
this.updateState({ currentTime: video.currentTime });
|
|
75
|
+
});
|
|
76
|
+
video.addEventListener('durationchange', () => {
|
|
77
|
+
this.updateState({ duration: video.duration });
|
|
78
|
+
});
|
|
79
|
+
video.addEventListener('loadeddata', () => {
|
|
80
|
+
this.updateState({ isLoading: false });
|
|
81
|
+
});
|
|
82
|
+
video.addEventListener('waiting', () => {
|
|
83
|
+
this.updateState({ isLoading: true });
|
|
84
|
+
});
|
|
85
|
+
video.addEventListener('playing', () => {
|
|
86
|
+
this.updateState({ isLoading: false, isPlaying: true });
|
|
87
|
+
});
|
|
88
|
+
video.addEventListener('volumechange', () => {
|
|
89
|
+
this.updateState({ isMuted: video.muted, volume: video.volume });
|
|
90
|
+
});
|
|
91
|
+
video.addEventListener('error', () => {
|
|
92
|
+
this.updateState({ error: 'Video playback error' });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
window.__videoElement = video;
|
|
96
|
+
this.setupVisibilityHandling();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return window.__videoElement;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load a video source (supports HLS and regular video)
|
|
104
|
+
*/
|
|
105
|
+
async loadSource(src: string): Promise<void> {
|
|
106
|
+
const video = this.getVideo();
|
|
107
|
+
|
|
108
|
+
// Check if this is an HLS stream
|
|
109
|
+
if (src.includes('.m3u8')) {
|
|
110
|
+
await this.loadHls(src);
|
|
111
|
+
} else {
|
|
112
|
+
// Regular video file
|
|
113
|
+
video.src = src;
|
|
114
|
+
video.load();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Load HLS stream using hls.js
|
|
120
|
+
*/
|
|
121
|
+
private async loadHls(src: string): Promise<void> {
|
|
122
|
+
const video = this.getVideo();
|
|
123
|
+
|
|
124
|
+
// Load HLS.js from CDN if not already loaded
|
|
125
|
+
if (!this.hlsScriptLoaded && !(window as any).Hls) {
|
|
126
|
+
await new Promise<void>((resolve, reject) => {
|
|
127
|
+
const script = document.createElement('script');
|
|
128
|
+
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1';
|
|
129
|
+
script.onload = () => {
|
|
130
|
+
this.hlsScriptLoaded = true;
|
|
131
|
+
resolve();
|
|
132
|
+
};
|
|
133
|
+
script.onerror = reject;
|
|
134
|
+
document.head.appendChild(script);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const Hls = (window as any).Hls;
|
|
139
|
+
|
|
140
|
+
// Clean up existing HLS instance
|
|
141
|
+
if (window.__videoHls) {
|
|
142
|
+
window.__videoHls.destroy();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (Hls.isSupported()) {
|
|
146
|
+
const hls = new Hls({
|
|
147
|
+
enableWorker: true,
|
|
148
|
+
lowLatencyMode: true,
|
|
149
|
+
});
|
|
150
|
+
hls.loadSource(src);
|
|
151
|
+
hls.attachMedia(video);
|
|
152
|
+
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
153
|
+
this.updateState({ isLoading: false });
|
|
154
|
+
});
|
|
155
|
+
hls.on(Hls.Events.ERROR, (_: any, data: any) => {
|
|
156
|
+
if (data.fatal) {
|
|
157
|
+
this.updateState({ error: 'HLS playback error' });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
window.__videoHls = hls;
|
|
161
|
+
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
162
|
+
// Native HLS support (Safari)
|
|
163
|
+
video.src = src;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Set up visibility handling via TileBridge
|
|
169
|
+
*/
|
|
170
|
+
private setupVisibilityHandling(): void {
|
|
171
|
+
if (typeof window === 'undefined') return;
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const bridge = getTileBridge();
|
|
175
|
+
|
|
176
|
+
bridge.onVisibilityChange((state) => {
|
|
177
|
+
const video = this.getVideo();
|
|
178
|
+
window.__videoVisible = state.visible;
|
|
179
|
+
|
|
180
|
+
if (state.visible && !state.muted) {
|
|
181
|
+
video.muted = false;
|
|
182
|
+
video.play().catch(() => {
|
|
183
|
+
video.muted = true;
|
|
184
|
+
video.play().catch(() => {});
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
video.muted = true;
|
|
188
|
+
if (!state.visible) {
|
|
189
|
+
video.pause();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.log('[VideoManager] TileBridge not available');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Attach video element to a container
|
|
200
|
+
*/
|
|
201
|
+
attachTo(container: HTMLElement): void {
|
|
202
|
+
const video = this.getVideo();
|
|
203
|
+
if (!container.contains(video)) {
|
|
204
|
+
container.appendChild(video);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Detach video element (but keep it in memory)
|
|
210
|
+
*/
|
|
211
|
+
detach(): void {
|
|
212
|
+
const video = this.getVideo();
|
|
213
|
+
if (video.parentElement) {
|
|
214
|
+
video.parentElement.removeChild(video);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Playback controls
|
|
219
|
+
play(): void {
|
|
220
|
+
this.getVideo().play().catch(() => {});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pause(): void {
|
|
224
|
+
this.getVideo().pause();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
toggle(): void {
|
|
228
|
+
const video = this.getVideo();
|
|
229
|
+
if (video.paused) {
|
|
230
|
+
video.play().catch(() => {});
|
|
231
|
+
} else {
|
|
232
|
+
video.pause();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
seek(time: number): void {
|
|
237
|
+
this.getVideo().currentTime = time;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
setMuted(muted: boolean): void {
|
|
241
|
+
this.getVideo().muted = muted;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
setVolume(volume: number): void {
|
|
245
|
+
this.getVideo().volume = Math.max(0, Math.min(1, volume));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// State management
|
|
249
|
+
getState(): VideoState {
|
|
250
|
+
return { ...this.currentState };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
onStateChange(callback: StateCallback): () => void {
|
|
254
|
+
this.stateCallbacks.add(callback);
|
|
255
|
+
callback(this.currentState);
|
|
256
|
+
return () => this.stateCallbacks.delete(callback);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private updateState(partial: Partial<VideoState>): void {
|
|
260
|
+
this.currentState = { ...this.currentState, ...partial };
|
|
261
|
+
this.stateCallbacks.forEach(cb => cb(this.currentState));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const VideoManager = new VideoManagerClass();
|
|
266
|
+
`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Template Exports
|
|
3
|
+
*
|
|
4
|
+
* These templates are used by tile-deploy/agent-service to generate
|
|
5
|
+
* video tiles with persistent playback across route navigation.
|
|
6
|
+
*/
|
|
7
|
+
export { videoLayoutTemplate } from './layout.tsx.template';
|
|
8
|
+
export { videoManagerTemplate } from './VideoManager.ts.template';
|
|
9
|
+
export { persistentVideoTemplate } from './PersistentVideo.tsx.template';
|
|
10
|
+
export declare const videoTemplateFiles: {
|
|
11
|
+
'src/shared/lib/VideoManager.ts': string;
|
|
12
|
+
'src/shared/components/PersistentVideo.tsx': string;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/templates/video/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Template Exports
|
|
3
|
+
*
|
|
4
|
+
* These templates are used by tile-deploy/agent-service to generate
|
|
5
|
+
* video tiles with persistent playback across route navigation.
|
|
6
|
+
*/
|
|
7
|
+
export { videoLayoutTemplate } from './layout.tsx.template';
|
|
8
|
+
export { videoManagerTemplate } from './VideoManager.ts.template';
|
|
9
|
+
export { persistentVideoTemplate } from './PersistentVideo.tsx.template';
|
|
10
|
+
// Template file structure for video tiles
|
|
11
|
+
export const videoTemplateFiles = {
|
|
12
|
+
'src/shared/lib/VideoManager.ts': 'videoManagerTemplate',
|
|
13
|
+
'src/shared/components/PersistentVideo.tsx': 'persistentVideoTemplate',
|
|
14
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Template - Layout
|
|
3
|
+
*
|
|
4
|
+
* This layout wraps both /tile and /page routes.
|
|
5
|
+
* The VideoPlayer is rendered here so it persists across route navigation.
|
|
6
|
+
*/
|
|
7
|
+
export declare const videoLayoutTemplate = "'use client';\n\nimport { VideoPlayer } from '@thewhateverapp/tile-sdk/react';\nimport { usePathname } from 'next/navigation';\n\nexport default function VideoLayout({ children }: { children: React.ReactNode }) {\n const pathname = usePathname();\n const isPageView = pathname === '/page';\n\n return (\n <div className=\"relative w-full h-full bg-black\">\n {/* VideoPlayer persists across route changes */}\n <VideoPlayer className=\"absolute inset-0\">\n {/* Children are the route-specific overlays */}\n {children}\n </VideoPlayer>\n </div>\n );\n}\n";
|
|
8
|
+
//# sourceMappingURL=layout.tsx.template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.tsx.template.d.ts","sourceRoot":"","sources":["../../../src/templates/video/layout.tsx.template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,ulBAmB/B,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Template - Layout
|
|
3
|
+
*
|
|
4
|
+
* This layout wraps both /tile and /page routes.
|
|
5
|
+
* The VideoPlayer is rendered here so it persists across route navigation.
|
|
6
|
+
*/
|
|
7
|
+
export const videoLayoutTemplate = `'use client';
|
|
8
|
+
|
|
9
|
+
import { VideoPlayer } from '@thewhateverapp/tile-sdk/react';
|
|
10
|
+
import { usePathname } from 'next/navigation';
|
|
11
|
+
|
|
12
|
+
export default function VideoLayout({ children }: { children: React.ReactNode }) {
|
|
13
|
+
const pathname = usePathname();
|
|
14
|
+
const isPageView = pathname === '/page';
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="relative w-full h-full bg-black">
|
|
18
|
+
{/* VideoPlayer persists across route changes */}
|
|
19
|
+
<VideoPlayer className="absolute inset-0">
|
|
20
|
+
{/* Children are the route-specific overlays */}
|
|
21
|
+
{children}
|
|
22
|
+
</VideoPlayer>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thewhateverapp/tile-sdk",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.20",
|
|
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",
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
"./tools": {
|
|
17
17
|
"types": "./dist/tools/index.d.ts",
|
|
18
18
|
"import": "./dist/tools/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./templates": {
|
|
21
|
+
"types": "./dist/templates/index.d.ts",
|
|
22
|
+
"import": "./dist/templates/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./templates/video": {
|
|
25
|
+
"types": "./dist/templates/video/index.d.ts",
|
|
26
|
+
"import": "./dist/templates/video/index.js"
|
|
19
27
|
}
|
|
20
28
|
},
|
|
21
29
|
"files": [
|