unified-video-framework 1.4.411 → 1.4.413
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/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +62 -6
- 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 +20 -0
- package/packages/web/dist/drm/DRMManager.d.ts.map +1 -0
- package/packages/web/dist/drm/DRMManager.js +126 -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/index.d.ts +15 -0
- package/packages/web/dist/drm/index.d.ts.map +1 -0
- package/packages/web/dist/drm/index.js +13 -0
- package/packages/web/dist/drm/index.js.map +1 -0
- package/packages/web/dist/drm/providers/BunnyNetProvider.d.ts +19 -0
- package/packages/web/dist/drm/providers/BunnyNetProvider.d.ts.map +1 -0
- package/packages/web/dist/drm/providers/BunnyNetProvider.js +112 -0
- package/packages/web/dist/drm/providers/BunnyNetProvider.js.map +1 -0
- package/packages/web/dist/drm/providers/GenericProvider.d.ts +30 -0
- package/packages/web/dist/drm/providers/GenericProvider.d.ts.map +1 -0
- package/packages/web/dist/drm/providers/GenericProvider.js +102 -0
- package/packages/web/dist/drm/providers/GenericProvider.js.map +1 -0
- package/packages/web/dist/drm/systems/BaseDRM.d.ts +18 -0
- package/packages/web/dist/drm/systems/BaseDRM.d.ts.map +1 -0
- package/packages/web/dist/drm/systems/BaseDRM.js +29 -0
- package/packages/web/dist/drm/systems/BaseDRM.js.map +1 -0
- package/packages/web/dist/drm/systems/FairPlayDRM.d.ts +32 -0
- package/packages/web/dist/drm/systems/FairPlayDRM.d.ts.map +1 -0
- package/packages/web/dist/drm/systems/FairPlayDRM.js +198 -0
- package/packages/web/dist/drm/systems/FairPlayDRM.js.map +1 -0
- package/packages/web/dist/drm/systems/PlayReadyDRM.d.ts +9 -0
- package/packages/web/dist/drm/systems/PlayReadyDRM.d.ts.map +1 -0
- package/packages/web/dist/drm/systems/PlayReadyDRM.js +92 -0
- package/packages/web/dist/drm/systems/PlayReadyDRM.js.map +1 -0
- package/packages/web/dist/drm/systems/WidevineDRM.d.ts +9 -0
- package/packages/web/dist/drm/systems/WidevineDRM.d.ts.map +1 -0
- package/packages/web/dist/drm/systems/WidevineDRM.js +73 -0
- package/packages/web/dist/drm/systems/WidevineDRM.js.map +1 -0
- package/packages/web/dist/drm/types/BunnyNetTypes.d.ts +20 -0
- package/packages/web/dist/drm/types/BunnyNetTypes.d.ts.map +1 -0
- package/packages/web/dist/drm/types/BunnyNetTypes.js +8 -0
- package/packages/web/dist/drm/types/BunnyNetTypes.js.map +1 -0
- package/packages/web/dist/drm/types/DRMTypes.d.ts +59 -0
- package/packages/web/dist/drm/types/DRMTypes.d.ts.map +1 -0
- package/packages/web/dist/drm/types/DRMTypes.js +21 -0
- package/packages/web/dist/drm/types/DRMTypes.js.map +1 -0
- package/packages/web/dist/drm/utils/BrowserDetector.d.ts +20 -0
- package/packages/web/dist/drm/utils/BrowserDetector.d.ts.map +1 -0
- package/packages/web/dist/drm/utils/BrowserDetector.js +168 -0
- package/packages/web/dist/drm/utils/BrowserDetector.js.map +1 -0
- package/packages/web/dist/drm/utils/CertificateManager.d.ts +15 -0
- package/packages/web/dist/drm/utils/CertificateManager.d.ts.map +1 -0
- package/packages/web/dist/drm/utils/CertificateManager.js +46 -0
- package/packages/web/dist/drm/utils/CertificateManager.js.map +1 -0
- package/packages/web/dist/drm/utils/DRMErrorHandler.d.ts +7 -0
- package/packages/web/dist/drm/utils/DRMErrorHandler.d.ts.map +1 -0
- package/packages/web/dist/drm/utils/DRMErrorHandler.js +49 -0
- package/packages/web/dist/drm/utils/DRMErrorHandler.js.map +1 -0
- package/packages/web/dist/drm/utils/LicenseRequestHandler.d.ts +15 -0
- package/packages/web/dist/drm/utils/LicenseRequestHandler.d.ts.map +1 -0
- package/packages/web/dist/drm/utils/LicenseRequestHandler.js +110 -0
- package/packages/web/dist/drm/utils/LicenseRequestHandler.js.map +1 -0
- package/packages/web/src/WebPlayer.ts +80 -12
- package/packages/web/src/drm/DRMManager.ts +213 -0
- package/packages/web/src/drm/index.ts +37 -0
- package/packages/web/src/drm/providers/BunnyNetProvider.ts +170 -0
- package/packages/web/src/drm/providers/GenericProvider.ts +148 -0
- package/packages/web/src/drm/systems/BaseDRM.ts +68 -0
- package/packages/web/src/drm/systems/FairPlayDRM.ts +305 -0
- package/packages/web/src/drm/systems/PlayReadyDRM.ts +131 -0
- package/packages/web/src/drm/systems/WidevineDRM.ts +105 -0
- package/packages/web/src/drm/types/BunnyNetTypes.ts +35 -0
- package/packages/web/src/drm/types/DRMTypes.ts +91 -0
- package/packages/web/src/drm/utils/BrowserDetector.ts +232 -0
- package/packages/web/src/drm/utils/CertificateManager.ts +86 -0
- package/packages/web/src/drm/utils/DRMErrorHandler.ts +84 -0
- package/packages/web/src/drm/utils/LicenseRequestHandler.ts +180 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunny.net DRM Provider
|
|
3
|
+
* Helper utilities for Bunny.net MediaCage DRM integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DRMType, DRMConfig } from '@unified-video/core';
|
|
7
|
+
import { ExtendedDRMConfig } from '../types/DRMTypes';
|
|
8
|
+
import { BunnyNetConfig, BUNNY_NET_ENDPOINTS } from '../types/BunnyNetTypes';
|
|
9
|
+
|
|
10
|
+
export class BunnyNetProvider {
|
|
11
|
+
/**
|
|
12
|
+
* Generate complete DRM config from Bunny.net credentials
|
|
13
|
+
*/
|
|
14
|
+
static createDRMConfig(config: BunnyNetConfig): ExtendedDRMConfig {
|
|
15
|
+
const { libraryId, videoId, drmType = DRMType.WIDEVINE } = config;
|
|
16
|
+
|
|
17
|
+
const drmConfig: ExtendedDRMConfig = {
|
|
18
|
+
type: drmType,
|
|
19
|
+
licenseUrl: this.getLicenseUrl(drmType, libraryId, videoId),
|
|
20
|
+
provider: 'bunny',
|
|
21
|
+
libraryId,
|
|
22
|
+
videoId,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Add FairPlay-specific settings
|
|
26
|
+
if (drmType === DRMType.FAIRPLAY) {
|
|
27
|
+
drmConfig.certificateUrl = this.getCertificateUrl(libraryId);
|
|
28
|
+
drmConfig.fairplayOptions = {
|
|
29
|
+
certificateUrl: this.getCertificateUrl(libraryId),
|
|
30
|
+
licenseUrl: this.getLicenseUrl(drmType, libraryId, videoId),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Add Widevine-specific settings
|
|
35
|
+
if (drmType === DRMType.WIDEVINE) {
|
|
36
|
+
drmConfig.widevineOptions = {
|
|
37
|
+
licenseUrl: this.getLicenseUrl(drmType, libraryId, videoId),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add PlayReady-specific settings
|
|
42
|
+
if (drmType === DRMType.PLAYREADY) {
|
|
43
|
+
drmConfig.playreadyOptions = {
|
|
44
|
+
licenseUrl: this.getLicenseUrl(drmType, libraryId, videoId),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return drmConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get license URL for specific DRM type
|
|
53
|
+
*/
|
|
54
|
+
static getLicenseUrl(drmType: DRMType, libraryId: string, videoId: string): string {
|
|
55
|
+
switch (drmType) {
|
|
56
|
+
case DRMType.WIDEVINE:
|
|
57
|
+
return BUNNY_NET_ENDPOINTS.WIDEVINE_LICENSE(libraryId, videoId);
|
|
58
|
+
|
|
59
|
+
case DRMType.FAIRPLAY:
|
|
60
|
+
return BUNNY_NET_ENDPOINTS.FAIRPLAY_LICENSE(libraryId, videoId);
|
|
61
|
+
|
|
62
|
+
case DRMType.PLAYREADY:
|
|
63
|
+
return BUNNY_NET_ENDPOINTS.PLAYREADY_LICENSE(libraryId, videoId);
|
|
64
|
+
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(`Unsupported DRM type for Bunny.net: ${drmType}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get certificate URL (FairPlay only)
|
|
72
|
+
*/
|
|
73
|
+
static getCertificateUrl(libraryId: string): string {
|
|
74
|
+
return BUNNY_NET_ENDPOINTS.FAIRPLAY_CERTIFICATE(libraryId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate Bunny.net DRM configuration
|
|
79
|
+
*/
|
|
80
|
+
static validateConfig(config: BunnyNetConfig): { valid: boolean; error?: string } {
|
|
81
|
+
if (!config.libraryId || !config.videoId) {
|
|
82
|
+
return {
|
|
83
|
+
valid: false,
|
|
84
|
+
error: 'Bunny.net DRM requires libraryId and videoId',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!/^\d+$/.test(config.libraryId)) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: 'libraryId must be a numeric string',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { valid: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Auto-detect if a DRM config is for Bunny.net
|
|
100
|
+
*/
|
|
101
|
+
static isBunnyNetConfig(config: DRMConfig): boolean {
|
|
102
|
+
return !!(
|
|
103
|
+
config.licenseUrl?.includes('bunnycdn.com') ||
|
|
104
|
+
config.certificateUrl?.includes('bunnycdn.com') ||
|
|
105
|
+
config.fairplayOptions?.licenseUrl?.includes('bunnycdn.com') ||
|
|
106
|
+
config.fairplayOptions?.certificateUrl?.includes('bunnycdn.com') ||
|
|
107
|
+
config.widevineOptions?.licenseUrl?.includes('bunnycdn.com') ||
|
|
108
|
+
config.playreadyOptions?.licenseUrl?.includes('bunnycdn.com')
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract library and video IDs from Bunny.net URLs
|
|
114
|
+
*/
|
|
115
|
+
static extractIdsFromUrl(url: string): { libraryId?: string; videoId?: string } {
|
|
116
|
+
if (!url) return {};
|
|
117
|
+
|
|
118
|
+
const widevineMatch = url.match(/WidevineLicense\/([^/]+)\/([^/?]+)/);
|
|
119
|
+
const fairplayLicenseMatch = url.match(/FairPlay\/([^/]+)\/license.*videoId=([^&]+)/);
|
|
120
|
+
const fairplayCertMatch = url.match(/FairPlay\/([^/]+)\/certificate/);
|
|
121
|
+
const playreadyMatch = url.match(/PlayReadyLicense\/([^/]+)\/([^/?]+)/);
|
|
122
|
+
|
|
123
|
+
if (widevineMatch) {
|
|
124
|
+
return { libraryId: widevineMatch[1], videoId: widevineMatch[2] };
|
|
125
|
+
}
|
|
126
|
+
if (fairplayLicenseMatch) {
|
|
127
|
+
return { libraryId: fairplayLicenseMatch[1], videoId: fairplayLicenseMatch[2] };
|
|
128
|
+
}
|
|
129
|
+
if (fairplayCertMatch) {
|
|
130
|
+
return { libraryId: fairplayCertMatch[1] };
|
|
131
|
+
}
|
|
132
|
+
if (playreadyMatch) {
|
|
133
|
+
return { libraryId: playreadyMatch[1], videoId: playreadyMatch[2] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create multi-DRM config for Bunny.net (auto-detects browser)
|
|
141
|
+
*/
|
|
142
|
+
static createMultiDRMConfig(libraryId: string, videoId: string): ExtendedDRMConfig {
|
|
143
|
+
// Primary Widevine config with FairPlay fallback
|
|
144
|
+
const config: ExtendedDRMConfig = {
|
|
145
|
+
type: DRMType.WIDEVINE,
|
|
146
|
+
licenseUrl: this.getLicenseUrl(DRMType.WIDEVINE, libraryId, videoId),
|
|
147
|
+
provider: 'bunny',
|
|
148
|
+
libraryId,
|
|
149
|
+
videoId,
|
|
150
|
+
|
|
151
|
+
// Widevine options
|
|
152
|
+
widevineOptions: {
|
|
153
|
+
licenseUrl: this.getLicenseUrl(DRMType.WIDEVINE, libraryId, videoId),
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// FairPlay options (for Safari fallback)
|
|
157
|
+
fairplayOptions: {
|
|
158
|
+
certificateUrl: this.getCertificateUrl(libraryId),
|
|
159
|
+
licenseUrl: this.getLicenseUrl(DRMType.FAIRPLAY, libraryId, videoId),
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// PlayReady options (for Edge fallback)
|
|
163
|
+
playreadyOptions: {
|
|
164
|
+
licenseUrl: this.getLicenseUrl(DRMType.PLAYREADY, libraryId, videoId),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return config;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic DRM Provider
|
|
3
|
+
* Helper utilities for generic DRM configurations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DRMType, DRMConfig } from '@unified-video/core';
|
|
7
|
+
import { ExtendedDRMConfig } from '../types/DRMTypes';
|
|
8
|
+
|
|
9
|
+
export class GenericProvider {
|
|
10
|
+
/**
|
|
11
|
+
* Create generic DRM config with sensible defaults
|
|
12
|
+
*/
|
|
13
|
+
static createDRMConfig(config: {
|
|
14
|
+
drmType: DRMType;
|
|
15
|
+
licenseUrl: string;
|
|
16
|
+
certificateUrl?: string;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
customData?: string;
|
|
19
|
+
}): ExtendedDRMConfig {
|
|
20
|
+
const drmConfig: ExtendedDRMConfig = {
|
|
21
|
+
type: config.drmType,
|
|
22
|
+
licenseUrl: config.licenseUrl,
|
|
23
|
+
certificateUrl: config.certificateUrl,
|
|
24
|
+
headers: config.headers,
|
|
25
|
+
customData: config.customData,
|
|
26
|
+
provider: 'generic',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Add type-specific options
|
|
30
|
+
switch (config.drmType) {
|
|
31
|
+
case DRMType.FAIRPLAY:
|
|
32
|
+
if (config.certificateUrl) {
|
|
33
|
+
drmConfig.fairplayOptions = {
|
|
34
|
+
certificateUrl: config.certificateUrl,
|
|
35
|
+
licenseUrl: config.licenseUrl,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
|
|
40
|
+
case DRMType.WIDEVINE:
|
|
41
|
+
drmConfig.widevineOptions = {
|
|
42
|
+
licenseUrl: config.licenseUrl,
|
|
43
|
+
};
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case DRMType.PLAYREADY:
|
|
47
|
+
drmConfig.playreadyOptions = {
|
|
48
|
+
licenseUrl: config.licenseUrl,
|
|
49
|
+
customData: config.customData,
|
|
50
|
+
};
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return drmConfig;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validate generic DRM configuration
|
|
59
|
+
*/
|
|
60
|
+
static validateConfig(config: DRMConfig): { valid: boolean; error?: string } {
|
|
61
|
+
if (!config.type) {
|
|
62
|
+
return {
|
|
63
|
+
valid: false,
|
|
64
|
+
error: 'DRM type is required',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!config.licenseUrl) {
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
error: 'License URL is required',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// FairPlay requires certificate URL
|
|
76
|
+
if (config.type === DRMType.FAIRPLAY) {
|
|
77
|
+
const certificateUrl = config.certificateUrl || config.fairplayOptions?.certificateUrl;
|
|
78
|
+
if (!certificateUrl) {
|
|
79
|
+
return {
|
|
80
|
+
valid: false,
|
|
81
|
+
error: 'FairPlay requires certificateUrl',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate URL format
|
|
87
|
+
try {
|
|
88
|
+
new URL(config.licenseUrl);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return {
|
|
91
|
+
valid: false,
|
|
92
|
+
error: 'Invalid license URL format',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (config.certificateUrl) {
|
|
97
|
+
try {
|
|
98
|
+
new URL(config.certificateUrl);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
error: 'Invalid certificate URL format',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { valid: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create multi-DRM config for generic provider
|
|
112
|
+
*/
|
|
113
|
+
static createMultiDRMConfig(config: {
|
|
114
|
+
widevine: { licenseUrl: string };
|
|
115
|
+
fairplay: { licenseUrl: string; certificateUrl: string };
|
|
116
|
+
playready?: { licenseUrl: string; customData?: string };
|
|
117
|
+
headers?: Record<string, string>;
|
|
118
|
+
}): ExtendedDRMConfig {
|
|
119
|
+
// Primary Widevine config with FairPlay and PlayReady fallbacks
|
|
120
|
+
const drmConfig: ExtendedDRMConfig = {
|
|
121
|
+
type: DRMType.WIDEVINE,
|
|
122
|
+
licenseUrl: config.widevine.licenseUrl,
|
|
123
|
+
provider: 'generic',
|
|
124
|
+
headers: config.headers,
|
|
125
|
+
|
|
126
|
+
// Widevine options
|
|
127
|
+
widevineOptions: {
|
|
128
|
+
licenseUrl: config.widevine.licenseUrl,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// FairPlay options
|
|
132
|
+
fairplayOptions: {
|
|
133
|
+
certificateUrl: config.fairplay.certificateUrl,
|
|
134
|
+
licenseUrl: config.fairplay.licenseUrl,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// PlayReady options (optional)
|
|
138
|
+
playreadyOptions: config.playready
|
|
139
|
+
? {
|
|
140
|
+
licenseUrl: config.playready.licenseUrl,
|
|
141
|
+
customData: config.playready.customData,
|
|
142
|
+
}
|
|
143
|
+
: undefined,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return drmConfig;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base DRM System
|
|
3
|
+
* Abstract base class for all DRM system implementations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ExtendedDRMConfig, LicenseRequest, LicenseResponse } from '../types/DRMTypes';
|
|
7
|
+
import { LicenseRequestHandler } from '../utils/LicenseRequestHandler';
|
|
8
|
+
|
|
9
|
+
export abstract class BaseDRM {
|
|
10
|
+
protected videoElement: HTMLVideoElement;
|
|
11
|
+
protected config: ExtendedDRMConfig;
|
|
12
|
+
protected debug: boolean;
|
|
13
|
+
protected licenseHandler: LicenseRequestHandler;
|
|
14
|
+
protected mediaKeys: MediaKeys | null = null;
|
|
15
|
+
protected keySession: MediaKeySession | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(videoElement: HTMLVideoElement, config: ExtendedDRMConfig, debug: boolean = false) {
|
|
18
|
+
this.videoElement = videoElement;
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.debug = debug;
|
|
21
|
+
this.licenseHandler = new LicenseRequestHandler(config, debug);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize DRM system
|
|
26
|
+
*/
|
|
27
|
+
abstract initialize(): Promise<void>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get key system identifier
|
|
31
|
+
*/
|
|
32
|
+
abstract getKeySystem(): string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get HLS.js configuration
|
|
36
|
+
*/
|
|
37
|
+
abstract getHLSConfig(): any;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get dash.js protection data
|
|
41
|
+
*/
|
|
42
|
+
abstract getDashProtectionData(): any;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cleanup resources
|
|
46
|
+
*/
|
|
47
|
+
async destroy(): Promise<void> {
|
|
48
|
+
if (this.keySession) {
|
|
49
|
+
try {
|
|
50
|
+
await this.keySession.close();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
this.log('Error closing key session:', error);
|
|
53
|
+
}
|
|
54
|
+
this.keySession = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.mediaKeys = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Debug logging
|
|
62
|
+
*/
|
|
63
|
+
protected log(...args: any[]): void {
|
|
64
|
+
if (this.debug) {
|
|
65
|
+
console.log(`[${this.constructor.name}]`, ...args);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FairPlay DRM Implementation
|
|
3
|
+
* Implements FairPlay DRM for Safari/iOS using WebKit EME APIs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BaseDRM } from './BaseDRM';
|
|
7
|
+
import { KEY_SYSTEMS, DRMErrorCode } from '../types/DRMTypes';
|
|
8
|
+
import { DRMType } from '@unified-video/core';
|
|
9
|
+
import { CertificateManager } from '../utils/CertificateManager';
|
|
10
|
+
import { DRMErrorHandler } from '../utils/DRMErrorHandler';
|
|
11
|
+
|
|
12
|
+
// WebKit-specific types
|
|
13
|
+
declare global {
|
|
14
|
+
interface HTMLVideoElement {
|
|
15
|
+
webkitSetMediaKeys?: (mediaKeys: any) => void;
|
|
16
|
+
webkitKeys?: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Window {
|
|
20
|
+
WebKitMediaKeys?: any;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class FairPlayDRM extends BaseDRM {
|
|
25
|
+
private certificateManager: CertificateManager;
|
|
26
|
+
private serverCertificate: ArrayBuffer | null = null;
|
|
27
|
+
private encryptedEventHandler: ((event: any) => Promise<void>) | null = null;
|
|
28
|
+
private needKeyEventHandler: ((event: any) => Promise<void>) | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(videoElement: HTMLVideoElement, config: any, debug: boolean = false) {
|
|
31
|
+
super(videoElement, config, debug);
|
|
32
|
+
this.certificateManager = CertificateManager.getInstance();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async initialize(): Promise<void> {
|
|
36
|
+
this.log('Initializing FairPlay DRM...');
|
|
37
|
+
|
|
38
|
+
// Load server certificate
|
|
39
|
+
await this.loadCertificate();
|
|
40
|
+
|
|
41
|
+
// Set up encrypted event listeners
|
|
42
|
+
this.setupEncryptedEventListeners();
|
|
43
|
+
|
|
44
|
+
this.log('FairPlay DRM initialized');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getKeySystem(): string {
|
|
48
|
+
return KEY_SYSTEMS[DRMType.FAIRPLAY][0];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getHLSConfig(): any {
|
|
52
|
+
this.log('Generating HLS.js config for FairPlay');
|
|
53
|
+
|
|
54
|
+
const certificateUrl = this.config.certificateUrl || this.config.fairplayOptions?.certificateUrl;
|
|
55
|
+
const licenseUrl = this.config.licenseUrl || this.config.fairplayOptions?.licenseUrl;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
emeEnabled: true,
|
|
59
|
+
drmSystems: {
|
|
60
|
+
[this.getKeySystem()]: {
|
|
61
|
+
licenseUrl: licenseUrl,
|
|
62
|
+
serverCertificateUrl: certificateUrl,
|
|
63
|
+
serverCertificate: this.serverCertificate,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
requestMediaKeySystemAccessFunc: this.createMediaKeySystemAccess.bind(this),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getDashProtectionData(): any {
|
|
71
|
+
this.log('Generating dash.js protection data for FairPlay');
|
|
72
|
+
|
|
73
|
+
const licenseUrl = this.config.licenseUrl || this.config.fairplayOptions?.licenseUrl;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
[this.getKeySystem()]: {
|
|
77
|
+
serverURL: licenseUrl,
|
|
78
|
+
serverCertificate: this.serverCertificate,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async destroy(): Promise<void> {
|
|
84
|
+
// Remove event listeners
|
|
85
|
+
if (this.encryptedEventHandler) {
|
|
86
|
+
this.videoElement.removeEventListener('encrypted', this.encryptedEventHandler as any);
|
|
87
|
+
this.encryptedEventHandler = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.needKeyEventHandler) {
|
|
91
|
+
this.videoElement.removeEventListener('webkitneedkey', this.needKeyEventHandler as any);
|
|
92
|
+
this.needKeyEventHandler = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await super.destroy();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async loadCertificate(): Promise<void> {
|
|
99
|
+
const certificateUrl = this.config.certificateUrl || this.config.fairplayOptions?.certificateUrl;
|
|
100
|
+
|
|
101
|
+
if (!certificateUrl) {
|
|
102
|
+
throw DRMErrorHandler.createError(
|
|
103
|
+
DRMErrorCode.CONFIGURATION_ERROR,
|
|
104
|
+
'FairPlay certificate URL not provided',
|
|
105
|
+
true
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.log('Loading FairPlay certificate from:', certificateUrl);
|
|
110
|
+
|
|
111
|
+
// Check cache first
|
|
112
|
+
const cached = this.certificateManager.getCertificate(certificateUrl);
|
|
113
|
+
if (cached) {
|
|
114
|
+
this.log('Using cached certificate');
|
|
115
|
+
this.serverCertificate = cached;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fetch certificate
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(certificateUrl);
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.serverCertificate = await response.arrayBuffer();
|
|
127
|
+
|
|
128
|
+
// Cache for future use
|
|
129
|
+
this.certificateManager.setCertificate(certificateUrl, this.serverCertificate);
|
|
130
|
+
|
|
131
|
+
this.log('FairPlay certificate loaded, size:', this.serverCertificate.byteLength, 'bytes');
|
|
132
|
+
} catch (error: any) {
|
|
133
|
+
throw DRMErrorHandler.createError(
|
|
134
|
+
DRMErrorCode.CERTIFICATE_LOAD_FAILED,
|
|
135
|
+
`Failed to load FairPlay certificate: ${error.message}`,
|
|
136
|
+
true,
|
|
137
|
+
error
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private setupEncryptedEventListeners(): void {
|
|
143
|
+
// Modern EME encrypted event
|
|
144
|
+
this.encryptedEventHandler = this.onEncrypted.bind(this);
|
|
145
|
+
this.videoElement.addEventListener('encrypted', this.encryptedEventHandler as any);
|
|
146
|
+
|
|
147
|
+
// Legacy WebKit needkey event
|
|
148
|
+
this.needKeyEventHandler = this.onWebKitNeedKey.bind(this);
|
|
149
|
+
this.videoElement.addEventListener('webkitneedkey', this.needKeyEventHandler as any);
|
|
150
|
+
|
|
151
|
+
this.log('FairPlay encrypted event listeners set up');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async onEncrypted(event: any): Promise<void> {
|
|
155
|
+
this.log('Encrypted event received:', event);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const initData = event.initData;
|
|
159
|
+
const initDataType = event.initDataType;
|
|
160
|
+
|
|
161
|
+
await this.createSession(initData, initDataType);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.log('Error handling encrypted event:', error);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async onWebKitNeedKey(event: any): Promise<void> {
|
|
169
|
+
this.log('WebKit needkey event received');
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const initData = event.initData;
|
|
173
|
+
await this.createSession(initData, 'skd');
|
|
174
|
+
} catch (error) {
|
|
175
|
+
this.log('Error handling webkitneedkey event:', error);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async createSession(initData: ArrayBuffer, initDataType: string): Promise<void> {
|
|
181
|
+
// Create media keys if not already created
|
|
182
|
+
if (!this.mediaKeys) {
|
|
183
|
+
try {
|
|
184
|
+
const keySystemAccess = await navigator.requestMediaKeySystemAccess(
|
|
185
|
+
this.getKeySystem(),
|
|
186
|
+
this.getKeySystemConfiguration()
|
|
187
|
+
);
|
|
188
|
+
this.mediaKeys = await keySystemAccess.createMediaKeys();
|
|
189
|
+
await this.videoElement.setMediaKeys(this.mediaKeys);
|
|
190
|
+
|
|
191
|
+
this.log('FairPlay MediaKeys created and set');
|
|
192
|
+
} catch (error: any) {
|
|
193
|
+
throw DRMErrorHandler.createError(
|
|
194
|
+
DRMErrorCode.MEDIA_KEYS_CREATION_FAILED,
|
|
195
|
+
`Failed to create FairPlay MediaKeys: ${error.message}`,
|
|
196
|
+
true,
|
|
197
|
+
error
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Create session
|
|
203
|
+
try {
|
|
204
|
+
this.keySession = this.mediaKeys.createSession();
|
|
205
|
+
this.log('FairPlay session created');
|
|
206
|
+
|
|
207
|
+
// Handle license request
|
|
208
|
+
this.keySession.addEventListener('message', async (event: any) => {
|
|
209
|
+
await this.onMessage(event);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Generate license request
|
|
213
|
+
await this.keySession.generateRequest(initDataType, initData);
|
|
214
|
+
} catch (error: any) {
|
|
215
|
+
throw DRMErrorHandler.createError(
|
|
216
|
+
DRMErrorCode.SESSION_CREATION_FAILED,
|
|
217
|
+
`Failed to create FairPlay session: ${error.message}`,
|
|
218
|
+
true,
|
|
219
|
+
error
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async onMessage(event: any): Promise<void> {
|
|
225
|
+
this.log('FairPlay message event:', event.messageType);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const message = event.message;
|
|
229
|
+
|
|
230
|
+
// Convert message to base64 for FairPlay license request
|
|
231
|
+
const messageBase64 = this.arrayBufferToBase64(message);
|
|
232
|
+
|
|
233
|
+
const licenseUrl = this.config.licenseUrl || this.config.fairplayOptions?.licenseUrl;
|
|
234
|
+
if (!licenseUrl) {
|
|
235
|
+
throw DRMErrorHandler.createError(
|
|
236
|
+
DRMErrorCode.CONFIGURATION_ERROR,
|
|
237
|
+
'FairPlay license URL not provided',
|
|
238
|
+
true
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Request license from server
|
|
243
|
+
const license = await this.licenseHandler.requestLicense({
|
|
244
|
+
url: licenseUrl,
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers: {
|
|
247
|
+
'Content-Type': 'application/octet-stream',
|
|
248
|
+
...this.config.headers,
|
|
249
|
+
},
|
|
250
|
+
body: message,
|
|
251
|
+
responseType: 'arraybuffer',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Update session with license (CKC - Content Key Context)
|
|
255
|
+
await this.keySession!.update(license.license);
|
|
256
|
+
|
|
257
|
+
this.log('FairPlay license applied successfully');
|
|
258
|
+
} catch (error: any) {
|
|
259
|
+
throw DRMErrorHandler.createError(
|
|
260
|
+
DRMErrorCode.LICENSE_REQUEST_FAILED,
|
|
261
|
+
`Failed to process FairPlay license: ${error.message}`,
|
|
262
|
+
true,
|
|
263
|
+
error
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private getKeySystemConfiguration(): MediaKeySystemConfiguration[] {
|
|
269
|
+
return [{
|
|
270
|
+
initDataTypes: ['sinf', 'skd'],
|
|
271
|
+
audioCapabilities: [{
|
|
272
|
+
contentType: 'audio/mp4; codecs="mp4a.40.2"',
|
|
273
|
+
}],
|
|
274
|
+
videoCapabilities: [
|
|
275
|
+
{
|
|
276
|
+
contentType: 'application/vnd.apple.mpegurl',
|
|
277
|
+
robustness: 'SW_SECURE_CRYPTO',
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
contentType: 'video/mp4; codecs="avc1.42E01E"',
|
|
281
|
+
robustness: 'SW_SECURE_CRYPTO',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
distinctiveIdentifier: 'not-allowed',
|
|
285
|
+
persistentState: 'not-allowed',
|
|
286
|
+
sessionTypes: ['temporary'],
|
|
287
|
+
}];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async createMediaKeySystemAccess(): Promise<MediaKeySystemAccess> {
|
|
291
|
+
return navigator.requestMediaKeySystemAccess(
|
|
292
|
+
this.getKeySystem(),
|
|
293
|
+
this.getKeySystemConfiguration()
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
298
|
+
const bytes = new Uint8Array(buffer);
|
|
299
|
+
let binary = '';
|
|
300
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
301
|
+
binary += String.fromCharCode(bytes[i]);
|
|
302
|
+
}
|
|
303
|
+
return btoa(binary);
|
|
304
|
+
}
|
|
305
|
+
}
|