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,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base player class that provides common functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
IVideoPlayer,
|
|
7
|
+
VideoSource,
|
|
8
|
+
PlayerConfig,
|
|
9
|
+
PlayerState,
|
|
10
|
+
PlayerEvents,
|
|
11
|
+
PlayerError,
|
|
12
|
+
Quality,
|
|
13
|
+
SubtitleTrack
|
|
14
|
+
} from './interfaces/IVideoPlayer';
|
|
15
|
+
import { EventEmitter } from './utils/EventEmitter';
|
|
16
|
+
|
|
17
|
+
export abstract class BasePlayer implements IVideoPlayer {
|
|
18
|
+
protected container: HTMLElement | null = null;
|
|
19
|
+
protected config: PlayerConfig;
|
|
20
|
+
protected events: EventEmitter;
|
|
21
|
+
protected state: PlayerState;
|
|
22
|
+
protected source: VideoSource | null = null;
|
|
23
|
+
protected subtitles: SubtitleTrack[] = [];
|
|
24
|
+
protected currentSubtitleIndex: number = -1;
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
this.config = this.getDefaultConfig();
|
|
28
|
+
this.events = new EventEmitter();
|
|
29
|
+
this.state = this.getDefaultState();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected getDefaultConfig(): PlayerConfig {
|
|
33
|
+
return {
|
|
34
|
+
autoPlay: false,
|
|
35
|
+
muted: false,
|
|
36
|
+
volume: 1.0,
|
|
37
|
+
controls: true,
|
|
38
|
+
loop: false,
|
|
39
|
+
preload: 'metadata',
|
|
40
|
+
playsInline: true,
|
|
41
|
+
enableAdaptiveBitrate: true,
|
|
42
|
+
debug: false,
|
|
43
|
+
freeDuration: 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected getDefaultState(): PlayerState {
|
|
48
|
+
return {
|
|
49
|
+
isPlaying: false,
|
|
50
|
+
isPaused: true,
|
|
51
|
+
isBuffering: false,
|
|
52
|
+
isEnded: false,
|
|
53
|
+
isError: false,
|
|
54
|
+
currentTime: 0,
|
|
55
|
+
duration: 0,
|
|
56
|
+
bufferedPercentage: 0,
|
|
57
|
+
volume: 1.0,
|
|
58
|
+
isMuted: false,
|
|
59
|
+
playbackRate: 1.0,
|
|
60
|
+
availableQualities: []
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async initialize(container: HTMLElement | string, config?: PlayerConfig): Promise<void> {
|
|
65
|
+
if (typeof container === 'string') {
|
|
66
|
+
const element = document.querySelector(container) as HTMLElement;
|
|
67
|
+
if (!element) {
|
|
68
|
+
throw new Error(`Container element not found: ${container}`);
|
|
69
|
+
}
|
|
70
|
+
this.container = element;
|
|
71
|
+
} else {
|
|
72
|
+
this.container = container;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.config = { ...this.getDefaultConfig(), ...config };
|
|
76
|
+
this.state.volume = this.config.volume || 1.0;
|
|
77
|
+
this.state.isMuted = this.config.muted || false;
|
|
78
|
+
|
|
79
|
+
await this.setupPlayer();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected abstract setupPlayer(): Promise<void>;
|
|
83
|
+
|
|
84
|
+
abstract destroy(): Promise<void>;
|
|
85
|
+
|
|
86
|
+
abstract load(source: VideoSource): Promise<void>;
|
|
87
|
+
|
|
88
|
+
async play(): Promise<void> {
|
|
89
|
+
this.state.isPlaying = true;
|
|
90
|
+
this.state.isPaused = false;
|
|
91
|
+
this.emit('onPlay');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pause(): void {
|
|
95
|
+
this.state.isPlaying = false;
|
|
96
|
+
this.state.isPaused = true;
|
|
97
|
+
this.emit('onPause');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
stop(): void {
|
|
101
|
+
this.pause();
|
|
102
|
+
this.seek(0);
|
|
103
|
+
this.state.isEnded = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
abstract seek(time: number): void;
|
|
107
|
+
|
|
108
|
+
setVolume(level: number): void {
|
|
109
|
+
const volume = Math.max(0, Math.min(1, level));
|
|
110
|
+
this.state.volume = volume;
|
|
111
|
+
this.emit('onVolumeChanged', volume);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
mute(): void {
|
|
115
|
+
this.state.isMuted = true;
|
|
116
|
+
this.emit('onVolumeChanged', 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
unmute(): void {
|
|
120
|
+
this.state.isMuted = false;
|
|
121
|
+
this.emit('onVolumeChanged', this.state.volume);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
toggleMute(): void {
|
|
125
|
+
if (this.state.isMuted) {
|
|
126
|
+
this.unmute();
|
|
127
|
+
} else {
|
|
128
|
+
this.mute();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
abstract getQualities(): Quality[];
|
|
133
|
+
abstract getCurrentQuality(): Quality | null;
|
|
134
|
+
abstract setQuality(index: number): void;
|
|
135
|
+
abstract setAutoQuality(enabled: boolean): void;
|
|
136
|
+
|
|
137
|
+
setPlaybackRate(rate: number): void {
|
|
138
|
+
this.state.playbackRate = rate;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getPlaybackRate(): number {
|
|
142
|
+
return this.state.playbackRate;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getCurrentTime(): number {
|
|
146
|
+
return this.state.currentTime;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getDuration(): number {
|
|
150
|
+
return this.state.duration;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getBufferedPercentage(): number {
|
|
154
|
+
return this.state.bufferedPercentage;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getState(): PlayerState {
|
|
158
|
+
return { ...this.state };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
isPlaying(): boolean {
|
|
162
|
+
return this.state.isPlaying;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
isPaused(): boolean {
|
|
166
|
+
return this.state.isPaused;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
isEnded(): boolean {
|
|
170
|
+
return this.state.isEnded;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
abstract enterFullscreen(): Promise<void>;
|
|
174
|
+
abstract exitFullscreen(): Promise<void>;
|
|
175
|
+
|
|
176
|
+
async toggleFullscreen(): Promise<void> {
|
|
177
|
+
if (document.fullscreenElement) {
|
|
178
|
+
await this.exitFullscreen();
|
|
179
|
+
} else {
|
|
180
|
+
await this.enterFullscreen();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
abstract enterPictureInPicture(): Promise<void>;
|
|
185
|
+
abstract exitPictureInPicture(): Promise<void>;
|
|
186
|
+
|
|
187
|
+
on(event: keyof PlayerEvents, handler: Function): void {
|
|
188
|
+
this.events.on(event, handler as any);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
off(event: keyof PlayerEvents, handler?: Function): void {
|
|
192
|
+
this.events.off(event, handler as any);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
once(event: keyof PlayerEvents, handler: Function): void {
|
|
196
|
+
this.events.once(event, handler as any);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
protected emit(event: keyof PlayerEvents, ...args: any[]): void {
|
|
200
|
+
this.events.emit(event, ...args);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Optional runtime free preview controls (no-op base; platform implementations may override)
|
|
204
|
+
setFreeDuration?(seconds: number): void;
|
|
205
|
+
resetFreePreviewGate?(): void;
|
|
206
|
+
|
|
207
|
+
getSubtitles(): SubtitleTrack[] {
|
|
208
|
+
return this.subtitles;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
setSubtitleTrack(index: number): void {
|
|
212
|
+
if (index >= 0 && index < this.subtitles.length) {
|
|
213
|
+
this.currentSubtitleIndex = index;
|
|
214
|
+
this.applySubtitleTrack(this.subtitles[index]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
disableSubtitles(): void {
|
|
219
|
+
this.currentSubtitleIndex = -1;
|
|
220
|
+
this.removeSubtitles();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
protected abstract applySubtitleTrack(track: SubtitleTrack): void;
|
|
224
|
+
protected abstract removeSubtitles(): void;
|
|
225
|
+
|
|
226
|
+
protected handleError(error: PlayerError): void {
|
|
227
|
+
this.state.isError = true;
|
|
228
|
+
this.state.isPlaying = false;
|
|
229
|
+
this.emit('onError', error);
|
|
230
|
+
|
|
231
|
+
if (this.config.debug) {
|
|
232
|
+
console.error('[VideoPlayer Error]', error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
protected updateTime(time: number): void {
|
|
237
|
+
this.state.currentTime = time;
|
|
238
|
+
this.emit('onTimeUpdate', time);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
protected updateBuffered(percentage: number): void {
|
|
242
|
+
this.state.bufferedPercentage = percentage;
|
|
243
|
+
this.emit('onProgress', percentage);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
protected setBuffering(isBuffering: boolean): void {
|
|
247
|
+
this.state.isBuffering = isBuffering;
|
|
248
|
+
this.emit('onBuffering', isBuffering);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import {
|
|
3
|
+
VideoPlayerConfig,
|
|
4
|
+
VideoSource,
|
|
5
|
+
PlayerState,
|
|
6
|
+
PlayerEvent,
|
|
7
|
+
EventHandler,
|
|
8
|
+
Quality,
|
|
9
|
+
SubtitleTrack,
|
|
10
|
+
AudioTrack,
|
|
11
|
+
PlayerMetrics,
|
|
12
|
+
PlayerError,
|
|
13
|
+
DRMConfig
|
|
14
|
+
} from './interfaces';
|
|
15
|
+
|
|
16
|
+
export abstract class VideoPlayer {
|
|
17
|
+
protected config: VideoPlayerConfig;
|
|
18
|
+
protected eventEmitter: EventEmitter;
|
|
19
|
+
protected state: PlayerState;
|
|
20
|
+
protected currentSource?: VideoSource;
|
|
21
|
+
protected metrics: PlayerMetrics;
|
|
22
|
+
protected errors: PlayerError[] = [];
|
|
23
|
+
|
|
24
|
+
constructor(config: VideoPlayerConfig = {}) {
|
|
25
|
+
this.config = {
|
|
26
|
+
autoPlay: false,
|
|
27
|
+
muted: false,
|
|
28
|
+
controls: true,
|
|
29
|
+
loop: false,
|
|
30
|
+
preload: 'metadata',
|
|
31
|
+
playsInline: true,
|
|
32
|
+
...config
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this.eventEmitter = new EventEmitter();
|
|
36
|
+
this.state = PlayerState.IDLE;
|
|
37
|
+
this.metrics = this.initializeMetrics();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Core playback methods
|
|
41
|
+
abstract load(source: VideoSource): Promise<void>;
|
|
42
|
+
abstract play(): Promise<void>;
|
|
43
|
+
abstract pause(): void;
|
|
44
|
+
abstract stop(): void;
|
|
45
|
+
abstract seek(position: number): void;
|
|
46
|
+
abstract setVolume(volume: number): void;
|
|
47
|
+
abstract setPlaybackRate(rate: number): void;
|
|
48
|
+
abstract getCurrentTime(): number;
|
|
49
|
+
abstract getDuration(): number;
|
|
50
|
+
abstract getVolume(): number;
|
|
51
|
+
abstract getPlaybackRate(): number;
|
|
52
|
+
abstract isMuted(): boolean;
|
|
53
|
+
abstract setMuted(muted: boolean): void;
|
|
54
|
+
|
|
55
|
+
// Quality management
|
|
56
|
+
abstract getAvailableQualities(): Quality[];
|
|
57
|
+
abstract getCurrentQuality(): Quality | null;
|
|
58
|
+
abstract setQuality(quality: Quality): void;
|
|
59
|
+
abstract enableAutoQuality(enabled: boolean): void;
|
|
60
|
+
|
|
61
|
+
// Subtitle/Audio tracks
|
|
62
|
+
abstract getSubtitleTracks(): SubtitleTrack[];
|
|
63
|
+
abstract getCurrentSubtitleTrack(): SubtitleTrack | null;
|
|
64
|
+
abstract setSubtitleTrack(track: SubtitleTrack | null): void;
|
|
65
|
+
abstract getAudioTracks(): AudioTrack[];
|
|
66
|
+
abstract getCurrentAudioTrack(): AudioTrack | null;
|
|
67
|
+
abstract setAudioTrack(track: AudioTrack): void;
|
|
68
|
+
|
|
69
|
+
// Platform-specific features
|
|
70
|
+
abstract enterFullscreen(): void;
|
|
71
|
+
abstract exitFullscreen(): void;
|
|
72
|
+
abstract isFullscreen(): boolean;
|
|
73
|
+
abstract enterPictureInPicture(): void;
|
|
74
|
+
abstract exitPictureInPicture(): void;
|
|
75
|
+
abstract isPictureInPicture(): boolean;
|
|
76
|
+
|
|
77
|
+
// Events
|
|
78
|
+
on(event: PlayerEvent, handler: EventHandler): void {
|
|
79
|
+
this.eventEmitter.on(event, handler);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
off(event: PlayerEvent, handler: EventHandler): void {
|
|
83
|
+
this.eventEmitter.off(event, handler);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
once(event: PlayerEvent, handler: EventHandler): void {
|
|
87
|
+
this.eventEmitter.once(event, handler);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
removeAllListeners(event?: PlayerEvent): void {
|
|
91
|
+
if (event) {
|
|
92
|
+
this.eventEmitter.removeAllListeners(event);
|
|
93
|
+
} else {
|
|
94
|
+
this.eventEmitter.removeAllListeners();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
protected emit(event: PlayerEvent, data?: any): void {
|
|
99
|
+
this.eventEmitter.emit(event, data);
|
|
100
|
+
|
|
101
|
+
// Track analytics events
|
|
102
|
+
if (this.config.analytics?.enabled) {
|
|
103
|
+
this.trackAnalytics(event, data);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// State management
|
|
108
|
+
getState(): PlayerState {
|
|
109
|
+
return this.state;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
protected setState(newState: PlayerState): void {
|
|
113
|
+
const oldState = this.state;
|
|
114
|
+
this.state = newState;
|
|
115
|
+
|
|
116
|
+
if (oldState !== newState) {
|
|
117
|
+
this.emit('statechange' as PlayerEvent, { oldState, newState });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Configuration
|
|
122
|
+
getConfig(): VideoPlayerConfig {
|
|
123
|
+
return { ...this.config };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
updateConfig(config: Partial<VideoPlayerConfig>): void {
|
|
127
|
+
this.config = { ...this.config, ...config };
|
|
128
|
+
this.applyConfig();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
protected abstract applyConfig(): void;
|
|
132
|
+
|
|
133
|
+
// Metrics
|
|
134
|
+
getMetrics(): PlayerMetrics {
|
|
135
|
+
return {
|
|
136
|
+
...this.metrics,
|
|
137
|
+
errors: [...this.errors]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
protected initializeMetrics(): PlayerMetrics {
|
|
142
|
+
return {
|
|
143
|
+
sessionId: this.generateSessionId(),
|
|
144
|
+
totalPlayTime: 0,
|
|
145
|
+
bufferingCount: 0,
|
|
146
|
+
bufferingDuration: 0,
|
|
147
|
+
averageBitrate: 0,
|
|
148
|
+
qualityChanges: 0,
|
|
149
|
+
errors: []
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected generateSessionId(): string {
|
|
154
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Error handling
|
|
158
|
+
protected handleError(error: PlayerError): void {
|
|
159
|
+
this.errors.push(error);
|
|
160
|
+
this.emit('error', error);
|
|
161
|
+
|
|
162
|
+
if (error.fatal) {
|
|
163
|
+
this.setState(PlayerState.ERROR);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Analytics
|
|
168
|
+
protected trackAnalytics(event: string, data?: any): void {
|
|
169
|
+
if (!this.config.analytics?.providers) return;
|
|
170
|
+
|
|
171
|
+
const analyticsData = {
|
|
172
|
+
event,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
sessionId: this.metrics.sessionId,
|
|
175
|
+
currentTime: this.getCurrentTime(),
|
|
176
|
+
duration: this.getDuration(),
|
|
177
|
+
state: this.state,
|
|
178
|
+
...data
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.config.analytics.providers.forEach(provider => {
|
|
182
|
+
try {
|
|
183
|
+
provider.track(event, analyticsData);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(`Analytics provider ${provider.name} failed:`, error);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// DRM
|
|
191
|
+
protected abstract configureDRM(drmConfig: DRMConfig): Promise<void>;
|
|
192
|
+
|
|
193
|
+
// Cleanup
|
|
194
|
+
abstract destroy(): void;
|
|
195
|
+
|
|
196
|
+
protected cleanup(): void {
|
|
197
|
+
this.removeAllListeners();
|
|
198
|
+
this.state = PlayerState.IDLE;
|
|
199
|
+
this.currentSource = undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Utility methods
|
|
203
|
+
protected formatTime(seconds: number): string {
|
|
204
|
+
if (!isFinite(seconds)) return '00:00';
|
|
205
|
+
|
|
206
|
+
const hours = Math.floor(seconds / 3600);
|
|
207
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
208
|
+
const secs = Math.floor(seconds % 60);
|
|
209
|
+
|
|
210
|
+
if (hours > 0) {
|
|
211
|
+
return `${hours}:${this.pad(minutes)}:${this.pad(secs)}`;
|
|
212
|
+
}
|
|
213
|
+
return `${minutes}:${this.pad(secs)}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private pad(num: number): string {
|
|
217
|
+
return num.toString().padStart(2, '0');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Buffer management
|
|
221
|
+
abstract getBufferedRanges(): TimeRanges;
|
|
222
|
+
abstract getSeekableRanges(): TimeRanges;
|
|
223
|
+
|
|
224
|
+
protected isBuffering(): boolean {
|
|
225
|
+
return this.state === PlayerState.BUFFERING;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Network
|
|
229
|
+
abstract getBandwidth(): number;
|
|
230
|
+
abstract getNetworkState(): number;
|
|
231
|
+
|
|
232
|
+
// Video properties
|
|
233
|
+
abstract getVideoWidth(): number;
|
|
234
|
+
abstract getVideoHeight(): number;
|
|
235
|
+
abstract getDroppedFrames(): number;
|
|
236
|
+
abstract getDecodedFrames(): number;
|
|
237
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating video player instances based on platform
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IVideoPlayer, PlayerConfig } from './interfaces/IVideoPlayer';
|
|
6
|
+
|
|
7
|
+
export type Platform =
|
|
8
|
+
| 'web'
|
|
9
|
+
| 'ios'
|
|
10
|
+
| 'android'
|
|
11
|
+
| 'tizen'
|
|
12
|
+
| 'webos'
|
|
13
|
+
| 'roku'
|
|
14
|
+
| 'androidtv'
|
|
15
|
+
| 'appletv'
|
|
16
|
+
| 'windows';
|
|
17
|
+
|
|
18
|
+
export class VideoPlayerFactory {
|
|
19
|
+
/**
|
|
20
|
+
* Create a video player instance for the specified platform
|
|
21
|
+
*/
|
|
22
|
+
static async create(
|
|
23
|
+
platform: Platform,
|
|
24
|
+
container: HTMLElement | string | any,
|
|
25
|
+
config?: PlayerConfig
|
|
26
|
+
): Promise<IVideoPlayer> {
|
|
27
|
+
// Dynamic imports will be resolved at runtime
|
|
28
|
+
// This allows the factory to work even if not all platform packages are installed
|
|
29
|
+
|
|
30
|
+
switch (platform) {
|
|
31
|
+
case 'web':
|
|
32
|
+
// Dynamic imports will be resolved at runtime when the package is available
|
|
33
|
+
try {
|
|
34
|
+
const WebModule = await (eval('import("@unified-video/web")') as Promise<any>);
|
|
35
|
+
if (WebModule?.WebPlayer) {
|
|
36
|
+
const player = new WebModule.WebPlayer();
|
|
37
|
+
await player.initialize(container, config);
|
|
38
|
+
return player;
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// Package not installed or not available
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case 'ios':
|
|
46
|
+
case 'android':
|
|
47
|
+
try {
|
|
48
|
+
const RNModule = await (eval('import("@unified-video/react-native")') as Promise<any>);
|
|
49
|
+
if (RNModule?.ReactNativePlayer) {
|
|
50
|
+
// React Native player is a component, handle differently
|
|
51
|
+
return RNModule.ReactNativePlayer;
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// Package not installed or not available
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
|
|
58
|
+
case 'tizen':
|
|
59
|
+
case 'webos':
|
|
60
|
+
try {
|
|
61
|
+
const EnactModule = await (eval('import("@unified-video/enact")') as Promise<any>);
|
|
62
|
+
if (EnactModule?.EnactPlayer) {
|
|
63
|
+
const player = new EnactModule.EnactPlayer();
|
|
64
|
+
await player.initialize(container, config);
|
|
65
|
+
return player;
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// Package not installed or not available
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case 'roku':
|
|
73
|
+
try {
|
|
74
|
+
const RokuModule = await (eval('import("@unified-video/roku")') as Promise<any>);
|
|
75
|
+
if (RokuModule?.RokuPlayer) {
|
|
76
|
+
const player = new RokuModule.RokuPlayer();
|
|
77
|
+
await player.initialize(container, config);
|
|
78
|
+
return player;
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Package not installed or not available
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Platform '${platform}' is not supported`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error(`Failed to load player for platform '${platform}'`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Detect the current platform
|
|
94
|
+
*/
|
|
95
|
+
static detectPlatform(): Platform {
|
|
96
|
+
// Check if running in React Native
|
|
97
|
+
if (typeof global !== 'undefined' && (global as any).nativeCallSyncHook) {
|
|
98
|
+
// React Native environment
|
|
99
|
+
const { Platform } = require('react-native');
|
|
100
|
+
return Platform.OS as Platform;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if running in browser
|
|
104
|
+
if (typeof window !== 'undefined') {
|
|
105
|
+
const userAgent = window.navigator.userAgent.toLowerCase();
|
|
106
|
+
|
|
107
|
+
// Check for Smart TV platforms
|
|
108
|
+
if (userAgent.includes('tizen')) return 'tizen';
|
|
109
|
+
if (userAgent.includes('webos')) return 'webos';
|
|
110
|
+
if (userAgent.includes('roku')) return 'roku';
|
|
111
|
+
|
|
112
|
+
// Check for mobile browsers (might be Android TV)
|
|
113
|
+
if (userAgent.includes('android') && userAgent.includes('tv')) {
|
|
114
|
+
return 'androidtv';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for Apple TV
|
|
118
|
+
if (userAgent.includes('appletv')) return 'appletv';
|
|
119
|
+
|
|
120
|
+
// Check for Windows
|
|
121
|
+
if (userAgent.includes('windows')) return 'windows';
|
|
122
|
+
|
|
123
|
+
// Default to web
|
|
124
|
+
return 'web';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check if running in Node.js (server-side)
|
|
128
|
+
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
|
129
|
+
return 'web'; // Default to web for SSR
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new Error('Unable to detect platform');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a video player for the current platform
|
|
137
|
+
*/
|
|
138
|
+
static async createForCurrentPlatform(
|
|
139
|
+
container: HTMLElement | string | any,
|
|
140
|
+
config?: PlayerConfig
|
|
141
|
+
): Promise<IVideoPlayer> {
|
|
142
|
+
const platform = this.detectPlatform();
|
|
143
|
+
return this.create(platform, container, config);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unified-video/core
|
|
3
|
+
* Core interfaces and implementations for the Unified Video Framework
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Export all interfaces
|
|
7
|
+
export * from './interfaces/IVideoPlayer';
|
|
8
|
+
|
|
9
|
+
// Export base classes
|
|
10
|
+
export { BasePlayer } from './BasePlayer';
|
|
11
|
+
|
|
12
|
+
// Export factory
|
|
13
|
+
export { VideoPlayerFactory } from './VideoPlayerFactory';
|
|
14
|
+
export type { Platform } from './VideoPlayerFactory';
|
|
15
|
+
|
|
16
|
+
// Export utilities
|
|
17
|
+
export { EventEmitter } from './utils/EventEmitter';
|
|
18
|
+
|
|
19
|
+
// Export version
|
|
20
|
+
export const VERSION = '1.0.0';
|