EME Logger

Inject EME interface and log its function calls.

  1. // ==UserScript==
  2. // @name EME Logger
  3. // @namespace http://greasyfork.org/
  4. // @version 2.0
  5. // @description Inject EME interface and log its function calls.
  6. // @author cramer
  7. // @match *://*/*
  8. // @run-at document-start
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // ==/UserScript==
  13.  
  14. (async () => {
  15. const disabledKeySystems = GM_getValue('disabledKeySystems', []);
  16.  
  17. const commonKeySystems = {
  18. 'Widevine': /widevine/i,
  19. 'PlayReady': /playready/i,
  20. 'FairPlay': /fairplay|fps/i,
  21. 'ClearKey': /clearkey/i,
  22. };
  23. for (const [keySystem, rule] of Object.entries(commonKeySystems)) {
  24. if (disabledKeySystems.indexOf(keySystem) >= 0) {
  25. GM_registerMenuCommand(`Enable ${keySystem} (and refresh)`, function() {
  26. GM_setValue('disabledKeySystems', disabledKeySystems.filter(k => k !== keySystem));
  27. location.reload();
  28. });
  29. } else {
  30. GM_registerMenuCommand(`Disable ${keySystem} (and refresh)`, function() {
  31. GM_setValue('disabledKeySystems', [...disabledKeySystems, keySystem]);
  32. location.reload();
  33. });
  34. }
  35. }
  36.  
  37. function isKeySystemDisabled(keySystem) {
  38. for (const disabledKeySystem of disabledKeySystems) {
  39. if (keySystem.match(commonKeySystems[disabledKeySystem])) return true;
  40. }
  41. return false
  42. }
  43.  
  44. // Color constants
  45. const $ = {
  46. INFO: '#66d9ef',
  47. VALUE: '#4ec9a4',
  48. METHOD: '#569cd6',
  49. SUCCESS: '#a6e22e',
  50. FAILURE: '#6d1212',
  51. WARNING: '#fd971f',
  52. };
  53.  
  54. const indent = (s,n=4) => s.split('\n').map(l=>Array(n).fill(' ').join('')+l).join('\n');
  55.  
  56. const b64 = {
  57. decode: s => Uint8Array.from(atob(s), c => c.charCodeAt(0)),
  58. encode: b => btoa(String.fromCharCode(...new Uint8Array(b)))
  59. };
  60.  
  61. const fnproxy = (object, func) => new Proxy(object, { apply: func });
  62.  
  63. const proxy = (object, key, func) => Object.hasOwnProperty.call(object, key) && Object.defineProperty(object, key, {
  64. value: fnproxy(object[key], func)
  65. });
  66.  
  67. function messageHandler(event) {
  68. const keySession = event.target;
  69. const {sessionId} = keySession;
  70. const {message, messageType} = event;
  71. const listeners = keySession.getEventListeners('message').filter(l => l !== messageHandler);
  72. console.groupCollapsed(
  73. `%c[EME] (EVENT)%c MediaKeySession::message%c\n` +
  74. `Session ID: %c%s%c\n` +
  75. `Message Type: %c%s%c\n` +
  76. `Message: %c%s%c\n` +
  77. `Listeners:`,
  78.  
  79. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  80. `color: ${$.VALUE}; font-weight: bold;`, sessionId || '(not available)', `color: inherit; font-weight: normal;`,
  81. `color: ${$.VALUE}; font-weight: bold;`, messageType, `color: inherit; font-weight: normal;`,
  82. `font-weight: bold;`, b64.encode(message), `font-weight: normal;`,
  83. listeners,
  84. );
  85. console.trace();
  86. console.groupEnd();
  87. }
  88.  
  89. function keyStatusColor(status) {
  90. switch(status.toLowerCase()) {
  91. case 'usable':
  92. return $.SUCCESS;
  93. case 'output-restricted':
  94. case 'output-downscaled':
  95. case 'usable-in-future':
  96. case 'status-pending':
  97. return $.WARNING;
  98. case 'expired':
  99. case 'released':
  100. case 'internal-error':
  101. default:
  102. return $.FAILURE;
  103. }
  104.  
  105. }
  106.  
  107. function keystatuseschangeHandler(event) {
  108. const keySession = event.target;
  109. const {sessionId} = keySession;
  110. const listeners = keySession.getEventListeners('keystatuseschange').filter(l => l !== keystatuseschangeHandler);
  111. let keysFmt = '';
  112. const keysText = [];
  113. keySession.keyStatuses.forEach((status, keyId) => {
  114. keysFmt += ` %c[%s]%c %s%c\n`;
  115. keysText.push(
  116. `color: ${keyStatusColor(status)}; font-weight: bold;`,
  117. status.toUpperCase(),
  118. `color: ${$.VALUE};`,
  119. b64.encode(keyId),
  120. `color: inherit; font-weight: normal;`,
  121. );
  122. });
  123. console.groupCollapsed(
  124. `%c[EME] (EVENT)%c MediaKeySession::keystatuseschange%c\n` +
  125. `Session ID: %c%s%c\n` +
  126. `Key Statuses:\n` + keysFmt +
  127. 'Listeners:',
  128.  
  129. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  130. `color: ${$.VALUE}; font-weight: bold;`, sessionId || '(not available)', `color: inherit; font-weight: normal;`,
  131. ...keysText,
  132. listeners,
  133. );
  134. console.trace();
  135. console.groupEnd();
  136. }
  137.  
  138. function getEventListeners(type) {
  139. if (this == null) return [];
  140. const store = this[Symbol.for(getEventListeners)];
  141. if (store == null || store[type] == null) return [];
  142. return store[type];
  143. }
  144.  
  145. EventTarget.prototype.getEventListeners = getEventListeners;
  146.  
  147. typeof Navigator !== 'undefined' && proxy(Navigator.prototype, 'requestMediaKeySystemAccess', async (_target, _this, _args) => {
  148. const [keySystem, supportedConfigurations] = _args;
  149. const enterMessage = [
  150. `%c[EME] (CALL)%c Navigator::requestMediaKeySystemAccess%c\n` +
  151. `Key System: %c%s%c\n` +
  152. `Supported Configurations:\n`,
  153.  
  154. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  155. `color: ${$.VALUE}; font-weight: bold;`, keySystem, `color: inherit; font-weight: normal;`,
  156. indent(JSON.stringify(supportedConfigurations, null, ' ')),
  157. ];
  158. let result, err;
  159. try {
  160. if (isKeySystemDisabled(keySystem)) {
  161. throw new DOMException(`Unsupported keySystem or supportedConfigurations.`, `NotSupportedError`);
  162. }
  163. result = await _target.apply(_this, _args);
  164. return result;
  165. } catch(e) {
  166. err = e;
  167. throw e;
  168. } finally {
  169. console.groupCollapsed(...enterMessage);
  170. if (err) {
  171. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  172. } else {
  173. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, result);
  174. }
  175. console.trace();
  176. console.groupEnd();
  177. }
  178. });
  179.  
  180. typeof MediaKeySystemAccess !== 'undefined' && proxy(MediaKeySystemAccess.prototype, 'createMediaKeys', async (_target, _this, _args) => {
  181. const enterMessage = [
  182. `%c[EME] (CALL)%c MediaKeySystemAccess::createMediaKeys%c\n` +
  183. `Key System: %c%s%c\n` +
  184. `Configurations:\n`,
  185.  
  186. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  187. `color: ${$.VALUE}; font-weight: bold;`, _this.keySystem, `color: inherit; font-weight: normal;`,
  188. indent(JSON.stringify(_this.getConfiguration(), null, ' ')),
  189. ];
  190. let result, err;
  191. try {
  192. result = await _target.apply(_this, _args);
  193. return result;
  194. } catch(e) {
  195. err = e;
  196. throw e;
  197. } finally {
  198. console.groupCollapsed(...enterMessage);
  199. if (err) {
  200. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  201. } else {
  202. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, result);
  203. }
  204. console.trace();
  205. console.groupEnd();
  206. }
  207. });
  208.  
  209. if (typeof MediaKeys !== 'undefined') {
  210. proxy(MediaKeys.prototype, 'setServerCertificate', async (_target, _this, _args) => {
  211. const [serverCertificate] = _args;
  212. const enterMessage = [
  213. `%c[EME] (CALL)%c MediaKeys::setServerCertificate%c\n` +
  214. `Server Certificate:`,
  215.  
  216. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  217. b64.encode(serverCertificate),
  218. ];
  219. let result, err;
  220. try {
  221. result = await _target.apply(_this, _args);
  222. return result;
  223. } catch(e) {
  224. err = e;
  225. throw e;
  226. } finally {
  227. console.groupCollapsed(...enterMessage);
  228. if (err) {
  229. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  230. } else {
  231. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, result);
  232. }
  233. console.trace();
  234. console.groupEnd();
  235. }
  236. });
  237.  
  238. proxy(MediaKeys.prototype, 'createSession', (_target, _this, _args) => {
  239. const [sessionType] = _args;
  240. const enterMessage = [
  241. `%c[EME] (CALL)%c MediaKeys::createSession%c\n` +
  242. `Session Type: %c%s`,
  243.  
  244. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  245. `color: ${$.VALUE}; font-weight: bold;`, sessionType || 'temporary (default)',
  246. ];
  247. let session, err;
  248. try {
  249. session = _target.apply(_this, _args);
  250. session.addEventListener('message', messageHandler);
  251. session.addEventListener('keystatuseschange', keystatuseschangeHandler);
  252. return session;
  253. } catch(e) {
  254. err = e;
  255. throw e;
  256. } finally {
  257. console.groupCollapsed(...enterMessage);
  258. if (err) {
  259. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  260. } else {
  261. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, session);
  262. }
  263. console.trace();
  264. console.groupEnd();
  265. }
  266. });
  267. }
  268.  
  269. if (typeof EventTarget !== 'undefined') {
  270. proxy(EventTarget.prototype, 'addEventListener', async (_target, _this, _args) => {
  271. if (_this != null) {
  272. const [type, listener] = _args;
  273. const storeKey = Symbol.for(getEventListeners);
  274. if (!(storeKey in _this)) _this[storeKey] = {};
  275. const store = _this[storeKey];
  276. if (!(type in store)) store[type] = [];
  277. const listeners = store[type];
  278. if (listeners.indexOf(listener) < 0) {
  279. listeners.push(listener);
  280. }
  281. }
  282. return _target.apply(_this, _args);
  283. });
  284.  
  285. proxy(EventTarget.prototype, 'removeEventListener', async (_target, _this, _args) => {
  286. if (_this != null) {
  287. const [type, listener] = _args;
  288. const storeKey = Symbol.for(getEventListeners);
  289. if (!(storeKey in _this)) return;
  290. const store = _this[storeKey];
  291. if (!(type in store)) return;
  292. const listeners = store[type];
  293. const index = listeners.indexOf(listener);
  294. if (index >= 0) {
  295. if (listeners.length === 1) {
  296. delete store[type];
  297. } else {
  298. listeners.splice(index, 1);
  299. }
  300. }
  301. }
  302. return _target.apply(_this, _args);
  303. });
  304. }
  305.  
  306. if (typeof MediaKeySession !== 'undefined') {
  307. proxy(MediaKeySession.prototype, 'generateRequest', async (_target, _this, _args) => {
  308. const [initDataType, initData] = _args;
  309. const enterMessage = [
  310. `%c[EME] (CALL)%c MediaKeySession::generateRequest%c\n` +
  311. `Session ID: %c%s%c\n` +
  312. `Init Data Type: %c%s%c\n` +
  313. `Init Data:`,
  314.  
  315. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  316. `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)', `color: inherit; font-weight: normal;`,
  317. `color: ${$.VALUE}; font-weight: bold;`, initDataType, `color: inherit; font-weight: normal;`,
  318. b64.encode(initData),
  319. ];
  320. let result, err;
  321. try {
  322. result = await _target.apply(_this, _args);
  323. return result;
  324. } catch(e) {
  325. err = e;
  326. throw e;
  327. } finally {
  328. console.groupCollapsed(...enterMessage);
  329. if (err) {
  330. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  331. } else {
  332. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, result);
  333. }
  334. console.trace();
  335. console.groupEnd();
  336. }
  337. });
  338.  
  339. proxy(MediaKeySession.prototype, 'load', async (_target, _this, _args) => {
  340. const [sessionId] = _args;
  341. const enterMessage = [
  342. `%c[EME] (CALL)%c MediaKeySession::load%c\n` +
  343. `Session ID: %c%s`,
  344.  
  345. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  346. `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)',
  347. ];
  348. let result, err;
  349. try {
  350. result = await _target.apply(_this, _args);
  351. return result;
  352. } catch(e) {
  353. err = e;
  354. throw e;
  355. } finally {
  356. console.groupCollapsed(...enterMessage);
  357. if (err) {
  358. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  359. } else {
  360. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`, result);
  361. }
  362. console.trace();
  363. console.groupEnd();
  364. }
  365. });
  366.  
  367. proxy(MediaKeySession.prototype, 'update', async (_target, _this, _args) => {
  368. const [response] = _args;
  369. const enterMessage = [
  370. `%c[EME] (CALL)%c MediaKeySession::update%c\n` +
  371. `Session ID: %c%s%c\n` +
  372. `Response:`,
  373.  
  374. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  375. `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)', `color: inherit; font-weight: normal;`,
  376. b64.encode(response),
  377. ];
  378. let err;
  379. try {
  380. return await _target.apply(_this, _args);
  381. } catch(e) {
  382. err = e;
  383. throw e;
  384. } finally {
  385. console.groupCollapsed(...enterMessage);
  386. if (err) {
  387. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  388. } else {
  389. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`);
  390. }
  391. console.trace();
  392. console.groupEnd();
  393. }
  394. });
  395.  
  396. proxy(MediaKeySession.prototype, 'close', async (_target, _this, _args) => {
  397. const enterMessage = [
  398. `%c[EME] (CALL)%c MediaKeySession::close%c\n` +
  399. `Session ID: %c%s`,
  400.  
  401. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  402. `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)',
  403. ];
  404. let err;
  405. try {
  406. return await _target.apply(_this, _args);
  407. } catch(e) {
  408. err = e;
  409. throw e;
  410. } finally {
  411. console.groupCollapsed(...enterMessage);
  412. if (err) {
  413. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  414. } else {
  415. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`);
  416. }
  417. console.trace();
  418. console.groupEnd();
  419. }
  420. });
  421.  
  422. proxy(MediaKeySession.prototype, 'remove', async (_target, _this, _args) => {
  423. const enterMessage = [
  424. `%c[EME] (CALL)%c MediaKeySession::remove%c\n` +
  425. `Session ID: %c%s`,
  426.  
  427. `color: ${$.INFO}; font-weight: bold;`, `color: ${$.METHOD};`, `font-weight: normal;`,
  428. `color: ${$.VALUE}; font-weight: bold;`, _this.sessionId || '(not available)',
  429. ];
  430. let err;
  431. try {
  432. return await _target.apply(_this, _args);
  433. } catch(e) {
  434. err = e;
  435. throw e;
  436. } finally {
  437. console.groupCollapsed(...enterMessage);
  438. if (err) {
  439. console.error(`%c[FAILURE]`, `color: ${$.FAILURE}; font-weight: bold;`, err);
  440. } else {
  441. console.log(`%c[SUCCESS]`, `color: ${$.SUCCESS}; font-weight: bold;`);
  442. }
  443. console.trace();
  444. console.groupEnd();
  445. }
  446. });
  447. }
  448. })();