Add files via upload

This commit is contained in:
YOLOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
2025-02-01 08:22:42 +08:00
committed by GitHub
parent 3bc500dba2
commit 2e84bd28ce
6 changed files with 1214 additions and 0 deletions

284
background.js Normal file
View File

@@ -0,0 +1,284 @@
import {
uint8ArrayToBase64,
SettingsManager,
RemoteCDMManager,
PSSHFromKID,
stringToUTF16LEBytes,
} from "./util.js";
import { RemoteCdm } from "./remote_cdm.js";
let manifests = new Map();
let requests = new Map();
let logs = [];
chrome.webRequest.onBeforeSendHeaders.addListener(
function (details) {
if (details.method === "GET") {
if (!requests.has(details.url)) {
const headers = details.requestHeaders
.filter(
(item) =>
!(
item.name.startsWith("sec-ch-ua") ||
item.name.startsWith("Sec-Fetch") ||
item.name.startsWith("Accept-") ||
item.name.startsWith("Host") ||
item.name === "Connection"
)
)
.reduce((acc, item) => {
acc[item.name] = item.value;
return acc;
}, {});
requests.set(details.url, headers);
}
}
},
{ urls: ["<all_urls>"] },
[
"requestHeaders",
chrome.webRequest.OnSendHeadersOptions.EXTRA_HEADERS,
].filter(Boolean)
);
async function generateChallengeRemote(body, sendResponse) {
try {
// Decode the base64-encoded body into a binary string
const binaryString = decodeBase64(body); // Use the decodeBase64 function
const byteArray = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i);
}
// Decode using UTF-16LE encoding
const decoder = new TextDecoder("utf-16le");
var xmlString = decoder.decode(byteArray);
var xmlDecoded;
// Extract the Challenge element from the XML string
const challengeRegex = /<Challenge[^>]*>([\s\S]*?)<\/Challenge>/i;
const challengeMatch = challengeRegex.exec(xmlString);
var encoding;
if (challengeMatch) {
const challengeContent = challengeMatch[1].trim();
const encodingRegex = /<Challenge[^>]*encoding="([^"]+)"[^>]*>/i;
const encodingMatch = encodingRegex.exec(xmlString);
encoding = encodingMatch ? encodingMatch[1] : null;
// If encoding is base64encoded, decode the challenge content
if (encoding === "base64encoded") {
const challengeBinaryString = decodeBase64(challengeContent); // Use the decodeBase64 function
const challengeByteArray = new Uint8Array(challengeBinaryString.length);
for (let i = 0; i < challengeBinaryString.length; i++) {
challengeByteArray[i] = challengeBinaryString.charCodeAt(i);
}
const utf8Decoder = new TextDecoder("utf-8");
xmlDecoded = utf8Decoder.decode(challengeByteArray);
}
} else {
console.error("Challenge element not found in XML.");
sendResponse(body);
return;
}
// Extract the KID element
const kidRegex = /<KID>([^<]+)<\/KID>/i;
const kidMatch = kidRegex.exec(xmlDecoded);
var kidBase64;
if (kidMatch) {
kidBase64 = kidMatch[1].trim();
} else {
console.log("[PlayreadyProxy]", "NO_KID_IN_CHALLENGE");
sendResponse(body);
return;
}
// Get PSSH from KID
const pssh = PSSHFromKID(kidBase64);
if (!pssh) {
console.log("[PlayreadyProxy]", "NO_PSSH_DATA_IN_CHALLENGE");
sendResponse(body);
return;
}
// Fetch the selected remote CDM and load it
const selected_remote_cdm_name = await RemoteCDMManager.getSelectedRemoteCDM();
if (!selected_remote_cdm_name) {
sendResponse(body);
return;
}
const selected_remote_cdm = await RemoteCDMManager.loadRemoteCDM(selected_remote_cdm_name);
let remoteCdmObj;
try {
// Check if the selected_remote_cdm is Base64-encoded XML
if (selected_remote_cdm.startsWith("PD94bWwgdm")) {
const decodedString = decodeBase64(selected_remote_cdm); // Use the decodeBase64 function
const parser = new DOMParser();
const xmlDoc = parseXML(decodedString); // Use parseXML function
// Convert the XML document into a RemoteCdm object
remoteCdmObj = RemoteCdm.from_xml(xmlDoc);
} else {
// Otherwise, parse as JSON
remoteCdmObj = JSON.parse(selected_remote_cdm);
}
} catch (e) {
console.error("Error parsing remote CDM:", e);
sendResponse(body);
return;
}
const remote_cdm = RemoteCdm.from_object(remoteCdmObj);
// Get the license challenge
const challenge = await remote_cdm.get_license_challenge(pssh);
// Replace the challenge content in the original XML with the new challenge
const newXmlString = xmlString.replace(
/(<Challenge[^>]*>)([\s\S]*?)(<\/Challenge>)/i,
`$1${challenge}$3`
);
// Convert the new XML string to UTF-16LE and then to base64
const utf16leBytes = stringToUTF16LEBytes(newXmlString);
const responseBase64 = uint8ArrayToBase64(utf16leBytes);
// Send the base64-encoded response
sendResponse(responseBase64);
} catch (error) {
console.error("Error in generateChallengeRemote:", error);
sendResponse(body);
}
}
async function parseLicenseRemote(body, sendResponse, tab_url) {
const response = atob(body);
const selected_remote_cdm_name =
await RemoteCDMManager.getSelectedRemoteCDM();
if (!selected_remote_cdm_name) {
sendResponse();
return;
}
const selected_remote_cdm = JSON.parse(
await RemoteCDMManager.loadRemoteCDM(selected_remote_cdm_name)
);
const remote_cdm = RemoteCdm.from_object(selected_remote_cdm);
const returned_keys = await remote_cdm.get_keys(btoa(response));
if (returned_keys.length === 0) {
sendResponse();
return;
}
const keys = returned_keys.map((s) => {
return {
k: s.key,
kid: s.key_id,
};
});
console.log("[PlayreadyProxy]", "KEYS", JSON.stringify(keys), tab_url);
const log = {
type: "PLAYREADY",
keys: keys,
url: tab_url,
timestamp: Math.floor(Date.now() / 1000),
manifests: manifests.has(tab_url) ? manifests.get(tab_url) : [],
};
logs.push(log);
sendResponse();
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
const tab_url = sender.tab ? sender.tab.url : null;
switch (message.type) {
case "REQUEST":
if (!(await SettingsManager.getEnabled())) {
sendResponse(message.body);
manifests.clear();
return;
}
try {
JSON.parse(atob(message.body));
sendResponse(message.body);
return;
} catch {
if (message.body) {
await generateChallengeRemote(
message.body,
sendResponse
);
}
}
break;
case "RESPONSE":
if (!(await SettingsManager.getEnabled())) {
sendResponse(message.body);
manifests.clear();
return;
}
try {
await parseClearKey(message.body, sendResponse, tab_url);
return;
} catch (e) {
await parseLicenseRemote(
message.body,
sendResponse,
tab_url
);
return;
}
case "GET_LOGS":
sendResponse(logs);
break;
case "OPEN_PICKER":
chrome.windows.create({
url: "picker/filePicker.html",
type: "popup",
width: 300,
height: 200,
});
break;
case "CLEAR":
logs = [];
manifests.clear();
break;
case "MANIFEST":
const parsed = JSON.parse(message.body);
const element = {
type: parsed.type,
url: parsed.url,
headers: requests.has(parsed.url)
? requests.get(parsed.url)
: [],
};
if (!manifests.has(tab_url)) {
manifests.set(tab_url, [element]);
} else {
let elements = manifests.get(tab_url);
if (!elements.some((e) => e.url === parsed.url)) {
elements.push(element);
manifests.set(tab_url, elements);
}
}
sendResponse();
}
})();
return true;
});

