unified-video-framework 1.4.410 → 1.4.412
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/package.json +1 -1
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts +22 -0
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
- package/packages/core/src/interfaces/IVideoPlayer.ts +27 -0
- package/packages/web/dist/WebPlayer.d.ts +4 -0
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +68 -4
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/drm/BunnyCDNDRMProvider.d.ts +13 -0
- package/packages/web/dist/drm/BunnyCDNDRMProvider.d.ts.map +1 -0
- package/packages/web/dist/drm/BunnyCDNDRMProvider.js +65 -0
- package/packages/web/dist/drm/BunnyCDNDRMProvider.js.map +1 -0
- package/packages/web/dist/drm/DRMManager.d.ts +13 -0
- package/packages/web/dist/drm/DRMManager.d.ts.map +1 -0
- package/packages/web/dist/drm/DRMManager.js +59 -0
- package/packages/web/dist/drm/DRMManager.js.map +1 -0
- package/packages/web/dist/drm/FairPlayDRMHandler.d.ts +24 -0
- package/packages/web/dist/drm/FairPlayDRMHandler.d.ts.map +1 -0
- package/packages/web/dist/drm/FairPlayDRMHandler.js +190 -0
- package/packages/web/dist/drm/FairPlayDRMHandler.js.map +1 -0
- package/packages/web/dist/drm/WidevineDRMHandler.d.ts +21 -0
- package/packages/web/dist/drm/WidevineDRMHandler.d.ts.map +1 -0
- package/packages/web/dist/drm/WidevineDRMHandler.js +143 -0
- package/packages/web/dist/drm/WidevineDRMHandler.js.map +1 -0
- package/packages/web/dist/drm/types/DRMTypes.d.ts +52 -0
- package/packages/web/dist/drm/types/DRMTypes.d.ts.map +1 -0
- package/packages/web/dist/drm/types/DRMTypes.js +15 -0
- package/packages/web/dist/drm/types/DRMTypes.js.map +1 -0
- package/packages/web/dist/react/WebPlayerView.d.ts +18 -1
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +28 -0
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +96 -4
- package/packages/web/src/drm/BunnyCDNDRMProvider.ts +104 -0
- package/packages/web/src/drm/DRMManager.ts +99 -0
- package/packages/web/src/drm/FairPlayDRMHandler.ts +322 -0
- package/packages/web/src/drm/WidevineDRMHandler.ts +246 -0
- package/packages/web/src/drm/types/DRMTypes.ts +97 -0
- package/packages/web/src/react/WebPlayerView.tsx +55 -3
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widevine DRM Handler
|
|
3
|
+
* Implements DRM for Chrome, Edge, Firefox using Encrypted Media Extensions (EME)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IDRMHandler, WebDRMConfig, DRM_KEY_SYSTEMS, DRMError } from './types/DRMTypes';
|
|
7
|
+
import { BunnyCDNDRMProvider } from './BunnyCDNDRMProvider';
|
|
8
|
+
|
|
9
|
+
export class WidevineDRMHandler implements IDRMHandler {
|
|
10
|
+
private mediaKeys: MediaKeys | null = null;
|
|
11
|
+
private config: WebDRMConfig;
|
|
12
|
+
private bunnyProvider: BunnyCDNDRMProvider | null = null;
|
|
13
|
+
private videoElement: HTMLVideoElement | null = null;
|
|
14
|
+
private pendingLicenseRequests: Map<string, (license: ArrayBuffer) => void> = new Map();
|
|
15
|
+
|
|
16
|
+
constructor(config: WebDRMConfig) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
|
|
19
|
+
if (config.provider === 'bunny' && config.bunny) {
|
|
20
|
+
this.bunnyProvider = new BunnyCDNDRMProvider(config.bunny);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if Widevine is supported in current browser
|
|
26
|
+
*/
|
|
27
|
+
isSupported(): boolean {
|
|
28
|
+
return !!(
|
|
29
|
+
typeof navigator.requestMediaKeySystemAccess === 'function' &&
|
|
30
|
+
typeof window.MediaKeySystemAccess !== 'undefined'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize Widevine DRM system
|
|
36
|
+
*/
|
|
37
|
+
async initialize(): Promise<void> {
|
|
38
|
+
if (!this.isSupported()) {
|
|
39
|
+
throw new DRMError(
|
|
40
|
+
'Widevine DRM is not supported in this browser',
|
|
41
|
+
'WIDEVINE_NOT_SUPPORTED'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('[Widevine] Initializing DRM system...');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Request access to Widevine key system
|
|
49
|
+
const keySystemAccess = await navigator.requestMediaKeySystemAccess(
|
|
50
|
+
DRM_KEY_SYSTEMS.WIDEVINE,
|
|
51
|
+
this.getKeySystemConfiguration()
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Create MediaKeys instance
|
|
55
|
+
this.mediaKeys = await keySystemAccess.createMediaKeys();
|
|
56
|
+
|
|
57
|
+
console.log('[Widevine] MediaKeys created successfully');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('[Widevine] Failed to initialize:', error);
|
|
60
|
+
throw new DRMError(
|
|
61
|
+
'Failed to initialize Widevine DRM',
|
|
62
|
+
'WIDEVINE_INIT_ERROR',
|
|
63
|
+
error
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get key system configuration for Widevine
|
|
70
|
+
*/
|
|
71
|
+
private getKeySystemConfiguration(): MediaKeySystemConfiguration[] {
|
|
72
|
+
return [{
|
|
73
|
+
initDataTypes: ['cenc', 'webm'],
|
|
74
|
+
audioCapabilities: [
|
|
75
|
+
{ contentType: 'audio/mp4; codecs="mp4a.40.2"' },
|
|
76
|
+
{ contentType: 'audio/webm; codecs="opus"' }
|
|
77
|
+
],
|
|
78
|
+
videoCapabilities: [
|
|
79
|
+
{ contentType: 'video/mp4; codecs="avc1.42E01E"' },
|
|
80
|
+
{ contentType: 'video/mp4; codecs="avc1.4D401E"' },
|
|
81
|
+
{ contentType: 'video/mp4; codecs="avc1.640028"' },
|
|
82
|
+
{ contentType: 'video/webm; codecs="vp9"' }
|
|
83
|
+
],
|
|
84
|
+
distinctiveIdentifier: 'optional',
|
|
85
|
+
persistentState: 'optional',
|
|
86
|
+
sessionTypes: ['temporary']
|
|
87
|
+
}];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Setup DRM for video element
|
|
92
|
+
*/
|
|
93
|
+
async setupDRM(videoElement: HTMLVideoElement, manifestUrl: string): Promise<void> {
|
|
94
|
+
if (!this.mediaKeys) {
|
|
95
|
+
throw new DRMError('MediaKeys not initialized', 'MEDIAKEYS_NOT_INITIALIZED');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.videoElement = videoElement;
|
|
99
|
+
|
|
100
|
+
// Attach MediaKeys to video element
|
|
101
|
+
await videoElement.setMediaKeys(this.mediaKeys);
|
|
102
|
+
console.log('[Widevine] MediaKeys attached to video element');
|
|
103
|
+
|
|
104
|
+
// Listen for encrypted events
|
|
105
|
+
videoElement.addEventListener('encrypted', this.onEncrypted.bind(this));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle 'encrypted' event from video element
|
|
110
|
+
*/
|
|
111
|
+
private async onEncrypted(event: MediaEncryptedEvent): Promise<void> {
|
|
112
|
+
console.log('[Widevine] Encrypted event received:', event.initDataType);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
if (!this.mediaKeys) {
|
|
116
|
+
throw new DRMError('MediaKeys not available', 'MEDIAKEYS_NOT_AVAILABLE');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create media key session
|
|
120
|
+
const session = this.mediaKeys.createSession();
|
|
121
|
+
|
|
122
|
+
// Listen for message event (license request)
|
|
123
|
+
session.addEventListener('message', async (messageEvent: MediaKeyMessageEvent) => {
|
|
124
|
+
await this.onMessage(session, messageEvent);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Generate license request
|
|
128
|
+
await session.generateRequest(event.initDataType!, event.initData!);
|
|
129
|
+
console.log('[Widevine] License request generated');
|
|
130
|
+
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('[Widevine] Failed to handle encrypted event:', error);
|
|
133
|
+
throw new DRMError(
|
|
134
|
+
'Failed to process encrypted media',
|
|
135
|
+
'ENCRYPTED_EVENT_ERROR',
|
|
136
|
+
error
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Handle license request message
|
|
143
|
+
*/
|
|
144
|
+
private async onMessage(
|
|
145
|
+
session: MediaKeySession,
|
|
146
|
+
event: MediaKeyMessageEvent
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
console.log('[Widevine] License request message:', event.messageType);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Get license from server
|
|
152
|
+
const license = await this.requestLicense(event.message);
|
|
153
|
+
|
|
154
|
+
// Update session with license
|
|
155
|
+
await session.update(license);
|
|
156
|
+
console.log('[Widevine] License applied successfully');
|
|
157
|
+
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('[Widevine] Failed to process license:', error);
|
|
160
|
+
throw new DRMError(
|
|
161
|
+
'Failed to process DRM license',
|
|
162
|
+
'LICENSE_REQUEST_ERROR',
|
|
163
|
+
error
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Request license from license server
|
|
170
|
+
*/
|
|
171
|
+
private async requestLicense(challenge: ArrayBuffer): Promise<ArrayBuffer> {
|
|
172
|
+
const licenseUrl = this.getLicenseUrl();
|
|
173
|
+
const headers = await this.getLicenseHeaders();
|
|
174
|
+
|
|
175
|
+
console.log('[Widevine] Requesting license from:', licenseUrl);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const response = await fetch(licenseUrl, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers,
|
|
181
|
+
body: challenge
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
throw new Error(`License request failed: ${response.status} ${response.statusText}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const license = await response.arrayBuffer();
|
|
189
|
+
console.log('[Widevine] License received:', license.byteLength, 'bytes');
|
|
190
|
+
|
|
191
|
+
return license;
|
|
192
|
+
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error('[Widevine] License request failed:', error);
|
|
195
|
+
throw new DRMError(
|
|
196
|
+
'Failed to retrieve DRM license',
|
|
197
|
+
'LICENSE_FETCH_ERROR',
|
|
198
|
+
error
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get license URL based on provider
|
|
205
|
+
*/
|
|
206
|
+
private getLicenseUrl(): string {
|
|
207
|
+
if (this.config.provider === 'bunny' && this.bunnyProvider) {
|
|
208
|
+
return this.bunnyProvider.getWidevineLicenseUrl();
|
|
209
|
+
} else if (this.config.provider === 'generic' && this.config.generic) {
|
|
210
|
+
return this.config.generic.licenseUrl;
|
|
211
|
+
}
|
|
212
|
+
throw new DRMError('No license URL configured', 'NO_LICENSE_URL');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get headers for license request
|
|
217
|
+
*/
|
|
218
|
+
private async getLicenseHeaders(): Promise<Record<string, string>> {
|
|
219
|
+
if (this.config.provider === 'bunny' && this.bunnyProvider) {
|
|
220
|
+
return await this.bunnyProvider.getLicenseHeaders();
|
|
221
|
+
} else if (this.config.provider === 'generic' && this.config.generic) {
|
|
222
|
+
return this.config.generic.headers || { 'Content-Type': 'application/octet-stream' };
|
|
223
|
+
}
|
|
224
|
+
return { 'Content-Type': 'application/octet-stream' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Handle license request (called by DRM manager)
|
|
229
|
+
*/
|
|
230
|
+
async onLicenseRequest(licenseRequest: ArrayBuffer): Promise<ArrayBuffer> {
|
|
231
|
+
return await this.requestLicense(licenseRequest);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Cleanup resources
|
|
236
|
+
*/
|
|
237
|
+
destroy(): void {
|
|
238
|
+
if (this.videoElement) {
|
|
239
|
+
this.videoElement.removeEventListener('encrypted', this.onEncrypted.bind(this));
|
|
240
|
+
this.videoElement = null;
|
|
241
|
+
}
|
|
242
|
+
this.mediaKeys = null;
|
|
243
|
+
this.bunnyProvider = null;
|
|
244
|
+
this.pendingLicenseRequests.clear();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DRM Types and Interfaces
|
|
3
|
+
* Core types for the web DRM system supporting Bunny CDN and generic DRM providers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Token provider interface - allows flexible token injection strategies
|
|
7
|
+
export interface DRMTokenProvider {
|
|
8
|
+
// Get token for manifest/playlist requests
|
|
9
|
+
getManifestToken(): Promise<DRMToken | null>;
|
|
10
|
+
|
|
11
|
+
// Get token for media segment requests
|
|
12
|
+
getSegmentToken(): Promise<DRMToken | null>;
|
|
13
|
+
|
|
14
|
+
// Get token for license server requests
|
|
15
|
+
getLicenseToken(): Promise<DRMToken | null>;
|
|
16
|
+
|
|
17
|
+
// Check if token needs renewal
|
|
18
|
+
shouldRenewToken(token: DRMToken): boolean;
|
|
19
|
+
|
|
20
|
+
// Refresh/renew token
|
|
21
|
+
renewToken(token: DRMToken): Promise<DRMToken>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Token structure
|
|
25
|
+
export interface DRMToken {
|
|
26
|
+
token: string; // The actual token value
|
|
27
|
+
expires: number; // Unix timestamp (seconds)
|
|
28
|
+
tokenVer?: string; // Optional token version
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Bunny CDN specific configuration
|
|
32
|
+
export interface BunnyCDNDRMConfig {
|
|
33
|
+
libraryId: string; // Bunny CDN library ID
|
|
34
|
+
videoId: string; // Video ID on Bunny CDN
|
|
35
|
+
pullZoneUrl: string; // e.g., "stream.example.com"
|
|
36
|
+
tokenProvider: DRMTokenProvider; // Token provider implementation
|
|
37
|
+
certificateUrl?: string; // Override default FairPlay certificate URL
|
|
38
|
+
licenseUrl?: string; // Override default license URL
|
|
39
|
+
enableReferrerProtection?: boolean; // Enable referrer header validation
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Generic DRM configuration (for non-Bunny providers)
|
|
43
|
+
export interface GenericDRMConfig {
|
|
44
|
+
type: 'widevine' | 'fairplay';
|
|
45
|
+
licenseUrl: string;
|
|
46
|
+
certificateUrl?: string; // Required for FairPlay
|
|
47
|
+
headers?: Record<string, string>; // Custom headers for license requests
|
|
48
|
+
certificateHeaders?: Record<string, string>; // Custom headers for certificate requests
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Unified DRM configuration
|
|
52
|
+
export interface WebDRMConfig {
|
|
53
|
+
// Provider type
|
|
54
|
+
provider: 'bunny' | 'generic';
|
|
55
|
+
|
|
56
|
+
// Provider-specific config
|
|
57
|
+
bunny?: BunnyCDNDRMConfig;
|
|
58
|
+
generic?: GenericDRMConfig;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// DRM Handler interface (implemented by Widevine and FairPlay handlers)
|
|
62
|
+
export interface IDRMHandler {
|
|
63
|
+
// Initialize DRM system
|
|
64
|
+
initialize(): Promise<void>;
|
|
65
|
+
|
|
66
|
+
// Setup DRM for video element
|
|
67
|
+
setupDRM(videoElement: HTMLVideoElement, manifestUrl: string): Promise<void>;
|
|
68
|
+
|
|
69
|
+
// Handle license requests
|
|
70
|
+
onLicenseRequest(licenseRequest: ArrayBuffer): Promise<ArrayBuffer>;
|
|
71
|
+
|
|
72
|
+
// Cleanup resources
|
|
73
|
+
destroy(): void;
|
|
74
|
+
|
|
75
|
+
// Check if DRM is supported in current browser
|
|
76
|
+
isSupported(): boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// EME key system identifiers
|
|
80
|
+
export const DRM_KEY_SYSTEMS = {
|
|
81
|
+
WIDEVINE: 'com.widevine.alpha',
|
|
82
|
+
FAIRPLAY: 'com.apple.fps.1_0',
|
|
83
|
+
PLAYREADY: 'com.microsoft.playready',
|
|
84
|
+
CLEARKEY: 'org.w3.clearkey'
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
87
|
+
// Error types
|
|
88
|
+
export class DRMError extends Error {
|
|
89
|
+
constructor(
|
|
90
|
+
message: string,
|
|
91
|
+
public code: string,
|
|
92
|
+
public details?: any
|
|
93
|
+
) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = 'DRMError';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
3
3
|
import type { CSSProperties } from 'react';
|
|
4
|
-
import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from '../../core/dist';
|
|
4
|
+
import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig, DRMTokenProvider, DRMToken, BunnyCDNDRMConfig } from '../../core/dist';
|
|
5
5
|
import { WebPlayer } from '../WebPlayer';
|
|
6
6
|
import { GoogleAdsManager } from '../ads/GoogleAdsManager';
|
|
7
7
|
// EPG imports - conditionally loaded
|
|
@@ -224,6 +224,26 @@ export type WebPlayerViewProps = {
|
|
|
224
224
|
// Source config
|
|
225
225
|
url: string;
|
|
226
226
|
type?: 'mp4' | 'hls' | 'dash' | 'webm' | 'auto';
|
|
227
|
+
|
|
228
|
+
// DRM configuration (optional)
|
|
229
|
+
drm?: {
|
|
230
|
+
bunny?: {
|
|
231
|
+
libraryId: string;
|
|
232
|
+
videoId: string;
|
|
233
|
+
pullZoneUrl: string;
|
|
234
|
+
tokenProvider: DRMTokenProvider;
|
|
235
|
+
certificateUrl?: string;
|
|
236
|
+
licenseUrl?: string;
|
|
237
|
+
enableReferrerProtection?: boolean;
|
|
238
|
+
};
|
|
239
|
+
generic?: {
|
|
240
|
+
type: 'widevine' | 'fairplay';
|
|
241
|
+
licenseUrl: string;
|
|
242
|
+
certificateUrl?: string;
|
|
243
|
+
headers?: Record<string, string>;
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
227
247
|
subtitles?: SubtitleTrack[];
|
|
228
248
|
metadata?: VideoMetadata;
|
|
229
249
|
|
|
@@ -1020,6 +1040,13 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1020
1040
|
fallbackRetryDelay: props.fallbackRetryDelay,
|
|
1021
1041
|
fallbackRetryAttempts: props.fallbackRetryAttempts,
|
|
1022
1042
|
onAllSourcesFailed: props.onAllSourcesFailed,
|
|
1043
|
+
|
|
1044
|
+
// Pass DRM config if provided
|
|
1045
|
+
drm: props.drm ? {
|
|
1046
|
+
type: props.drm.bunny ? 'widevine' : (props.drm.generic?.type || 'widevine'),
|
|
1047
|
+
licenseUrl: props.drm.bunny ? '' : (props.drm.generic?.licenseUrl || ''),
|
|
1048
|
+
bunny: props.drm.bunny
|
|
1049
|
+
} as any : undefined
|
|
1023
1050
|
};
|
|
1024
1051
|
|
|
1025
1052
|
await player.load(source);
|
|
@@ -1049,17 +1076,40 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1049
1076
|
// Helper function to inject ad markers
|
|
1050
1077
|
const injectAdMarkersFromTimes = (adTimes: number[]) => {
|
|
1051
1078
|
if (!adTimes || adTimes.length === 0) return;
|
|
1052
|
-
|
|
1079
|
+
|
|
1053
1080
|
const duration = (player as any).getDuration?.() || 0;
|
|
1081
|
+
|
|
1082
|
+
// If duration is available, inject immediately
|
|
1054
1083
|
if (duration > 0) {
|
|
1055
1084
|
const adMarkers = generateAdMarkers(adTimes, duration);
|
|
1056
1085
|
const mergedChapters = mergeAdMarkersWithChapters(props.chapters, adMarkers, duration);
|
|
1057
|
-
|
|
1086
|
+
|
|
1058
1087
|
// Inject ad markers into the chapter system
|
|
1059
1088
|
if (typeof (player as any).loadChapters === 'function' && mergedChapters.data) {
|
|
1060
1089
|
(player as any).loadChapters(mergedChapters.data);
|
|
1061
1090
|
console.log('✅ Ad markers injected:', adMarkers.length, 'markers at times:', adTimes);
|
|
1062
1091
|
}
|
|
1092
|
+
} else {
|
|
1093
|
+
// Duration not available yet - wait for loadedmetadata event
|
|
1094
|
+
console.log('⏳ Duration not available yet, waiting for metadata to inject ad markers...');
|
|
1095
|
+
const videoElement = (player as any).video || (player as any).getVideoElement?.();
|
|
1096
|
+
if (videoElement) {
|
|
1097
|
+
const retryInject = () => {
|
|
1098
|
+
const retryDuration = (player as any).getDuration?.() || 0;
|
|
1099
|
+
if (retryDuration > 0) {
|
|
1100
|
+
const adMarkers = generateAdMarkers(adTimes, retryDuration);
|
|
1101
|
+
const mergedChapters = mergeAdMarkersWithChapters(props.chapters, adMarkers, retryDuration);
|
|
1102
|
+
|
|
1103
|
+
if (typeof (player as any).loadChapters === 'function' && mergedChapters.data) {
|
|
1104
|
+
(player as any).loadChapters(mergedChapters.data);
|
|
1105
|
+
console.log('✅ Ad markers injected (after metadata):', adMarkers.length, 'markers at times:', adTimes);
|
|
1106
|
+
}
|
|
1107
|
+
} else {
|
|
1108
|
+
console.warn('⚠️ Duration still not available after metadata loaded');
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
videoElement.addEventListener('loadedmetadata', retryInject, { once: true });
|
|
1112
|
+
}
|
|
1063
1113
|
}
|
|
1064
1114
|
};
|
|
1065
1115
|
|
|
@@ -1373,7 +1423,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1373
1423
|
|
|
1374
1424
|
// Inject markers from VMAP cue points (if midrollTimes not provided)
|
|
1375
1425
|
if (!props.googleAds?.midrollTimes || props.googleAds.midrollTimes.length === 0) {
|
|
1426
|
+
const currentDuration = (player as any).getDuration?.() || 0;
|
|
1376
1427
|
console.log('🔵 Using VMAP cue points for ad markers:', cuePoints);
|
|
1428
|
+
console.log('📊 Current video duration:', currentDuration, 'seconds');
|
|
1377
1429
|
injectAdMarkersFromTimes(cuePoints);
|
|
1378
1430
|
}
|
|
1379
1431
|
},
|