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,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native implementation of the video player
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useRef, useImperativeHandle, forwardRef, useCallback, useEffect, useState } from 'react';
|
|
6
|
+
import { View, StyleSheet, Platform, ViewStyle } from 'react-native';
|
|
7
|
+
import Video, {
|
|
8
|
+
OnLoadData,
|
|
9
|
+
OnProgressData,
|
|
10
|
+
OnSeekData,
|
|
11
|
+
LoadError,
|
|
12
|
+
OnBufferData,
|
|
13
|
+
OnBandwidthUpdateData,
|
|
14
|
+
VideoProperties,
|
|
15
|
+
TextTrackType,
|
|
16
|
+
SelectedTrackType
|
|
17
|
+
} from 'react-native-video';
|
|
18
|
+
import {
|
|
19
|
+
IVideoPlayer,
|
|
20
|
+
VideoSource,
|
|
21
|
+
PlayerConfig,
|
|
22
|
+
PlayerState,
|
|
23
|
+
Quality,
|
|
24
|
+
SubtitleTrack,
|
|
25
|
+
PlayerError,
|
|
26
|
+
PlayerEvents
|
|
27
|
+
} from '@unified-video/core';
|
|
28
|
+
import { EventEmitter } from './utils/EventEmitter';
|
|
29
|
+
|
|
30
|
+
interface ReactNativePlayerProps {
|
|
31
|
+
style?: ViewStyle;
|
|
32
|
+
config?: PlayerConfig;
|
|
33
|
+
onReady?: () => void;
|
|
34
|
+
onError?: (error: PlayerError) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ReactNativePlayerRef extends IVideoPlayer {
|
|
38
|
+
getVideoRef: () => Video | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ReactNativePlayer = forwardRef<ReactNativePlayerRef, ReactNativePlayerProps>(
|
|
42
|
+
({ style, config = {}, onReady, onError }, ref) => {
|
|
43
|
+
const videoRef = useRef<Video>(null);
|
|
44
|
+
const events = useRef(new EventEmitter()).current;
|
|
45
|
+
|
|
46
|
+
const [source, setSource] = useState<VideoSource | null>(null);
|
|
47
|
+
const [paused, setPaused] = useState(!config.autoPlay);
|
|
48
|
+
const [volume, setVolume] = useState(config.volume ?? 1.0);
|
|
49
|
+
const [muted, setMuted] = useState(config.muted ?? false);
|
|
50
|
+
const [rate, setRate] = useState(1.0);
|
|
51
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
52
|
+
const [duration, setDuration] = useState(0);
|
|
53
|
+
const [buffering, setBuffering] = useState(false);
|
|
54
|
+
const [qualities, setQualities] = useState<Quality[]>([]);
|
|
55
|
+
const [selectedQuality, setSelectedQuality] = useState<Quality | null>(null);
|
|
56
|
+
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
|
|
57
|
+
|
|
58
|
+
// State getter
|
|
59
|
+
const getState = useCallback((): PlayerState => ({
|
|
60
|
+
isPlaying: !paused,
|
|
61
|
+
isPaused: paused,
|
|
62
|
+
isBuffering: buffering,
|
|
63
|
+
isEnded: false,
|
|
64
|
+
isError: false,
|
|
65
|
+
currentTime,
|
|
66
|
+
duration,
|
|
67
|
+
bufferedPercentage: 0,
|
|
68
|
+
volume,
|
|
69
|
+
isMuted: muted,
|
|
70
|
+
playbackRate: rate,
|
|
71
|
+
currentQuality: selectedQuality,
|
|
72
|
+
availableQualities: qualities
|
|
73
|
+
}), [paused, buffering, currentTime, duration, volume, muted, rate, selectedQuality, qualities]);
|
|
74
|
+
|
|
75
|
+
// Implement IVideoPlayer interface
|
|
76
|
+
const playerMethods: ReactNativePlayerRef = {
|
|
77
|
+
async initialize(container: any, cfg?: PlayerConfig): Promise<void> {
|
|
78
|
+
// In React Native, initialization is handled by component mount
|
|
79
|
+
if (onReady) onReady();
|
|
80
|
+
events.emit('onReady');
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async destroy(): Promise<void> {
|
|
84
|
+
setSource(null);
|
|
85
|
+
events.removeAllListeners();
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async load(videoSource: VideoSource): Promise<void> {
|
|
89
|
+
const videoSrc: any = {
|
|
90
|
+
uri: videoSource.url,
|
|
91
|
+
type: videoSource.type
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Add DRM if provided
|
|
95
|
+
if (videoSource.drm) {
|
|
96
|
+
videoSrc.drm = {
|
|
97
|
+
type: Platform.select({
|
|
98
|
+
ios: videoSource.drm.type === 'widevine' ? 'fairplay' : videoSource.drm.type,
|
|
99
|
+
android: videoSource.drm.type
|
|
100
|
+
}),
|
|
101
|
+
licenseServer: videoSource.drm.licenseUrl,
|
|
102
|
+
headers: videoSource.drm.headers
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (videoSource.drm.certificateUrl) {
|
|
106
|
+
videoSrc.drm.certificateUrl = videoSource.drm.certificateUrl;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setSource(videoSource);
|
|
111
|
+
events.emit('onLoad', videoSource);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async play(): Promise<void> {
|
|
115
|
+
setPaused(false);
|
|
116
|
+
events.emit('onPlay');
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
pause(): void {
|
|
120
|
+
setPaused(true);
|
|
121
|
+
events.emit('onPause');
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
stop(): void {
|
|
125
|
+
setPaused(true);
|
|
126
|
+
setCurrentTime(0);
|
|
127
|
+
if (videoRef.current) {
|
|
128
|
+
videoRef.current.seek(0);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
seek(time: number): void {
|
|
133
|
+
if (videoRef.current) {
|
|
134
|
+
videoRef.current.seek(time);
|
|
135
|
+
setCurrentTime(time);
|
|
136
|
+
events.emit('onSeeking');
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
setVolume(level: number): void {
|
|
141
|
+
const vol = Math.max(0, Math.min(1, level));
|
|
142
|
+
setVolume(vol);
|
|
143
|
+
events.emit('onVolumeChanged', vol);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
mute(): void {
|
|
147
|
+
setMuted(true);
|
|
148
|
+
events.emit('onVolumeChanged', 0);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
unmute(): void {
|
|
152
|
+
setMuted(false);
|
|
153
|
+
events.emit('onVolumeChanged', volume);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
toggleMute(): void {
|
|
157
|
+
setMuted(!muted);
|
|
158
|
+
events.emit('onVolumeChanged', muted ? volume : 0);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
getQualities(): Quality[] {
|
|
162
|
+
return qualities;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
getCurrentQuality(): Quality | null {
|
|
166
|
+
return selectedQuality;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
setQuality(index: number): void {
|
|
170
|
+
if (qualities[index]) {
|
|
171
|
+
setSelectedQuality(qualities[index]);
|
|
172
|
+
// Platform-specific quality switching would go here
|
|
173
|
+
events.emit('onQualityChanged', qualities[index]);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
setAutoQuality(enabled: boolean): void {
|
|
178
|
+
// Auto quality selection logic
|
|
179
|
+
if (enabled && qualities.length > 0) {
|
|
180
|
+
// Select best quality based on bandwidth
|
|
181
|
+
const bestQuality = qualities[qualities.length - 1];
|
|
182
|
+
setSelectedQuality(bestQuality);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
setPlaybackRate(rate: number): void {
|
|
187
|
+
setRate(rate);
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
getPlaybackRate(): number {
|
|
191
|
+
return rate;
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
getCurrentTime(): number {
|
|
195
|
+
return currentTime;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
getDuration(): number {
|
|
199
|
+
return duration;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
getBufferedPercentage(): number {
|
|
203
|
+
return 0; // Would need to calculate from buffer data
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
getState,
|
|
207
|
+
|
|
208
|
+
isPlaying(): boolean {
|
|
209
|
+
return !paused;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
isPaused(): boolean {
|
|
213
|
+
return paused;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
isEnded(): boolean {
|
|
217
|
+
return currentTime >= duration && duration > 0;
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
async enterFullscreen(): Promise<void> {
|
|
221
|
+
if (videoRef.current) {
|
|
222
|
+
(videoRef.current as any).presentFullscreenPlayer?.();
|
|
223
|
+
events.emit('onFullscreenChanged', true);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async exitFullscreen(): Promise<void> {
|
|
228
|
+
if (videoRef.current) {
|
|
229
|
+
(videoRef.current as any).dismissFullscreenPlayer?.();
|
|
230
|
+
events.emit('onFullscreenChanged', false);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async toggleFullscreen(): Promise<void> {
|
|
235
|
+
// Toggle implementation would check current state
|
|
236
|
+
await this.enterFullscreen();
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async enterPictureInPicture(): Promise<void> {
|
|
240
|
+
if (Platform.OS === 'ios' && videoRef.current) {
|
|
241
|
+
(videoRef.current as any).restoreUserInterfaceForPictureInPictureStop?.();
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
async exitPictureInPicture(): Promise<void> {
|
|
246
|
+
// PiP exit implementation
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
on(event: keyof PlayerEvents, handler: Function): void {
|
|
250
|
+
events.on(event as string, handler);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
off(event: keyof PlayerEvents, handler?: Function): void {
|
|
254
|
+
events.off(event as string, handler);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
once(event: keyof PlayerEvents, handler: Function): void {
|
|
258
|
+
events.once(event as string, handler);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
getSubtitles(): SubtitleTrack[] {
|
|
262
|
+
return source?.subtitles || [];
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
setSubtitleTrack(index: number): void {
|
|
266
|
+
setSelectedTextTrack(index);
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
disableSubtitles(): void {
|
|
270
|
+
setSelectedTextTrack(-1);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
getVideoRef: () => videoRef.current
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
useImperativeHandle(ref, () => playerMethods, [
|
|
277
|
+
paused, volume, muted, rate, currentTime, duration,
|
|
278
|
+
qualities, selectedQuality, source, buffering
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
// Video event handlers
|
|
282
|
+
const handleLoad = useCallback((data: OnLoadData) => {
|
|
283
|
+
setDuration(data.duration);
|
|
284
|
+
|
|
285
|
+
// Extract quality levels if available
|
|
286
|
+
if (data.videoTracks && data.videoTracks.length > 0) {
|
|
287
|
+
const qualityLevels = data.videoTracks.map((track, index) => ({
|
|
288
|
+
height: track.height || 0,
|
|
289
|
+
width: track.width || 0,
|
|
290
|
+
bitrate: track.bitrate || 0,
|
|
291
|
+
label: `${track.height}p`,
|
|
292
|
+
index
|
|
293
|
+
}));
|
|
294
|
+
setQualities(qualityLevels);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
events.emit('onLoadedMetadata', {
|
|
298
|
+
duration: data.duration,
|
|
299
|
+
width: data.naturalSize?.width,
|
|
300
|
+
height: data.naturalSize?.height
|
|
301
|
+
});
|
|
302
|
+
}, [events]);
|
|
303
|
+
|
|
304
|
+
const handleProgress = useCallback((data: OnProgressData) => {
|
|
305
|
+
setCurrentTime(data.currentTime);
|
|
306
|
+
events.emit('onTimeUpdate', data.currentTime);
|
|
307
|
+
|
|
308
|
+
if (data.playableDuration && duration > 0) {
|
|
309
|
+
const bufferedPercentage = (data.playableDuration / duration) * 100;
|
|
310
|
+
events.emit('onProgress', bufferedPercentage);
|
|
311
|
+
}
|
|
312
|
+
}, [duration, events]);
|
|
313
|
+
|
|
314
|
+
const handleBuffer = useCallback((data: OnBufferData) => {
|
|
315
|
+
setBuffering(data.isBuffering);
|
|
316
|
+
events.emit('onBuffering', data.isBuffering);
|
|
317
|
+
}, [events]);
|
|
318
|
+
|
|
319
|
+
const handleError = useCallback((error: LoadError) => {
|
|
320
|
+
const playerError: PlayerError = {
|
|
321
|
+
code: error.error?.code || 'UNKNOWN',
|
|
322
|
+
message: error.error?.localizedDescription || 'Unknown error',
|
|
323
|
+
type: 'media',
|
|
324
|
+
fatal: true,
|
|
325
|
+
details: error.error
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (onError) onError(playerError);
|
|
329
|
+
events.emit('onError', playerError);
|
|
330
|
+
}, [events, onError]);
|
|
331
|
+
|
|
332
|
+
const handleEnd = useCallback(() => {
|
|
333
|
+
events.emit('onEnded');
|
|
334
|
+
}, [events]);
|
|
335
|
+
|
|
336
|
+
const handleSeek = useCallback((data: OnSeekData) => {
|
|
337
|
+
setCurrentTime(data.currentTime);
|
|
338
|
+
events.emit('onSeeked');
|
|
339
|
+
}, [events]);
|
|
340
|
+
|
|
341
|
+
const handleBandwidthUpdate = useCallback((data: OnBandwidthUpdateData) => {
|
|
342
|
+
// Could use this for adaptive bitrate switching
|
|
343
|
+
console.log('Bandwidth update:', data.bitrate);
|
|
344
|
+
}, []);
|
|
345
|
+
|
|
346
|
+
if (!source) {
|
|
347
|
+
return <View style={[styles.container, style]} />;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Convert source to react-native-video format
|
|
351
|
+
const videoSource: any = {
|
|
352
|
+
uri: source.url
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
if (source.drm) {
|
|
356
|
+
videoSource.drm = {
|
|
357
|
+
type: Platform.select({
|
|
358
|
+
ios: source.drm.type === 'widevine' ? 'fairplay' : source.drm.type,
|
|
359
|
+
android: source.drm.type
|
|
360
|
+
}),
|
|
361
|
+
licenseServer: source.drm.licenseUrl,
|
|
362
|
+
headers: source.drm.headers
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Convert subtitles to text tracks
|
|
367
|
+
const textTracks = source.subtitles?.map(subtitle => ({
|
|
368
|
+
type: 'text/vtt' as TextTrackType,
|
|
369
|
+
language: subtitle.language,
|
|
370
|
+
title: subtitle.label,
|
|
371
|
+
uri: subtitle.url
|
|
372
|
+
}));
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<View style={[styles.container, style]}>
|
|
376
|
+
<Video
|
|
377
|
+
ref={videoRef}
|
|
378
|
+
source={videoSource}
|
|
379
|
+
style={styles.video}
|
|
380
|
+
paused={paused}
|
|
381
|
+
volume={volume}
|
|
382
|
+
muted={muted}
|
|
383
|
+
rate={rate}
|
|
384
|
+
resizeMode="contain"
|
|
385
|
+
repeat={config.loop || false}
|
|
386
|
+
controls={config.controls !== false}
|
|
387
|
+
playInBackground={false}
|
|
388
|
+
playWhenInactive={false}
|
|
389
|
+
ignoreSilentSwitch="ignore"
|
|
390
|
+
progressUpdateInterval={250}
|
|
391
|
+
textTracks={textTracks}
|
|
392
|
+
selectedTextTrack={
|
|
393
|
+
selectedTextTrack >= 0
|
|
394
|
+
? { type: SelectedTrackType.INDEX, value: selectedTextTrack }
|
|
395
|
+
: { type: SelectedTrackType.DISABLED }
|
|
396
|
+
}
|
|
397
|
+
onLoad={handleLoad}
|
|
398
|
+
onProgress={handleProgress}
|
|
399
|
+
onBuffer={handleBuffer}
|
|
400
|
+
onError={handleError}
|
|
401
|
+
onEnd={handleEnd}
|
|
402
|
+
onSeek={handleSeek}
|
|
403
|
+
onBandwidthUpdate={handleBandwidthUpdate}
|
|
404
|
+
onTimedMetadata={(metadata) => console.log('Metadata:', metadata)}
|
|
405
|
+
/>
|
|
406
|
+
</View>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const styles = StyleSheet.create({
|
|
412
|
+
container: {
|
|
413
|
+
flex: 1,
|
|
414
|
+
backgroundColor: '#000'
|
|
415
|
+
},
|
|
416
|
+
video: {
|
|
417
|
+
position: 'absolute',
|
|
418
|
+
top: 0,
|
|
419
|
+
left: 0,
|
|
420
|
+
right: 0,
|
|
421
|
+
bottom: 0
|
|
422
|
+
}
|
|
423
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Video Player Implementation
|
|
3
|
+
* This would wrap react-native-video or similar native video libraries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useRef, useEffect, useCallback } from 'react';
|
|
7
|
+
import { View, StyleSheet, Platform } from 'react-native';
|
|
8
|
+
// Would import: import Video from 'react-native-video';
|
|
9
|
+
import type {
|
|
10
|
+
VideoSource,
|
|
11
|
+
VideoPlayerConfig,
|
|
12
|
+
VideoPlayerInterface,
|
|
13
|
+
VideoPlayerState
|
|
14
|
+
} from '../../core/src/interfaces';
|
|
15
|
+
|
|
16
|
+
export class ReactNativeVideoPlayer implements VideoPlayerInterface {
|
|
17
|
+
private videoRef: any;
|
|
18
|
+
private config: VideoPlayerConfig;
|
|
19
|
+
private state: VideoPlayerState = 'idle';
|
|
20
|
+
private listeners: Map<string, Function[]> = new Map();
|
|
21
|
+
|
|
22
|
+
constructor(container: any, config: VideoPlayerConfig) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
// In real implementation, would initialize react-native-video here
|
|
25
|
+
console.log('ReactNativeVideoPlayer initialized for', Platform.OS);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async load(source: VideoSource): Promise<void> {
|
|
29
|
+
// Implementation would load video into react-native-video
|
|
30
|
+
this.state = 'loading';
|
|
31
|
+
|
|
32
|
+
// Would set source on native player
|
|
33
|
+
// this.videoRef.source = {
|
|
34
|
+
// uri: source.url,
|
|
35
|
+
// type: source.type,
|
|
36
|
+
// headers: source.headers
|
|
37
|
+
// };
|
|
38
|
+
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async play(): Promise<void> {
|
|
43
|
+
// Would call native play method
|
|
44
|
+
// this.videoRef.play();
|
|
45
|
+
this.state = 'playing';
|
|
46
|
+
this.emit('play');
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pause(): void {
|
|
51
|
+
// Would call native pause method
|
|
52
|
+
// this.videoRef.pause();
|
|
53
|
+
this.state = 'paused';
|
|
54
|
+
this.emit('pause');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
seek(position: number): void {
|
|
58
|
+
// Would seek in native player
|
|
59
|
+
// this.videoRef.seek(position);
|
|
60
|
+
this.emit('seeking', { position });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setVolume(volume: number): void {
|
|
64
|
+
// Would set volume on native player
|
|
65
|
+
// this.videoRef.volume = volume;
|
|
66
|
+
this.emit('volumechange', { volume });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getCurrentTime(): number {
|
|
70
|
+
// Would get from native player
|
|
71
|
+
// return this.videoRef.currentTime;
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getDuration(): number {
|
|
76
|
+
// Would get from native player
|
|
77
|
+
// return this.videoRef.duration;
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getVolume(): number {
|
|
82
|
+
// Would get from native player
|
|
83
|
+
// return this.videoRef.volume;
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isMuted(): boolean {
|
|
88
|
+
// Would get from native player
|
|
89
|
+
// return this.videoRef.muted;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
mute(): void {
|
|
94
|
+
// Would mute native player
|
|
95
|
+
// this.videoRef.muted = true;
|
|
96
|
+
this.emit('volumechange', { muted: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
unmute(): void {
|
|
100
|
+
// Would unmute native player
|
|
101
|
+
// this.videoRef.muted = false;
|
|
102
|
+
this.emit('volumechange', { muted: false });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setPlaybackRate(rate: number): void {
|
|
106
|
+
// Would set rate on native player
|
|
107
|
+
// this.videoRef.rate = rate;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getPlaybackRate(): number {
|
|
111
|
+
// Would get from native player
|
|
112
|
+
// return this.videoRef.rate;
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
enterFullscreen(): void {
|
|
117
|
+
// Would use native fullscreen APIs
|
|
118
|
+
if (Platform.OS === 'ios') {
|
|
119
|
+
// iOS specific fullscreen
|
|
120
|
+
} else if (Platform.OS === 'android') {
|
|
121
|
+
// Android specific fullscreen
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
exitFullscreen(): void {
|
|
126
|
+
// Would exit native fullscreen
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
enterPictureInPicture(): void {
|
|
130
|
+
// Would use native PiP APIs if available
|
|
131
|
+
if (Platform.OS === 'ios' && Platform.Version >= 14) {
|
|
132
|
+
// iOS PiP implementation
|
|
133
|
+
} else if (Platform.OS === 'android' && Platform.Version >= 26) {
|
|
134
|
+
// Android PiP implementation
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
exitPictureInPicture(): void {
|
|
139
|
+
// Would exit native PiP
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
on(event: string, handler: Function): void {
|
|
143
|
+
if (!this.listeners.has(event)) {
|
|
144
|
+
this.listeners.set(event, []);
|
|
145
|
+
}
|
|
146
|
+
this.listeners.get(event)?.push(handler);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
off(event: string, handler: Function): void {
|
|
150
|
+
const handlers = this.listeners.get(event);
|
|
151
|
+
if (handlers) {
|
|
152
|
+
const index = handlers.indexOf(handler);
|
|
153
|
+
if (index > -1) {
|
|
154
|
+
handlers.splice(index, 1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private emit(event: string, data?: any): void {
|
|
160
|
+
const handlers = this.listeners.get(event);
|
|
161
|
+
if (handlers) {
|
|
162
|
+
handlers.forEach(handler => handler(data));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
destroy(): void {
|
|
167
|
+
// Clean up native resources
|
|
168
|
+
this.listeners.clear();
|
|
169
|
+
this.state = 'idle';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getState(): VideoPlayerState {
|
|
173
|
+
return this.state;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// React Component Wrapper
|
|
178
|
+
export const VideoPlayer: React.FC<{
|
|
179
|
+
source: VideoSource;
|
|
180
|
+
config?: VideoPlayerConfig;
|
|
181
|
+
style?: any;
|
|
182
|
+
onReady?: () => void;
|
|
183
|
+
onPlay?: () => void;
|
|
184
|
+
onPause?: () => void;
|
|
185
|
+
onEnd?: () => void;
|
|
186
|
+
onError?: (error: any) => void;
|
|
187
|
+
}> = ({ source, config, style, ...callbacks }) => {
|
|
188
|
+
const videoRef = useRef(null);
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
// Initialize player when component mounts
|
|
192
|
+
// Set up native video callbacks
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<View style={[styles.container, style]}>
|
|
197
|
+
{/* In real implementation, would render react-native-video here */}
|
|
198
|
+
{/* <Video
|
|
199
|
+
ref={videoRef}
|
|
200
|
+
source={{ uri: source.url }}
|
|
201
|
+
style={styles.video}
|
|
202
|
+
controls={config?.controls}
|
|
203
|
+
paused={!config?.autoPlay}
|
|
204
|
+
{...callbacks}
|
|
205
|
+
/> */}
|
|
206
|
+
</View>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const styles = StyleSheet.create({
|
|
211
|
+
container: {
|
|
212
|
+
flex: 1,
|
|
213
|
+
backgroundColor: 'black',
|
|
214
|
+
},
|
|
215
|
+
video: {
|
|
216
|
+
position: 'absolute',
|
|
217
|
+
top: 0,
|
|
218
|
+
left: 0,
|
|
219
|
+
bottom: 0,
|
|
220
|
+
right: 0,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
export default ReactNativeVideoPlayer;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unified-video/react-native
|
|
3
|
+
* React Native implementation for iOS and Android
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Export main video player
|
|
7
|
+
export { ReactNativeVideoPlayer, VideoPlayer } from './VideoPlayer';
|
|
8
|
+
|
|
9
|
+
// Re-export core types for convenience
|
|
10
|
+
export type {
|
|
11
|
+
VideoSource,
|
|
12
|
+
VideoPlayerConfig,
|
|
13
|
+
VideoPlayerInterface,
|
|
14
|
+
DRMConfig,
|
|
15
|
+
SubtitleTrack,
|
|
16
|
+
AudioTrack,
|
|
17
|
+
Quality,
|
|
18
|
+
PlayerState,
|
|
19
|
+
PlayerEvent,
|
|
20
|
+
PlayerError,
|
|
21
|
+
PlayerMetrics
|
|
22
|
+
} from '@unified-video/core';
|
|
23
|
+
|
|
24
|
+
// Export version
|
|
25
|
+
export const VERSION = '1.0.0';
|
|
26
|
+
|
|
27
|
+
// Export platform identifier
|
|
28
|
+
export const PLATFORM = 'react-native';
|