338
content_script.js Normal file
View File

@@ -0,0 +1,338 @@
function uint8ArrayToBase64(uint8array) {
return btoa(String.fromCharCode.apply(null, uint8array));
}
function uint8ArrayToString(uint8array) {
return String.fromCharCode.apply(null, uint8array);
}
function base64toUint8Array(base64_string) {
return Uint8Array.from(atob(base64_string), (c) => c.charCodeAt(0));
}
function compareUint8Arrays(arr1, arr2) {
if (arr1.length !== arr2.length) return false;
return Array.from(arr1).every((value, index) => value === arr2[index]);
}
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);
const requestEvent = new CustomEvent("response", {
detail: {
type: type,
body: data,
requestId: requestId,
},
});
document.dispatchEvent(requestEvent);
});
}
const fnproxy = (object, func) => new Proxy(object, { apply: func });
const proxy = (object, key, func) =>
Object.hasOwnProperty.call(object, key) &&
Object.defineProperty(object, key, {
value: fnproxy(object[key], func),
});
function getEventListeners(type) {
if (this == null) return [];
const store = this[Symbol.for(getEventListeners)];
if (store == null || store[type] == null) return [];
return store[type];
}
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";
}
}
}
(async () => {
if (typeof EventTarget !== "undefined") {
proxy(
EventTarget.prototype,
"addEventListener",
async (_target, _this, _args) => {
if (_this != null) {
const [type, listener] = _args;
const storeKey = Symbol.for(getEventListeners);
if (!(storeKey in _this)) _this[storeKey] = {};
const store = _this[storeKey];
if (!(type in store)) store[type] = [];
const listeners = store[type];
let wrappedListener = listener;
if (
type === "message" &&
!!listener &&
!listener._isWrapped
) {
wrappedListener = async function (event) {
if (event instanceof MediaKeyMessageEvent) {
if (event._isCustomEvent) {
if (listener.handleEvent) {
listener.handleEvent(event);
} else {
listener.call(this, event);
}
return;
}
let newBody = new Uint8Array(event.message);
if (
!compareUint8Arrays(
new Uint8Array([0x08, 0x04]),
new Uint8Array(event.message)
)
) {
console.log(
"[PlayreadyProxy]",
"PLAYREADY_PROXY",
"MESSAGE",
listener
);
if (listener.name !== "messageHandler") {
const oldChallenge = uint8ArrayToBase64(
new Uint8Array(event.message)
);
const newChallenge =
await emitAndWaitForResponse(
"REQUEST",
oldChallenge
);
if (oldChallenge !== newChallenge) {
// Playback will fail if the challenges are the same (aka. the background script
// returned the same challenge because the addon is disabled), but I still
// override the challenge anyway, so check beforehand (in base64 form)
newBody =
base64toUint8Array(
newChallenge
);
}
} else {
// trick EME Logger
// better suggestions for avoiding EME Logger interference are welcome
await emitAndWaitForResponse(
"REQUEST",
""
);
}
}
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: newBody.buffer,
messageType: event.messageType,
returnValue: event.returnValue,
srcElement: event.srcElement,
target: event.target,
timeStamp: event.timeStamp,
}
);
newEvent._isCustomEvent = true;
_this.dispatchEvent(newEvent);
event.stopImmediatePropagation();
return;
}
if (listener.handleEvent) {
listener.handleEvent(event);
} else {
listener.call(this, event);
}
};
wrappedListener._isWrapped = true;
wrappedListener.originalListener = listener;
}
const alreadyAdded = listeners.some(
(storedListener) =>
storedListener &&
storedListener.originalListener === listener
);
if (!alreadyAdded) {
listeners.push(wrappedListener);
_args[1] = wrappedListener;
}
}
return _target.apply(_this, _args);
}
);
}
if (typeof MediaKeySession !== "undefined") {
proxy(
MediaKeySession.prototype,
"update",
async (_target, _this, _args) => {
const [response] = _args;
console.log("[PlayreadyProxy]", "PLAYREADY_PROXY", "UPDATE");
await emitAndWaitForResponse(
"RESPONSE",
uint8ArrayToBase64(new Uint8Array(response))
);
return await _target.apply(_this, _args);
}
);
}
})();
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);
};

