unified-video-framework 1.4.21 → 1.4.22

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.
@@ -1,5 +1,13 @@
1
1
  import { PaywallConfig } from '@unified-video/core';
2
2
  import { EmailAuthController, EmailAuthControllerOptions } from './EmailAuthController';
3
+ import {
4
+ PaymentGatewayManager,
5
+ PaymentGatewayAdapter,
6
+ CashfreePaymentGateway,
7
+ GenericPaymentGateway,
8
+ PaymentRequest,
9
+ PaymentResponse
10
+ } from './PaymentGatewayAdapter';
3
11
 
4
12
  export type PaywallControllerOptions = {
5
13
  getOverlayContainer: () => HTMLElement | null;
@@ -17,11 +25,16 @@ export class PaywallController {
17
25
  private emailAuth: EmailAuthController | null = null;
18
26
  private authenticatedUserId: string | null = null;
19
27
  private sessionToken: string | null = null;
28
+ private paymentManager: PaymentGatewayManager = new PaymentGatewayManager();
29
+ private currentPaymentData: { gateway: string; orderId?: string; sessionId?: string } | null = null;
20
30
 
21
31
  constructor(config: PaywallConfig | null, opts: PaywallControllerOptions) {
22
32
  this.config = config;
23
33
  this.opts = opts;
24
34
 
35
+ // Initialize payment gateway manager with default gateways
36
+ this.initializePaymentGateways();
37
+
25
38
  // Initialize EmailAuthController if email auth is enabled
26
39
  this.initializeEmailAuth();
27
40
 
@@ -34,29 +47,11 @@ export class PaywallController {
34
47
  }
35
48
 
36
49
  updateConfig(config: PaywallConfig | null) {
37
- // Defensive logic: if new config is null/undefined but we have a working email auth,
38
- // preserve the email auth instance to prevent destruction during re-initialization
39
- const hadWorkingEmailAuth = this.config?.emailAuth?.enabled && !!this.emailAuth;
40
- const newConfigLacksEmailAuth = !config?.emailAuth?.enabled;
41
-
42
- if (hadWorkingEmailAuth && newConfigLacksEmailAuth) {
43
- console.log('[PaywallController] Preserving email auth instance during config update');
44
- // Only update non-email auth related config, keep the email auth part
45
- this.config = {
46
- ...config,
47
- emailAuth: this.config?.emailAuth // Preserve existing email auth config
48
- };
49
-
50
- if (this.emailAuth) {
51
- this.emailAuth.updateConfig(this.config);
52
- }
53
- } else {
54
- // Normal config update
55
- this.config = config;
56
- this.initializeEmailAuth();
57
- if (this.emailAuth) {
58
- this.emailAuth.updateConfig(config);
59
- }
50
+ this.config = config;
51
+ this.initializePaymentGateways();
52
+ this.initializeEmailAuth();
53
+ if (this.emailAuth) {
54
+ this.emailAuth.updateConfig(config);
60
55
  }
61
56
  }
62
57
 
@@ -74,34 +69,21 @@ export class PaywallController {
74
69
  // Check authentication first if email auth is enabled
75
70
  if (this.config.emailAuth?.enabled) {
76
71
  console.log('[PaywallController] Email auth is enabled, checking authentication');
72
+ const isAuthenticated = this.emailAuth?.isAuthenticated();
73
+ console.log('[PaywallController] User authenticated:', isAuthenticated);
77
74
 
78
- // If email auth is enabled but instance doesn't exist, try to initialize it
79
- if (!this.emailAuth) {
80
- console.log('[PaywallController] Email auth enabled but no instance found, initializing now');
81
- this.initializeEmailAuth();
82
- }
83
-
84
- // If still no instance after initialization, show error
85
- if (!this.emailAuth) {
86
- console.error('[PaywallController] Failed to initialize email auth, proceeding to payment overlay');
87
- // Continue to payment overlay as fallback
75
+ if (!isAuthenticated) {
76
+ console.log('[PaywallController] User not authenticated, opening email auth modal');
77
+ // Show email authentication modal first
78
+ this.emailAuth?.openAuthModal();
79
+ return;
88
80
  } else {
89
- const isAuthenticated = this.emailAuth.isAuthenticated();
90
- console.log('[PaywallController] User authenticated:', isAuthenticated);
91
-
92
- if (!isAuthenticated) {
93
- console.log('[PaywallController] User not authenticated, opening email auth modal');
94
- // Show email authentication modal first
95
- this.emailAuth.openAuthModal();
96
- return;
97
- } else {
98
- console.log('[PaywallController] User already authenticated, proceeding to payment overlay');
99
- // Update userId for authenticated user
100
- this.authenticatedUserId = this.emailAuth.getAuthenticatedUserId() || this.config.userId || null;
101
- // Update config with authenticated userId for API calls
102
- if (this.authenticatedUserId && this.config) {
103
- this.config.userId = this.authenticatedUserId;
104
- }
81
+ console.log('[PaywallController] User already authenticated, proceeding to payment overlay');
82
+ // Update userId for authenticated user
83
+ this.authenticatedUserId = this.emailAuth?.getAuthenticatedUserId() || this.config.userId || null;
84
+ // Update config with authenticated userId for API calls
85
+ if (this.authenticatedUserId && this.config) {
86
+ this.config.userId = this.authenticatedUserId;
105
87
  }
106
88
  }
107
89
  }
@@ -109,47 +91,16 @@ export class PaywallController {
109
91
  // Show payment overlay
110
92
  console.log('[PaywallController] Showing payment overlay');
111
93
  const root = this.ensureOverlay();
112
- if (!root) {
113
- console.log('[PaywallController] Failed to create overlay');
114
- return;
115
- }
116
-
117
- // Show overlay with proper animation
94
+ if (!root) return;
118
95
  root.style.display = 'flex';
119
96
  root.classList.add('active');
120
-
121
- // Force reflow then fade in with animation
122
- void root.offsetWidth;
123
- root.style.opacity = '1';
124
-
125
- // Also animate the modal inside
126
- const modal = root.querySelector('.uvf-paywall-modal') as HTMLElement;
127
- if (modal) {
128
- modal.style.transform = 'translateY(0)';
129
- modal.style.opacity = '1';
130
- }
131
-
132
- console.log('[PaywallController] Payment overlay displayed successfully');
133
97
  this.opts.onShow?.();
134
98
  }
135
99
 
136
100
  closeOverlay() {
137
101
  if (this.overlayEl) {
138
- // Animate out
139
- this.overlayEl.style.opacity = '0';
140
- const modal = this.overlayEl.querySelector('.uvf-paywall-modal') as HTMLElement;
141
- if (modal) {
142
- modal.style.transform = 'translateY(20px)';
143
- modal.style.opacity = '0';
144
- }
145
-
146
- // Hide after animation
147
- setTimeout(() => {
148
- if (this.overlayEl) {
149
- this.overlayEl.classList.remove('active');
150
- this.overlayEl.style.display = 'none';
151
- }
152
- }, 300); // Match the CSS transition duration
102
+ this.overlayEl.classList.remove('active');
103
+ this.overlayEl.style.display = 'none';
153
104
  }
154
105
  this.opts.onClose?.();
155
106
  }
@@ -162,38 +113,11 @@ export class PaywallController {
162
113
  ov.className = 'uvf-paywall-overlay';
163
114
  ov.setAttribute('role', 'dialog');
164
115
  ov.setAttribute('aria-modal', 'true');
165
- ov.style.cssText = `
166
- position: absolute;
167
- inset: 0;
168
- background: rgba(0, 0, 0, 0.95);
169
- z-index: 2147483647;
170
- display: none;
171
- align-items: center;
172
- justify-content: center;
173
- opacity: 0;
174
- transition: opacity 0.3s ease;
175
- `;
116
+ ov.style.cssText = 'position:absolute;inset:0;background:rgba(0,0,0,0.85);z-index:2147483000;display:none;align-items:center;justify-content:center;';
176
117
 
177
118
  const modal = document.createElement('div');
178
119
  modal.className = 'uvf-paywall-modal';
179
- modal.style.cssText = `
180
- width: 90vw;
181
- height: 85vh;
182
- max-width: 1000px;
183
- max-height: 700px;
184
- background: #0f0f10;
185
- border: 1px solid rgba(255, 255, 255, 0.2);
186
- border-radius: 16px;
187
- display: flex;
188
- flex-direction: column;
189
- overflow: hidden;
190
- box-shadow:
191
- 0 20px 60px rgba(0, 0, 0, 0.7),
192
- 0 0 0 1px rgba(255, 255, 255, 0.1);
193
- transform: translateY(20px);
194
- opacity: 0;
195
- transition: transform 0.3s ease, opacity 0.3s ease;
196
- `;
120
+ modal.style.cssText = 'width:80vw;height:80vh;max-width:1100px;max-height:800px;background:#0f0f10;border:1px solid rgba(255,255,255,0.15);border-radius:12px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.7)';
197
121
 
198
122
  const header = document.createElement('div');
199
123
  header.style.cssText = 'display:flex;gap:16px;align-items:center;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,0.1)';
@@ -247,68 +171,104 @@ export class PaywallController {
247
171
  const wrap = document.createElement('div');
248
172
  wrap.style.cssText = 'display:flex;gap:12px;flex-wrap:wrap;justify-content:center;';
249
173
 
250
- for (const g of (this.config.gateways || [])) {
174
+ // Get available payment gateways from manager
175
+ const availableGateways = this.paymentManager.getGatewayNames();
176
+ const configuredGateways = this.config.gateways || availableGateways;
177
+
178
+ for (const gatewayName of configuredGateways) {
179
+ const gateway = this.paymentManager.getGateway(gatewayName);
180
+ if (!gateway) continue;
181
+
251
182
  const btn = document.createElement('button');
252
- btn.textContent = g === 'cashfree' ? 'Cashfree' : 'Stripe';
183
+ btn.textContent = gateway.getDisplayName();
253
184
  btn.style.cssText = 'background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);border-radius:8px;padding:12px 16px;cursor:pointer;min-width:120px;';
254
- btn.addEventListener('click', () => this.openGateway(g as 'stripe' | 'cashfree'));
185
+ btn.addEventListener('click', () => this.openGateway(gatewayName));
255
186
  wrap.appendChild(btn);
256
187
  }
257
188
  this.gatewayStepEl!.appendChild(title);
258
189
  this.gatewayStepEl!.appendChild(wrap);
259
190
  }
260
191
 
261
- private async openGateway(gateway: 'stripe' | 'cashfree') {
192
+ private async openGateway(gatewayName: string) {
262
193
  try {
263
194
  if (!this.config) return;
264
- const { apiBase, userId, videoId } = this.config;
265
- const w = Math.min(window.screen.width - 100, this.config.popup?.width || 1000);
266
- const h = Math.min(window.screen.height - 100, this.config.popup?.height || 800);
267
- const left = Math.max(0, Math.round((window.screen.width - w) / 2));
268
- const top = Math.max(0, Math.round((window.screen.height - h) / 2));
195
+
196
+ console.log(`[PaywallController] Opening gateway: ${gatewayName}`);
197
+
198
+ // Get authenticated user email and video slug
199
+ const userEmail = this.emailAuth?.getAuthenticatedUserId() || this.authenticatedUserId;
200
+ const videoSlug = (this.config as any)?.metadata?.slug || this.config?.videoSlug;
201
+
202
+ if (!userEmail) {
203
+ console.error('[PaywallController] User not authenticated - email required for payment');
204
+ alert('Please complete authentication first');
205
+ return;
206
+ }
207
+
208
+ if (!videoSlug) {
209
+ console.error('[PaywallController] Missing video slug for payment');
210
+ alert('Video information missing. Please refresh and try again.');
211
+ return;
212
+ }
269
213
 
270
- if (gateway === 'stripe') {
271
- const res = await fetch(`${apiBase}/api/rentals/stripe/checkout-session`, {
272
- method: 'POST', headers: { 'Content-Type': 'application/json' },
273
- body: JSON.stringify({
274
- userId, videoId,
275
- successUrl: window.location.origin + window.location.pathname + '?rental=success&popup=1',
276
- cancelUrl: window.location.origin + window.location.pathname + '?rental=cancel&popup=1'
277
- })
278
- });
279
- const data = await res.json();
280
- if (data?.url) {
281
- try { this.popup && !this.popup.closed && this.popup.close(); } catch (_) {}
282
- this.popup = window.open(data.url, 'uvfCheckout', `popup=1,width=${w},height=${h},left=${left},top=${top}`);
283
- this.startPolling();
214
+ // Prepare payment request using email as userId and slug as videoId
215
+ const paymentRequest: PaymentRequest = {
216
+ userId: userEmail, // Use email as user identifier
217
+ videoId: videoSlug, // Use slug as video identifier
218
+ amount: this.config.pricing?.amount || 100,
219
+ currency: this.config.pricing?.currency || 'INR',
220
+ successUrl: `${window.location.origin}${window.location.pathname}?payment=success&popup=1`,
221
+ cancelUrl: `${window.location.origin}${window.location.pathname}?payment=cancel&popup=1`,
222
+ metadata: {
223
+ videoSlug: videoSlug,
224
+ userEmail: userEmail,
225
+ title: this.config.pricing?.title || 'Video Access'
284
226
  }
227
+ };
228
+
229
+ console.log('[PaywallController] Payment request:', paymentRequest);
230
+
231
+ // Create payment using gateway manager
232
+ const paymentResponse = await this.paymentManager.createPayment(gatewayName, paymentRequest);
233
+
234
+ console.log('[PaywallController] Payment response:', paymentResponse);
235
+
236
+ if (!paymentResponse.success) {
237
+ console.error('[PaywallController] Payment creation failed:', paymentResponse.error);
238
+ alert(paymentResponse.message || 'Payment creation failed');
285
239
  return;
286
240
  }
287
241
 
288
- if (gateway === 'cashfree') {
289
- const features = `popup=1,width=${w},height=${h},left=${left},top=${top}`;
290
- // Pre-open a blank popup in direct response to the click to avoid popup blockers
291
- let pre: Window | null = null;
292
- try { pre = window.open('', 'uvfCheckout', features); } catch(_) { pre = null; }
293
- const res = await fetch(`${apiBase}/api/rentals/cashfree/order`, {
294
- method: 'POST', headers: { 'Content-Type': 'application/json' },
295
- body: JSON.stringify({ userId, videoId, returnUrl: window.location.origin + window.location.pathname })
296
- });
297
- const data = await res.json();
298
- if (data?.paymentLink && data?.orderId) {
299
- try { this.popup && !this.popup.closed && this.popup.close(); } catch (_) {}
300
- this.popup = pre && !pre.closed ? pre : window.open('', 'uvfCheckout', features);
301
- try { if (this.popup) this.popup.location.href = data.paymentLink; } catch(_) {}
302
- (window as any)._uvf_cfOrderId = data.orderId;
303
- this.startPolling();
304
- } else {
305
- // Close the pre-opened popup if we didn't get a link
306
- try { pre && !pre.closed && pre.close(); } catch(_) {}
307
- }
242
+ if (!paymentResponse.paymentUrl) {
243
+ console.error('[PaywallController] No payment URL received');
244
+ alert('Payment URL not available');
308
245
  return;
309
246
  }
310
- } catch (_) {
311
- // noop
247
+
248
+ // Store payment data for verification
249
+ this.currentPaymentData = {
250
+ gateway: gatewayName,
251
+ orderId: paymentResponse.orderId,
252
+ sessionId: paymentResponse.sessionId
253
+ };
254
+
255
+ // Open payment popup
256
+ const w = Math.min(window.screen.width - 100, this.config.popup?.width || 1000);
257
+ const h = Math.min(window.screen.height - 100, this.config.popup?.height || 800);
258
+ const left = Math.max(0, Math.round((window.screen.width - w) / 2));
259
+ const top = Math.max(0, Math.round((window.screen.height - h) / 2));
260
+ const features = `popup=1,width=${w},height=${h},left=${left},top=${top}`;
261
+
262
+ try {
263
+ this.popup && !this.popup.closed && this.popup.close();
264
+ } catch (_) {}
265
+
266
+ this.popup = window.open(paymentResponse.paymentUrl, 'uvfCheckout', features);
267
+ this.startPolling();
268
+
269
+ } catch (error) {
270
+ console.error('[PaywallController] Payment gateway error:', error);
271
+ alert('Payment system error. Please try again.');
312
272
  }
313
273
  }
314
274
 
@@ -330,49 +290,120 @@ export class PaywallController {
330
290
  if (!d || d.type !== 'uvfCheckout') return;
331
291
  try { if (this.popup && !this.popup.closed) this.popup.close(); } catch (_) {}
332
292
  this.popup = null;
293
+
333
294
  if (d.status === 'cancel') {
334
295
  this.showGateways();
335
296
  return;
336
297
  }
298
+
337
299
  if (d.status === 'success') {
338
300
  try {
339
- if (d.sessionId && this.config) {
340
- await fetch(`${this.config.apiBase}/api/rentals/stripe/confirm`, {
341
- method: 'POST', headers: { 'Content-Type': 'application/json' },
342
- body: JSON.stringify({ sessionId: d.sessionId })
343
- });
344
- }
345
- if (d.orderId && this.config) {
346
- await fetch(`${this.config.apiBase}/api/rentals/cashfree/verify?orderId=${encodeURIComponent(d.orderId)}&userId=${encodeURIComponent(this.config.userId || '')}&videoId=${encodeURIComponent(this.config.videoId || '')}`);
301
+ // Get current user and video info
302
+ const userEmail = this.emailAuth?.getAuthenticatedUserId() || this.authenticatedUserId;
303
+ const videoSlug = (this.config as any)?.metadata?.slug || this.config?.videoSlug;
304
+
305
+ // Verify payment using the appropriate gateway
306
+ if (this.currentPaymentData && this.config && userEmail && videoSlug) {
307
+ const verificationRequest = {
308
+ orderId: this.currentPaymentData.orderId || d.orderId,
309
+ sessionId: this.currentPaymentData.sessionId || d.sessionId,
310
+ userId: userEmail, // Use email
311
+ videoId: videoSlug, // Use slug
312
+ customData: {
313
+ email: userEmail,
314
+ slug: videoSlug,
315
+ ...d
316
+ }
317
+ };
318
+
319
+ console.log('[PaywallController] Verifying payment:', verificationRequest);
320
+
321
+ const verificationResult = await this.paymentManager.verifyPayment(
322
+ this.currentPaymentData.gateway,
323
+ verificationRequest
324
+ );
325
+
326
+ console.log('[PaywallController] Payment verification result:', verificationResult);
327
+
328
+ if (verificationResult.success && verificationResult.verified) {
329
+ console.log('[PaywallController] Payment verified successfully - unlocking video');
330
+ this.closeOverlay();
331
+ this.opts.onResume();
332
+ } else {
333
+ console.error('[PaywallController] Payment verification failed:', verificationResult.error);
334
+ alert('Payment verification failed. Please contact support.');
335
+ this.showGateways();
336
+ }
337
+ } else {
338
+ console.log('[PaywallController] Payment successful - assuming verification via return URL');
339
+ // For Cashfree, payment success via return URL usually means payment is complete
340
+ this.closeOverlay();
341
+ this.opts.onResume();
347
342
  }
348
- } catch (_) {}
349
- this.closeOverlay();
350
- this.opts.onResume();
343
+ } catch (error) {
344
+ console.error('[PaywallController] Payment verification error:', error);
345
+ console.log('[PaywallController] Proceeding with video unlock despite verification error');
346
+ this.closeOverlay();
347
+ this.opts.onResume(); // Allow playback anyway since payment was marked as successful
348
+ }
351
349
  }
352
350
  };
353
351
 
354
352
 
353
+ /**
354
+ * Initialize payment gateways based on configuration
355
+ */
356
+ private initializePaymentGateways() {
357
+ if (!this.config?.apiBase) return;
358
+
359
+ console.log('[PaywallController] Initializing payment gateways');
360
+
361
+ // Clear existing gateways
362
+ this.paymentManager = new PaymentGatewayManager();
363
+
364
+ // Register Cashfree gateway with your specific API endpoint
365
+ const cashfreeGateway = new CashfreePaymentGateway({
366
+ name: 'cashfree',
367
+ displayName: 'Cashfree',
368
+ apiBase: this.config.apiBase,
369
+ endpoints: {
370
+ createPayment: '/Front-End/cashfree/ppv-payment'
371
+ },
372
+ headers: {
373
+ 'Accept': 'application/json'
374
+ }
375
+ });
376
+
377
+ this.paymentManager.registerGateway(cashfreeGateway);
378
+
379
+ // Register Stripe gateway (if needed)
380
+ if (this.config.gateways?.includes('stripe')) {
381
+ const stripeGateway = new GenericPaymentGateway({
382
+ name: 'stripe',
383
+ displayName: 'Stripe',
384
+ apiBase: this.config.apiBase,
385
+ endpoints: {
386
+ createPayment: '/api/rentals/stripe/checkout-session',
387
+ verifyPayment: '/api/rentals/stripe/confirm'
388
+ }
389
+ });
390
+
391
+ this.paymentManager.registerGateway(stripeGateway);
392
+ }
393
+
394
+ console.log('[PaywallController] Payment gateways initialized:', this.paymentManager.getGatewayNames());
395
+ }
396
+
355
397
  /**
356
398
  * Initialize EmailAuthController if email authentication is enabled
357
399
  */
358
400
  private initializeEmailAuth() {
359
401
  console.log('[PaywallController] initializeEmailAuth called');
360
402
  console.log('[PaywallController] email auth config:', this.config?.emailAuth);
361
- console.log('[PaywallController] config enabled:', this.config?.enabled);
362
403
 
363
- // If paywall is disabled entirely, clean up everything
364
- if (!this.config?.enabled) {
365
- console.log('[PaywallController] Paywall completely disabled, cleaning up email auth');
366
- if (this.emailAuth) {
367
- this.emailAuth.destroy();
368
- this.emailAuth = null;
369
- }
370
- return;
371
- }
372
-
373
- // If email auth specifically is disabled, clean up only email auth
374
404
  if (!this.config?.emailAuth?.enabled) {
375
405
  console.log('[PaywallController] Email auth disabled, cleaning up existing instance');
406
+ // Clean up existing EmailAuth if disabled
376
407
  if (this.emailAuth) {
377
408
  this.emailAuth.destroy();
378
409
  this.emailAuth = null;
@@ -467,33 +498,11 @@ export class PaywallController {
467
498
  * Open payment overlay directly (bypassing auth check)
468
499
  */
469
500
  private openPaymentOverlay() {
470
- console.log('[PaywallController] Opening payment overlay');
471
501
  const root = this.ensureOverlay();
472
- if (!root) {
473
- console.error('[PaywallController] Failed to create overlay');
474
- return;
475
- }
476
-
477
- try {
478
- root.style.display = 'flex';
479
- root.classList.add('active');
480
-
481
- // Force reflow then fade in with animation
482
- void root.offsetWidth;
483
- root.style.opacity = '1';
484
-
485
- // Also animate the modal inside
486
- const modal = root.querySelector('.uvf-paywall-modal') as HTMLElement;
487
- if (modal) {
488
- modal.style.transform = 'translateY(0)';
489
- modal.style.opacity = '1';
490
- }
491
-
492
- this.opts.onShow?.();
493
- console.log('[PaywallController] Payment overlay shown');
494
- } catch (err) {
495
- console.error('[PaywallController] Error showing overlay:', err);
496
- }
502
+ if (!root) return;
503
+ root.style.display = 'flex';
504
+ root.classList.add('active');
505
+ this.opts.onShow?.();
497
506
  }
498
507
 
499
508
  /**
@@ -405,16 +405,7 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
405
405
  useEffect(() => {
406
406
  const p = playerRef.current as any;
407
407
  if (p && typeof p.setPaywallConfig === 'function' && props.paywall) {
408
- // Only update if paywall is enabled and properly configured
409
- const paywall = props.paywall as any;
410
- if (paywall.enabled && (paywall.apiBase || paywall.userId || paywall.videoId)) {
411
- try {
412
- console.log('[WebPlayerView] Updating paywall config:', paywall);
413
- p.setPaywallConfig(paywall);
414
- } catch(err) {
415
- console.warn('[WebPlayerView] Failed to update paywall config:', err);
416
- }
417
- }
408
+ try { p.setPaywallConfig(props.paywall as any); } catch(_) {}
418
409
  }
419
410
  }, [JSON.stringify(props.paywall)]);
420
411
 
@@ -1,84 +0,0 @@
1
- # UnifiedVideoPlayer (iOS SDK)
2
-
3
- A unified iOS video player SDK with:
4
- - HLS playback (AVPlayer)
5
- - FairPlay DRM (SPC/CKC)
6
- - Subtitles/audio track selection (AVMediaSelection)
7
- - Picture-in-Picture (AVPictureInPictureController)
8
- - AirPlay (AVRoutePickerView)
9
- - Remote Command Center + Now Playing (lock screen controls)
10
- - Background audio (AVAudioSession)
11
-
12
- ## Installation
13
-
14
- ### CocoaPods
15
- 1) Ensure this SDK is in a public Git repository. Update the podspec `s.source` to point at that repo and tag.
16
- 2) In your Podfile:
17
- ```ruby
18
- platform :ios, '13.0'
19
- use_frameworks!
20
-
21
- target 'YourApp' do
22
- pod 'UnifiedVideoPlayer', :git => 'https://github.com/yourcompany/unified-video-ios.git', :tag => '1.0.0'
23
- end
24
- ```
25
- 3) Run:
26
- ```bash
27
- pod install
28
- ```
29
-
30
- ### Swift Package Manager
31
- Add the package at the repository URL or use Add Local Package pointing to `packages/ios`.
32
-
33
- ## Quick Start
34
- ```swift
35
- import UnifiedVideoPlayer
36
-
37
- let containerView = UIView(frame: .zero)
38
- let player = UnifiedVideoPlayer()
39
- let config = PlayerConfiguration()
40
- config.autoPlay = true
41
-
42
- player.initialize(container: containerView, configuration: config)
43
-
44
- let source = MediaSource(url: "https://example.com/stream.m3u8")
45
- source.metadata = ["title": "Demo Stream"]
46
- player.onReady = { print("ready") }
47
- player.onQualityChange = { br in print("bitrate: \(br)") }
48
-
49
- player.load(source: source)
50
- ```
51
-
52
- ### FairPlay DRM
53
- ```swift
54
- let drm = DRMConfiguration(type: "fairplay", licenseUrl: "https://license.example.com/fps")
55
- drm.certificateUrl = "https://license.example.com/cert"
56
- drm.headers = ["X-Tenant-ID": "default"]
57
-
58
- let source = MediaSource(url: "https://cdn.example.com/protected/playlist.m3u8")
59
- source.drm = drm
60
- player.load(source: source)
61
- ```
62
-
63
- ### Tracks
64
- ```swift
65
- let audios = player.audioTracks() // [String]
66
- let subs = player.subtitleTracks() // [String]
67
- player.selectAudioTrack(index: 0)
68
- player.selectSubtitleTrack(index: -1) // off
69
- ```
70
-
71
- ### PiP & AirPlay
72
- ```swift
73
- player.startPictureInPicture()
74
- player.stopPictureInPicture()
75
- let airPlay = player.makeAirPlayPickerView()
76
- ```
77
-
78
- ## Capabilities
79
- - Enable Background Modes > Audio
80
- - For DRM endpoints, configure ATS exceptions if necessary.
81
-
82
- ## License
83
- MIT
84
-