iqiyi player switch

iqiyi player switch between flash and html5

目前為 2017-05-01 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name iqiyi player switch
  3. // @namespace https://github.com/gooyie/userscript-iqiyi-player-switch
  4. // @homepageURL https://github.com/gooyie/userscript-iqiyi-player-switch
  5. // @supportURL https://github.com/gooyie/userscript-iqiyi-player-switch/issues
  6. // @version 1.5.0
  7. // @description iqiyi player switch between flash and html5
  8. // @author gooyie
  9. // @license MIT License
  10. //
  11. // @include *://*.iqiyi.com/*
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_info
  16. // @grant GM_log
  17. // @grant unsafeWindow
  18. // @require https://greasyfork.org/scripts/29319-web-streams-polyfill/code/web-streams-polyfill.js?version=191261
  19. // @require https://greasyfork.org/scripts/29306-fetch-readablestream/code/fetch-readablestream.js?version=191832
  20. // @require https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/3.3.4/adapter.min.js
  21. // @require https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.7.0/js/md5.min.js
  22. // @run-at document-start
  23. // ==/UserScript==
  24.  
  25. (function() {
  26. 'use strict';
  27.  
  28. const PLAYER_TYPE = {
  29. Html5VOD: 'h5_VOD',
  30. FlashVOD: 'flash_VOD'
  31. };
  32.  
  33. class Logger {
  34.  
  35. static get tag() {
  36. return `[${GM_info.script.name}]: `;
  37. }
  38.  
  39. static log(msg) {
  40. GM_log(this.tag + msg);
  41. }
  42.  
  43. }
  44.  
  45. class Cookies {
  46.  
  47. static get(key) {
  48. let value;
  49. if (new RegExp('^[^\\x00-\\x20\\x7f\\(\\)<>@,;:\\\\\\"\\[\\]\\?=\\{\\}\\/\\u0080-\\uffff]+$').test(key)) {
  50. let re = new RegExp('(^| )' + key + '=([^;]*)(;|$)');
  51. let rs = re.exec(document.cookie);
  52. value = rs ? rs[2] : '';
  53. }
  54. return value ? decodeURIComponent(value) : '';
  55. }
  56.  
  57. static set(k, v, o={}) {
  58. let n = o.expires;
  59. if ('number' == typeof o.expires) {
  60. n = new Date();
  61. n.setTime(n.getTime() + o.expires);
  62. }
  63. let key = k;
  64. let value = encodeURIComponent(v);
  65. let path = o.path ? '; path=' + o.path : '';
  66. let expires = n ? '; expires=' + n.toGMTString() : '';
  67. let domain = o.domain ? '; domain=' + o.domain : '';
  68. document.cookie = `${key}=${value}${path}${expires}${domain}`;
  69. }
  70.  
  71. static remove(k, o={}) {
  72. o.expires = new Date(0);
  73. this.set(k, '', o);
  74. }
  75.  
  76. }
  77.  
  78. class Detector {
  79.  
  80. static isSupportHtml5() {
  81. let v = document.createElement('video');
  82. return !!(
  83. v.canPlayType('audio/mp4; codecs="mp4a.40.2"') &&
  84. v.canPlayType('video/mp4; codecs="avc1.640029"') &&
  85. v.canPlayType('video/mp4; codecs="avc1.640029, mp4a.40.2"')
  86. );
  87. }
  88.  
  89. static isSupportVms() {
  90. return !!(
  91. window.MediaSource && window.URL && window.WebSocket && window.ReadableStream &&
  92. (window.RTCSessionDescription || window.webkitRTCSessionDescription) &&
  93. (window.RTCPeerConnection || window.webkitRTCPeerConnection) &&
  94. (window.RTCIceCandidate || window.webkitRTCIceCandidate)
  95. );
  96. }
  97.  
  98. static isSupportM3u8() {
  99. let v = document.createElement('video');
  100. return !!(
  101. v.canPlayType('application/x-mpegurl') &&
  102. v.canPlayType('application/vnd.apple.mpegurl')
  103. );
  104. }
  105.  
  106. static isFirefox() {
  107. return /firefox/i.test(navigator.userAgent);
  108. }
  109.  
  110. static isEdge() {
  111. return /edge/i.test(navigator.userAgent);
  112. }
  113.  
  114. }
  115.  
  116. class Hooker {
  117.  
  118. static hookCall(cb = ()=>{}) {
  119.  
  120. const call = Function.prototype.call;
  121. Function.prototype.call = function(...args) {
  122. let ret = call.bind(this)(...args);
  123. if (args) cb(...args);
  124. return ret;
  125. };
  126.  
  127. Function.prototype.call.toString = Function.prototype.call.toLocaleString = function() {
  128. return 'function call() { [native code] }';
  129. };
  130.  
  131. }
  132.  
  133. static _isFactoryCall(args) { // module.exports, module, module.exports, require
  134. return args.length === 4 && 'object' === typeof args[1] && args[1].hasOwnProperty('exports');
  135. }
  136.  
  137. static hookFactoryCall(cb = ()=>{}) {
  138. this.hookCall((...args) => {if (this._isFactoryCall(args)) cb(...args);});
  139. }
  140.  
  141. static _isJqueryFactoryCall(exports) {
  142. return exports.hasOwnProperty('fn') && exports.fn.hasOwnProperty('jquery');
  143. }
  144.  
  145. static hookJquery(cb = ()=>{}) {
  146. this.hookFactoryCall((...args) => {if (this._isJqueryFactoryCall(args[1].exports)) cb(...args);});
  147. }
  148.  
  149. static hookJqueryAjax(cb = ()=>{}) {
  150. this.hookJquery((...args) => {
  151. let exports = args[1].exports;
  152.  
  153. const ajax = exports.ajax.bind(exports);
  154.  
  155. exports.ajax = function(url, options = {}) {
  156. if (typeof url === 'object') {
  157. [url, options] = [url.url, url];
  158. }
  159.  
  160. let isHijacked = cb(url, options);
  161. if (isHijacked) return;
  162.  
  163. return ajax(url, options);
  164. };
  165. });
  166. }
  167.  
  168. static _isHttpFactoryCall(exports = {}) {
  169. return exports.hasOwnProperty('jsonp') && exports.hasOwnProperty('ajax');
  170. }
  171.  
  172. static hookHttp(cb = ()=>{}) {
  173. this.hookFactoryCall((...args) => {if (this._isHttpFactoryCall(args[1].exports)) cb(...args);});
  174. }
  175.  
  176. static hookHttpJsonp(cb = ()=>{}) {
  177. this.hookHttp((...args) => {
  178. let exports = args[1].exports;
  179.  
  180. const jsonp = exports.jsonp.bind(exports);
  181.  
  182. exports.jsonp = function(options) {
  183. let isHijacked = cb(options);
  184. if (isHijacked) return;
  185. return jsonp(options);
  186. };
  187. });
  188. }
  189.  
  190. }
  191.  
  192. class Faker {
  193.  
  194. static fakeMacPlatform() {
  195. const PLAFORM_MAC = 'mac';
  196. Object.defineProperty(unsafeWindow.navigator, 'platform', {get: () => PLAFORM_MAC});
  197. }
  198.  
  199. static fakeSafari() {
  200. const UA_SAFARY = 'safari';
  201. Object.defineProperty(unsafeWindow.navigator, 'userAgent', {get: () => UA_SAFARY});
  202. }
  203.  
  204. static fakeChrome() {
  205. const UA_CHROME = 'chrome';
  206. Object.defineProperty(unsafeWindow.navigator, 'userAgent', {get: () => UA_CHROME});
  207. }
  208.  
  209. static _calcSign(authcookie) {
  210. const RESPONSE_KEY = '-0J1d9d^ESd)9jSsja';
  211. return md5(authcookie.substring(5, 39).split('').reverse().join('') + '<1<' + RESPONSE_KEY);
  212. }
  213.  
  214. static fakeVipRes(authcookie) {
  215. let json = {
  216. code: 'A00000',
  217. data: {
  218. sign: this._calcSign(authcookie)
  219. }
  220. };
  221. return json;
  222. }
  223.  
  224. static fakeAdRes() {
  225. let json = {};
  226. return json;
  227. }
  228.  
  229. static fakePassportCookie() {
  230. Cookies.set('P00001', 'faked_passport', {domain: '.iqiyi.com'});
  231. Logger.log(`faked passport cookie`);
  232. }
  233.  
  234. }
  235.  
  236. class Mocker {
  237.  
  238. static mock() {
  239. let currType = GM_getValue('player_forcedType', PLAYER_TYPE.Html5VOD);
  240. if (currType !== PLAYER_TYPE.Html5VOD) return;
  241. if (!Detector.isSupportHtml5()) return alert('╮(╯▽╰)╭ 你的浏览器播放不了html5视频~~~~');
  242.  
  243. this.forceHtml5();
  244. this.mockForBestDefintion();
  245. this.mockAd();
  246. this.mockVip();
  247.  
  248. window.addEventListener('unload', event => this.destroy());
  249. }
  250.  
  251. static forceHtml5() {
  252. Logger.log(`setting player_forcedType cookie as ${PLAYER_TYPE.Html5VOD}`);
  253. Cookies.set('player_forcedType', PLAYER_TYPE.Html5VOD, {domain: '.iqiyi.com'});
  254. }
  255.  
  256. static mockToUseVms() {
  257. Faker.fakeMacPlatform();
  258. Faker.fakeChrome();
  259. }
  260.  
  261. static mockToUseM3u8() {
  262. Faker.fakeMacPlatform();
  263. Faker.fakeSafari();
  264. }
  265.  
  266. static _isVideoReq(url) {
  267. return /^https?:\/\/(?:\d+.?){4}\/videos\/v.*$/.test(url);
  268. }
  269.  
  270. static mockForBestDefintion() {
  271. // apply shims
  272. if (Detector.isFirefox()) {
  273. const fetch = unsafeWindow.fetch.bind(unsafeWindow);
  274.  
  275. unsafeWindow.fetch = (url, opts) => {
  276. if (this._isVideoReq(url)) {
  277. Logger.log(`fetching stream ${url}`);
  278. return fetchStream(url, opts); // xhr with moz-chunked-arraybuffer
  279. } else {
  280. return fetch(url, opts);
  281. }
  282. };
  283. } else if (Detector.isEdge()) {
  284. // export to the global window object
  285. unsafeWindow.RTCIceCandidate = window.RTCIceCandidate;
  286. unsafeWindow.RTCPeerConnection = window.RTCPeerConnection;
  287. unsafeWindow.RTCSessionDescription = window.RTCSessionDescription;
  288. }
  289. // auto fall-back
  290. if (Detector.isSupportVms()) {
  291. this.mockToUseVms(); // vms, 1080p or higher
  292. } else if (Detector.isSupportM3u8()) {
  293. this.mockToUseM3u8(); // tmts m3u8
  294. } else {
  295. // by default, tmts mp4 ...
  296. }
  297. }
  298.  
  299. static _isAdReq(url) {
  300. const AD_URL = 'http://t7z.cupid.iqiyi.com/show2';
  301. return url.indexOf(AD_URL) === 0;
  302. }
  303.  
  304. static mockAd() {
  305. Hooker.hookJqueryAjax((url, options) => {
  306. if (this._isAdReq(url)) {
  307. let res = Faker.fakeAdRes();
  308. options.complete({responseJSON: res}, 'success');
  309. Logger.log(`mocked ad request ${url}`);
  310. return true;
  311. }
  312. });
  313. }
  314.  
  315. static _isCheckVipReq(url) {
  316. const CHECK_VIP_URL = 'https://cmonitor.iqiyi.com/apis/user/check_vip.action';
  317. return url === CHECK_VIP_URL;
  318. }
  319.  
  320. static _isLogin() {
  321. return !!Cookies.get('P00001');
  322. }
  323.  
  324. static mockVip() {
  325. if (!this._isLogin()) Faker.fakePassportCookie();
  326.  
  327. Hooker.hookHttpJsonp((options) => {
  328. let url = options.url;
  329.  
  330. if (this._isCheckVipReq(url)) {
  331. let res = Faker.fakeVipRes(options.params.authcookie);
  332. options.success(res);
  333. Logger.log(`mocked check vip request ${url}`);
  334. return true;
  335. }
  336. });
  337. }
  338.  
  339. static destroy() {
  340. Cookies.remove('player_forcedType', {domain: '.iqiyi.com'});
  341. if (Cookies.get('P00001') === 'faked_passport') Cookies.remove('P00001', {domain: '.iqiyi.com'});
  342. Logger.log(`removed cookies.`);
  343. }
  344.  
  345. }
  346.  
  347. class Switcher {
  348.  
  349. static switchTo(toType) {
  350. Logger.log(`switching to ${toType} ...`);
  351.  
  352. GM_setValue('player_forcedType', toType);
  353. document.location.reload();
  354. }
  355.  
  356. }
  357.  
  358. function registerMenu() {
  359. const MENU_NAME = {
  360. HTML5: 'HTML5播放器',
  361. FLASH: 'Flash播放器'
  362. };
  363.  
  364. let currType = GM_getValue('player_forcedType', PLAYER_TYPE.Html5VOD); // 默认为Html5播放器,免去切换。
  365. let [toType, name] = currType === PLAYER_TYPE.Html5VOD ? [PLAYER_TYPE.FlashVOD, MENU_NAME.FLASH] : [PLAYER_TYPE.Html5VOD, MENU_NAME.HTML5];
  366. GM_registerMenuCommand(name, () => Switcher.switchTo(toType), null);
  367. Logger.log(`registered menu.`);
  368. }
  369.  
  370.  
  371. registerMenu();
  372. Mocker.mock();
  373.  
  374. })();