48
manifest.json Normal file
View File

@@ -0,0 +1,48 @@
{
"manifest_version": 3,
"name": "PlayreadyProxy",
"version": "1.3.3.7",
"permissions": [
"activeTab",
"tabs",
"storage",
"unlimitedStorage",
"webRequest"
],
"host_permissions": ["*://*/*"],
"action": {
"default_popup": "panel/panel.html",
"default_icon": {
"128": "images/icon-128.png"
}
},
"icons": {
"128": "images/icon-128.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["message_proxy.js"],
"run_at": "document_start",
"world": "ISOLATED",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"js": ["content_script.js"],
"run_at": "document_start",
"world": "MAIN",
"all_frames": true
}
],
"browser_specific_settings": {
"gecko": {
"id": "ThatNotEasy@Deny@DevLARLEY",
"strict_min_version": "58.0"
}
}
}

30
message_proxy.js Normal file
View File

@@ -0,0 +1,30 @@
async function processMessage(detail) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
{
type: detail.type,
body: detail.body,
},
(response) => {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError);
}
resolve(response);
}
);
});
}
document.addEventListener("response", async (event) => {
const { detail } = event;
try {
const responseData = await processMessage(detail);
const responseEvent = new CustomEvent("responseReceived", {
detail: detail.requestId.concat(responseData),
});
document.dispatchEvent(responseEvent);
} catch (error) {
console.error("Error processing message:", error);
// Optionally handle the error, maybe notify the user
}
});

