stormcloud-video-player 0.1.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.
@@ -0,0 +1,258 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { StormcloudVideoPlayer } from "../player/StormcloudVideoPlayer";
3
+ import type { StormcloudVideoPlayerConfig } from "../types";
4
+
5
+ export type StormcloudVideoPlayerProps = Omit<
6
+ StormcloudVideoPlayerConfig,
7
+ "videoElement"
8
+ > &
9
+ React.VideoHTMLAttributes<HTMLVideoElement> & {
10
+ onReady?: (player: StormcloudVideoPlayer) => void;
11
+ wrapperClassName?: string;
12
+ wrapperStyle?: React.CSSProperties;
13
+ };
14
+
15
+ export const StormcloudVideoPlayerComponent: React.FC<
16
+ StormcloudVideoPlayerProps
17
+ > = (props) => {
18
+ const {
19
+ src,
20
+ autoplay,
21
+ muted,
22
+ lowLatencyMode,
23
+ allowNativeHls,
24
+ driftToleranceMs,
25
+ adSchedule,
26
+ immediateManifestAds,
27
+ debugAdTiming,
28
+ showCustomControls,
29
+ onVolumeToggle,
30
+ onFullscreenToggle,
31
+ onControlClick,
32
+ onReady,
33
+ wrapperClassName,
34
+ wrapperStyle,
35
+ className,
36
+ style,
37
+ controls,
38
+ playsInline,
39
+ preload,
40
+ poster,
41
+ children,
42
+ ...restVideoAttrs
43
+ } = props;
44
+
45
+ const videoRef = useRef<HTMLVideoElement | null>(null);
46
+ const playerRef = useRef<StormcloudVideoPlayer | null>(null);
47
+ const [adStatus, setAdStatus] = React.useState<{
48
+ showAds: boolean;
49
+ currentIndex: number;
50
+ totalAds: number;
51
+ }>({ showAds: false, currentIndex: 0, totalAds: 0 });
52
+
53
+ const [shouldShowNativeControls, setShouldShowNativeControls] =
54
+ React.useState(true);
55
+
56
+ useEffect(() => {
57
+ if (typeof window === "undefined") return;
58
+ const el = videoRef.current;
59
+ if (!el || !src) return;
60
+
61
+ if (playerRef.current) {
62
+ try {
63
+ playerRef.current.destroy();
64
+ } catch {}
65
+ playerRef.current = null;
66
+ }
67
+
68
+ const cfg: StormcloudVideoPlayerConfig = {
69
+ src,
70
+ videoElement: el,
71
+ } as StormcloudVideoPlayerConfig;
72
+ if (autoplay !== undefined) cfg.autoplay = autoplay;
73
+ if (muted !== undefined) cfg.muted = muted;
74
+ if (lowLatencyMode !== undefined) cfg.lowLatencyMode = lowLatencyMode;
75
+ if (allowNativeHls !== undefined) cfg.allowNativeHls = allowNativeHls;
76
+ if (driftToleranceMs !== undefined) cfg.driftToleranceMs = driftToleranceMs;
77
+ if (adSchedule !== undefined) cfg.adSchedule = adSchedule;
78
+ if (immediateManifestAds !== undefined)
79
+ cfg.immediateManifestAds = immediateManifestAds;
80
+ if (debugAdTiming !== undefined) cfg.debugAdTiming = debugAdTiming;
81
+ if (showCustomControls !== undefined)
82
+ cfg.showCustomControls = showCustomControls;
83
+ if (onVolumeToggle !== undefined) cfg.onVolumeToggle = onVolumeToggle;
84
+ if (onFullscreenToggle !== undefined)
85
+ cfg.onFullscreenToggle = onFullscreenToggle;
86
+ if (onControlClick !== undefined) cfg.onControlClick = onControlClick;
87
+
88
+ const player = new StormcloudVideoPlayer(cfg);
89
+ playerRef.current = player;
90
+ player
91
+ .load()
92
+ .then(() => {
93
+ const showNative = player.shouldShowNativeControls();
94
+ setShouldShowNativeControls(showNative);
95
+ onReady?.(player);
96
+ })
97
+ .catch(() => {});
98
+
99
+ return () => {
100
+ try {
101
+ player.destroy();
102
+ } catch {}
103
+ playerRef.current = null;
104
+ };
105
+ }, [
106
+ src,
107
+ autoplay,
108
+ muted,
109
+ lowLatencyMode,
110
+ allowNativeHls,
111
+ driftToleranceMs,
112
+ immediateManifestAds,
113
+ debugAdTiming,
114
+ showCustomControls,
115
+ onVolumeToggle,
116
+ onFullscreenToggle,
117
+ onControlClick,
118
+ onReady,
119
+ ]);
120
+
121
+ useEffect(() => {
122
+ if (!playerRef.current) return;
123
+ if (adSchedule) {
124
+ try {
125
+ playerRef.current.setAdSchedule(adSchedule);
126
+ } catch {}
127
+ }
128
+ }, [adSchedule]);
129
+
130
+ useEffect(() => {
131
+ if (!playerRef.current) return;
132
+
133
+ const checkAdStatus = () => {
134
+ if (playerRef.current) {
135
+ const showAds = playerRef.current.isShowingAds();
136
+ const currentIndex = playerRef.current.getCurrentAdIndex();
137
+ const totalAds = playerRef.current.getTotalAdsInBreak();
138
+
139
+ setAdStatus((prev) => {
140
+ if (
141
+ prev.showAds !== showAds ||
142
+ prev.currentIndex !== currentIndex ||
143
+ prev.totalAds !== totalAds
144
+ ) {
145
+ return { showAds, currentIndex, totalAds };
146
+ }
147
+ return prev;
148
+ });
149
+ }
150
+ };
151
+
152
+ const interval = setInterval(checkAdStatus, 500);
153
+ return () => clearInterval(interval);
154
+ }, []);
155
+
156
+ return (
157
+ <div
158
+ className={wrapperClassName}
159
+ style={{ position: "relative", overflow: "hidden", ...wrapperStyle }}
160
+ >
161
+ <video
162
+ ref={videoRef}
163
+ className={className}
164
+ style={{ display: "block", width: "100%", height: "100%", ...style }}
165
+ controls={shouldShowNativeControls && controls}
166
+ playsInline={playsInline}
167
+ preload={preload}
168
+ poster={poster}
169
+ {...restVideoAttrs}
170
+ >
171
+ {children}
172
+ </video>
173
+
174
+ {adStatus.showAds && adStatus.totalAds > 0 && (
175
+ <div
176
+ style={{
177
+ position: "absolute",
178
+ top: "10px",
179
+ right: "10px",
180
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
181
+ color: "white",
182
+ padding: "4px 8px",
183
+ borderRadius: "4px",
184
+ fontSize: "12px",
185
+ fontFamily: "Arial, sans-serif",
186
+ zIndex: 10,
187
+ }}
188
+ >
189
+ Ad {adStatus.currentIndex}/{adStatus.totalAds}
190
+ </div>
191
+ )}
192
+
193
+ {!shouldShowNativeControls && showCustomControls && (
194
+ <div
195
+ style={{
196
+ position: "absolute",
197
+ bottom: "10px",
198
+ right: "10px",
199
+ display: "flex",
200
+ gap: "8px",
201
+ zIndex: 10,
202
+ }}
203
+ >
204
+ {onVolumeToggle && (
205
+ <button
206
+ onClick={onVolumeToggle}
207
+ style={{
208
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
209
+ color: "white",
210
+ border: "1px solid rgba(255, 255, 255, 0.3)",
211
+ borderRadius: "4px",
212
+ padding: "8px",
213
+ cursor: "pointer",
214
+ fontSize: "14px",
215
+ }}
216
+ title="Toggle Volume"
217
+ >
218
+ 🔊
219
+ </button>
220
+ )}
221
+
222
+ {onFullscreenToggle && (
223
+ <button
224
+ onClick={onFullscreenToggle}
225
+ style={{
226
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
227
+ color: "white",
228
+ border: "1px solid rgba(255, 255, 255, 0.3)",
229
+ borderRadius: "4px",
230
+ padding: "8px",
231
+ cursor: "pointer",
232
+ fontSize: "14px",
233
+ }}
234
+ title="Toggle Fullscreen"
235
+ >
236
+
237
+ </button>
238
+ )}
239
+ </div>
240
+ )}
241
+
242
+ {onControlClick && (
243
+ <div
244
+ onClick={onControlClick}
245
+ style={{
246
+ position: "absolute",
247
+ top: 0,
248
+ left: 0,
249
+ right: 0,
250
+ bottom: 0,
251
+ zIndex: 1,
252
+ cursor: "pointer",
253
+ }}
254
+ />
255
+ )}
256
+ </div>
257
+ );
258
+ };
@@ -0,0 +1,251 @@
1
+ import type { ClientInfo, TrackingData, HeartbeatData } from "../types";
2
+
3
+ export function getClientInfo(): ClientInfo {
4
+ const ua = navigator.userAgent;
5
+ const platform = navigator.platform;
6
+ const vendor = navigator.vendor || "";
7
+ const maxTouchPoints = navigator.maxTouchPoints || 0;
8
+ const memory = (navigator as any).deviceMemory || null;
9
+ const hardwareConcurrency = navigator.hardwareConcurrency || 1;
10
+
11
+ const screenInfo = {
12
+ width: screen?.width,
13
+ height: screen?.height,
14
+ availWidth: screen?.availWidth,
15
+ availHeight: screen?.availHeight,
16
+ orientation: (screen?.orientation as any)?.type || "",
17
+ pixelDepth: screen?.pixelDepth,
18
+ };
19
+
20
+ let deviceType: "tv" | "mobile" | "tablet" | "desktop" = "desktop";
21
+ let brand = "Unknown";
22
+ let os = "Unknown";
23
+ let model = "";
24
+ let isSmartTV = false;
25
+ let isAndroid = false;
26
+ let isWebView = false;
27
+ let isWebApp = false;
28
+
29
+ if (ua.includes("Web0S")) {
30
+ brand = "LG";
31
+ os = "webOS";
32
+ isSmartTV = true;
33
+ deviceType = "tv";
34
+ const webosMatch = ua.match(/Web0S\/([^\s]+)/);
35
+ model = webosMatch ? `webOS ${webosMatch[1]}` : "webOS TV";
36
+ } else if (ua.includes("Tizen")) {
37
+ brand = "Samsung";
38
+ os = "Tizen";
39
+ isSmartTV = true;
40
+ deviceType = "tv";
41
+ const tizenMatch = ua.match(/Tizen\/([^\s]+)/);
42
+ const tvMatch = ua.match(/(?:Smart-TV|SMART-TV|TV)/i) ? "Smart TV" : "";
43
+ model = tizenMatch
44
+ ? `Tizen ${tizenMatch[1]} ${tvMatch}`.trim()
45
+ : "Tizen TV";
46
+ } else if (ua.includes("Philips")) {
47
+ brand = "Philips";
48
+ os = "Saphi";
49
+ isSmartTV = true;
50
+ deviceType = "tv";
51
+ } else if (ua.includes("Sharp") || ua.includes("AQUOS")) {
52
+ brand = "Sharp";
53
+ os = "Android TV";
54
+ isSmartTV = true;
55
+ deviceType = "tv";
56
+ } else if (
57
+ ua.includes("Android") &&
58
+ (ua.includes("Sony") || vendor.includes("Sony"))
59
+ ) {
60
+ brand = "Sony";
61
+ os = "Android TV";
62
+ isSmartTV = true;
63
+ deviceType = "tv";
64
+ } else if (
65
+ ua.includes("Android") &&
66
+ (ua.includes("NetCast") || ua.includes("LG"))
67
+ ) {
68
+ brand = "LG";
69
+ os = "Android TV";
70
+ isSmartTV = true;
71
+ deviceType = "tv";
72
+ } else if (ua.includes(" Roku") || ua.includes("Roku/")) {
73
+ brand = "Roku";
74
+ os = "Roku OS";
75
+ isSmartTV = true;
76
+ deviceType = "tv";
77
+ } else if (ua.includes("AppleTV")) {
78
+ brand = "Apple";
79
+ os = "tvOS";
80
+ isSmartTV = true;
81
+ deviceType = "tv";
82
+ }
83
+
84
+ if (ua.includes("Android")) {
85
+ isAndroid = true;
86
+ os = "Android";
87
+ deviceType = /Mobile/.test(ua) ? "mobile" : "tablet";
88
+
89
+ if (
90
+ ua.includes("Android") &&
91
+ (maxTouchPoints === 0 ||
92
+ ua.includes("Google TV") ||
93
+ ua.includes("XiaoMi"))
94
+ ) {
95
+ deviceType = "tv";
96
+ isSmartTV = true;
97
+ brand = brand === "Unknown" ? "Android TV" : brand;
98
+ }
99
+
100
+ const androidModelMatch = ua.match(/\(([^)]*Android[^)]*)\)/);
101
+ if (androidModelMatch && androidModelMatch[1]) {
102
+ model = androidModelMatch[1];
103
+ }
104
+ }
105
+
106
+ if (/iPad|iPhone|iPod/.test(ua)) {
107
+ os = "iOS";
108
+ deviceType = "mobile";
109
+ brand = "Apple";
110
+ if (navigator.maxTouchPoints > 1 && /iPad/.test(ua)) {
111
+ deviceType = "tablet";
112
+ }
113
+ }
114
+
115
+ if (!isAndroid && !isSmartTV && !/Mobile/.test(ua)) {
116
+ if (ua.includes("Windows")) {
117
+ os = "Windows";
118
+ deviceType = "desktop";
119
+ } else if (ua.includes("Mac") && !/iPhone/.test(ua)) {
120
+ os = "macOS";
121
+ deviceType = "desktop";
122
+ if (maxTouchPoints > 1) deviceType = "tablet";
123
+ } else if (ua.includes("Linux")) {
124
+ os = "Linux";
125
+ deviceType = "desktop";
126
+ }
127
+ }
128
+
129
+ if (brand === "Unknown") {
130
+ if (vendor.includes("Google") || ua.includes("Chrome")) brand = "Google";
131
+ if (vendor.includes("Apple")) brand = "Apple";
132
+ if (vendor.includes("Samsung") || ua.includes("SM-")) brand = "Samsung";
133
+ }
134
+
135
+ isWebView = /wv|WebView|Linux; U;/.test(ua);
136
+
137
+ if (window?.outerHeight === 0 && window?.outerWidth === 0) {
138
+ isWebView = true;
139
+ }
140
+
141
+ isWebApp =
142
+ window.matchMedia("(display-mode: standalone)").matches ||
143
+ (window.navigator as any).standalone === true ||
144
+ window.screen?.orientation?.angle !== undefined;
145
+
146
+ return {
147
+ brand,
148
+ os,
149
+ model: model || ua.substring(0, 50) + "...",
150
+ deviceType,
151
+ isSmartTV,
152
+ isAndroid,
153
+ isWebView,
154
+ isWebApp,
155
+ domain: window.location.hostname,
156
+ origin: window.location.origin,
157
+ path: window.location.pathname,
158
+ userAgent: ua,
159
+ vendor,
160
+ platform,
161
+ screen: screenInfo,
162
+ hardwareConcurrency,
163
+ deviceMemory: memory,
164
+ maxTouchPoints,
165
+ language: navigator.language,
166
+ languages: navigator.languages?.join(",") || "",
167
+ cookieEnabled: navigator.cookieEnabled,
168
+ doNotTrack: navigator.doNotTrack || "",
169
+ referrer: document.referrer,
170
+ visibilityState: document.visibilityState,
171
+ };
172
+ }
173
+
174
+ export async function getBrowserID(clientInfo: ClientInfo): Promise<string> {
175
+ const fingerprintString = JSON.stringify(clientInfo);
176
+ const hashBuffer = await crypto.subtle.digest(
177
+ "SHA-256",
178
+ new TextEncoder().encode(fingerprintString)
179
+ );
180
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
181
+ const hashHex = hashArray
182
+ .map((b) => b.toString(16).padStart(2, "0"))
183
+ .join("");
184
+ return hashHex;
185
+ }
186
+
187
+ export async function sendInitialTracking(): Promise<void> {
188
+ try {
189
+ const clientInfo = getClientInfo();
190
+ const browserId = await getBrowserID(clientInfo);
191
+
192
+ const trackingData: TrackingData = {
193
+ browserId,
194
+ ...clientInfo,
195
+ };
196
+
197
+ const response = await fetch(
198
+ "https://adstorm.co/api-adstorm-dev/adstorm/player-tracking/track",
199
+ {
200
+ method: "POST",
201
+ headers: { "Content-Type": "application/json" },
202
+ body: JSON.stringify(trackingData),
203
+ }
204
+ );
205
+
206
+ if (!response.ok) {
207
+ throw new Error(`HTTP error! status: ${response.status}`);
208
+ }
209
+
210
+ const data = await response.json();
211
+ console.log(
212
+ "[StormcloudVideoPlayer] Initial tracking data sent successfully:",
213
+ data
214
+ );
215
+ } catch (error) {
216
+ console.error(
217
+ "[StormcloudVideoPlayer] Error sending initial tracking data:",
218
+ error
219
+ );
220
+ }
221
+ }
222
+
223
+ export async function sendHeartbeat(): Promise<void> {
224
+ try {
225
+ const clientInfo = getClientInfo();
226
+ const browserId = await getBrowserID(clientInfo);
227
+
228
+ const heartbeatData: HeartbeatData = {
229
+ browserId,
230
+ timestamp: new Date().toISOString(),
231
+ };
232
+
233
+ const response = await fetch(
234
+ "https://adstorm.co/api-adstorm-dev/adstorm/player-tracking/heartbeat",
235
+ {
236
+ method: "POST",
237
+ headers: { "Content-Type": "application/json" },
238
+ body: JSON.stringify(heartbeatData),
239
+ }
240
+ );
241
+
242
+ if (!response.ok) {
243
+ throw new Error(`HTTP error! status: ${response.status}`);
244
+ }
245
+
246
+ const data = await response.json();
247
+ console.log("[StormcloudVideoPlayer] Heartbeat sent successfully:", data);
248
+ } catch (error) {
249
+ console.error("[StormcloudVideoPlayer] Error sending heartbeat:", error);
250
+ }
251
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "esnext",
11
+ "target": "es2020",
12
+ "types": [],
13
+ "lib": ["es2020", "dom"],
14
+ // For nodejs:
15
+ // "lib": ["esnext"],
16
+ // "types": ["node"],
17
+ // and npm install -D @types/node
18
+
19
+ // Other Outputs
20
+ "sourceMap": true,
21
+ "declaration": true,
22
+ "declarationMap": true,
23
+
24
+ // Stricter Typechecking Options
25
+ "noUncheckedIndexedAccess": true,
26
+ "exactOptionalPropertyTypes": true,
27
+
28
+ // Style Options
29
+ // "noImplicitReturns": true,
30
+ // "noImplicitOverride": true,
31
+ // "noUnusedLocals": true,
32
+ // "noUnusedParameters": true,
33
+ // "noFallthroughCasesInSwitch": true,
34
+ // "noPropertyAccessFromIndexSignature": true,
35
+
36
+ // Recommended Options
37
+ "strict": true,
38
+ "jsx": "react-jsx",
39
+ "verbatimModuleSyntax": true,
40
+ "isolatedModules": true,
41
+ "noUncheckedSideEffectImports": true,
42
+ "moduleDetection": "force",
43
+ "skipLibCheck": true,
44
+
45
+ "moduleResolution": "bundler",
46
+ "esModuleInterop": true,
47
+ "resolveJsonModule": true,
48
+ }
49
+ }