@tegis/player 0.1.0 → 0.1.1
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/CHANGELOG.md +6 -1
- package/dist/bundle-entry.js +33 -0
- package/dist/index.js +216 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@tegis/player` are documented here. This project follows [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
-
## [0.1.
|
|
5
|
+
## [0.1.1]
|
|
6
|
+
|
|
7
|
+
- **Fix:** `0.1.0` shipped a non-bundled stub `dist/index.js` (a newer `bun build` regressed bundling); the
|
|
8
|
+
entry is now a verified self-contained bundle. Use `>=0.1.1` — `0.1.0` is broken.
|
|
9
|
+
|
|
10
|
+
## [0.1.0]
|
|
6
11
|
|
|
7
12
|
Initial extraction from the Tegis reference SDK (formerly the private `@aegis/sdk`).
|
|
8
13
|
|
package/dist/bundle-entry.js
CHANGED
|
@@ -156,6 +156,39 @@ class TegisPlayer {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// src/handshake-wasm.ts
|
|
159
|
+
async function loadWasmHandshake(secret, wasmBytes) {
|
|
160
|
+
const { instance } = await WebAssembly.instantiate(wasmBytes, {
|
|
161
|
+
env: { abort: () => {
|
|
162
|
+
throw new Error("wasm abort");
|
|
163
|
+
} }
|
|
164
|
+
});
|
|
165
|
+
const ex = instance.exports;
|
|
166
|
+
const te2 = new TextEncoder;
|
|
167
|
+
const mem = () => new Uint8Array(ex.memory.buffer);
|
|
168
|
+
function write(data) {
|
|
169
|
+
const p = ex.alloc(data.length);
|
|
170
|
+
mem().set(data, p);
|
|
171
|
+
return p;
|
|
172
|
+
}
|
|
173
|
+
function sha256(data) {
|
|
174
|
+
const p = write(data);
|
|
175
|
+
const out = ex.alloc(32);
|
|
176
|
+
ex.sha256(p, data.length, out);
|
|
177
|
+
return mem().slice(out, out + 32);
|
|
178
|
+
}
|
|
179
|
+
function hmac(key, msg) {
|
|
180
|
+
const kp = write(key);
|
|
181
|
+
const mp = write(msg);
|
|
182
|
+
const out = ex.alloc(32);
|
|
183
|
+
ex.hmac(kp, key.length, mp, msg.length, out);
|
|
184
|
+
return mem().slice(out, out + 32);
|
|
185
|
+
}
|
|
186
|
+
return async (att, ent, nonce, t) => {
|
|
187
|
+
const entDigest = b64u(sha256(te2.encode(ent)));
|
|
188
|
+
const msg = te2.encode(`${att}.${entDigest}.${nonce}.${t}`);
|
|
189
|
+
return b64u(hmac(secret, msg));
|
|
190
|
+
};
|
|
191
|
+
}
|
|
159
192
|
async function loadWhitenedHandshake(wasmBytes) {
|
|
160
193
|
const { instance } = await WebAssembly.instantiate(wasmBytes, {
|
|
161
194
|
env: { abort: () => {
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,219 @@
|
|
|
1
|
+
// src/crypto.ts
|
|
2
|
+
var te = new TextEncoder;
|
|
3
|
+
function b64u(buf) {
|
|
4
|
+
const b = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
5
|
+
let s = "";
|
|
6
|
+
for (const x of b)
|
|
7
|
+
s += String.fromCharCode(x);
|
|
8
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
9
|
+
}
|
|
10
|
+
function unb64u(s) {
|
|
11
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
12
|
+
while (s.length % 4)
|
|
13
|
+
s += "=";
|
|
14
|
+
const bin = atob(s);
|
|
15
|
+
const out = new Uint8Array(bin.length);
|
|
16
|
+
for (let i = 0;i < bin.length; i++)
|
|
17
|
+
out[i] = bin.charCodeAt(i);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
async function hmacSha256(secret, msg) {
|
|
21
|
+
const key = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
22
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", key, te.encode(msg)));
|
|
23
|
+
}
|
|
24
|
+
async function sha256b64u(s) {
|
|
25
|
+
return b64u(await crypto.subtle.digest("SHA-256", te.encode(s)));
|
|
26
|
+
}
|
|
27
|
+
async function handshake(secret, att, ent, nonce, t) {
|
|
28
|
+
const d = await sha256b64u(ent);
|
|
29
|
+
return b64u(await hmacSha256(secret, `${att}.${d}.${nonce}.${t}`));
|
|
30
|
+
}
|
|
31
|
+
async function hbSign(hbKeyB64u, hbJSON) {
|
|
32
|
+
return b64u(await hmacSha256(unb64u(hbKeyB64u), hbJSON));
|
|
33
|
+
}
|
|
34
|
+
async function decryptSegment(keyRaw, blob) {
|
|
35
|
+
const iv = blob.slice(0, 16);
|
|
36
|
+
const ct = blob.slice(16);
|
|
37
|
+
const key = await crypto.subtle.importKey("raw", keyRaw, { name: "AES-CTR" }, false, ["decrypt"]);
|
|
38
|
+
const pt = await crypto.subtle.decrypt({ name: "AES-CTR", counter: iv, length: 128 }, key, ct);
|
|
39
|
+
return new Uint8Array(pt);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/player.ts
|
|
43
|
+
function randHex(n) {
|
|
44
|
+
const b = new Uint8Array(n);
|
|
45
|
+
crypto.getRandomValues(b);
|
|
46
|
+
return [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class TegisPlayer {
|
|
50
|
+
cfg;
|
|
51
|
+
att;
|
|
52
|
+
attSes;
|
|
53
|
+
constructor(cfg) {
|
|
54
|
+
this.cfg = cfg;
|
|
55
|
+
}
|
|
56
|
+
get f() {
|
|
57
|
+
return this.cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
58
|
+
}
|
|
59
|
+
hdr(extra = {}) {
|
|
60
|
+
const h = { "content-type": "application/json", ...extra };
|
|
61
|
+
if (this.cfg.demoHeaders) {
|
|
62
|
+
h["x-aegis-tenant"] = this.cfg.tid;
|
|
63
|
+
if (this.cfg.clientIp)
|
|
64
|
+
h["x-aegis-client-ip"] = this.cfg.clientIp;
|
|
65
|
+
}
|
|
66
|
+
return h;
|
|
67
|
+
}
|
|
68
|
+
async post(path, body) {
|
|
69
|
+
const r = await this.f(this.cfg.mint + path, { method: "POST", headers: this.hdr(), body: JSON.stringify(body) });
|
|
70
|
+
return { status: r.status, json: await r.json().catch(() => ({})) };
|
|
71
|
+
}
|
|
72
|
+
handshake(att, ent, nonce, t) {
|
|
73
|
+
return (this.cfg.handshakeFn ?? ((a, e, n, tt) => handshake(this.cfg.handshakeSecret, a, e, n, tt)))(att, ent, nonce, t);
|
|
74
|
+
}
|
|
75
|
+
async prewarm(opts = {}) {
|
|
76
|
+
const ses = opts.ses ?? "ses_" + randHex(4);
|
|
77
|
+
const body = { ses, fph: opts.fph ?? "fp_" + ses };
|
|
78
|
+
if (opts.solution) {
|
|
79
|
+
body.nonce = opts.nonce;
|
|
80
|
+
body.solution = opts.solution;
|
|
81
|
+
}
|
|
82
|
+
if (opts.token)
|
|
83
|
+
body.token = opts.token;
|
|
84
|
+
const r = await this.post("/attest/v1/verify", body);
|
|
85
|
+
if (!r.json.att)
|
|
86
|
+
throw new Error("attestation failed: " + JSON.stringify(r.json));
|
|
87
|
+
this.att = r.json.att;
|
|
88
|
+
this.attSes = ses;
|
|
89
|
+
return this.att;
|
|
90
|
+
}
|
|
91
|
+
async mint(opts) {
|
|
92
|
+
const ses = this.attSes ?? opts.ses ?? "ses_" + randHex(4);
|
|
93
|
+
if (!this.att)
|
|
94
|
+
await this.prewarm({ ses, fph: opts.fph, token: opts.token });
|
|
95
|
+
const nonce = (await this.post("/mint/v1/nonce", { ses })).json.nonce;
|
|
96
|
+
const t = Math.floor(Date.now() / 1000);
|
|
97
|
+
const hs = await this.handshake(this.att, opts.entitlement, nonce, t);
|
|
98
|
+
const r = await this.post("/mint/v1", { assetId: opts.assetId, att: this.att, entitlement: opts.entitlement, nonce, handshake: hs, t });
|
|
99
|
+
if (r.status !== 200)
|
|
100
|
+
throw new Error("mint failed: " + r.status + " " + JSON.stringify(r.json));
|
|
101
|
+
return r.json;
|
|
102
|
+
}
|
|
103
|
+
async contentKey(assetId) {
|
|
104
|
+
const r = await this.f(`${this.cfg.mint}/key/v1/${assetId}?att=${this.att}`, { headers: this.hdr() });
|
|
105
|
+
if (r.status !== 200)
|
|
106
|
+
throw new Error("key fetch failed: " + r.status);
|
|
107
|
+
return unb64u((await r.json()).key);
|
|
108
|
+
}
|
|
109
|
+
async fetchBytes(url) {
|
|
110
|
+
const r = await this.f(url.startsWith("http") ? url : this.cfg.edge + url, { headers: this.hdr() });
|
|
111
|
+
if (r.status !== 200)
|
|
112
|
+
throw new Error("fetch failed " + r.status + ": " + url);
|
|
113
|
+
return new Uint8Array(await r.arrayBuffer());
|
|
114
|
+
}
|
|
115
|
+
async decryptedSegment(assetId, url, key) {
|
|
116
|
+
const k = key ?? await this.contentKey(assetId);
|
|
117
|
+
return decryptSegment(k, await this.fetchBytes(url));
|
|
118
|
+
}
|
|
119
|
+
async renew(playbackId, hbKeyB64u, progress) {
|
|
120
|
+
const hb = { pbk: playbackId, pos: progress.pos, seq: progress.seq, state: "playing", iat: Math.floor(Date.now() / 1000) };
|
|
121
|
+
const sig = await hbSign(hbKeyB64u, JSON.stringify(hb));
|
|
122
|
+
const r = await this.post("/mint/v1/renew", { playbackId, heartbeat: hb, sig });
|
|
123
|
+
if (r.status !== 200)
|
|
124
|
+
throw new Error("renew failed: " + r.status);
|
|
125
|
+
return r.json;
|
|
126
|
+
}
|
|
127
|
+
async play(video, opts) {
|
|
128
|
+
if (typeof MediaSource === "undefined")
|
|
129
|
+
throw new Error("MSE unavailable in this environment");
|
|
130
|
+
const g = await this.mint(opts);
|
|
131
|
+
const key = await this.contentKey(opts.assetId);
|
|
132
|
+
const ms = new MediaSource;
|
|
133
|
+
video.src = URL.createObjectURL(ms);
|
|
134
|
+
await new Promise((res) => ms.addEventListener("sourceopen", () => res(), { once: true }));
|
|
135
|
+
const mime = opts.mime ?? 'video/mp4; codecs="avc1.4d401e, mp4a.40.2"';
|
|
136
|
+
const sb = ms.addSourceBuffer(mime);
|
|
137
|
+
const append = (buf) => new Promise((res, rej) => {
|
|
138
|
+
sb.addEventListener("updateend", () => res(), { once: true });
|
|
139
|
+
sb.addEventListener("error", (e) => rej(e), { once: true });
|
|
140
|
+
sb.appendBuffer(buf);
|
|
141
|
+
});
|
|
142
|
+
await append(await this.fetchBytes(g.init));
|
|
143
|
+
for (const url of g.manifest) {
|
|
144
|
+
let seg;
|
|
145
|
+
try {
|
|
146
|
+
seg = await this.decryptedSegment(opts.assetId, url, key);
|
|
147
|
+
} catch {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
await append(seg);
|
|
151
|
+
}
|
|
152
|
+
ms.endOfStream();
|
|
153
|
+
await video.play().catch(() => {});
|
|
154
|
+
return g;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// src/handshake-wasm.ts
|
|
158
|
+
async function loadWasmHandshake(secret, wasmBytes) {
|
|
159
|
+
const { instance } = await WebAssembly.instantiate(wasmBytes, {
|
|
160
|
+
env: { abort: () => {
|
|
161
|
+
throw new Error("wasm abort");
|
|
162
|
+
} }
|
|
163
|
+
});
|
|
164
|
+
const ex = instance.exports;
|
|
165
|
+
const te2 = new TextEncoder;
|
|
166
|
+
const mem = () => new Uint8Array(ex.memory.buffer);
|
|
167
|
+
function write(data) {
|
|
168
|
+
const p = ex.alloc(data.length);
|
|
169
|
+
mem().set(data, p);
|
|
170
|
+
return p;
|
|
171
|
+
}
|
|
172
|
+
function sha256(data) {
|
|
173
|
+
const p = write(data);
|
|
174
|
+
const out = ex.alloc(32);
|
|
175
|
+
ex.sha256(p, data.length, out);
|
|
176
|
+
return mem().slice(out, out + 32);
|
|
177
|
+
}
|
|
178
|
+
function hmac(key, msg) {
|
|
179
|
+
const kp = write(key);
|
|
180
|
+
const mp = write(msg);
|
|
181
|
+
const out = ex.alloc(32);
|
|
182
|
+
ex.hmac(kp, key.length, mp, msg.length, out);
|
|
183
|
+
return mem().slice(out, out + 32);
|
|
184
|
+
}
|
|
185
|
+
return async (att, ent, nonce, t) => {
|
|
186
|
+
const entDigest = b64u(sha256(te2.encode(ent)));
|
|
187
|
+
const msg = te2.encode(`${att}.${entDigest}.${nonce}.${t}`);
|
|
188
|
+
return b64u(hmac(secret, msg));
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function loadWhitenedHandshake(wasmBytes) {
|
|
192
|
+
const { instance } = await WebAssembly.instantiate(wasmBytes, {
|
|
193
|
+
env: { abort: () => {
|
|
194
|
+
throw new Error("wasm abort");
|
|
195
|
+
} }
|
|
196
|
+
});
|
|
197
|
+
const ex = instance.exports;
|
|
198
|
+
const te2 = new TextEncoder;
|
|
199
|
+
const mem = () => new Uint8Array(ex.memory.buffer);
|
|
200
|
+
function write(data) {
|
|
201
|
+
const p = ex.alloc(data.length);
|
|
202
|
+
mem().set(data, p);
|
|
203
|
+
return p;
|
|
204
|
+
}
|
|
205
|
+
function call(fn, data) {
|
|
206
|
+
const p = write(data);
|
|
207
|
+
const out = ex.alloc(32);
|
|
208
|
+
fn(p, data.length, out);
|
|
209
|
+
return mem().slice(out, out + 32);
|
|
210
|
+
}
|
|
211
|
+
return async (att, ent, nonce, t) => {
|
|
212
|
+
const entDigest = b64u(call(ex.sha256, te2.encode(ent)));
|
|
213
|
+
const msg = te2.encode(`${att}.${entDigest}.${nonce}.${t}`);
|
|
214
|
+
return b64u(call(ex.signKeyed, msg));
|
|
215
|
+
};
|
|
216
|
+
}
|
|
1
217
|
export {
|
|
2
218
|
loadWhitenedHandshake,
|
|
3
219
|
loadWasmHandshake,
|
package/package.json
CHANGED