77
remote_cdm.js Normal file
View File

@@ -0,0 +1,77 @@
export class RemoteCdm {
constructor(security_level, host, secret, device_name, proxy = null) {
this.security_level = security_level;
this.host = host;
this.secret = secret;
this.device_name = device_name;
this.proxy = proxy; // Optional proxy parameter
}
static from_object(obj) {
return new RemoteCdm(
obj.security_level,
obj.host,
obj.secret,
obj.device_name ?? obj.name,
obj.proxy ?? null // Handle proxy from object if present
);
}
get_name() {
const type = this.security_level;
return `[${type}] ${this.host}/${this.device_name}`;
}
async fetch_with_proxy(url, options) {
// If proxy is set, prepend proxy URL to the original host URL
if (this.proxy) {
options.headers = {
...options.headers,
'X-Forwarded-For': this.proxy, // Optional: Forward the proxy information
};
url = `${this.proxy}${url}`;
}
const response = await fetch(url, options);
return response;
}
async get_license_challenge(pssh) {
const license_request = await this.fetch_with_proxy(`${this.host}/api/playready/extension`, {
method: "POST",
headers: {
"content-type": "application/json",
"X-API-KEY": this.secret,
},
body: JSON.stringify({
action: "Challenge?",
pssh: pssh,
}),
});
console.log(
"[PlayreadyProxy]",
"REMOTE_CDM",
"GET_LICENSE_CHALLENGE",
license_request.status
);
const license_request_json = await license_request.json();
return license_request_json.data;
}
async get_keys(license_challenge) {
const keys = await this.fetch_with_proxy(`${this.host}/api/playready/extension`, {
method: "POST",
headers: {
"content-type": "application/json",
"X-API-KEY": this.secret,
},
body: JSON.stringify({
action: "Keys?",
license: license_challenge,
}),
});
console.log("[PlayreadyProxy]", "REMOTE_CDM", "GET_KEYS", keys.status);
return await keys.json();
}
}

437
util.js Normal file
View File

