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.
Files changed (129) hide show
  1. package/.github/workflows/ci.yml +253 -0
  2. package/ANDROID_TV_IMPLEMENTATION.md +313 -0
  3. package/COMPLETION_STATUS.md +165 -0
  4. package/CONTRIBUTING.md +376 -0
  5. package/FINAL_STATUS_REPORT.md +170 -0
  6. package/FRAMEWORK_REVIEW.md +247 -0
  7. package/IMPROVEMENTS_SUMMARY.md +168 -0
  8. package/LICENSE +21 -0
  9. package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
  10. package/PAYWALL_RENTAL_FLOW.md +499 -0
  11. package/PLATFORM_SETUP_GUIDE.md +1636 -0
  12. package/README.md +315 -0
  13. package/RUN_LOCALLY.md +151 -0
  14. package/apps/demo/cast-sender-min.html +173 -0
  15. package/apps/demo/custom-player.html +883 -0
  16. package/apps/demo/demo.html +990 -0
  17. package/apps/demo/enhanced-player.html +3556 -0
  18. package/apps/demo/index.html +159 -0
  19. package/apps/rental-api/.env.example +24 -0
  20. package/apps/rental-api/README.md +23 -0
  21. package/apps/rental-api/migrations/001_init.sql +35 -0
  22. package/apps/rental-api/migrations/002_videos.sql +10 -0
  23. package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
  24. package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
  25. package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
  26. package/apps/rental-api/package-lock.json +2045 -0
  27. package/apps/rental-api/package.json +33 -0
  28. package/apps/rental-api/scripts/run-migration.js +42 -0
  29. package/apps/rental-api/scripts/update-video-currency.js +21 -0
  30. package/apps/rental-api/scripts/update-video-price.js +19 -0
  31. package/apps/rental-api/src/config.ts +14 -0
  32. package/apps/rental-api/src/db.ts +10 -0
  33. package/apps/rental-api/src/routes/cashfree.ts +167 -0
  34. package/apps/rental-api/src/routes/pesapal.ts +92 -0
  35. package/apps/rental-api/src/routes/rentals.ts +242 -0
  36. package/apps/rental-api/src/routes/webhooks.ts +73 -0
  37. package/apps/rental-api/src/server.ts +41 -0
  38. package/apps/rental-api/src/services/entitlements.ts +45 -0
  39. package/apps/rental-api/src/services/payments.ts +22 -0
  40. package/apps/rental-api/tsconfig.json +17 -0
  41. package/check-urls.ps1 +74 -0
  42. package/comparison-report.md +181 -0
  43. package/docs/PAYWALL.md +95 -0
  44. package/docs/PLAYER_UI_VISIBILITY.md +431 -0
  45. package/docs/README.md +7 -0
  46. package/docs/SYSTEM_ARCHITECTURE.md +612 -0
  47. package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
  48. package/examples/android/JavaSampleApp/MainActivity.java +641 -0
  49. package/examples/android/JavaSampleApp/activity_main.xml +226 -0
  50. package/examples/android/SampleApp/MainActivity.kt +430 -0
  51. package/examples/ios/SampleApp/ViewController.swift +337 -0
  52. package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
  53. package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
  54. package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
  55. package/jest.config.js +33 -0
  56. package/jitpack.yml +5 -0
  57. package/lerna.json +35 -0
  58. package/package.json +69 -0
  59. package/packages/PLATFORM_STATUS.md +163 -0
  60. package/packages/android/build.gradle +135 -0
  61. package/packages/android/src/main/AndroidManifest.xml +36 -0
  62. package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
  63. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
  64. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
  65. package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
  66. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
  67. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
  68. package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
  69. package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
  70. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
  71. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
  72. package/packages/core/package.json +34 -0
  73. package/packages/core/src/BasePlayer.ts +250 -0
  74. package/packages/core/src/VideoPlayer.ts +237 -0
  75. package/packages/core/src/VideoPlayerFactory.ts +145 -0
  76. package/packages/core/src/index.ts +20 -0
  77. package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
  78. package/packages/core/src/interfaces.ts +240 -0
  79. package/packages/core/src/utils/EventEmitter.ts +66 -0
  80. package/packages/core/src/utils/PlatformDetector.ts +300 -0
  81. package/packages/core/tsconfig.json +20 -0
  82. package/packages/enact/package.json +51 -0
  83. package/packages/enact/src/VideoPlayer.js +365 -0
  84. package/packages/enact/src/adapters/TizenAdapter.js +354 -0
  85. package/packages/enact/src/index.js +82 -0
  86. package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
  87. package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
  88. package/packages/ios/GETTING_STARTED.md +100 -0
  89. package/packages/ios/Package.swift +35 -0
  90. package/packages/ios/README.md +84 -0
  91. package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
  92. package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
  93. package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
  94. package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
  95. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
  96. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
  97. package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
  98. package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
  99. package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
  100. package/packages/ios/build_framework.sh +55 -0
  101. package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
  102. package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
  103. package/packages/react-native/package.json +51 -0
  104. package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
  105. package/packages/react-native/src/VideoPlayer.tsx +224 -0
  106. package/packages/react-native/src/index.ts +28 -0
  107. package/packages/react-native/src/utils/EventEmitter.ts +66 -0
  108. package/packages/react-native/tsconfig.json +31 -0
  109. package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
  110. package/packages/roku/package.json +44 -0
  111. package/packages/roku/source/VideoPlayer.brs +231 -0
  112. package/packages/roku/source/main.brs +28 -0
  113. package/packages/web/GETTING_STARTED.md +292 -0
  114. package/packages/web/jest.config.js +28 -0
  115. package/packages/web/jest.setup.ts +110 -0
  116. package/packages/web/package.json +50 -0
  117. package/packages/web/src/SecureVideoPlayer.ts +1164 -0
  118. package/packages/web/src/WebPlayer.ts +3110 -0
  119. package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
  120. package/packages/web/src/index.ts +14 -0
  121. package/packages/web/src/paywall/PaywallController.ts +215 -0
  122. package/packages/web/src/react/WebPlayerView.tsx +177 -0
  123. package/packages/web/tsconfig.json +23 -0
  124. package/packages/web/webpack.config.js +45 -0
  125. package/server.js +131 -0
  126. package/server.py +84 -0
  127. package/test-urls.ps1 +97 -0
  128. package/test-video-urls.ps1 +87 -0
  129. package/tsconfig.json +39 -0
