-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patheme.js
More file actions
121 lines (106 loc) · 3.78 KB
/
Copy patheme.js
File metadata and controls
121 lines (106 loc) · 3.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// Widevine EME setup: negotiate a MediaKeys session and forward license
// requests to the proxy. Nothing is actually encrypted — the stream is
// flagged as encrypted while every sample is sent marked clear.
import { buildWidevinePsshBox } from './mp4.js?v=drainv1';
const KEY_SYSTEM = 'com.widevine.alpha';
const ROBUSTNESS_CHAIN = ['SW_SECURE_CRYPTO', ''];
export async function setupEme({
video,
licenseUrl,
defaultKid,
fetchLicense,
log = () => {},
}) {
if (!defaultKid || defaultKid.length !== 16) {
throw new Error('setupEme: defaultKid must be a 16-byte Uint8Array');
}
const tag = async (name, fn) => {
try { return await fn(); }
catch (e) {
const errName = e?.name ?? typeof e;
const errMsg = e?.message ?? '';
throw new Error(`eme.${name} failed: ${errName}${errMsg ? ' — ' + errMsg : ''}`);
}
};
const config = [{
initDataTypes: ['cenc'],
videoCapabilities: ROBUSTNESS_CHAIN.map((robustness) => ({
contentType: 'video/mp4; codecs="avc1.42E01E"',
robustness,
})),
persistentState: 'optional',
sessionTypes: ['temporary'],
}];
const access = await tag('requestMediaKeySystemAccess',
() => navigator.requestMediaKeySystemAccess(KEY_SYSTEM, config));
log(` access ok (keySystem=${access.keySystem})`);
const mediaKeys = await tag('createMediaKeys', () => access.createMediaKeys());
log(` mediaKeys ok`);
await tag('setMediaKeys', () => video.setMediaKeys(mediaKeys));
log(` setMediaKeys ok`);
const session = await tag('createSession',
async () => mediaKeys.createSession('temporary'));
log(` createSession ok`);
let discoveredKid = null;
const messagePromise = new Promise((resolve, reject) => {
session.addEventListener('message', async (event) => {
try {
log(` license-request message (${event.message.byteLength} bytes)`);
const raw = await (fetchLicense
? fetchLicense(event.message)
: defaultFetchLicense(licenseUrl, event.message));
// fetchLicense may return either an ArrayBuffer or {body, kid}.
const body = (raw && raw.body) ? raw.body : raw;
if (raw && raw.kid) {
discoveredKid = raw.kid;
const hex = Array.from(discoveredKid)
.map((b) => b.toString(16).padStart(2, '0')).join('');
log(` license carries KID ${hex}`);
}
await session.update(body);
log(` session.update ok`);
resolve();
} catch (err) {
reject(err);
}
}, { once: true });
});
const initData = buildWidevinePsshBox(defaultKid);
await tag('generateRequest',
() => session.generateRequest('cenc', initData));
log(` generateRequest ok (${initData.length}B widevine pssh)`);
await messagePromise;
return { session, discoveredKid };
}
function hexToBytes(hex) {
if (typeof hex !== 'string' || hex.length !== 32) return null;
const out = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
const b = parseInt(hex.substr(i * 2, 2), 16);
if (Number.isNaN(b)) return null;
out[i] = b;
}
return out;
}
async function defaultFetchLicense(licenseUrl, message) {
if (!licenseUrl) {
throw new Error('setupEme: licenseUrl or fetchLicense required');
}
const resp = await fetch(licenseUrl, {
method: 'POST',
body: message,
headers: { 'Content-Type': 'application/octet-stream' },
});
if (!resp.ok) {
throw new Error(`License fetch failed: ${resp.status} ${resp.statusText}`);
}
const kidHeader = resp.headers.get('X-Widevine-Key-IDs');
const body = await resp.arrayBuffer();
// Proxy returns a comma-separated list of hex KIDs; take the first.
let kid = null;
if (kidHeader) {
const firstHex = kidHeader.split(',')[0].trim();
kid = hexToBytes(firstHex);
}
return { body, kid };
}