@@ -0,0 +1,437 @@
import { RemoteCdm } from "./remote_cdm.js";
export class AsyncSyncStorage {
static async setStorage(items) {
return new Promise((resolve, reject) => {
chrome.storage.sync.set(items, () => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve();
}
});
});
}
static async getStorage(keys) {
return new Promise((resolve, reject) => {
chrome.storage.sync.get(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve(result);
}
});
});
}
static async removeStorage(keys) {
return new Promise((resolve, reject) => {
chrome.storage.sync.remove(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve(result);
}
});
});
}
}
export class AsyncLocalStorage {
static async setStorage(items) {
return new Promise((resolve, reject) => {
chrome.storage.local.set(items, () => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve();
}
});
});
}
static async getStorage(keys) {
return new Promise((resolve, reject) => {
chrome.storage.local.get(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve(result);
}
});
});
}
static async removeStorage(keys) {
return new Promise((resolve, reject) => {
chrome.storage.local.remove(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve(result);
}
});
});
}
}
export class RemoteCDMManager {
static async saveRemoteCDM(name, obj) {
const result = await AsyncSyncStorage.getStorage(["remote_cdms"]);
const array =
result.remote_cdms === undefined ? [] : result.remote_cdms;
array.push(name);
await AsyncSyncStorage.setStorage({ remote_cdms: array });
await AsyncSyncStorage.setStorage({ [name]: obj });
}
static async loadRemoteCDM(name) {
const result = await AsyncSyncStorage.getStorage([name]);
return JSON.stringify(result[name] || {});
}
static setRemoteCDM(name, value) {
const remote_combobox = document.getElementById("remote-combobox");
const remote_element = document.createElement("option");
remote_element.text = name;
remote_element.value = value;
remote_combobox.appendChild(remote_element);
}
static async loadSetAllRemoteCDMs() {
const result = await AsyncSyncStorage.getStorage(["remote_cdms"]);
const array = result.remote_cdms || [];
for (const item of array) {
this.setRemoteCDM(item, await this.loadRemoteCDM(item));
}
}
static async saveSelectedRemoteCDM(name) {
await AsyncSyncStorage.setStorage({ selected_remote_cdm: name });
}
static async getSelectedRemoteCDM() {
const result = await AsyncSyncStorage.getStorage([
"selected_remote_cdm",
]);
return result["selected_remote_cdm"] || "";
}
static async selectRemoteCDM(name) {
document.getElementById("remote-combobox").value =
await this.loadRemoteCDM(name);
}
static async removeSelectedRemoteCDM() {
const selected_remote_cdm_name =
await RemoteCDMManager.getSelectedRemoteCDM();
const result = await AsyncSyncStorage.getStorage(["remote_cdms"]);
const array =
result.remote_cdms === undefined ? [] : result.remote_cdms;
const index = array.indexOf(selected_remote_cdm_name);
if (index > -1) {
array.splice(index, 1);
}
await AsyncSyncStorage.setStorage({ remote_cdms: array });
await AsyncSyncStorage.removeStorage([selected_remote_cdm_name]);
}
static async removeSelectedRemoteCDMKey() {
await AsyncSyncStorage.removeStorage(["selected_remote_cdm"]);
}
}
export class SettingsManager {
static async setEnabled(enabled) {
await AsyncSyncStorage.setStorage({ enabled: enabled });
}
static async getEnabled() {
const result = await AsyncSyncStorage.getStorage(["enabled"]);
return result["enabled"] === undefined ? false : result["enabled"];
}
static downloadFile(content, filename) {
const blob = new Blob([content], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
static async saveDarkMode(dark_mode) {
await AsyncSyncStorage.setStorage({ dark_mode: dark_mode });
}
static async getDarkMode() {
const result = await AsyncSyncStorage.getStorage(["dark_mode"]);
return result["dark_mode"] || false;
}
static async loadRemoteCDM(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async function (loaded) {
const result = loaded.target.result;
let json_file = void 0;
try {
json_file = JSON.parse(result);
} catch {
resolve();
return;
}
console.log("LOADED DEVICE:", json_file);
const remote_cdm = new RemoteCdm(
json_file.security_level,
json_file.host,
json_file.key,
json_file.device_name
);
const device_name = remote_cdm.get_name();
console.log("NAME:", device_name);
if (
(await RemoteCDMManager.loadRemoteCDM(device_name)) === "{}"
) {
await RemoteCDMManager.saveRemoteCDM(
device_name,
json_file
);
}
await RemoteCDMManager.saveSelectedRemoteCDM(device_name);
resolve();
};
reader.readAsText(file);
});
}
static async saveSelectedDeviceType(selected_type) {
await AsyncSyncStorage.setStorage({ device_type: selected_type });
}
static async getSelectedDeviceType() {
const result = await AsyncSyncStorage.getStorage(["device_type"]);
return result["device_type"] || "WVD";
}
static async getSelectedDeviceType_PRD() {
const result = await AsyncSyncStorage.getStorage(["device_type"]);
return result["device_type"] || "PRD";
}
static async saveUseShakaPackager(use_shaka) {
await AsyncSyncStorage.setStorage({ use_shaka: use_shaka });
}
static async saveUseDDownloader(use_ddownloader) {
await AsyncSyncStorage.setStorage({ use_ddownloader: use_ddownloader });
}
static async getUseShakaPackager() {
const result = await AsyncSyncStorage.getStorage(["use_shaka"]);
return result["use_shaka"] ?? true;
}
static async getUseDDownloader() {
const result = await AsyncSyncStorage.getStorage(["use_ddownloader"]);
return result["use_ddownloader"] ?? true;
}
static async saveExecutableName(exe_name) {
await AsyncSyncStorage.setStorage({ exe_name: exe_name });
}
static async getExecutableName() {
const result = await AsyncSyncStorage.getStorage(["exe_name"]);
return result["exe_name"] ?? "DDownloader";
}
// Proxy methods
static async setProxy(proxyAddress) {
await AsyncSyncStorage.setStorage({ proxyAddress: proxyAddress });
}
// Get the proxy address
static async getProxy() {
const result = await AsyncSyncStorage.getStorage(["proxyAddress"]);
return result["proxyAddress"] || null; // Ensure null is returned if proxyAddress is not found
}
// Save the proxy enabled status (whether proxy is on or off)
static async setProxyEnabled(enabled) {
await AsyncSyncStorage.setStorage({ proxyEnabled: enabled });
}
// Get the proxy enabled status
static async getProxyEnabled() {
const result = await AsyncSyncStorage.getStorage(["proxyEnabled"]);
return result["proxyEnabled"] !== undefined ? result["proxyEnabled"] : false; // Default to false if not found
}
// Save the proxy port
static async saveProxy(port) {
await AsyncSyncStorage.setStorage({ proxyPort: port });
}
// Get the proxy port
static async getProxyPort() {
const result = await AsyncSyncStorage.getStorage(["proxyPort"]);
return result["proxyPort"] || null; // Return null if proxyPort is not found
}
// Save both the proxy address and port together
static async saveProxyConfig(proxyAddress) {
await SettingsManager.setProxy(proxyAddress); // Save the proxy address
const proxyPort = proxyAddress.split(":")[1]; // Extract the port from the address if available
if (proxyPort) {
await SettingsManager.saveProxy(proxyPort); // Save the port if it's present
} else {
await SettingsManager.saveProxy(""); // Clear the port if not available
}
}
// Get both proxy URL and port together (optional convenience method)
static async getProxyConfig() {
const proxyUrl = await SettingsManager.getProxy();
const proxyPort = await SettingsManager.getProxyPort();
return proxyUrl && proxyPort ? `${proxyUrl}:${proxyPort}` : proxyUrl || '';
}
}
export function intToUint8Array(num) {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, num, false);
return new Uint8Array(buffer);
}
export function compareUint8Arrays(arr1, arr2) {
if (arr1.length !== arr2.length) return false;
return Array.from(arr1).every((value, index) => value === arr2[index]);
}
export function uint8ArrayToHex(buffer) {
return Array.prototype.map
.call(buffer, (x) => x.toString(16).padStart(2, "0"))
.join("");
}
export function uint8ArrayToString(uint8array) {
return String.fromCharCode.apply(null, uint8array);
}
export function uint8ArrayToBase64(uint8array) {
return btoa(String.fromCharCode.apply(null, uint8array));
}
export function stringToUint8Array(string) {
return Uint8Array.from(string.split("").map((x) => x.charCodeAt()));
}
export function stringToHex(string) {
return string
.split("")
.map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
.join("");
}
export function PSSHFromKID(kidBase64) {
const kidBytes = base64toUint8Array(kidBase64);
const kidBase64ForHeader = btoa(String.fromCharCode(...kidBytes));
const wrmHeaderXml =
`<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${kidBase64ForHeader}</KID></DATA></WRMHEADER>`.trim();
const wrmHeaderBytes = stringToUTF16LEBytes(wrmHeaderXml);
const playReadyObjectLength = 2 + 2 + wrmHeaderBytes.length;
const playReadyObjectBuffer = new ArrayBuffer(playReadyObjectLength);
const playReadyObjectView = new DataView(playReadyObjectBuffer);
let offset = 0;
playReadyObjectView.setUint16(offset, 0x0001, true);
offset += 2;
playReadyObjectView.setUint16(offset, wrmHeaderBytes.length, true);
offset += 2;
new Uint8Array(playReadyObjectBuffer).set(wrmHeaderBytes, offset);
const recordCount = 1;
const recordListLength = 2 + playReadyObjectLength;
const recordListBuffer = new ArrayBuffer(recordListLength);
const recordListView = new DataView(recordListBuffer);
offset = 0;
recordListView.setUint16(offset, recordCount, true);
offset += 2;
new Uint8Array(recordListBuffer).set(
new Uint8Array(playReadyObjectBuffer),
offset
);
const systemIDHex = "9a04f07998404286ab92e65be0885f95";
const systemIDBytes = hexStringToUint8Array(systemIDHex);
const psshSize = 4 + 4 + 4 + 16 + 4 + recordListLength;
const psshBuffer = new ArrayBuffer(psshSize);
const psshView = new DataView(psshBuffer);
const psshUint8Array = new Uint8Array(psshBuffer);
offset = 0;
psshView.setUint32(offset, psshSize, false);
offset += 4;
psshUint8Array.set([0x70, 0x73, 0x73, 0x68], offset);
offset += 4;
psshView.setUint32(offset, 0, false);
offset += 4;
psshUint8Array.set(systemIDBytes, offset);
offset += 16;
psshView.setUint32(offset, recordListLength, false);
offset += 4;
psshUint8Array.set(new Uint8Array(recordListBuffer), offset);
return uint8ArrayToBase64(psshUint8Array);
}
export function base64toUint8Array(base64_string) {
return Uint8Array.from(atob(base64_string), (c) => c.charCodeAt(0));
}
export function stringToUTF16LEBytes(str) {
const bytes = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
bytes[i * 2] = code & 0xff;
bytes[i * 2 + 1] = (code >> 8) & 0xff;
}
return bytes;
}
export function hexStringToUint8Array(hexString) {
const bytes = new Uint8Array(hexString.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
}
return bytes;
}