@@ -0,0 +1,365 @@
1
+ import kind from '@enact/core/kind';
2
+ import React, { Component } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import Spotlight from '@enact/spotlight';
5
+ import { Panel } from '@enact/sandstone/Panels';
6
+ import VideoPlayerBase from '@enact/sandstone/VideoPlayer';
7
+ import { adaptEvent, forward, handle } from '@enact/core/handle';
8
+ import { platform } from '@enact/core/platform';
9
+
10
+ // Platform-specific adapters
11
+ import TizenAdapter from './adapters/TizenAdapter';
12
+ import WebOSAdapter from './adapters/WebOSAdapter';
13
+
14
+ const EnactVideoPlayer = kind({
15
+ name: 'EnactVideoPlayer',
16
+
17
+ propTypes: {
18
+ source: PropTypes.shape({
19
+ url: PropTypes.string.isRequired,
20
+ type: PropTypes.string,
21
+ drm: PropTypes.object,
22
+ title: PropTypes.string,
23
+ description: PropTypes.string,
24
+ thumbnail: PropTypes.string
25
+ }).isRequired,
26
+ autoplay: PropTypes.bool,
27
+ controls: PropTypes.bool,
28
+ loop: PropTypes.bool,
29
+ muted: PropTypes.bool,
30
+ onError: PropTypes.func,
31
+ onLoadStart: PropTypes.func,
32
+ onLoadedMetadata: PropTypes.func,
33
+ onPlay: PropTypes.func,
34
+ onPause: PropTypes.func,
35
+ onEnded: PropTypes.func,
36
+ onTimeUpdate: PropTypes.func,
37
+ onProgress: PropTypes.func,
38
+ onQualityChange: PropTypes.func,
39
+ onSubtitleChange: PropTypes.func,
40
+ analytics: PropTypes.object
41
+ },
42
+
43
+ defaultProps: {
44
+ autoplay: false,
45
+ controls: true,
46
+ loop: false,
47
+ muted: false
48
+ },
49
+
50
+ styles: {
51
+ css: require('./VideoPlayer.module.less'),
52
+ className: 'videoPlayer'
53
+ },
54
+
55
+ handlers: {
56
+ onPlay: handle(
57
+ forward('onPlay'),
58
+ (ev, props, context) => {
59
+ if (props.analytics) {
60
+ props.analytics.track('play', {
61
+ url: props.source.url,
62
+ title: props.source.title,
63
+ timestamp: Date.now()
64
+ });
65
+ }
66
+ }
67
+ ),
68
+
69
+ onPause: handle(
70
+ forward('onPause'),
71
+ (ev, props, context) => {
72
+ if (props.analytics) {
73
+ props.analytics.track('pause', {
74
+ url: props.source.url,
75
+ currentTime: ev.currentTime,
76
+ timestamp: Date.now()
77
+ });
78
+ }
79
+ }
80
+ ),
81
+
82
+ onError: handle(
83
+ forward('onError'),
84
+ (ev, props, context) => {
85
+ if (props.analytics) {
86
+ props.analytics.track('error', {
87
+ error: ev.error,
88
+ url: props.source.url,
89
+ timestamp: Date.now()
90
+ });
91
+ }
92
+ }
93
+ ),
94
+
95
+ onEnded: handle(
96
+ forward('onEnded'),
97
+ (ev, props, context) => {
98
+ if (props.analytics) {
99
+ props.analytics.track('ended', {
100
+ url: props.source.url,
101
+ duration: ev.duration,
102
+ timestamp: Date.now()
103
+ });
104
+ }
105
+ }
106
+ )
107
+ },
108
+
109
+ computed: {
110
+ platformAdapter: ({source}) => {
111
+ if (platform.tv) {
112
+ if (platform.tizen) {
113
+ return new TizenAdapter();
114
+ } else if (platform.webos) {
115
+ return new WebOSAdapter();
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+ },
121
+
122
+ render: ({source, platformAdapter, ...rest}) => {
123
+ // Use platform-specific player if available
124
+ if (platformAdapter) {
125
+ return (
126
+ <div className={rest.className}>
127
+ <PlatformVideoPlayer
128
+ adapter={platformAdapter}
129
+ source={source}
130
+ {...rest}
131
+ />
132
+ </div>
133
+ );
134
+ }
135
+
136
+ // Fallback to Enact's standard VideoPlayer
137
+ return (
138
+ <VideoPlayerBase
139
+ {...rest}
140
+ source={source.url}
141
+ title={source.title}
142
+ poster={source.thumbnail}
143
+ infoComponents={source.description}
144
+ />
145
+ );
146
+ }
147
+ });
148
+
149
+ // Platform-specific video player component
150
+ class PlatformVideoPlayer extends Component {
151
+ constructor(props) {
152
+ super(props);
153
+ this.videoRef = React.createRef();
154
+ this.state = {
155
+ isReady: false,
156
+ isPlaying: false,
157
+ currentTime: 0,
158
+ duration: 0,
159
+ buffered: 0,
160
+ volume: 1,
161
+ isMuted: false,
162
+ currentQuality: null,
163
+ availableQualities: [],
164
+ currentSubtitle: null,
165
+ availableSubtitles: [],
166
+ error: null
167
+ };
168
+ }
169
+
170
+ async componentDidMount() {
171
+ const { adapter, source } = this.props;
172
+
173
+ try {
174
+ // Initialize platform-specific player
175
+ await adapter.initialize(this.videoRef.current);
176
+
177
+ // Configure DRM if needed
178
+ if (source.drm) {
179
+ await adapter.configureDRM(source.drm);
180
+ }
181
+
182
+ // Load source
183
+ await adapter.load(source);
184
+
185
+ // Set up event listeners
186
+ this.setupEventListeners();
187
+
188
+ // Get initial qualities and subtitles
189
+ const qualities = await adapter.getAvailableQualities();
190
+ const subtitles = await adapter.getAvailableSubtitles();
191
+
192
+ this.setState({
193
+ isReady: true,
194
+ availableQualities: qualities,
195
+ availableSubtitles: subtitles
196
+ });
197
+
198
+ // Auto-play if configured
199
+ if (this.props.autoplay) {
200
+ this.play();
201
+ }
202
+ } catch (error) {
203
+ this.handleError(error);
204
+ }
205
+ }
206
+
207
+ componentWillUnmount() {
208
+ if (this.props.adapter) {
209
+ this.props.adapter.destroy();
210
+ }
211
+ }
212
+
213
+ setupEventListeners() {
214
+ const { adapter } = this.props;
215
+
216
+ adapter.on('play', () => {
217
+ this.setState({ isPlaying: true });
218
+ this.props.onPlay?.();
219
+ });
220
+
221
+ adapter.on('pause', () => {
222
+ this.setState({ isPlaying: false });
223
+ this.props.onPause?.();
224
+ });
225
+
226
+ adapter.on('timeupdate', (time) => {
227
+ this.setState({ currentTime: time });
228
+ this.props.onTimeUpdate?.({ currentTime: time });
229
+ });
230
+
231
+ adapter.on('durationchange', (duration) => {
232
+ this.setState({ duration: duration });
233
+ });
234
+
235
+ adapter.on('progress', (buffered) => {
236
+ this.setState({ buffered: buffered });
237
+ this.props.onProgress?.({ buffered });
238
+ });
239
+
240
+ adapter.on('volumechange', (volume, muted) => {
241
+ this.setState({ volume, isMuted: muted });
242
+ });
243
+
244
+ adapter.on('ended', () => {
245
+ this.setState({ isPlaying: false });
246
+ this.props.onEnded?.({ duration: this.state.duration });
247
+
248
+ if (this.props.loop) {
249
+ this.play();
250
+ }
251
+ });
252
+
253
+ adapter.on('error', (error) => {
254
+ this.handleError(error);
255
+ });
256
+
257
+ adapter.on('qualitychange', (quality) => {
258
+ this.setState({ currentQuality: quality });
259
+ this.props.onQualityChange?.({ quality });
260
+ });
261
+
262
+ adapter.on('subtitlechange', (subtitle) => {
263
+ this.setState({ currentSubtitle: subtitle });
264
+ this.props.onSubtitleChange?.({ subtitle });
265
+ });
266
+ }
267
+
268
+ handleError(error) {
269
+ console.error('Video player error:', error);
270
+ this.setState({ error: error.message || 'An error occurred' });
271
+ this.props.onError?.({ error });
272
+ }
273
+
274
+ play = () => {
275
+ this.props.adapter.play();
276
+ }
277
+
278
+ pause = () => {
279
+ this.props.adapter.pause();
280
+ }
281
+
282
+ seek = (time) => {
283
+ this.props.adapter.seek(time);
284
+ }
285
+
286
+ setVolume = (volume) => {
287
+ this.props.adapter.setVolume(volume);
288
+ }
289
+
290
+ toggleMute = () => {
291
+ const newMuted = !this.state.isMuted;
292
+ this.props.adapter.setMuted(newMuted);
293
+ }
294
+
295
+ setQuality = (quality) => {
296
+ this.props.adapter.setQuality(quality);
297
+ }
298
+
299
+ setSubtitle = (subtitle) => {
300
+ this.props.adapter.setSubtitle(subtitle);
301
+ }
302
+
303
+ enterFullscreen = () => {
304
+ this.props.adapter.enterFullscreen();
305
+ }
306
+
307
+ exitFullscreen = () => {
308
+ this.props.adapter.exitFullscreen();
309
+ }
310
+
311
+ render() {
312
+ const { isReady, error, isPlaying } = this.state;
313
+ const { controls, source } = this.props;
314
+
315
+ if (error) {
316
+ return (
317
+ <div className="video-error">
318
+ <h3>Playback Error</h3>
319
+ <p>{error}</p>
320
+ </div>
321
+ );
322
+ }
323
+
324
+ return (
325
+ <div className="platform-video-container">
326
+ <div ref={this.videoRef} className="platform-video-player">
327
+ {!isReady && (
328
+ <div className="loading">
329
+ <span>Loading...</span>
330
+ </div>
331
+ )}
332
+ </div>
333
+
334
+ {controls && isReady && (
335
+ <VideoControls
336
+ isPlaying={this.state.isPlaying}
337
+ currentTime={this.state.currentTime}
338
+ duration={this.state.duration}
339
+ buffered={this.state.buffered}
340
+ volume={this.state.volume}
341
+ isMuted={this.state.isMuted}
342
+ currentQuality={this.state.currentQuality}
343
+ availableQualities={this.state.availableQualities}
344
+ currentSubtitle={this.state.currentSubtitle}
345
+ availableSubtitles={this.state.availableSubtitles}
346
+ onPlay={this.play}
347
+ onPause={this.pause}
348
+ onSeek={this.seek}
349
+ onVolumeChange={this.setVolume}
350
+ onMuteToggle={this.toggleMute}
351
+ onQualityChange={this.setQuality}
352
+ onSubtitleChange={this.setSubtitle}
353
+ onFullscreen={this.enterFullscreen}
354
+ />
355
+ )}
356
+ </div>
357
+ );
358
+ }
359
+ }
360
+
361
+ // Import VideoControls component
362
+ import VideoControls from './components/VideoControls';
363
+
364
+ export default EnactVideoPlayer;
365
+ export { PlatformVideoPlayer };
@@ -0,0 +1,354 @@
1
+ import EventEmitter from 'events';
2
+
3
+ class TizenAdapter extends EventEmitter {
4
+ constructor() {
5
+ super();
6
+ this.avplay = null;
7
+ this.element = null;
8
+ this.listener = null;
9
+ this.currentSource = null;
10
+ this.qualities = [];
11
+ this.subtitles = [];
12
+ this.currentQuality = null;
13
+ this.currentSubtitle = null;
14
+ }
15
+
16
+ async initialize(element) {
17
+ this.element = element;
18
+
19
+ // Check if running on Tizen
20
+ if (!window.tizen || !window.webapis) {
21
+ console.warn('Tizen APIs not available, using fallback player');
22
+ throw new Error('Tizen APIs not available');
23
+ }
24
+
25
+ this.avplay = window.webapis.avplay;
26
+
27
+ // Set up AVPlay listener
28
+ this.listener = {
29
+ onbufferingstart: () => {
30
+ console.log('Buffering started');
31
+ this.emit('bufferingstart');
32
+ },
33
+
34
+ onbufferingprogress: (percent) => {
35
+ console.log('Buffering progress:', percent);
36
+ this.emit('bufferingprogress', percent);
37
+ },
38
+
39
+ onbufferingcomplete: () => {
40
+ console.log('Buffering complete');
41
+ this.emit('bufferingcomplete');
42
+ },
43
+
44
+ oncurrentplaytime: (milliseconds) => {
45
+ this.emit('timeupdate', milliseconds / 1000);
46
+ },
47
+
48
+ onevent: (eventType, eventData) => {
49
+ console.log('Event:', eventType, eventData);
50
+ this.handleTizenEvent(eventType, eventData);
51
+ },
52
+
53
+ onerror: (eventType) => {
54
+ console.error('Error:', eventType);
55
+ this.emit('error', new Error(eventType));
56
+ },
57
+
58
+ ondrmevent: (drmEvent, drmData) => {
59
+ console.log('DRM Event:', drmEvent, drmData);
60
+ this.handleDRMEvent(drmEvent, drmData);
61
+ },
62
+
63
+ onsubtitlechange: (duration, text, data) => {
64
+ this.emit('subtitlechange', { duration, text, data });
65
+ },
66
+
67
+ onstreamcompleted: () => {
68
+ console.log('Stream completed');
69
+ this.emit('ended');
70
+ }
71
+ };
72
+
73
+ this.avplay.setListener(this.listener);
74
+ }
75
+
76
+ async load(source) {
77
+ try {
78
+ this.currentSource = source;
79
+
80
+ // Open the media URL
81
+ await this.avplay.open(source.url);
82
+
83
+ // Set display method
84
+ this.avplay.setDisplayMethod('PLAYER_DISPLAY_MODE_LETTER_BOX');
85
+
86
+ // Get display rect
87
+ const rect = this.element.getBoundingClientRect();
88
+ this.avplay.setDisplayRect(
89
+ rect.left,
90
+ rect.top,
91
+ rect.width,
92
+ rect.height
93
+ );
94
+
95
+ // Set stream info if needed
96
+ if (source.streamInfo) {
97
+ Object.entries(source.streamInfo).forEach(([key, value]) => {
98
+ this.avplay.setStreamingProperty(key, value);
99
+ });
100
+ }
101
+
102
+ // Parse available qualities from stream info
103
+ this.parseStreamInfo();
104
+
105
+ // Set subtitles if available
106
+ if (source.subtitles && source.subtitles.length > 0) {
107
+ this.subtitles = source.subtitles;
108
+ this.setSubtitleTrack(source.subtitles[0]);
109
+ }
110
+
111
+ // Prepare the player
112
+ await this.prepareAsync();
113
+
114
+ // Get duration
115
+ const duration = this.avplay.getDuration() / 1000;
116
+ this.emit('durationchange', duration);
117
+
118
+ this.emit('ready');
119
+ } catch (error) {
120
+ this.emit('error', error);
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ prepareAsync() {
126
+ return new Promise((resolve, reject) => {
127
+ this.avplay.prepareAsync(
128
+ () => resolve(),
129
+ (error) => reject(new Error(error))
130
+ );
131
+ });
132
+ }
133
+
134
+ async configureDRM(drmConfig) {
135
+ if (!drmConfig) return;
136
+
137
+ const drmParam = {
138
+ LicenseServer: drmConfig.licenseUrl,
139
+ CustomData: drmConfig.customData || '',
140
+ DeleteLicenseAfterUse: drmConfig.deleteLicenseAfterUse || false
141
+ };
142
+
143
+ // Add headers if provided
144
+ if (drmConfig.headers) {
145
+ drmParam.HttpRequestHeaders = Object.entries(drmConfig.headers)
146
+ .map(([key, value]) => `${key}: ${value}`)
147
+ .join('\\r\\n');
148
+ }
149
+
150
+ // Set DRM operation
151
+ if (drmConfig.type === 'PLAYREADY' || drmConfig.type === 'playready') {
152
+ this.avplay.setDrm('PLAYREADY', 'SetProperties', JSON.stringify(drmParam));
153
+ } else if (drmConfig.type === 'WIDEVINE' || drmConfig.type === 'widevine') {
154
+ this.avplay.setDrm('WIDEVINE', 'SetProperties', JSON.stringify(drmParam));
155
+ }
156
+ }
157
+
158
+ play() {
159
+ this.avplay.play();
160
+ this.emit('play');
161
+ }
162
+
163
+ pause() {
164
+ this.avplay.pause();
165
+ this.emit('pause');
166
+ }
167
+
168
+ seek(seconds) {
169
+ const milliseconds = Math.floor(seconds * 1000);
170
+ this.avplay.seekTo(milliseconds);
171
+ this.emit('seek', seconds);
172
+ }
173
+
174
+ stop() {
175
+ this.avplay.stop();
176
+ this.emit('stop');
177
+ }
178
+
179
+ setVolume(level) {
180
+ // Tizen volume is 0-100
181
+ const tizenVolume = Math.floor(level * 100);
182
+ this.avplay.setVolume(tizenVolume);
183
+ this.emit('volumechange', level, false);
184
+ }
185
+
186
+ setMuted(muted) {
187
+ if (muted) {
188
+ this.avplay.setVolume(0);
189
+ } else {
190
+ this.setVolume(1); // Restore to full volume
191
+ }
192
+ this.emit('volumechange', muted ? 0 : 1, muted);
193
+ }
194
+
195
+ getCurrentTime() {
196
+ return this.avplay.getCurrentTime() / 1000;
197
+ }
198
+
199
+ getDuration() {
200
+ return this.avplay.getDuration() / 1000;
201
+ }
202
+
203
+ getState() {
204
+ return this.avplay.getState();
205
+ }
206
+
207
+ // Quality management
208
+ getAvailableQualities() {
209
+ return this.qualities;
210
+ }
211
+
212
+ setQuality(quality) {
213
+ if (!quality || !this.qualities.includes(quality)) return;
214
+
215
+ // Store current position
216
+ const currentTime = this.getCurrentTime();
217
+
218
+ // Set bitrate properties
219
+ this.avplay.setStreamingProperty('ADAPTIVE_INFO', JSON.stringify({
220
+ BITRATES: quality.bitrate.toString()
221
+ }));
222
+
223
+ this.currentQuality = quality;
224
+ this.emit('qualitychange', quality);
225
+
226
+ // Restore position
227
+ this.seek(currentTime);
228
+ }
229
+
230
+ // Subtitle management
231
+ getAvailableSubtitles() {
232
+ return this.subtitles;
233
+ }
234
+
235
+ setSubtitle(subtitle) {
236
+ if (!subtitle) {
237
+ // Disable subtitles
238
+ this.avplay.setSelectTrack('TEXT', -1);
239
+ this.currentSubtitle = null;
240
+ } else {
241
+ // Enable specific subtitle track
242
+ const trackIndex = this.subtitles.indexOf(subtitle);
243
+ if (trackIndex >= 0) {
244
+ this.avplay.setSelectTrack('TEXT', trackIndex);
245
+ this.currentSubtitle = subtitle;
246
+ }
247
+ }
248
+ this.emit('subtitlechange', subtitle);
249
+ }
250
+
251
+ // Fullscreen (Tizen TVs are always fullscreen)
252
+ enterFullscreen() {
253
+ // No-op on TV
254
+ }
255
+
256
+ exitFullscreen() {
257
+ // No-op on TV
258
+ }
259
+
260
+ parseStreamInfo() {
261
+ try {
262
+ const streamInfo = this.avplay.getCurrentStreamInfo();
263
+ if (streamInfo) {
264
+ // Parse video stream info for qualities
265
+ for (let i = 0; i < streamInfo.length; i++) {
266
+ const info = streamInfo[i];
267
+ if (info.type === 'VIDEO') {
268
+ this.qualities.push({
269
+ id: `quality_${i}`,
270
+ label: `${info.height}p`,
271
+ height: info.height,
272
+ width: info.width,
273
+ bitrate: info.bitrate || 0,
274
+ codec: info.codec
275
+ });
276
+ }
277
+ }
278
+ }
279
+ } catch (error) {
280
+ console.error('Failed to parse stream info:', error);
281
+ }
282
+ }
283
+
284
+ handleTizenEvent(eventType, eventData) {
285
+ switch (eventType) {
286
+ case 'PLAYER_MSG_RESOLUTION_CHANGED':
287
+ this.emit('resolutionchanged', eventData);
288
+ this.parseStreamInfo(); // Update qualities
289
+ break;
290
+ case 'PLAYER_MSG_BITRATE_CHANGE':
291
+ this.emit('bitratechange', eventData);
292
+ break;
293
+ case 'PLAYER_MSG_FRAGMENT_INFO':
294
+ this.emit('fragmentinfo', eventData);
295
+ break;
296
+ case 'PLAYER_MSG_HTTP_ERROR':
297
+ this.emit('error', new Error(`HTTP Error: ${eventData}`));
298
+ break;
299
+ case 'PLAYER_MSG_NONE':
300
+ // Playback started successfully
301
+ break;
302
+ default:
303
+ this.emit('tizenevent', { type: eventType, data: eventData });
304
+ }
305
+ }
306
+
307
+ handleDRMEvent(drmEvent, drmData) {
308
+ switch (drmEvent) {
309
+ case 'DRM_LICENSE_ACQUIRED':
310
+ console.log('DRM license acquired');
311
+ this.emit('drmlicenseacquired');
312
+ break;
313
+ case 'DRM_LICENSE_EXPIRED':
314
+ console.error('DRM license expired');
315
+ this.emit('drmlicenseexpired');
316
+ this.emit('error', new Error('DRM license expired'));
317
+ break;
318
+ default:
319
+ this.emit('drmevent', { event: drmEvent, data: drmData });
320
+ }
321
+ }
322
+
323
+ // Get buffered percentage
324
+ getBuffered() {
325
+ try {
326
+ const buffered = this.avplay.getBufferedRange();
327
+ if (buffered && buffered.length > 0) {
328
+ const duration = this.getDuration();
329
+ const bufferedEnd = buffered[buffered.length - 1] / 1000;
330
+ return (bufferedEnd / duration) * 100;
331
+ }
332
+ } catch (error) {
333
+ console.error('Failed to get buffered range:', error);
334
+ }
335
+ return 0;
336
+ }
337
+
338
+ destroy() {
339
+ if (this.avplay) {
340
+ try {
341
+ this.avplay.stop();
342
+ this.avplay.close();
343
+ } catch (error) {
344
+ console.error('Error destroying player:', error);
345
+ }
346
+ this.avplay = null;
347
+ }
348
+ this.removeAllListeners();
349
+ this.element = null;
350
+ this.listener = null;
351
+ }
352
+ }
353
+
354
+ export default TizenAdapter;