// ==UserScript==
// @name iqiyi player switch
// @namespace https://github.com/gooyie/userscript-iqiyi-player-switch
// @homepageURL https://github.com/gooyie/userscript-iqiyi-player-switch
// @supportURL https://github.com/gooyie/userscript-iqiyi-player-switch/issues
// @version 1.5.1
// @description iqiyi player switch between flash and html5
// @author gooyie
// @license MIT License
//
// @include *://*.iqiyi.com/*
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_info
// @grant GM_log
// @grant unsafeWindow
// @require https://greasyfork.org/scripts/29319-web-streams-polyfill/code/web-streams-polyfill.js?version=191261
// @require https://greasyfork.org/scripts/29306-fetch-readablestream/code/fetch-readablestream.js?version=191832
// @require https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/3.3.4/adapter.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.7.0/js/md5.min.js
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const PLAYER_TYPE = {
Html5VOD: 'h5_VOD',
FlashVOD: 'flash_VOD'
};
class Logger {
static get tag() {
return `[${GM_info.script.name}]: `;
}
static log(msg) {
GM_log(this.tag + msg);
}
}
class Cookies {
static get(key) {
let value;
if (new RegExp('^[^\\x00-\\x20\\x7f\\(\\)<>@,;:\\\\\\"\\[\\]\\?=\\{\\}\\/\\u0080-\\uffff]+$').test(key)) {
let re = new RegExp('(^| )' + key + '=([^;]*)(;|$)');
let rs = re.exec(document.cookie);
value = rs ? rs[2] : '';
}
return value ? decodeURIComponent(value) : '';
}
static set(k, v, o={}) {
let n = o.expires;
if ('number' == typeof o.expires) {
n = new Date();
n.setTime(n.getTime() + o.expires);
}
let key = k;
let value = encodeURIComponent(v);
let path = o.path ? '; path=' + o.path : '';
let expires = n ? '; expires=' + n.toGMTString() : '';
let domain = o.domain ? '; domain=' + o.domain : '';
document.cookie = `${key}=${value}${path}${expires}${domain}`;
}
static remove(k, o={}) {
o.expires = new Date(0);
this.set(k, '', o);
}
}
class Detector {
static isSupportHtml5() {
let v = document.createElement('video');
return !!(
v.canPlayType('audio/mp4; codecs="mp4a.40.2"') &&
v.canPlayType('video/mp4; codecs="avc1.640029"') &&
v.canPlayType('video/mp4; codecs="avc1.640029, mp4a.40.2"')
);
}
static isSupportVms() {
return !!(
window.MediaSource && window.URL && window.WebSocket && window.ReadableStream &&
(window.RTCSessionDescription || window.webkitRTCSessionDescription) &&
(window.RTCPeerConnection || window.webkitRTCPeerConnection) &&
(window.RTCIceCandidate || window.webkitRTCIceCandidate)
);
}
static isSupportM3u8() {
let v = document.createElement('video');
return !!(
v.canPlayType('application/x-mpegurl') &&
v.canPlayType('application/vnd.apple.mpegurl')
);
}
static isFirefox() {
return /firefox/i.test(navigator.userAgent);
}
static isEdge() {
return /edge/i.test(navigator.userAgent);
}
}
class Hooker {
static hookCall(cb = ()=>{}) {
const call = Function.prototype.call;
Function.prototype.call = function(...args) {
let ret = call.bind(this)(...args);
if (args) cb(...args);
return ret;
};
Function.prototype.call.toString = Function.prototype.call.toLocaleString = function() {
return 'function call() { [native code] }';
};
}
static _isFactoryCall(args) { // module.exports, module, module.exports, require
return args.length === 4 && 'object' === typeof args[1] && args[1].hasOwnProperty('exports');
}
static hookFactoryCall(cb = ()=>{}) {
this.hookCall((...args) => {if (this._isFactoryCall(args)) cb(...args);});
}
static _isJqueryFactoryCall(exports) {
return exports.hasOwnProperty('fn') && exports.fn.hasOwnProperty('jquery');
}
static hookJquery(cb = ()=>{}) {
this.hookFactoryCall((...args) => {if (this._isJqueryFactoryCall(args[1].exports)) cb(...args);});
}
static hookJqueryAjax(cb = ()=>{}) {
this.hookJquery((...args) => {
let exports = args[1].exports;
const ajax = exports.ajax.bind(exports);
exports.ajax = function(url, options = {}) {
if (typeof url === 'object') {
[url, options] = [url.url, url];
}
let isHijacked = cb(url, options);
if (isHijacked) return;
return ajax(url, options);
};
});
}
static _isHttpFactoryCall(exports = {}) {
return exports.hasOwnProperty('jsonp') && exports.hasOwnProperty('ajax');
}
static hookHttp(cb = ()=>{}) {
this.hookFactoryCall((...args) => {if (this._isHttpFactoryCall(args[1].exports)) cb(...args);});
}
static hookHttpJsonp(cb = ()=>{}) {
this.hookHttp((...args) => {
let exports = args[1].exports;
const jsonp = exports.jsonp.bind(exports);
exports.jsonp = function(options) {
let isHijacked = cb(options);
if (isHijacked) return;
return jsonp(options);
};
});
}
}
class Faker {
static fakeMacPlatform() {
const PLAFORM_MAC = 'mac';
Object.defineProperty(unsafeWindow.navigator, 'platform', {get: () => PLAFORM_MAC});
}
static fakeSafari() {
const UA_SAFARY = 'safari';
Object.defineProperty(unsafeWindow.navigator, 'userAgent', {get: () => UA_SAFARY});
}
static fakeChrome() {
const UA_CHROME = 'chrome';
Object.defineProperty(unsafeWindow.navigator, 'userAgent', {get: () => UA_CHROME});
}
static _calcSign(authcookie) {
const RESPONSE_KEY = '-0J1d9d^ESd)9jSsja';
return md5(authcookie.substring(5, 39).split('').reverse().join('') + '<1<' + RESPONSE_KEY);
}
static fakeVipRes(authcookie) {
let json = {
code: 'A00000',
data: {
sign: this._calcSign(authcookie)
}
};
return json;
}
static fakeAdRes() {
let json = {};
return json;
}
static fakePassportCookie() {
Cookies.set('P00001', 'faked_passport', {domain: '.iqiyi.com'});
Logger.log(`faked passport cookie`);
}
}
class Mocker {
static mock() {
let currType = GM_getValue('player_forcedType', PLAYER_TYPE.Html5VOD);
if (currType !== PLAYER_TYPE.Html5VOD) return;
if (!Detector.isSupportHtml5()) return alert('╮(╯▽╰)╭ 你的浏览器播放不了html5视频~~~~');
this.forceHtml5();
this.mockForBestDefintion();
this.mockAd();
this.mockVip();
window.addEventListener('unload', event => this.destroy());
}
static forceHtml5() {
Logger.log(`setting player_forcedType cookie as ${PLAYER_TYPE.Html5VOD}`);
Cookies.set('player_forcedType', PLAYER_TYPE.Html5VOD, {domain: '.iqiyi.com'});
}
static mockToUseVms() {
Faker.fakeMacPlatform();
Faker.fakeChrome();
}
static mockToUseM3u8() {
Faker.fakeMacPlatform();
Faker.fakeSafari();
}
static _isVideoReq(url) {
return /^https?:\/\/(?:\d+.?){4}\/videos\/v.*$/.test(url);
}
static mockForBestDefintion() {
// apply shims
if (Detector.isFirefox()) {
const fetch = unsafeWindow.fetch.bind(unsafeWindow);
unsafeWindow.fetch = (url, opts) => {
if (this._isVideoReq(url)) {
Logger.log(`fetching stream ${url}`);
return fetchStream(url, opts); // xhr with moz-chunked-arraybuffer
} else {
return fetch(url, opts);
}
};
} else if (Detector.isEdge()) {
// export to the global window object
unsafeWindow.RTCIceCandidate = window.RTCIceCandidate;
unsafeWindow.RTCPeerConnection = window.RTCPeerConnection;
unsafeWindow.RTCSessionDescription = window.RTCSessionDescription;
}
// auto fall-back
if (Detector.isSupportVms()) {
this.mockToUseVms(); // vms, 1080p or higher
} else if (Detector.isSupportM3u8()) {
this.mockToUseM3u8(); // tmts m3u8
} else {
// by default, tmts mp4 ...
}
}
static _isAdReq(url) {
const AD_URL = 'http://t7z.cupid.iqiyi.com/show2';
return url.indexOf(AD_URL) === 0;
}
static mockAd() {
Hooker.hookJqueryAjax((url, options) => {
if (this._isAdReq(url)) {
let res = Faker.fakeAdRes();
(options.complete || options.success)({responseJSON: res}, 'success');
Logger.log(`mocked ad request ${url}`);
return true;
}
});
}
static _isCheckVipReq(url) {
const CHECK_VIP_URL = 'https://cmonitor.iqiyi.com/apis/user/check_vip.action';
return url === CHECK_VIP_URL;
}
static _isLogin() {
return !!Cookies.get('P00001');
}
static mockVip() {
if (!this._isLogin()) Faker.fakePassportCookie();
Hooker.hookHttpJsonp((options) => {
let url = options.url;
if (this._isCheckVipReq(url)) {
let res = Faker.fakeVipRes(options.params.authcookie);
options.success(res);
Logger.log(`mocked check vip request ${url}`);
return true;
}
});
}
static destroy() {
Cookies.remove('player_forcedType', {domain: '.iqiyi.com'});
if (Cookies.get('P00001') === 'faked_passport') Cookies.remove('P00001', {domain: '.iqiyi.com'});
Logger.log(`removed cookies.`);
}
}
class Switcher {
static switchTo(toType) {
Logger.log(`switching to ${toType} ...`);
GM_setValue('player_forcedType', toType);
document.location.reload();
}
}
function registerMenu() {
const MENU_NAME = {
HTML5: 'HTML5播放器',
FLASH: 'Flash播放器'
};
let currType = GM_getValue('player_forcedType', PLAYER_TYPE.Html5VOD); // 默认为Html5播放器,免去切换。
let [toType, name] = currType === PLAYER_TYPE.Html5VOD ? [PLAYER_TYPE.FlashVOD, MENU_NAME.FLASH] : [PLAYER_TYPE.Html5VOD, MENU_NAME.HTML5];
GM_registerMenuCommand(name, () => Switcher.switchTo(toType), null);
Logger.log(`registered menu.`);
}
registerMenu();
Mocker.mock();
})();