249 lines
8.8 KiB
JavaScript
249 lines
8.8 KiB
JavaScript
function emitAndWaitForResponse(type, data) {
|
|
return new Promise((resolve) => {
|
|
const requestId = Math.random().toString(16).substring(2, 9);
|
|
|
|
const responseHandler = (event) => {
|
|
const { detail } = event;
|
|
if (detail.substring(0, 7) === requestId) {
|
|
document.removeEventListener('responseReceived', responseHandler);
|
|
resolve(detail.substring(7));
|
|
}
|
|
};
|
|
document.addEventListener('responseReceived', responseHandler);
|
|
|
|
document.dispatchEvent(new CustomEvent('response', {
|
|
detail: {
|
|
type: type,
|
|
body: data,
|
|
requestId: requestId,
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
|
|
function uint8ArrayToBase64(uint8array) {
|
|
return btoa(String.fromCharCode.apply(null, uint8array));
|
|
}
|
|
|
|
function base64toUint8Array(base64_string){
|
|
return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0))
|
|
}
|
|
|
|
function uint8ArrayToString(uint8array) {
|
|
return String.fromCharCode.apply(null, uint8array)
|
|
}
|
|
|
|
function proxy(object, functionName, proxyFunction) {
|
|
if (object.hasOwnProperty(functionName)) {
|
|
const originalFunction = object[functionName];
|
|
object[functionName] = function(...args) {
|
|
return proxyFunction(originalFunction, this, args);
|
|
};
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
if (typeof EventTarget !== 'undefined') {
|
|
proxy(EventTarget.prototype, 'addEventListener', async (original, target, args) => {
|
|
const [eventType, listener] = args;
|
|
|
|
const storeKey = Symbol.for('eventListeners');
|
|
if (!target[storeKey])
|
|
target[storeKey] = {};
|
|
|
|
const store = target[storeKey];
|
|
if (!store[eventType])
|
|
store[eventType] = [];
|
|
|
|
let newListener = listener;
|
|
if (eventType === "message" && listener && !listener._isWrapped && typeof MediaKeyMessageEvent !== 'undefined') {
|
|
newListener = async function(event) {
|
|
if (event instanceof MediaKeyMessageEvent && !event._isCustomEvent) {
|
|
let eventMessage = new Uint8Array(event.message);
|
|
console.log("PROXYSESSION MESSAGE", event.target.sessionId);
|
|
|
|
const challenge = uint8ArrayToBase64(eventMessage);
|
|
|
|
const newChallenge = await emitAndWaitForResponse("REQUEST", `${event.target.sessionId}|${challenge}`);
|
|
console.log("[PlayreadyProxy2]", "REPLACING", challenge, newChallenge);
|
|
|
|
if (challenge !== newChallenge) {
|
|
eventMessage = base64toUint8Array(newChallenge);
|
|
}
|
|
|
|
const newEvent = new MediaKeyMessageEvent('message', {
|
|
isTrusted: event.isTrusted,
|
|
bubbles: event.bubbles,
|
|
cancelBubble: event.cancelBubble,
|
|
composed: event.composed,
|
|
currentTarget: event.currentTarget,
|
|
defaultPrevented: event.defaultPrevented,
|
|
eventPhase: event.eventPhase,
|
|
message: eventMessage.buffer,
|
|
messageType: event.messageType,
|
|
returnValue: event.returnValue,
|
|
srcElement: event.srcElement,
|
|
target: event.target,
|
|
timeStamp: event.timeStamp,
|
|
});
|
|
newEvent._isCustomEvent = true;
|
|
|
|
target.dispatchEvent(newEvent);
|
|
event.stopImmediatePropagation();
|
|
return;
|
|
}
|
|
|
|
if (listener.handleEvent) {
|
|
listener.handleEvent(event);
|
|
} else {
|
|
listener.call(this, event);
|
|
}
|
|
};
|
|
|
|
newListener._isWrapped = true;
|
|
newListener.originalListener = listener;
|
|
}
|
|
|
|
const alreadyAdded = store[eventType].some(
|
|
storedListener => storedListener && storedListener.originalListener === listener
|
|
);
|
|
|
|
if (!alreadyAdded) {
|
|
store[eventType].push(newListener);
|
|
args[1] = newListener;
|
|
}
|
|
|
|
return original.apply(target, args);
|
|
});
|
|
}
|
|
|
|
if (typeof MediaKeySession !== 'undefined') {
|
|
proxy(MediaKeySession.prototype, 'update', async (original, target, args) => {
|
|
const [response] = args;
|
|
console.log("PROXYSESSION UPDATE", target.sessionId);
|
|
|
|
console.log("[PlayreadyProxy2]", "UPDATE", response);
|
|
await emitAndWaitForResponse("RESPONSE", `${target.sessionId}|${uint8ArrayToBase64(new Uint8Array(response))}`);
|
|
|
|
return await original.apply(target, args);
|
|
});
|
|
}
|
|
})();
|
|
|
|
/*
|
|
* I'd love to put this in background.js, but we don't have access to the manifest's content there, and I'm not sending
|
|
* it through the event handler
|
|
* */
|
|
class Evaluator {
|
|
static isDASH(text) {
|
|
return text.includes('<mpd') && text.includes('</mpd>');
|
|
}
|
|
|
|
static isHLS(text) {
|
|
return text.includes('#extm3u');
|
|
}
|
|
|
|
static isHLSMaster(text) {
|
|
return text.includes('#ext-x-stream-inf');
|
|
}
|
|
|
|
static isMSS(text) {
|
|
return text.includes('<smoothstreamingmedia') && text.includes('</smoothstreamingmedia>');
|
|
}
|
|
|
|
static getManifestType(text) {
|
|
const lower = text.toLowerCase();
|
|
if (this.isDASH(lower)) {
|
|
return "DASH";
|
|
} else if (this.isHLS(lower)) {
|
|
if (this.isHLSMaster(lower)) {
|
|
return "HLS_MASTER";
|
|
} else {
|
|
return "HLS_PLAYLIST";
|
|
}
|
|
} else if (this.isMSS(lower)) {
|
|
return "MSS";
|
|
}
|
|
}
|
|
}
|
|
|
|
const originalFetch = window.fetch;
|
|
window.fetch = function() {
|
|
return new Promise(async (resolve, reject) => {
|
|
originalFetch.apply(this, arguments).then((response) => {
|
|
if (response) {
|
|
response.clone().text().then((text) => {
|
|
const manifest_type = Evaluator.getManifestType(text);
|
|
if (manifest_type) {
|
|
if (arguments.length === 1) {
|
|
emitAndWaitForResponse("MANIFEST", JSON.stringify({
|
|
"url": arguments[0].url,
|
|
"type": manifest_type,
|
|
}));
|
|
} else if (arguments.length === 2) {
|
|
emitAndWaitForResponse("MANIFEST", JSON.stringify({
|
|
"url": arguments[0],
|
|
"type": manifest_type,
|
|
}));
|
|
}
|
|
}
|
|
resolve(response);
|
|
}).catch(() => {
|
|
resolve(response);
|
|
})
|
|
} else {
|
|
resolve(response);
|
|
}
|
|
}).catch(() => {
|
|
resolve();
|
|
})
|
|
})
|
|
}
|
|
|
|
const open = XMLHttpRequest.prototype.open;
|
|
XMLHttpRequest.prototype.open = function(method, url) {
|
|
this._method = method;
|
|
return open.apply(this, arguments);
|
|
};
|
|
|
|
const send = XMLHttpRequest.prototype.send;
|
|
XMLHttpRequest.prototype.send = function(postData) {
|
|
this.addEventListener('load', async function() {
|
|
if (this._method === "GET") {
|
|
let body = void 0;
|
|
switch (this.responseType) {
|
|
case "":
|
|
case "text":
|
|
body = this.responseText ?? this.response;
|
|
break;
|
|
case "json":
|
|
// TODO: untested
|
|
body = JSON.stringify(this.response);
|
|
break;
|
|
case "arraybuffer":
|
|
// TODO: untested
|
|
if (this.response.byteLength) {
|
|
const response = new Uint8Array(this.response);
|
|
body = uint8ArrayToString(new Uint8Array([...response.slice(0, 2000), ...response.slice(-2000)]));
|
|
}
|
|
break;
|
|
case "document":
|
|
// todo
|
|
break;
|
|
case "blob":
|
|
body = await this.response.text();
|
|
break;
|
|
}
|
|
if (body) {
|
|
const manifest_type = Evaluator.getManifestType(body);
|
|
if (manifest_type) {
|
|
emitAndWaitForResponse("MANIFEST", JSON.stringify({
|
|
"url": this.responseURL,
|
|
"type": manifest_type,
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return send.apply(this, arguments);
|
|
}; |