diff --git a/background.js b/background.js index 047cda2..dd6962c 100644 --- a/background.js +++ b/background.js @@ -9,6 +9,8 @@ import { uint8ArrayToBase64, uint8ArrayToHex, getWvPsshFromConcatPssh, + makeCkInitData, + setIcon, SettingsManager, AsyncLocalStorage, RemoteCDMManager, @@ -27,6 +29,7 @@ import { utils } from "./jsplayready/noble-curves.min.js"; let manifests = new Map(); let requests = new Map(); let sessions = new Map(); +let sessionCnt = {}; let logs = []; chrome.webRequest.onBeforeSendHeaders.addListener( @@ -340,6 +343,13 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return; } + if (!sessionCnt[sender.tab.id]) { + sessionCnt[sender.tab.id] = 1; + setIcon("images/icon-active.png", sender.tab.id); + } else { + sessionCnt[sender.tab.id]++; + } + try { JSON.parse(atob(message.body)); sendResponse(message.body); @@ -348,8 +358,35 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.body) { if (message.body.startsWith("lookup:")) { const split = message.body.split(":"); - sessions.set(split[1], split[2]); - sendResponse(); + // Find first log that contains the requested KID + const log = logs.find(log => + log.keys.some(k => k.kid.toLowerCase() === split[2].toLowerCase()) + ); + if (!log) { + console.warn("[Vineless] Lookup failed: no log found for KID", kidHex); + sendResponse(); + return; + } + switch (log.type) { + case "CLEARKEY": // UNTESTED + const ckInitData = makeCkInitData(log.keys); + sendResponse(uint8ArrayToBase64(ckInitData)); + break; + case "WIDEVINE": + const device_type = await SettingsManager.getSelectedDeviceType(); + switch (device_type) { + case "WVD": + await generateChallenge(log.pssh_data, sendResponse); + break; + case "REMOTE": + await generateChallengeRemote(log.pssh_data, sendResponse); + break; + } + break; + case "PLAYREADY": // UNTESTED + await generatePRChallenge(log.pssh_data, sendResponse, split[1]); + break; + } } else if (message.body.startsWith("pr:")) { if (!await SettingsManager.getPREnabled()) { sendResponse(); @@ -389,38 +426,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { await parseClearKey(message.body, sendResponse, tab_url); return; } catch (e) { - if (message.body.startsWith("lookup:")) { - const split = message.body.split(':'); - const sessionId = split[1]; - const kidHex = sessions.get(sessionId); - - if (!kidHex) { - console.warn("[Vineless] Lookup failed: no session mapping for", sessionId); - sendResponse(); - return; - } - - // Find first log that contains the requested KID - const log = logs.find(log => - log.keys.some(k => k.kid.toLowerCase() === kidHex.toLowerCase()) - ); - - if (!log) { - console.warn("[Vineless] Lookup failed: no log found for KID", kidHex); - sendResponse(); - return; - } - - const response = { - keys: log.keys.map(k => ({ - k: k.k, - kid: k.kid - })) - }; - - sendResponse(JSON.stringify(response)); - return; - } else if (message.body.startsWith("pr:")) { + if (message.body.startsWith("pr:")) { if (!await SettingsManager.getPREnabled()) { sendResponse(); manifests.clear(); @@ -447,6 +453,13 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } return; } + case "CLOSE": + if (sessionCnt[sender.tab.id]) { + if (--sessionCnt[sender.tab.id] === 0) { + setIcon("images/icon.png", sender.tab.id); + } + } + break; case "GET_ENABLED": if (await SettingsManager.getEnabled()) { sendResponse(JSON.stringify({ @@ -510,3 +523,13 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { })(); return true; }); + +chrome.tabs.onRemoved.addListener((tabId) => { + delete sessionCnt[tabId]; +}); + +(async () => { + if (!await SettingsManager.getEnabled()) { + setIcon("images/icon-disabled.png"); + } +})(); \ No newline at end of file diff --git a/content_script.js b/content_script.js index 2873eed..f725f97 100644 --- a/content_script.js +++ b/content_script.js @@ -1,747 +1,740 @@ -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 uint8ArrayToHex(buffer) { - return Array.prototype.map.call(buffer, x => x.toString(16).padStart(2, '0')).join(''); -} - -function base64ToBase64Url(b64) { - return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); -} - -function hexToBase64(hexstring) { - return btoa(hexstring.match(/\w{2}/g).map(function(a) { - return String.fromCharCode(parseInt(a, 16)); - }).join("")); -} - -function hexToUint8Array(hex) { - if (typeof hex !== 'string' || hex.length % 2 !== 0) - throw new Error("Invalid hex string"); - - const bytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.substr(i, 2), 16); - } - return bytes; -} - -const genRanHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); - -function generateClearKeyLicense(keys) { - return JSON.stringify({ - keys: keys.map(({ k, kid }) => ({ - kty: "oct", - alg: "A128KW", - k: base64ToBase64Url(hexToBase64(k)), - kid: base64ToBase64Url(hexToBase64(kid)) - })), - type: "temporary" - }); -} - -function makeCkInitData(keys) { - const systemId = new Uint8Array([ - 0x10, 0x77, 0xef, 0xec, - 0xc0, 0xb2, - 0x4d, 0x02, - 0xac, 0xe3, - 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b - ]); - - const kidCount = keys.length; - const kidDataLength = kidCount * 16; - const dataSize = 0; - - const size = 4 + 4 + 4 + 16 + 4 + kidDataLength + 4 + dataSize; - const buffer = new ArrayBuffer(size); - const view = new DataView(buffer); - - let offset = 0; - - view.setUint32(offset, size); offset += 4; - view.setUint32(offset, 0x70737368); offset += 4; // 'pssh' - view.setUint8(offset++, 0x01); // version 1 - view.setUint8(offset++, 0x00); // flags (3 bytes) - view.setUint8(offset++, 0x00); - view.setUint8(offset++, 0x00); - - new Uint8Array(buffer, offset, 16).set(systemId); offset += 16; - - view.setUint32(offset, kidCount); offset += 4; - - for (const key of keys) { - const kidBytes = hexToUint8Array(key.kid); - if (kidBytes.length !== 16) throw new Error("Invalid KID length"); - new Uint8Array(buffer, offset, 16).set(kidBytes); - offset += 16; +(function () { + function uint8ArrayToBase64(uint8array) { + return btoa(String.fromCharCode.apply(null, uint8array)); } - view.setUint32(offset, dataSize); offset += 4; - - return new Uint8Array(buffer); -} - -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(''); + function uint8ArrayToString(uint8array) { + return String.fromCharCode.apply(null, uint8array) } - static isHLS(text) { - return text.includes('#extm3u'); + function base64toUint8Array(base64_string){ + return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0)) } - static isHLSMaster(text) { - return text.includes('#ext-x-stream-inf'); + function uint8ArrayToHex(buffer) { + return Array.prototype.map.call(buffer, x => x.toString(16).padStart(2, '0')).join(''); } - static isMSS(text) { - return text.includes(''); + function base64ToBase64Url(b64) { + return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } - 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 function sanitizeConfigForClearKey(configOrConfigs) { - const configs = Array.isArray(configOrConfigs) ? configOrConfigs : [configOrConfigs]; - const supportedConfigs = []; - - for (const config of configs) { - const videoCaps = config.videoCapabilities || []; - const audioCaps = config.audioCapabilities || []; - - const initDataTypes = config.initDataTypes || ["cenc"]; - const sessionTypes = config.sessionTypes || ["temporary"]; - - const cleanVideoCaps = []; - const cleanAudioCaps = []; - - for (const [type, caps, cleanList] of [ - ["video", videoCaps, cleanVideoCaps], - ["audio", audioCaps, cleanAudioCaps] - ]) { - for (const cap of caps) { - const contentType = cap.contentType; - if (!contentType || !MediaSource.isTypeSupported(contentType)) { - console.debug("[Vineless] Unsupported contentType:", contentType); - continue; // skip if not playable - } - - const mediaConfig = { - type: "media-source", - [type]: { - contentType, - robustness: "", // ClearKey must use empty robustness - bitrate: 100000, - framerate: 30, - channels: type === "audio" ? 2 : undefined, - width: type === "video" ? 1920 : undefined, - height: type === "video" ? 1080 : undefined, - samplerate: type === "audio" ? 48000 : undefined - }, - keySystemConfiguration: { - keySystem: "org.w3.clearkey", - initDataType: "cenc", - distinctiveIdentifier: "not-allowed", - persistentState: "optional", - sessionTypes - }, - _ck: true - }; - - let supported = true; - if (navigator.mediaCapabilities?.decodingInfo) { - try { - const result = await navigator.mediaCapabilities.decodingInfo(mediaConfig); - supported = result.supported; - } catch (e) { - supported = false; - } - } - - if (supported) { - cleanList.push({ contentType, robustness: "" }); - } - } - } - - if (cleanVideoCaps.length || cleanAudioCaps.length) { - supportedConfigs.push({ - initDataTypes, - distinctiveIdentifier: "not-allowed", - persistentState: "optional", - sessionTypes, - videoCapabilities: cleanVideoCaps.length ? cleanVideoCaps : undefined, - audioCapabilities: cleanAudioCaps.length ? cleanAudioCaps : undefined - }); + function hexToBase64(hexstring) { + return btoa(hexstring.match(/\w{2}/g).map(function(a) { + return String.fromCharCode(parseInt(a, 16)); + }).join("")); + } + + function hexToUint8Array(hex) { + if (typeof hex !== 'string' || hex.length % 2 !== 0) + throw new Error("Invalid hex string"); + + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); } + return bytes; } - if (!supportedConfigs.length) { - console.warn("[Vineless] No supported configs for ClearKey, returning empty array"); - return []; - } + const genRanHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); - console.debug("[Vineless] Sanitized config for ClearKey:", supportedConfigs); - return supportedConfigs; -} - -function hookKeySystem(Interface) { - const origKeySystemDescriptor = Object.getOwnPropertyDescriptor(Interface.prototype, 'keySystem'); - const origKeySystemGetter = origKeySystemDescriptor?.get; - - if (typeof origKeySystemGetter !== 'undefined') { - Object.defineProperty(Interface.prototype, 'keySystem', { - get() { - if (this._emeShim?.origKeySystem) { - console.debug("[Vineless] Shimmed keySystem"); - return this._emeShim.origKeySystem; - } - return origKeySystemGetter.call(this); - } + function generateClearKeyLicense(keys) { + return JSON.stringify({ + keys: keys.map(({ k, kid }) => ({ + kty: "oct", + alg: "A128KW", + k: base64ToBase64Url(hexToBase64(k)), + kid: base64ToBase64Url(hexToBase64(kid)) + })), + type: "temporary" }); } -} -async function getEnabledForKeySystem(keySystem, includeClearKey = true) { - if (!keySystem) { - return false; - } - const enabledData = JSON.parse(await emitAndWaitForResponse("GET_ENABLED")); - if (!enabledData) { - return false; - } - if (keySystem.startsWith( "com.widevine.alpha")) { - return enabledData.wv; - } else if (keySystem.startsWith("com.microsoft.playready")) { - return enabledData.pr; - } else if (keySystem === "org.w3.clearkey") { - return includeClearKey; - } - console.error("[Vineless] Unsupported keySystem!"); - return false; -} + function makeCkInitData(keys) { + const systemId = new Uint8Array([ + 0x10, 0x77, 0xef, 0xec, + 0xc0, 0xb2, + 0x4d, 0x02, + 0xac, 0xe3, + 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b + ]); -function flipUUIDByteOrder(u8arr) { - const out = new Uint8Array(16); - out.set([ - u8arr[3], u8arr[2], u8arr[1], u8arr[0], // 4 bytes reversed - u8arr[5], u8arr[4], // 2 bytes reversed - u8arr[7], u8arr[6], // 2 bytes reversed - ...u8arr.slice(8) // last 8 bytes unchanged - ]); - return out; -} + const kidCount = keys.length; + const kidDataLength = kidCount * 16; + const dataSize = 0; -(async () => { - const requestMediaKeySystemAccessUnaltered = navigator.requestMediaKeySystemAccess; - if (!requestMediaKeySystemAccessUnaltered) { - console.error("[Vineless] EME not available!"); - return; + const size = 4 + 4 + 4 + 16 + 4 + kidDataLength + 4 + dataSize; + const buffer = new ArrayBuffer(size); + const view = new DataView(buffer); + + let offset = 0; + + view.setUint32(offset, size); offset += 4; + view.setUint32(offset, 0x70737368); offset += 4; // 'pssh' + view.setUint8(offset++, 0x01); // version 1 + view.setUint8(offset++, 0x00); // flags (3 bytes) + view.setUint8(offset++, 0x00); + view.setUint8(offset++, 0x00); + + new Uint8Array(buffer, offset, 16).set(systemId); offset += 16; + + view.setUint32(offset, kidCount); offset += 4; + + for (const key of keys) { + const kidBytes = hexToUint8Array(key.kid); + if (kidBytes.length !== 16) throw new Error("Invalid KID length"); + new Uint8Array(buffer, offset, 16).set(kidBytes); + offset += 16; + } + + view.setUint32(offset, dataSize); offset += 4; + + return new Uint8Array(buffer); } - if (typeof Navigator !== 'undefined') { - proxy(Navigator.prototype, 'requestMediaKeySystemAccess', async (_target, _this, _args) => { - console.log("[Vineless] requestMediaKeySystemAccess", structuredClone(_args)); - const origKeySystem = _args[0]; - if (await getEnabledForKeySystem(origKeySystem, false)) { - _args[0] = "org.w3.clearkey"; - _args[1] = await sanitizeConfigForClearKey(_args[1]); - } - const systemAccess = await _target.apply(_this, _args); - systemAccess._emeShim = { - origKeySystem + 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)); + } }; - console.debug("[Vineless] requestMediaKeySystemAccess SUCCESS", systemAccess); - return systemAccess; - }); - } - - if (typeof MediaCapabilities !== 'undefined') { - proxy(MediaCapabilities.prototype, 'decodingInfo', async (_target, _this, _args) => { - const [config] = _args; - if (_args._ck) { - return await _target.apply(_this, _args); - } - const origKeySystem = config?.keySystemConfiguration?.keySystem; - - if (await getEnabledForKeySystem(origKeySystem, false)) { - console.log("[Vineless] Intercepted decodingInfo for", origKeySystem); - - try { - const ckConfig = structuredClone(config); - - // Convert decodingInfo-like config to RMKSA-like structure - const ksc = ckConfig.keySystemConfiguration = { - initDataTypes: ["cenc"], - distinctiveIdentifier: "not-allowed", - persistentState: "optional", - sessionTypes: ["temporary"], - videoCapabilities: [], - audioCapabilities: [] - }; - - if (ckConfig.video?.contentType) { - ksc.videoCapabilities.push({ - contentType: ckConfig.video.contentType, - robustness: ckConfig.video.robustness || "" - }); - } - if (ckConfig.audio?.contentType) { - ksc.audioCapabilities.push({ - contentType: ckConfig.audio.contentType, - robustness: ckConfig.audio.robustness || "" - }); - } - - const sanitized = await sanitizeConfigForClearKey(ksc); - if (!sanitized.length) { - console.warn("[Vineless] decodingInfo: no valid config after sanitization"); - return { - supported: false, - smooth: false, - powerEfficient: false, - keySystemAccess: null - }; - } - - const originalVideo = ksc.videoCapabilities?.length; - const originalAudio = ksc.audioCapabilities?.length; - const sanitizedVideo = sanitized[0].videoCapabilities?.length; - const sanitizedAudio = sanitized[0].audioCapabilities?.length; - - if ( - (originalVideo && !sanitizedVideo) || - (originalAudio && !sanitizedAudio) - ) { - console.warn("[Vineless] decodingInfo: partial capability failure detected"); - return { - supported: false, - smooth: false, - powerEfficient: false, - keySystemAccess: null - }; - } - - ckConfig.keySystemConfiguration = sanitized[0]; - ckConfig.keySystemConfiguration.keySystem = "org.w3.clearkey"; - - const ckResult = await _target.call(_this, ckConfig); - - // Generate a real MediaKeySystemAccess to attach - const access = await requestMediaKeySystemAccessUnaltered.call(navigator, "org.w3.clearkey", [ckConfig.keySystemConfiguration]); - - // Shim .keySystem - access._emeShim = { origKeySystem }; - - // Also patch `getConfiguration()` to reflect original input - const originalGetConfig = access.getConfiguration.bind(access); - access.getConfiguration = () => ({ - ...originalGetConfig(), - videoCapabilities: ckConfig.keySystemConfiguration.videoCapabilities, - audioCapabilities: ckConfig.keySystemConfiguration.audioCapabilities, - sessionTypes: ckConfig.keySystemConfiguration.sessionTypes, - initDataTypes: ckConfig.keySystemConfiguration.initDataTypes - }); - - return { - ...ckResult, - supported: true, - smooth: true, - powerEfficient: true, - keySystemAccess: access - }; - } catch (e) { - console.warn("[Vineless] decodingInfo fallback failed"); - return { - supported: true, - smooth: true, - powerEfficient: false, - keySystemAccess: null - }; - } - } - - return await _target.apply(_this, _args); - }); - } - - if (typeof HTMLMediaElement !== 'undefined') { - proxy(HTMLMediaElement.prototype, 'setMediaKeys', async (_target, _this, _args) => { - console.log("[Vineless] setMediaKeys", _args); - const keys = _args[0]; - const keySystem = keys?._emeShim?.origKeySystem; - if (!await getEnabledForKeySystem(keySystem)) { - return await _target.apply(_this, _args); - } - - // Replace with our own ClearKey MediaKeys - if (keys._ckConfig) { - if (!keys._ckKeys) { - const ckAccess = await requestMediaKeySystemAccessUnaltered.call(navigator, 'org.w3.clearkey', [keys._ckConfig]); - keys._ckKeys = await ckAccess.createMediaKeys(); - keys._ckKeys._emeShim = { - origMediaKeys: keys - }; - } - - console.log("[Vineless] Replaced mediaKeys with ClearKey one"); - - return _target.call(_this, keys._ckKeys); - } - - return _target.apply(_this, _args); - }); - - const origMediaKeysDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'mediaKeys'); - const origMediaKeysGetter = origMediaKeysDescriptor?.get; - - if (typeof origMediaKeysGetter !== 'undefined') { - Object.defineProperty(HTMLMediaElement.prototype, 'mediaKeys', { - get() { - const result = origMediaKeysGetter.call(this); - if (result?._emeShim?.origMediaKeys) { - console.debug("[Vineless] Shimmed HTMLMediaElement.mediaKeys"); - return result._emeShim.origMediaKeys; - } - return result; + 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) + }); + + class Evaluator { + static isDASH(text) { + return text.includes(''); + } + + static isHLS(text) { + return text.includes('#extm3u'); + } + + static isHLSMaster(text) { + return text.includes('#ext-x-stream-inf'); + } + + static isMSS(text) { + return text.includes(''); + } + + 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"; + } } } - if (typeof MediaKeySystemAccess !== 'undefined') { - proxy(MediaKeySystemAccess.prototype, 'createMediaKeys', async (_target, _this, _args) => { - console.log("[Vineless] createMediaKeys"); + async function sanitizeConfigForClearKey(configOrConfigs) { + const configs = Array.isArray(configOrConfigs) ? configOrConfigs : [configOrConfigs]; + const supportedConfigs = []; - const realKeys = _target.apply(_this, _args); - realKeys.then(res => { - res._ckConfig = _this.getConfiguration(); - res._emeShim = _this._emeShim; - }); + for (const config of configs) { + const videoCaps = config.videoCapabilities || []; + const audioCaps = config.audioCapabilities || []; - return realKeys; - }); + const initDataTypes = config.initDataTypes || ["cenc"]; + const sessionTypes = config.sessionTypes || ["temporary"]; - hookKeySystem(MediaKeySystemAccess); - } + const cleanVideoCaps = []; + const cleanAudioCaps = []; - if (typeof MediaKeys !== 'undefined') { - proxy(MediaKeys.prototype, 'createSession', (_target, _this, _args) => { - console.log("[Vineless] createSession"); - const session = _target.apply(_this, _args); - session._mediaKeys = _this; - - // Create a controlled closed Promise - let closeResolver; - session._closedPromise = new Promise(resolve => { - closeResolver = resolve; - }); - session._closeResolver = closeResolver; - - Object.defineProperty(session, 'closed', { - get: () => session._closedPromise - }); - - return session; - }); - - hookKeySystem(MediaKeys); - } - - if (typeof MediaKeySession !== 'undefined') { - proxy(MediaKeySession.prototype, 'generateRequest', async (_target, _this, _args) => { - console.log("[Vineless] generateRequest", _this._ck ? "(Internal)" : "", _args, _this.sessionId); - const keySystem = _this._mediaKeys?._emeShim?.origKeySystem; - if (!await getEnabledForKeySystem(keySystem) || _this._ck) { - return await _target.apply(_this, _args); - } - - try { - Object.defineProperty(_this, "sessionId", { - value: genRanHex(32).toUpperCase(), - writable: false - }); - - if (_args[0].toLowerCase() === "webm") { - throw "lookup not implemented"; // this somehow works better than my webm initData implementation for playback on specific site - // that site calls update, and another "license-request" message is made, then update is called again. Prob the client id encryption stuff? - _this._lookupOnly = true; - const kid = uint8ArrayToHex(_args[1]); - await emitAndWaitForResponse("REQUEST", `lookup:${_this.sessionId}:${kid}`); - const evt = new MediaKeyMessageEvent("message", { - message: new Uint8Array([0x08, 0x04]).buffer, - messageType: "license-request" - }); - _this.dispatchEvent(evt); - return; - } - - const base64Pssh = uint8ArrayToBase64(new Uint8Array(_args[1])); - const data = keySystem.startsWith("com.microsoft.playready") ? `pr:${_this.sessionId}:${base64Pssh}` : base64Pssh; - const challenge = await emitAndWaitForResponse("REQUEST", data); - const challengeBytes = base64toUint8Array(challenge); - - const evt = new MediaKeyMessageEvent("message", { - message: challengeBytes.buffer, - messageType: "license-request" - }); - _this.dispatchEvent(evt); - } catch (e) { - console.error("[Vineless] generateRequest FAILED,", e); - throw e; - } - - return; - }); - proxy(MediaKeySession.prototype, 'update', async (_target, _this, _args) => { - console.log("[Vineless] update", _this._ck ? "(Internal)" : "", _args, _this.sessionId); - const keySystem = _this._mediaKeys?._emeShim?.origKeySystem; - if (!await getEnabledForKeySystem(keySystem) || _this._ck) { - return await _target.apply(_this, _args); - } - - const [response] = _args; - const base64Response = uint8ArrayToBase64(new Uint8Array(response)); - const isPlayReady = keySystem.startsWith("com.microsoft.playready"); - const data = _this._lookupOnly ? `lookup:${_this.sessionId}` : - (isPlayReady ? `pr:${_this.sessionId}:${base64Response}` : base64Response); - const bgResponse = await emitAndWaitForResponse("RESPONSE", data); - - try { - const parsed = JSON.parse(bgResponse); - console.log("[Vineless] Receieved keys from the background script:", parsed, _this); - if (parsed && _this._mediaKeys) { - if (!_this._mediaKeys._ckKeys) { - const ckAccess = await requestMediaKeySystemAccessUnaltered.call(navigator, 'org.w3.clearkey', [_this._mediaKeys._ckConfig]); - _this._mediaKeys._ckKeys = await ckAccess.createMediaKeys(); - _this._mediaKeys._ckKeys._emeShim = { - origMediaKeys: _this._mediaKeys - }; + for (const [type, caps, cleanList] of [ + ["video", videoCaps, cleanVideoCaps], + ["audio", audioCaps, cleanAudioCaps] + ]) { + for (const cap of caps) { + const contentType = cap.contentType; + if (!contentType || !MediaSource.isTypeSupported(contentType)) { + console.debug("[Vineless] Unsupported contentType:", contentType); + continue; // skip if not playable } - const ckLicense = generateClearKeyLicense(parsed.keys); + const mediaConfig = { + type: "media-source", + [type]: { + contentType, + robustness: "", // ClearKey must use empty robustness + bitrate: 100000, + framerate: 30, + channels: type === "audio" ? 2 : undefined, + width: type === "video" ? 1920 : undefined, + height: type === "video" ? 1080 : undefined, + samplerate: type === "audio" ? 48000 : undefined + }, + keySystemConfiguration: { + keySystem: "org.w3.clearkey", + initDataType: "cenc", + distinctiveIdentifier: "not-allowed", + persistentState: "optional", + sessionTypes + }, + _ck: true + }; - _this._ckSession = _this._mediaKeys._ckKeys.createSession(); - _this._ckSession._ck = true; - - try { - await _this._ckSession.generateRequest('cenc', parsed.pssh); - } catch { - const pssh = makeCkInitData(parsed.keys); - await _this._ckSession.generateRequest('cenc', pssh); - } - - const encoder = new TextEncoder(); - const encodedLicense = encoder.encode(ckLicense); - await _this._ckSession.update(encodedLicense); - - const keyStatuses = new Map(); - const addedKeys = new Set(); - for (const { kid } of parsed.keys) { - // Some require unflipped one, others (PR only) require flipped one - // So include both unless duplicate - const raw = hexToUint8Array(kid); - if (isPlayReady) { - const flipped = flipUUIDByteOrder(raw); - - for (const keyBytes of [raw, flipped]) { - const keyHex = Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join(''); - if (!addedKeys.has(keyHex)) { - keyStatuses.set(keyBytes, "usable"); - addedKeys.add(keyHex); - } - } - } else { - // Some services hate having extra keys on Widevine - keyStatuses.set(raw, "usable"); + let supported = true; + if (navigator.mediaCapabilities?.decodingInfo) { + try { + const result = await navigator.mediaCapabilities.decodingInfo(mediaConfig); + supported = result.supported; + } catch (e) { + supported = false; } } - Object.defineProperty(_this, "keyStatuses", { - value: keyStatuses, + if (supported) { + cleanList.push({ contentType, robustness: "" }); + } + } + } + + if (cleanVideoCaps.length || cleanAudioCaps.length) { + supportedConfigs.push({ + initDataTypes, + distinctiveIdentifier: "not-allowed", + persistentState: "optional", + sessionTypes, + videoCapabilities: cleanVideoCaps.length ? cleanVideoCaps : undefined, + audioCapabilities: cleanAudioCaps.length ? cleanAudioCaps : undefined + }); + } + } + + if (!supportedConfigs.length) { + console.warn("[Vineless] No supported configs for ClearKey, returning empty array"); + return []; + } + + console.debug("[Vineless] Sanitized config for ClearKey:", supportedConfigs); + return supportedConfigs; + } + + function hookKeySystem(Interface) { + const origKeySystemDescriptor = Object.getOwnPropertyDescriptor(Interface.prototype, 'keySystem'); + const origKeySystemGetter = origKeySystemDescriptor?.get; + + if (typeof origKeySystemGetter !== 'undefined') { + Object.defineProperty(Interface.prototype, 'keySystem', { + get() { + if (this._emeShim?.origKeySystem) { + console.debug("[Vineless] Shimmed keySystem"); + return this._emeShim.origKeySystem; + } + return origKeySystemGetter.call(this); + } + }); + } + } + + async function getEnabledForKeySystem(keySystem, includeClearKey = true) { + if (!keySystem) { + return false; + } + const enabledData = JSON.parse(await emitAndWaitForResponse("GET_ENABLED")); + if (!enabledData) { + return false; + } + if (keySystem.startsWith( "com.widevine.alpha")) { + return enabledData.wv; + } else if (keySystem.startsWith("com.microsoft.playready")) { + return enabledData.pr; + } else if (keySystem === "org.w3.clearkey") { + return includeClearKey; + } + console.error("[Vineless] Unsupported keySystem!"); + return false; + } + + function flipUUIDByteOrder(u8arr) { + const out = new Uint8Array(16); + out.set([ + u8arr[3], u8arr[2], u8arr[1], u8arr[0], // 4 bytes reversed + u8arr[5], u8arr[4], // 2 bytes reversed + u8arr[7], u8arr[6], // 2 bytes reversed + ...u8arr.slice(8) // last 8 bytes unchanged + ]); + return out; + } + + (async () => { + const requestMediaKeySystemAccessUnaltered = navigator.requestMediaKeySystemAccess; + if (!requestMediaKeySystemAccessUnaltered) { + console.error("[Vineless] EME not available!"); + return; + } + + if (typeof Navigator !== 'undefined') { + proxy(Navigator.prototype, 'requestMediaKeySystemAccess', async (_target, _this, _args) => { + console.log("[Vineless] requestMediaKeySystemAccess", structuredClone(_args)); + const origKeySystem = _args[0]; + if (await getEnabledForKeySystem(origKeySystem, false)) { + _args[0] = "org.w3.clearkey"; + _args[1] = await sanitizeConfigForClearKey(_args[1]); + } + const systemAccess = await _target.apply(_this, _args); + systemAccess._emeShim = { + origKeySystem + }; + console.debug("[Vineless] requestMediaKeySystemAccess SUCCESS", systemAccess); + return systemAccess; + }); + } + + if (typeof MediaCapabilities !== 'undefined') { + proxy(MediaCapabilities.prototype, 'decodingInfo', async (_target, _this, _args) => { + const [config] = _args; + if (_args._ck) { + return await _target.apply(_this, _args); + } + const origKeySystem = config?.keySystemConfiguration?.keySystem; + + if (await getEnabledForKeySystem(origKeySystem, false)) { + console.log("[Vineless] Intercepted decodingInfo for", origKeySystem); + + try { + const ckConfig = structuredClone(config); + + // Convert decodingInfo-like config to RMKSA-like structure + const ksc = ckConfig.keySystemConfiguration = { + initDataTypes: ["cenc"], + distinctiveIdentifier: "not-allowed", + persistentState: "optional", + sessionTypes: ["temporary"], + videoCapabilities: [], + audioCapabilities: [] + }; + + if (ckConfig.video?.contentType) { + ksc.videoCapabilities.push({ + contentType: ckConfig.video.contentType, + robustness: ckConfig.video.robustness || "" + }); + } + if (ckConfig.audio?.contentType) { + ksc.audioCapabilities.push({ + contentType: ckConfig.audio.contentType, + robustness: ckConfig.audio.robustness || "" + }); + } + + const sanitized = await sanitizeConfigForClearKey(ksc); + if (!sanitized.length) { + console.warn("[Vineless] decodingInfo: no valid config after sanitization"); + return { + supported: false, + smooth: false, + powerEfficient: false, + keySystemAccess: null + }; + } + + const originalVideo = ksc.videoCapabilities?.length; + const originalAudio = ksc.audioCapabilities?.length; + const sanitizedVideo = sanitized[0].videoCapabilities?.length; + const sanitizedAudio = sanitized[0].audioCapabilities?.length; + + if ( + (originalVideo && !sanitizedVideo) || + (originalAudio && !sanitizedAudio) + ) { + console.warn("[Vineless] decodingInfo: partial capability failure detected"); + return { + supported: false, + smooth: false, + powerEfficient: false, + keySystemAccess: null + }; + } + + ckConfig.keySystemConfiguration = sanitized[0]; + ckConfig.keySystemConfiguration.keySystem = "org.w3.clearkey"; + + const ckResult = await _target.call(_this, ckConfig); + + // Generate a real MediaKeySystemAccess to attach + const access = await requestMediaKeySystemAccessUnaltered.call(navigator, "org.w3.clearkey", [ckConfig.keySystemConfiguration]); + + // Shim .keySystem + access._emeShim = { origKeySystem }; + + // Also patch `getConfiguration()` to reflect original input + const originalGetConfig = access.getConfiguration.bind(access); + access.getConfiguration = () => ({ + ...originalGetConfig(), + videoCapabilities: ckConfig.keySystemConfiguration.videoCapabilities, + audioCapabilities: ckConfig.keySystemConfiguration.audioCapabilities, + sessionTypes: ckConfig.keySystemConfiguration.sessionTypes, + initDataTypes: ckConfig.keySystemConfiguration.initDataTypes + }); + + return { + ...ckResult, + supported: true, + smooth: true, + powerEfficient: true, + keySystemAccess: access + }; + } catch (e) { + console.warn("[Vineless] decodingInfo fallback failed"); + return { + supported: true, + smooth: true, + powerEfficient: false, + keySystemAccess: null + }; + } + } + + return await _target.apply(_this, _args); + }); + } + + if (typeof HTMLMediaElement !== 'undefined') { + proxy(HTMLMediaElement.prototype, 'setMediaKeys', async (_target, _this, _args) => { + console.log("[Vineless] setMediaKeys", _args); + const keys = _args[0]; + const keySystem = keys?._emeShim?.origKeySystem; + if (!await getEnabledForKeySystem(keySystem)) { + return await _target.apply(_this, _args); + } + + // Replace with our own ClearKey MediaKeys + if (keys._ckConfig) { + if (!keys._ckKeys) { + const ckAccess = await requestMediaKeySystemAccessUnaltered.call(navigator, 'org.w3.clearkey', [keys._ckConfig]); + keys._ckKeys = await ckAccess.createMediaKeys(); + keys._ckKeys._emeShim = { + origMediaKeys: keys + }; + } + + console.log("[Vineless] Replaced mediaKeys with ClearKey one"); + + return _target.call(_this, keys._ckKeys); + } + + return _target.apply(_this, _args); + }); + + const origMediaKeysDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'mediaKeys'); + const origMediaKeysGetter = origMediaKeysDescriptor?.get; + + if (typeof origMediaKeysGetter !== 'undefined') { + Object.defineProperty(HTMLMediaElement.prototype, 'mediaKeys', { + get() { + const result = origMediaKeysGetter.call(this); + if (result?._emeShim?.origMediaKeys) { + console.debug("[Vineless] Shimmed HTMLMediaElement.mediaKeys"); + return result._emeShim.origMediaKeys; + } + return result; + } + }); + } + } + + if (typeof MediaKeySystemAccess !== 'undefined') { + proxy(MediaKeySystemAccess.prototype, 'createMediaKeys', async (_target, _this, _args) => { + console.log("[Vineless] createMediaKeys"); + + const realKeys = _target.apply(_this, _args); + realKeys.then(res => { + res._ckConfig = _this.getConfiguration(); + res._emeShim = _this._emeShim; + }); + + return realKeys; + }); + + hookKeySystem(MediaKeySystemAccess); + } + + if (typeof MediaKeys !== 'undefined') { + proxy(MediaKeys.prototype, 'createSession', (_target, _this, _args) => { + console.log("[Vineless] createSession"); + const session = _target.apply(_this, _args); + session._mediaKeys = _this; + + // Create a controlled closed Promise + let closeResolver; + session._closedPromise = new Promise(resolve => { + closeResolver = resolve; + }); + session._closeResolver = closeResolver; + + Object.defineProperty(session, 'closed', { + get: () => session._closedPromise + }); + + return session; + }); + + hookKeySystem(MediaKeys); + } + + if (typeof MediaKeySession !== 'undefined') { + proxy(MediaKeySession.prototype, 'generateRequest', async (_target, _this, _args) => { + console.log("[Vineless] generateRequest", _this._ck ? "(Internal)" : "", _args, _this.sessionId); + const keySystem = _this._mediaKeys?._emeShim?.origKeySystem; + if (!await getEnabledForKeySystem(keySystem) || _this._ck) { + return await _target.apply(_this, _args); + } + + try { + Object.defineProperty(_this, "sessionId", { + value: genRanHex(32).toUpperCase(), writable: false }); - const keyStatusEvent = new Event("keystatuseschange"); - _this.dispatchEvent(keyStatusEvent); - console.log(keyStatuses); + if (_args[0].toLowerCase() === "webm") { + const kid = uint8ArrayToHex(_args[1]); + const base64Pssh = await emitAndWaitForResponse("REQUEST", `lookup:${_this.sessionId}:${kid}`); + const evt = new MediaKeyMessageEvent("message", { + message: base64toUint8Array(base64Pssh).buffer, + messageType: "license-request" + }); + _this.dispatchEvent(evt); + return; + } - return; - } else { - console.error("[Vineless] update FAILED, no MediaKeys available!"); + const base64Pssh = uint8ArrayToBase64(new Uint8Array(_args[1])); + const data = keySystem.startsWith("com.microsoft.playready") ? `pr:${_this.sessionId}:${base64Pssh}` : base64Pssh; + const challenge = await emitAndWaitForResponse("REQUEST", data); + const challengeBytes = base64toUint8Array(challenge); + + const evt = new MediaKeyMessageEvent("message", { + message: challengeBytes.buffer, + messageType: "license-request" + }); + _this.dispatchEvent(evt); + } catch (e) { + console.error("[Vineless] generateRequest FAILED,", e); + throw e; } - } catch (e) { - console.error("[Vineless] update FAILED,", e); - // If parsing failed, fall through to original Widevine path - } - return await _target.apply(_this, _args); - }); - proxy(MediaKeySession.prototype, 'close', async (_target, _this, _args) => { - console.log("[Vineless] close"); - const keySystem = _this?._mediaKeys?._emeShim?.origKeySystem; - - // Mark closed - if (_this._closeResolver) { - _this._closeResolver(); - } - - if (!await getEnabledForKeySystem(keySystem) || _this._ck) { - return await _target.apply(_this, _args); - } - - // Close internal session if found - if (_this._ckSession) { - try { - await _this._ckSession.close(); - } catch (e) {} - } - - return Promise.resolve(); - }); - } -})(); - -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, - })); + return; + }); + proxy(MediaKeySession.prototype, 'update', async (_target, _this, _args) => { + console.log("[Vineless] update", _this._ck ? "(Internal)" : "", _args, "sessionId:", _this.sessionId); + const keySystem = _this._mediaKeys?._emeShim?.origKeySystem; + if (!await getEnabledForKeySystem(keySystem) || _this._ck) { + !_this._ck && _this.addEventListener('keystatuseschange', () => { + const kidStasuses = {}; + for (const [keyId, status] of _this.keyStatuses) { + kidStasuses[uint8ArrayToHex(new Uint8Array(keyId))] = status; } + console.log("[Vineless] keyStatuses:", kidStasuses); + }); + return await _target.apply(_this, _args); + } + + const [response] = _args; + const base64Response = uint8ArrayToBase64(new Uint8Array(response)); + const isPlayReady = keySystem.startsWith("com.microsoft.playready"); + const data = isPlayReady ? `pr:${_this.sessionId}:${base64Response}` : base64Response; + const bgResponse = await emitAndWaitForResponse("RESPONSE", data); + + try { + const parsed = JSON.parse(bgResponse); + console.log("[Vineless] Receieved keys from the background script:", parsed, _this); + if (parsed && _this._mediaKeys) { + if (!_this._mediaKeys._ckKeys) { + const ckAccess = await requestMediaKeySystemAccessUnaltered.call(navigator, 'org.w3.clearkey', [_this._mediaKeys._ckConfig]); + _this._mediaKeys._ckKeys = await ckAccess.createMediaKeys(); + _this._mediaKeys._ckKeys._emeShim = { + origMediaKeys: _this._mediaKeys + }; + } + + const ckLicense = generateClearKeyLicense(parsed.keys); + + _this._ckSession = _this._mediaKeys._ckKeys.createSession(); + _this._ckSession._ck = true; + + try { + await _this._ckSession.generateRequest('cenc', parsed.pssh); + } catch { + const pssh = makeCkInitData(parsed.keys); + await _this._ckSession.generateRequest('cenc', pssh); + } + + const encoder = new TextEncoder(); + const encodedLicense = encoder.encode(ckLicense); + await _this._ckSession.update(encodedLicense); + + const keyStatuses = new Map(); + const addedKeys = new Set(); + for (const { kid } of parsed.keys) { + // Some require unflipped one, others (PR only) require flipped one + // So include both unless duplicate + const raw = hexToUint8Array(kid); + if (isPlayReady) { + const flipped = flipUUIDByteOrder(raw); + + for (const keyBytes of [raw, flipped]) { + const keyHex = Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join(''); + if (!addedKeys.has(keyHex)) { + keyStatuses.set(keyBytes, "usable"); + addedKeys.add(keyHex); + } + } + } else { + // Some services hate having extra keys on Widevine + keyStatuses.set(raw, "usable"); + } + } + + Object.defineProperty(_this, "keyStatuses", { + value: keyStatuses, + writable: false + }); + + const keyStatusEvent = new Event("keystatuseschange"); + _this.dispatchEvent(keyStatusEvent); + + return; + } else { + console.error("[Vineless] update FAILED, no MediaKeys available!"); } + } catch (e) { + console.error("[Vineless] update FAILED,", e); + // If parsing failed, fall through to original Widevine path + } + + return await _target.apply(_this, _args); + }); + proxy(MediaKeySession.prototype, 'close', async (_target, _this, _args) => { + console.log("[Vineless] close"); + const keySystem = _this?._mediaKeys?._emeShim?.origKeySystem; + + // Mark closed + if (_this._closeResolver) { + _this._closeResolver(); + } + + if (!await getEnabledForKeySystem(keySystem) || _this._ck) { + return await _target.apply(_this, _args); + } + + // Close internal session if found + if (_this._ckSession) { + try { + await _this._ckSession.close(); + } catch (e) {} + } + + await emitAndWaitForResponse("CLOSE"); + + return Promise.resolve(); + }); + } + })(); + + 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(response); - }) - } else { - resolve(response); - } - }).catch(() => { - resolve(); + } + }).catch(() => { + resolve(); + }) }) - }) -} + } -const open = XMLHttpRequest.prototype.open; -XMLHttpRequest.prototype.open = function(method, url) { - this._method = method; - return open.apply(this, arguments); -}; + 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)])); + 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, + })); } - 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); -}; + }); + return send.apply(this, arguments); + }; +})(); \ No newline at end of file diff --git a/images/icon-active.png b/images/icon-active.png new file mode 100644 index 0000000..7c0695a Binary files /dev/null and b/images/icon-active.png differ diff --git a/images/icon-disabled.png b/images/icon-disabled.png new file mode 100644 index 0000000..c172a3d Binary files /dev/null and b/images/icon-disabled.png differ diff --git a/images/icon-128.png b/images/icon.png similarity index 100% rename from images/icon-128.png rename to images/icon.png diff --git a/manifest.json b/manifest.json index a205879..a4d84fc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Vineless", - "version": "1.2", + "version": "1.3", "description": "Play protected contents without a real CDM", "permissions": [ "activeTab", @@ -16,11 +16,11 @@ "action": { "default_popup": "panel/panel.html", "default_icon": { - "128": "images/icon-128.png" + "128": "images/icon.png" } }, "icons": { - "128": "images/icon-128.png" + "128": "images/icon.png" }, "background": { "scripts": ["background.js"], diff --git a/panel/panel.css b/panel/panel.css index e7d0deb..db0dde1 100644 --- a/panel/panel.css +++ b/panel/panel.css @@ -117,6 +117,11 @@ h1 { .header-right { font-size: 11px; } +#version { + position: absolute; + top: 0; + right: 0; +} #wvd > *, #remote > *, #prd > * { margin: 5px; diff --git a/panel/panel.html b/panel/panel.html index c9d4a61..1cd1cc0 100644 --- a/panel/panel.html +++ b/panel/panel.html @@ -7,12 +7,13 @@
- WidevineProxy Icon + WidevineProxy Icon

Vineless

Made by Ingan121
Based on WidevineProxy2 and PlayreadyProxy2
+

v1.x

Settings diff --git a/panel/panel.js b/panel/panel.js index 5d13e0e..75ddf0c 100644 --- a/panel/panel.js +++ b/panel/panel.js @@ -4,11 +4,13 @@ import { Utils } from '../jsplayready/utils.js'; import {AsyncLocalStorage, base64toUint8Array, stringToUint8Array, DeviceManager, RemoteCDMManager, PRDeviceManager, SettingsManager} from "../util.js"; const key_container = document.getElementById('key-container'); +const icon = document.getElementById('icon'); // ================ Main ================ const enabled = document.getElementById('enabled'); enabled.addEventListener('change', async function (){ await SettingsManager.setEnabled(enabled.checked); + icon.src = `../images/icon${enabled.checked ? '' : '-disabled'}.png`; }); const toggle = document.getElementById('darkModeToggle'); @@ -17,6 +19,9 @@ toggle.addEventListener('change', async () => { await SettingsManager.saveDarkMode(toggle.checked); }); +const version = document.getElementById('version'); +version.textContent = "v" + chrome.runtime.getManifest().version; + const wvEnabled = document.getElementById('wvEnabled'); wvEnabled.addEventListener('change', async function (){ await SettingsManager.setWVEnabled(wvEnabled.checked); @@ -276,6 +281,9 @@ function checkLogs() { document.addEventListener('DOMContentLoaded', async function () { enabled.checked = await SettingsManager.getEnabled(); + if (!enabled.checked) { + icon.src = "../images/icon-disabled.png"; + } SettingsManager.setDarkMode(await SettingsManager.getDarkMode()); wvEnabled.checked = await SettingsManager.getWVEnabled(true); prEnabled.checked = await SettingsManager.getPREnabled(true); diff --git a/util.js b/util.js index 2e79251..4fe7b61 100644 --- a/util.js +++ b/util.js @@ -275,6 +275,7 @@ export class RemoteCDMManager { export class SettingsManager { static async setEnabled(enabled) { await AsyncSyncStorage.setStorage({ enabled: enabled }); + setIcon(`images/icon${enabled ? '' : '-disabled'}.png`); } static async getEnabled() { @@ -533,3 +534,74 @@ export function getWvPsshFromConcatPssh(psshBase64) { return psshBase64; } +export function makeCkInitData(keys) { + const systemId = new Uint8Array([ + 0x10, 0x77, 0xef, 0xec, + 0xc0, 0xb2, + 0x4d, 0x02, + 0xac, 0xe3, + 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b + ]); + + const kidCount = keys.length; + const kidDataLength = kidCount * 16; + const dataSize = 0; + + const size = 4 + 4 + 4 + 16 + 4 + kidDataLength + 4 + dataSize; + const buffer = new ArrayBuffer(size); + const view = new DataView(buffer); + + let offset = 0; + + view.setUint32(offset, size); offset += 4; + view.setUint32(offset, 0x70737368); offset += 4; // 'pssh' + view.setUint8(offset++, 0x01); // version 1 + view.setUint8(offset++, 0x00); // flags (3 bytes) + view.setUint8(offset++, 0x00); + view.setUint8(offset++, 0x00); + + new Uint8Array(buffer, offset, 16).set(systemId); offset += 16; + + view.setUint32(offset, kidCount); offset += 4; + + for (const key of keys) { + const kidBytes = hexToUint8Array(key.kid); + if (kidBytes.length !== 16) throw new Error("Invalid KID length"); + new Uint8Array(buffer, offset, 16).set(kidBytes); + offset += 16; + } + + view.setUint32(offset, dataSize); offset += 4; + + return new Uint8Array(buffer); +} + +export async function setIcon(filename, tabId = undefined) { + const isMV3 = typeof chrome.action !== "undefined"; + if (!isMV3) { + chrome.browserAction.setIcon({ + path: { + 128: filename + }, + tabId + }); + return; + } + + const url = chrome.runtime.getURL(filename); + const res = await fetch(url); + const blob = await res.blob(); + const bitmap = await createImageBitmap(blob); + + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + const ctx = canvas.getContext("2d"); + ctx.drawImage(bitmap, 0, 0); + const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height); + + chrome.action.setIcon({ + imageData: { + [bitmap.width]: imageData + }, + ...(tabId ? { tabId } : {}) + }); +} \ No newline at end of file