unified-video-framework 1.0.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/.github/workflows/ci.yml +253 -0
- package/ANDROID_TV_IMPLEMENTATION.md +313 -0
- package/COMPLETION_STATUS.md +165 -0
- package/CONTRIBUTING.md +376 -0
- package/FINAL_STATUS_REPORT.md +170 -0
- package/FRAMEWORK_REVIEW.md +247 -0
- package/IMPROVEMENTS_SUMMARY.md +168 -0
- package/LICENSE +21 -0
- package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
- package/PAYWALL_RENTAL_FLOW.md +499 -0
- package/PLATFORM_SETUP_GUIDE.md +1636 -0
- package/README.md +315 -0
- package/RUN_LOCALLY.md +151 -0
- package/apps/demo/cast-sender-min.html +173 -0
- package/apps/demo/custom-player.html +883 -0
- package/apps/demo/demo.html +990 -0
- package/apps/demo/enhanced-player.html +3556 -0
- package/apps/demo/index.html +159 -0
- package/apps/rental-api/.env.example +24 -0
- package/apps/rental-api/README.md +23 -0
- package/apps/rental-api/migrations/001_init.sql +35 -0
- package/apps/rental-api/migrations/002_videos.sql +10 -0
- package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
- package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
- package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
- package/apps/rental-api/package-lock.json +2045 -0
- package/apps/rental-api/package.json +33 -0
- package/apps/rental-api/scripts/run-migration.js +42 -0
- package/apps/rental-api/scripts/update-video-currency.js +21 -0
- package/apps/rental-api/scripts/update-video-price.js +19 -0
- package/apps/rental-api/src/config.ts +14 -0
- package/apps/rental-api/src/db.ts +10 -0
- package/apps/rental-api/src/routes/cashfree.ts +167 -0
- package/apps/rental-api/src/routes/pesapal.ts +92 -0
- package/apps/rental-api/src/routes/rentals.ts +242 -0
- package/apps/rental-api/src/routes/webhooks.ts +73 -0
- package/apps/rental-api/src/server.ts +41 -0
- package/apps/rental-api/src/services/entitlements.ts +45 -0
- package/apps/rental-api/src/services/payments.ts +22 -0
- package/apps/rental-api/tsconfig.json +17 -0
- package/check-urls.ps1 +74 -0
- package/comparison-report.md +181 -0
- package/docs/PAYWALL.md +95 -0
- package/docs/PLAYER_UI_VISIBILITY.md +431 -0
- package/docs/README.md +7 -0
- package/docs/SYSTEM_ARCHITECTURE.md +612 -0
- package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
- package/examples/android/JavaSampleApp/MainActivity.java +641 -0
- package/examples/android/JavaSampleApp/activity_main.xml +226 -0
- package/examples/android/SampleApp/MainActivity.kt +430 -0
- package/examples/ios/SampleApp/ViewController.swift +337 -0
- package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
- package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
- package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
- package/jest.config.js +33 -0
- package/jitpack.yml +5 -0
- package/lerna.json +35 -0
- package/package.json +69 -0
- package/packages/PLATFORM_STATUS.md +163 -0
- package/packages/android/build.gradle +135 -0
- package/packages/android/src/main/AndroidManifest.xml +36 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
- package/packages/core/package.json +34 -0
- package/packages/core/src/BasePlayer.ts +250 -0
- package/packages/core/src/VideoPlayer.ts +237 -0
- package/packages/core/src/VideoPlayerFactory.ts +145 -0
- package/packages/core/src/index.ts +20 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
- package/packages/core/src/interfaces.ts +240 -0
- package/packages/core/src/utils/EventEmitter.ts +66 -0
- package/packages/core/src/utils/PlatformDetector.ts +300 -0
- package/packages/core/tsconfig.json +20 -0
- package/packages/enact/package.json +51 -0
- package/packages/enact/src/VideoPlayer.js +365 -0
- package/packages/enact/src/adapters/TizenAdapter.js +354 -0
- package/packages/enact/src/index.js +82 -0
- package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
- package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
- package/packages/ios/GETTING_STARTED.md +100 -0
- package/packages/ios/Package.swift +35 -0
- package/packages/ios/README.md +84 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
- package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
- package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
- package/packages/ios/build_framework.sh +55 -0
- package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
- package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
- package/packages/react-native/package.json +51 -0
- package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
- package/packages/react-native/src/VideoPlayer.tsx +224 -0
- package/packages/react-native/src/index.ts +28 -0
- package/packages/react-native/src/utils/EventEmitter.ts +66 -0
- package/packages/react-native/tsconfig.json +31 -0
- package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
- package/packages/roku/package.json +44 -0
- package/packages/roku/source/VideoPlayer.brs +231 -0
- package/packages/roku/source/main.brs +28 -0
- package/packages/web/GETTING_STARTED.md +292 -0
- package/packages/web/jest.config.js +28 -0
- package/packages/web/jest.setup.ts +110 -0
- package/packages/web/package.json +50 -0
- package/packages/web/src/SecureVideoPlayer.ts +1164 -0
- package/packages/web/src/WebPlayer.ts +3110 -0
- package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
- package/packages/web/src/index.ts +14 -0
- package/packages/web/src/paywall/PaywallController.ts +215 -0
- package/packages/web/src/react/WebPlayerView.tsx +177 -0
- package/packages/web/tsconfig.json +23 -0
- package/packages/web/webpack.config.js +45 -0
- package/server.js +131 -0
- package/server.py +84 -0
- package/test-urls.ps1 +97 -0
- package/test-video-urls.ps1 +87 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import React, { useEffect, useRef } from 'react';
|
|
3
|
+
import type { CSSProperties } from 'react';
|
|
4
|
+
import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from '@unified-video/core';
|
|
5
|
+
import { WebPlayer } from '../WebPlayer';
|
|
6
|
+
|
|
7
|
+
export type WebPlayerViewProps = {
|
|
8
|
+
// Player config
|
|
9
|
+
autoPlay?: boolean;
|
|
10
|
+
muted?: boolean;
|
|
11
|
+
enableAdaptiveBitrate?: boolean;
|
|
12
|
+
debug?: boolean;
|
|
13
|
+
freeDuration?: number;
|
|
14
|
+
// Paywall
|
|
15
|
+
paywall?: import('@unified-video/core').PaywallConfig;
|
|
16
|
+
paywallConfigUrl?: string; // optional endpoint returning PaywallConfig JSON
|
|
17
|
+
|
|
18
|
+
// Source config
|
|
19
|
+
url: string;
|
|
20
|
+
type?: 'mp4' | 'hls' | 'dash' | 'webm' | 'auto';
|
|
21
|
+
subtitles?: SubtitleTrack[];
|
|
22
|
+
metadata?: VideoMetadata;
|
|
23
|
+
|
|
24
|
+
// Optional Google Cast sender SDK loader
|
|
25
|
+
cast?: boolean;
|
|
26
|
+
|
|
27
|
+
// Styling
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
// Dynamic theming: pass a single accent color string or an object with fields
|
|
31
|
+
// { accent, accent2, iconColor, textPrimary, textSecondary }
|
|
32
|
+
playerTheme?: string | { accent?: string; accent2?: string; iconColor?: string; textPrimary?: string; textSecondary?: string };
|
|
33
|
+
|
|
34
|
+
// Callbacks
|
|
35
|
+
onReady?: (player: WebPlayer) => void;
|
|
36
|
+
onError?: (error: unknown) => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
40
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
const playerRef = useRef<WebPlayer | null>(null);
|
|
42
|
+
|
|
43
|
+
// Checkout return bridge: forward rental=success/cancel with session_id/order_id back to opener and close
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
try {
|
|
46
|
+
const params = new URLSearchParams(window.location.search);
|
|
47
|
+
const popup = (params.get('popup') || '').toLowerCase() === '1';
|
|
48
|
+
const status = (params.get('rental') || '').toLowerCase();
|
|
49
|
+
const orderId = params.get('order_id') || '';
|
|
50
|
+
const sessionId = params.get('session_id') || '';
|
|
51
|
+
if (popup && (status === 'success' || status === 'cancel')) {
|
|
52
|
+
try { window.opener?.postMessage({ type: 'uvfCheckout', status, orderId, sessionId }, '*'); } catch (_) {}
|
|
53
|
+
try { window.close(); } catch (_) {}
|
|
54
|
+
}
|
|
55
|
+
} catch (_) {}
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
let cancelled = false;
|
|
60
|
+
|
|
61
|
+
async function boot() {
|
|
62
|
+
if (!containerRef.current) return;
|
|
63
|
+
|
|
64
|
+
const player = new WebPlayer();
|
|
65
|
+
playerRef.current = player;
|
|
66
|
+
|
|
67
|
+
// Optionally load Google Cast sender SDK
|
|
68
|
+
if (props.cast) {
|
|
69
|
+
try {
|
|
70
|
+
const existing = document.querySelector('script[data-cast-sdk="1"]');
|
|
71
|
+
if (!existing) {
|
|
72
|
+
const s = document.createElement('script');
|
|
73
|
+
s.src = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
|
|
74
|
+
s.async = true;
|
|
75
|
+
s.setAttribute('data-cast-sdk', '1');
|
|
76
|
+
document.head.appendChild(s);
|
|
77
|
+
}
|
|
78
|
+
} catch (_) {
|
|
79
|
+
// ignore load issues in SSR or restricted environments
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Resolve paywall config: inline or fetched
|
|
84
|
+
let paywallCfg = props.paywall as any;
|
|
85
|
+
if (!paywallCfg && props.paywallConfigUrl) {
|
|
86
|
+
try {
|
|
87
|
+
const resp = await fetch(props.paywallConfigUrl);
|
|
88
|
+
if (resp.ok) paywallCfg = await resp.json();
|
|
89
|
+
} catch(_) {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const config: PlayerConfig = {
|
|
93
|
+
autoPlay: props.autoPlay ?? false,
|
|
94
|
+
muted: props.muted ?? false,
|
|
95
|
+
enableAdaptiveBitrate: props.enableAdaptiveBitrate ?? true,
|
|
96
|
+
debug: props.debug ?? false,
|
|
97
|
+
freeDuration: props.freeDuration,
|
|
98
|
+
paywall: paywallCfg
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await player.initialize(containerRef.current, config);
|
|
103
|
+
|
|
104
|
+
// Apply theme before loading source (so poster and UI show themed styles)
|
|
105
|
+
try {
|
|
106
|
+
if (props.playerTheme && (player as any).setTheme) {
|
|
107
|
+
(player as any).setTheme(props.playerTheme as any);
|
|
108
|
+
}
|
|
109
|
+
} catch (_) {}
|
|
110
|
+
|
|
111
|
+
const source: VideoSource = {
|
|
112
|
+
url: props.url,
|
|
113
|
+
type: props.type ?? 'auto',
|
|
114
|
+
subtitles: props.subtitles,
|
|
115
|
+
metadata: props.metadata,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
await player.load(source);
|
|
119
|
+
if (!cancelled) props.onReady?.(player);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (!cancelled) props.onError?.(err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
void boot();
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
cancelled = true;
|
|
129
|
+
if (playerRef.current) {
|
|
130
|
+
playerRef.current.destroy().catch(() => {});
|
|
131
|
+
playerRef.current = null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}, [
|
|
135
|
+
props.autoPlay,
|
|
136
|
+
props.muted,
|
|
137
|
+
props.enableAdaptiveBitrate,
|
|
138
|
+
props.debug,
|
|
139
|
+
props.url,
|
|
140
|
+
props.type,
|
|
141
|
+
JSON.stringify(props.subtitles),
|
|
142
|
+
JSON.stringify(props.metadata),
|
|
143
|
+
props.cast,
|
|
144
|
+
props.freeDuration,
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
// Update free preview duration at runtime without full re-init
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const p = playerRef.current as any;
|
|
150
|
+
if (p && typeof p.setFreeDuration === 'function' && typeof props.freeDuration !== 'undefined') {
|
|
151
|
+
try { p.setFreeDuration(props.freeDuration as number); } catch(_) {}
|
|
152
|
+
}
|
|
153
|
+
}, [props.freeDuration]);
|
|
154
|
+
|
|
155
|
+
// Update paywall config at runtime if prop changes
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const p = playerRef.current as any;
|
|
158
|
+
if (p && typeof p.setPaywallConfig === 'function' && props.paywall) {
|
|
159
|
+
try { p.setPaywallConfig(props.paywall as any); } catch(_) {}
|
|
160
|
+
}
|
|
161
|
+
}, [JSON.stringify(props.paywall)]);
|
|
162
|
+
|
|
163
|
+
// Respond to theme updates without reinitializing the player
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const p = playerRef.current as any;
|
|
166
|
+
try {
|
|
167
|
+
if (p && typeof p.setTheme === 'function') {
|
|
168
|
+
p.setTheme(props.playerTheme as any);
|
|
169
|
+
}
|
|
170
|
+
} catch (_) {}
|
|
171
|
+
}, [JSON.stringify(props.playerTheme)]);
|
|
172
|
+
|
|
173
|
+
return <div ref={containerRef} className={props.className} style={props.style} />;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export default WebPlayerView;
|
|
177
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"composite": true,
|
|
7
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
8
|
+
"noUnusedLocals": false,
|
|
9
|
+
"noUnusedParameters": false
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*"
|
|
13
|
+
],
|
|
14
|
+
"exclude": [
|
|
15
|
+
"node_modules",
|
|
16
|
+
"dist",
|
|
17
|
+
"**/*.test.ts",
|
|
18
|
+
"**/*.spec.ts"
|
|
19
|
+
],
|
|
20
|
+
"references": [
|
|
21
|
+
{ "path": "../core" }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
mode: 'production',
|
|
5
|
+
entry: './src/index.ts',
|
|
6
|
+
output: {
|
|
7
|
+
path: path.resolve(__dirname, 'dist'),
|
|
8
|
+
filename: 'unified-video-web.js',
|
|
9
|
+
library: {
|
|
10
|
+
name: 'UnifiedVideoWeb',
|
|
11
|
+
type: 'umd'
|
|
12
|
+
},
|
|
13
|
+
globalObject: 'this'
|
|
14
|
+
},
|
|
15
|
+
module: {
|
|
16
|
+
rules: [
|
|
17
|
+
{
|
|
18
|
+
test: /\.tsx?$/,
|
|
19
|
+
use: 'ts-loader',
|
|
20
|
+
exclude: /node_modules/
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
resolve: {
|
|
25
|
+
extensions: ['.tsx', '.ts', '.js'],
|
|
26
|
+
alias: {
|
|
27
|
+
'@unified-video/core': path.resolve(__dirname, '../core/src')
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
externals: {
|
|
31
|
+
'hls.js': {
|
|
32
|
+
commonjs: 'hls.js',
|
|
33
|
+
commonjs2: 'hls.js',
|
|
34
|
+
amd: 'hls.js',
|
|
35
|
+
root: 'Hls'
|
|
36
|
+
},
|
|
37
|
+
'dashjs': {
|
|
38
|
+
commonjs: 'dashjs',
|
|
39
|
+
commonjs2: 'dashjs',
|
|
40
|
+
amd: 'dashjs',
|
|
41
|
+
root: 'dashjs'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
devtool: 'source-map'
|
|
45
|
+
};
|
package/server.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Simple HTTP server for local development
|
|
2
|
+
// This server properly handles CORS and MIME types for video streaming
|
|
3
|
+
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const url = require('url');
|
|
8
|
+
|
|
9
|
+
const PORT = 3000;
|
|
10
|
+
const HOST = 'localhost';
|
|
11
|
+
|
|
12
|
+
// MIME type mappings
|
|
13
|
+
const mimeTypes = {
|
|
14
|
+
'.html': 'text/html',
|
|
15
|
+
'.js': 'text/javascript',
|
|
16
|
+
'.css': 'text/css',
|
|
17
|
+
'.json': 'application/json',
|
|
18
|
+
'.png': 'image/png',
|
|
19
|
+
'.jpg': 'image/jpg',
|
|
20
|
+
'.gif': 'image/gif',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
'.wav': 'audio/wav',
|
|
23
|
+
'.mp4': 'video/mp4',
|
|
24
|
+
'.webm': 'video/webm',
|
|
25
|
+
'.mp3': 'audio/mpeg',
|
|
26
|
+
'.m3u8': 'application/x-mpegURL',
|
|
27
|
+
'.ts': 'video/MP2T',
|
|
28
|
+
'.mpd': 'application/dash+xml',
|
|
29
|
+
'.woff': 'application/font-woff',
|
|
30
|
+
'.ttf': 'application/font-ttf',
|
|
31
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
32
|
+
'.otf': 'application/font-otf',
|
|
33
|
+
'.wasm': 'application/wasm'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const server = http.createServer((req, res) => {
|
|
37
|
+
console.log(`${req.method} ${req.url}`);
|
|
38
|
+
|
|
39
|
+
// Parse URL
|
|
40
|
+
const parsedUrl = url.parse(req.url);
|
|
41
|
+
|
|
42
|
+
// Extract pathname
|
|
43
|
+
let pathname = `.${parsedUrl.pathname}`;
|
|
44
|
+
|
|
45
|
+
// Default to index.html for root
|
|
46
|
+
if (pathname === './') {
|
|
47
|
+
pathname = './apps/demo/demo.html';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Resolve full path
|
|
51
|
+
const filePath = path.resolve(pathname);
|
|
52
|
+
|
|
53
|
+
// Security check - ensure we're not serving files outside the project directory
|
|
54
|
+
const projectDir = path.resolve('./');
|
|
55
|
+
if (!filePath.startsWith(projectDir)) {
|
|
56
|
+
res.statusCode = 403;
|
|
57
|
+
res.end('Forbidden');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check if file exists
|
|
62
|
+
fs.exists(filePath, (exist) => {
|
|
63
|
+
if (!exist) {
|
|
64
|
+
// File not found
|
|
65
|
+
res.statusCode = 404;
|
|
66
|
+
res.end(`File ${pathname} not found!`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If directory, try to serve index.html
|
|
71
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
72
|
+
const indexPath = path.join(filePath, 'index.html');
|
|
73
|
+
if (fs.existsSync(indexPath)) {
|
|
74
|
+
serveFile(indexPath, res);
|
|
75
|
+
} else {
|
|
76
|
+
res.statusCode = 403;
|
|
77
|
+
res.end('Directory listing not allowed');
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Serve the file
|
|
83
|
+
serveFile(filePath, res);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function serveFile(filePath, res) {
|
|
88
|
+
// Read file
|
|
89
|
+
fs.readFile(filePath, (err, data) => {
|
|
90
|
+
if (err) {
|
|
91
|
+
res.statusCode = 500;
|
|
92
|
+
res.end(`Error getting the file: ${err}.`);
|
|
93
|
+
} else {
|
|
94
|
+
// Set proper headers
|
|
95
|
+
const ext = path.parse(filePath).ext;
|
|
96
|
+
const mimeType = mimeTypes[ext] || 'application/octet-stream';
|
|
97
|
+
|
|
98
|
+
// Add CORS headers for development
|
|
99
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
100
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
101
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Range');
|
|
102
|
+
|
|
103
|
+
// Set content type
|
|
104
|
+
res.setHeader('Content-Type', mimeType);
|
|
105
|
+
|
|
106
|
+
// Enable byte-range requests for video streaming
|
|
107
|
+
if (mimeType.startsWith('video/') || mimeType.startsWith('audio/')) {
|
|
108
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Send the file
|
|
112
|
+
res.statusCode = 200;
|
|
113
|
+
res.end(data);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
server.listen(PORT, HOST, () => {
|
|
119
|
+
console.log(`
|
|
120
|
+
╔════════════════════════════════════════════════════════╗
|
|
121
|
+
║ ║
|
|
122
|
+
║ Unified Video Framework - Development Server ║
|
|
123
|
+
║ ║
|
|
124
|
+
╚════════════════════════════════════════════════════════╝
|
|
125
|
+
|
|
126
|
+
Server running at: http://${HOST}:${PORT}
|
|
127
|
+
Demo page: http://${HOST}:${PORT}/apps/demo/demo.html
|
|
128
|
+
|
|
129
|
+
Press Ctrl+C to stop the server
|
|
130
|
+
`);
|
|
131
|
+
});
|
package/server.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Simple HTTP server for local development of Unified Video Framework
|
|
4
|
+
Python 3 alternative to Node.js server
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import http.server
|
|
8
|
+
import socketserver
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
PORT = 3000
|
|
14
|
+
HOST = "localhost"
|
|
15
|
+
|
|
16
|
+
class CORSRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
17
|
+
"""HTTP request handler with CORS headers."""
|
|
18
|
+
|
|
19
|
+
def end_headers(self):
|
|
20
|
+
"""Add CORS headers to all responses."""
|
|
21
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
22
|
+
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
23
|
+
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Range')
|
|
24
|
+
|
|
25
|
+
# Add proper MIME types for video files
|
|
26
|
+
if self.path.endswith('.m3u8'):
|
|
27
|
+
self.send_header('Content-Type', 'application/x-mpegURL')
|
|
28
|
+
elif self.path.endswith('.mpd'):
|
|
29
|
+
self.send_header('Content-Type', 'application/dash+xml')
|
|
30
|
+
elif self.path.endswith('.ts'):
|
|
31
|
+
self.send_header('Content-Type', 'video/MP2T')
|
|
32
|
+
|
|
33
|
+
super().end_headers()
|
|
34
|
+
|
|
35
|
+
def do_OPTIONS(self):
|
|
36
|
+
"""Handle OPTIONS requests for CORS preflight."""
|
|
37
|
+
self.send_response(200)
|
|
38
|
+
self.end_headers()
|
|
39
|
+
|
|
40
|
+
def do_GET(self):
|
|
41
|
+
"""Handle GET requests."""
|
|
42
|
+
# Redirect root to demo page
|
|
43
|
+
if self.path == '/':
|
|
44
|
+
self.path = '/apps/demo/demo.html'
|
|
45
|
+
|
|
46
|
+
# Log the request
|
|
47
|
+
print(f"GET {self.path}")
|
|
48
|
+
|
|
49
|
+
# Serve the file
|
|
50
|
+
super().do_GET()
|
|
51
|
+
|
|
52
|
+
def run_server():
|
|
53
|
+
"""Start the development server."""
|
|
54
|
+
try:
|
|
55
|
+
# Change to the project directory
|
|
56
|
+
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
|
57
|
+
|
|
58
|
+
# Create server
|
|
59
|
+
with socketserver.TCPServer((HOST, PORT), CORSRequestHandler) as httpd:
|
|
60
|
+
print(f"""
|
|
61
|
+
╔════════════════════════════════════════════════════════╗
|
|
62
|
+
║ ║
|
|
63
|
+
║ Unified Video Framework - Development Server ║
|
|
64
|
+
║ (Python Version) ║
|
|
65
|
+
║ ║
|
|
66
|
+
╚════════════════════════════════════════════════════════╝
|
|
67
|
+
|
|
68
|
+
Server running at: http://{HOST}:{PORT}
|
|
69
|
+
Demo page: http://{HOST}:{PORT}/apps/demo/demo.html
|
|
70
|
+
|
|
71
|
+
Press Ctrl+C to stop the server
|
|
72
|
+
""")
|
|
73
|
+
# Start serving
|
|
74
|
+
httpd.serve_forever()
|
|
75
|
+
|
|
76
|
+
except KeyboardInterrupt:
|
|
77
|
+
print("\nServer stopped.")
|
|
78
|
+
sys.exit(0)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"Error starting server: {e}")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
run_server()
|
package/test-urls.ps1
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Test all video URLs in the framework to ensure they're accessible
|
|
2
|
+
|
|
3
|
+
Write-Host "Testing Video URLs in Unified Video Framework" -ForegroundColor Cyan
|
|
4
|
+
Write-Host "=" * 50
|
|
5
|
+
|
|
6
|
+
# Define all URLs to test
|
|
7
|
+
$urls = @(
|
|
8
|
+
@{Name="Mux HLS Stream"; URL="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"; Type="HLS"},
|
|
9
|
+
@{Name="Unified Streaming HLS"; URL="https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8"; Type="HLS"},
|
|
10
|
+
@{Name="Akamai DASH BBB"; URL="https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd"; Type="DASH"},
|
|
11
|
+
@{Name="Akamai DASH Envivio"; URL="https://dash.akamaized.net/envivio/EnvivioDash3/manifest.mpd"; Type="DASH"},
|
|
12
|
+
@{Name="Google Shaka DASH"; URL="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"; Type="DASH"},
|
|
13
|
+
@{Name="Big Buck Bunny MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; Type="MP4"},
|
|
14
|
+
@{Name="Elephants Dream MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"; Type="MP4"},
|
|
15
|
+
@{Name="Sintel MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"; Type="MP4"}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
$results = @()
|
|
19
|
+
|
|
20
|
+
foreach ($item in $urls) {
|
|
21
|
+
Write-Host "`nTesting: $($item.Name) [$($item.Type)]" -ForegroundColor Yellow
|
|
22
|
+
Write-Host "URL: $($item.URL)" -ForegroundColor Gray
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
$response = Invoke-WebRequest -Uri $item.URL -Method Head -TimeoutSec 10 -UseBasicParsing -ErrorAction Stop
|
|
26
|
+
$statusCode = $response.StatusCode
|
|
27
|
+
|
|
28
|
+
if ($statusCode -eq 200) {
|
|
29
|
+
Write-Host "✓ SUCCESS: Status $statusCode" -ForegroundColor Green
|
|
30
|
+
$status = "Working"
|
|
31
|
+
} else {
|
|
32
|
+
Write-Host "⚠ WARNING: Status $statusCode" -ForegroundColor Yellow
|
|
33
|
+
$status = "Warning - Status $statusCode"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
$errorMessage = $_.Exception.Message
|
|
38
|
+
|
|
39
|
+
if ($errorMessage -like "*403*") {
|
|
40
|
+
Write-Host "✗ FORBIDDEN: Access denied (403)" -ForegroundColor Red
|
|
41
|
+
$status = "Broken - 403 Forbidden"
|
|
42
|
+
}
|
|
43
|
+
elseif ($errorMessage -like "*404*") {
|
|
44
|
+
Write-Host "✗ NOT FOUND: URL not found (404)" -ForegroundColor Red
|
|
45
|
+
$status = "Broken - 404 Not Found"
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
# Some servers don't support HEAD requests, try GET with limited range
|
|
49
|
+
try {
|
|
50
|
+
$response2 = Invoke-WebRequest -Uri $item.URL -Method Get -Headers @{"Range"="bytes=0-1"} -TimeoutSec 10 -UseBasicParsing -ErrorAction Stop
|
|
51
|
+
Write-Host "✓ SUCCESS: Server responded (GET request)" -ForegroundColor Green
|
|
52
|
+
$status = "Working"
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
Write-Host "✗ ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
|
56
|
+
$status = "Error - $($_.Exception.Message)"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$results += [PSCustomObject]@{
|
|
62
|
+
Name = $item.Name
|
|
63
|
+
Type = $item.Type
|
|
64
|
+
URL = $item.URL
|
|
65
|
+
Status = $status
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Write-Host "`n" + ("=" * 50) -ForegroundColor Cyan
|
|
70
|
+
Write-Host "SUMMARY REPORT" -ForegroundColor Cyan
|
|
71
|
+
Write-Host ("=" * 50) -ForegroundColor Cyan
|
|
72
|
+
|
|
73
|
+
$working = $results | Where-Object { $_.Status -eq "Working" }
|
|
74
|
+
$broken = $results | Where-Object { $_.Status -like "Broken*" }
|
|
75
|
+
$errors = $results | Where-Object { $_.Status -like "Error*" -or $_.Status -like "Warning*" }
|
|
76
|
+
|
|
77
|
+
Write-Host "`nWorking URLs: $($working.Count)/$($results.Count)" -ForegroundColor Green
|
|
78
|
+
if ($broken.Count -gt 0) {
|
|
79
|
+
Write-Host "Broken URLs: $($broken.Count)" -ForegroundColor Red
|
|
80
|
+
$broken | ForEach-Object {
|
|
81
|
+
Write-Host " - $($_.Name): $($_.Status)" -ForegroundColor Red
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if ($errors.Count -gt 0) {
|
|
86
|
+
Write-Host "`nURLs with issues: $($errors.Count)" -ForegroundColor Yellow
|
|
87
|
+
$errors | ForEach-Object {
|
|
88
|
+
Write-Host " - $($_.Name): $($_.Status)" -ForegroundColor Yellow
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Write-Host "`nDetailed Results:" -ForegroundColor Cyan
|
|
93
|
+
$results | Format-Table -AutoSize
|
|
94
|
+
|
|
95
|
+
# Export results to JSON for reference
|
|
96
|
+
$results | ConvertTo-Json -Depth 3 | Out-File "url-test-results.json"
|
|
97
|
+
Write-Host "`nResults saved to url-test-results.json" -ForegroundColor Gray
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Test all video URLs in the framework to ensure they're accessible
|
|
2
|
+
|
|
3
|
+
Write-Host "`nTesting Video URLs in Unified Video Framework`n" -ForegroundColor Cyan
|
|
4
|
+
Write-Host ("=" * 60) -ForegroundColor Cyan
|
|
5
|
+
|
|
6
|
+
# Define all URLs to test
|
|
7
|
+
$urls = @(
|
|
8
|
+
@{Name="Mux HLS Stream"; URL="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"; Type="HLS"},
|
|
9
|
+
@{Name="Unified Streaming HLS"; URL="https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8"; Type="HLS"},
|
|
10
|
+
@{Name="Akamai DASH BBB"; URL="https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd"; Type="DASH"},
|
|
11
|
+
@{Name="Akamai DASH Envivio"; URL="https://dash.akamaized.net/envivio/EnvivioDash3/manifest.mpd"; Type="DASH"},
|
|
12
|
+
@{Name="Google Shaka DASH"; URL="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"; Type="DASH"},
|
|
13
|
+
@{Name="Big Buck Bunny MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; Type="MP4"},
|
|
14
|
+
@{Name="Elephants Dream MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"; Type="MP4"},
|
|
15
|
+
@{Name="Sintel MP4"; URL="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"; Type="MP4"}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
$results = @()
|
|
19
|
+
$workingCount = 0
|
|
20
|
+
$brokenCount = 0
|
|
21
|
+
|
|
22
|
+
foreach ($item in $urls) {
|
|
23
|
+
Write-Host "`nTesting: $($item.Name) [$($item.Type)]" -ForegroundColor Yellow
|
|
24
|
+
Write-Host "URL: $($item.URL)" -ForegroundColor Gray
|
|
25
|
+
|
|
26
|
+
$status = "Unknown"
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
# Try a simple web request with a timeout
|
|
30
|
+
$response = Invoke-WebRequest -Uri $item.URL -Method Get -Headers @{"Range"="bytes=0-1"} -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop
|
|
31
|
+
Write-Host "✓ SUCCESS: URL is accessible" -ForegroundColor Green
|
|
32
|
+
$status = "Working"
|
|
33
|
+
$workingCount++
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
$errorMsg = $_.Exception.Message
|
|
37
|
+
|
|
38
|
+
if ($errorMsg -match "403") {
|
|
39
|
+
Write-Host "✗ FORBIDDEN: Access denied (403)" -ForegroundColor Red
|
|
40
|
+
$status = "403 Forbidden"
|
|
41
|
+
$brokenCount++
|
|
42
|
+
}
|
|
43
|
+
elseif ($errorMsg -match "404") {
|
|
44
|
+
Write-Host "✗ NOT FOUND: URL not found (404)" -ForegroundColor Red
|
|
45
|
+
$status = "404 Not Found"
|
|
46
|
+
$brokenCount++
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
Write-Host "⚠ WARNING: Could not verify (may still work in browser)" -ForegroundColor Yellow
|
|
50
|
+
$status = "Uncertain"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$results += [PSCustomObject]@{
|
|
55
|
+
Name = $item.Name
|
|
56
|
+
Type = $item.Type
|
|
57
|
+
Status = $status
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Write-Host "`n" -NoNewline
|
|
62
|
+
Write-Host ("=" * 60) -ForegroundColor Cyan
|
|
63
|
+
Write-Host "SUMMARY REPORT" -ForegroundColor Cyan
|
|
64
|
+
Write-Host ("=" * 60) -ForegroundColor Cyan
|
|
65
|
+
|
|
66
|
+
Write-Host "`nTotal URLs tested: $($urls.Count)" -ForegroundColor White
|
|
67
|
+
Write-Host "✓ Working: $workingCount" -ForegroundColor Green
|
|
68
|
+
Write-Host "✗ Broken: $brokenCount" -ForegroundColor Red
|
|
69
|
+
Write-Host "⚠ Uncertain: $($urls.Count - $workingCount - $brokenCount)" -ForegroundColor Yellow
|
|
70
|
+
|
|
71
|
+
Write-Host "`nDetailed Results:" -ForegroundColor Cyan
|
|
72
|
+
$results | Format-Table Name, Type, Status -AutoSize
|
|
73
|
+
|
|
74
|
+
# Show which files use broken URLs if any found
|
|
75
|
+
if ($brokenCount -gt 0) {
|
|
76
|
+
Write-Host "`n⚠ Action Required:" -ForegroundColor Red
|
|
77
|
+
Write-Host "Found $brokenCount broken URL(s) that need to be replaced in the framework." -ForegroundColor Red
|
|
78
|
+
|
|
79
|
+
$brokenUrls = $results | Where-Object { $_.Status -match "403|404" }
|
|
80
|
+
foreach ($broken in $brokenUrls) {
|
|
81
|
+
Write-Host "`n • $($broken.Name) ($($broken.Status))" -ForegroundColor Yellow
|
|
82
|
+
Write-Host " Type: $($broken.Type)" -ForegroundColor Gray
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
Write-Host "`n✓ All URLs are working correctly!" -ForegroundColor Green
|
|
87
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./",
|
|
11
|
+
"composite": true,
|
|
12
|
+
"removeComments": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true,
|
|
16
|
+
"noImplicitReturns": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"allowSyntheticDefaultImports": true,
|
|
21
|
+
"forceConsistentCasingInFileNames": true,
|
|
22
|
+
"moduleResolution": "node",
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"isolatedModules": true,
|
|
25
|
+
"jsx": "react"
|
|
26
|
+
},
|
|
27
|
+
"exclude": [
|
|
28
|
+
"node_modules",
|
|
29
|
+
"dist",
|
|
30
|
+
"build",
|
|
31
|
+
"*.config.js",
|
|
32
|
+
"*.config.ts"
|
|
33
|
+
],
|
|
34
|
+
"references": [
|
|
35
|
+
{ "path": "./packages/core" },
|
|
36
|
+
{ "path": "./packages/web" },
|
|
37
|
+
{ "path": "./packages/react-native" }
|
|
38
|
+
]
|
|
39
|
+
}
|