├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── build ├── build.js ├── config.js └── pack.js ├── icon.png ├── icon.pxd ├── manifest.json ├── package.json ├── src ├── JSocket.ts ├── background.js ├── chrome.ts ├── danmu.less ├── danmuPlayer.ts ├── donate.ts ├── douyu │ ├── api.ts │ ├── contentScript.ts │ ├── inject.ts │ ├── signer.ts │ └── source.ts ├── embedSWF.ts ├── flash │ ├── builtin.abc │ ├── douyu.swf │ ├── flashemu.js │ └── playerglobal.abc ├── hookfetch.js ├── img │ ├── alipay.png │ ├── disabled.png │ ├── fullscreen.svg │ ├── muted.svg │ ├── reload.svg │ ├── volume.svg │ └── wechat.png ├── md5.ts ├── playerMenu.js ├── shared-worker-signer.js ├── sharedWorker │ ├── background.js │ ├── launcher.js │ ├── sharedWorker.html │ └── sharedWorker.js ├── source.ts └── utils.ts ├── tsconfig.json └── typings ├── flv.js.d.ts └── global.d.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | sourceType: 'module' 5 | }, 6 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 7 | extends: 'standard', 8 | env: { 9 | browser: true 10 | }, 11 | globals: { 12 | chrome: true, 13 | FlashEmu: true 14 | }, 15 | plugins: [], 16 | // add your custom rules here 17 | 'rules': { 18 | 'no-return-assign': 0, 19 | // allow paren-less arrow functions 20 | 'arrow-parens': 0, 21 | // allow debugger during development 22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 23 | 'key-spacing': 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | note/ 2 | node_modules/ 3 | dist/ 4 | *.zip 5 | versions/ 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // 将设置放入此文件中以覆盖默认值和用户设置。 2 | { 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | "files.autoGuessEncoding": false // always utf-8 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2016-present, spacemeowx2 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 斗鱼HTML5播放器 2 | 3 | 基于 [flv.js](https://github.com/Bilibili/flv.js) 的斗鱼HTML5播放器. 4 | 5 | 使用了 flv.js 内核提供的直播流播放, 用 JavaScript 实现了斗鱼的弹幕协议, 并支持发送弹幕和送礼物. 6 | 7 | ![screenshot](https://user-images.githubusercontent.com/8019167/33715813-d3f38294-db8e-11e7-95c7-c029d69ebf7e.jpg) 8 | 9 | # 使用 10 | 11 | **不要**使用 Chrome 直接加载本文件夹, 本扩展程序需要构建后才能使用. 12 | 13 | [Chrome 应用商店](https://chrome.google.com/webstore/detail/hbocinidadgpnbcamhjgfbgiebhpnmfj) 14 | 15 | [Firefox 附加组件](https://addons.mozilla.org/zh-CN/firefox/addon/douyuhtml5player/) 16 | 17 | [Greasy Fork](https://greasyfork.org/scripts/26901) (Firefox) 18 | 19 | 要求 Chrome 版本大于等于 49 (仅在54+版本测试过) 20 | 21 | 打开斗鱼的直播间, 如果没有错误, 播放器就已经被自动替换. 22 | 23 | 注: 如开启了 [chrome://flags/#extension-active-script-permission](chrome://flags/#extension-active-script-permission), 请注意允许扩展程序在所有网址上运行, 否则会没有权限运行. 24 | 25 | # 原理 26 | 27 | 视频播放基于 flv.js, 弹幕发射使用 CSS3, 弹幕使用 WebSocket 连接, 在 JavaScript 中实现斗鱼的弹幕协议. 28 | 29 | 由于斗鱼使用了 HTTPS, 受到 Mixed Content 限制, 只能在 Background 页面 fetch 视频内容再传到 Content Script 给 flv.js 进行播放. 30 | 31 | 具体原理请见我的 [blog](http://blog.imspace.cn/2016/10/29/DouyuHTML5Player/) 32 | 33 | # 构建 34 | 35 | 1. `npm install` 36 | 37 | 2. `npm run build` 38 | 39 | 3. `npm run pack` 在 versions 文件夹查看 zip 文件 40 | 41 | # 重要更新 42 | 43 | 0.8.4 开始使用 WebSocket 连接弹幕服务器, 完全摆脱 Flash 的依赖. 44 | 45 | 0.7.0 开始已经使用 [flash-emu](https://github.com/spacemeowx2/flash-emu) 进行签名 46 | 47 | # 捐赠 48 | 49 | 欢迎投食(逃 50 | 51 | 支付宝 52 | 53 | ![alipay](https://user-images.githubusercontent.com/8019167/28763218-faff38b6-75ee-11e7-80a0-0ecb031256e2.png) 54 | 55 | 56 | 微信 57 | 58 | ![wechat](https://user-images.githubusercontent.com/8019167/28763153-7e168bc4-75ee-11e7-8aa6-322a33a4c2de.png) 59 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | // rollup --environment TARGET:douyu-inject,NODE_ENV=production -c build/config.js 2 | // rollup --environment TARGET:content-script,NODE_ENV=production -c build/config.js 3 | const fs = require('fs') 4 | const path = require('path') 5 | const rollup = require('rollup') 6 | const less = require('less') 7 | // const uglify = require('uglify-js') 8 | const copy = (from, to) => { 9 | const c = require('copy') 10 | return new Promise((resolve, reject) => { 11 | c(from, to, (err, file) => { 12 | if (err) { 13 | reject(err) 14 | } else { 15 | resolve(file) 16 | } 17 | }) 18 | }) 19 | } 20 | const copyEach = (from, to, opts) => { 21 | const c = require('copy') 22 | return new Promise((resolve, reject) => { 23 | c.each(from, to, opts, (err, file) => { 24 | if (err) { 25 | reject(err) 26 | } else { 27 | resolve(file) 28 | } 29 | }) 30 | }) 31 | } 32 | 33 | if (!fs.existsSync('dist')) { 34 | fs.mkdirSync('dist') 35 | } 36 | 37 | const version = process.env.VERSION || require('../package.json').version 38 | let builds = require('./config').getAllBuilds() 39 | 40 | build(builds) 41 | 42 | function build (builds) { 43 | let built = 0 44 | const total = builds.length 45 | const next = () => { 46 | return buildEntry(builds[built]).then(() => { 47 | built++ 48 | if (built < total) { 49 | return next() 50 | } 51 | }) 52 | } 53 | const fileList = [ 54 | 'src/flash/builtin.abc', 55 | 'src/flash/playerglobal.abc', 56 | 'src/flash/douyu.swf', 57 | 'src/flash/flashemu.js', 58 | 'src/background.js', 59 | 'node_modules/flv.js/dist/flv.min.js' 60 | ] 61 | return copy('src/img/*', 'dist/img') 62 | .then(() => copyEach(fileList, 'dist', {flatten: true})) 63 | .then(() => read('src/danmu.less')) 64 | .then(lessSrc => less.render(lessSrc)) 65 | .then(css => write('dist/danmu.css', css.css)) 66 | .then(() => next()) 67 | .catch(logError) 68 | } 69 | 70 | function buildEntry (config) { 71 | return rollup.rollup(config).then(bundle => { 72 | const code = bundle.generate(config).code 73 | return write(config.dest, code) 74 | }) 75 | } 76 | 77 | function write (dest, code) { 78 | return new Promise(function (resolve, reject) { 79 | fs.writeFile(dest, code, function (err) { 80 | if (err) return reject(err) 81 | console.log(path.relative(process.cwd(), dest) + ' ' + getSize(code)) 82 | resolve() 83 | }) 84 | }) 85 | } 86 | 87 | function read (src) { 88 | return new Promise((resolve, reject) => { 89 | fs.readFile(src, 'utf8', (err, data) => { 90 | if (err) return reject(err) 91 | resolve(data) 92 | }) 93 | }) 94 | } 95 | 96 | function getSize (code) { 97 | return (code.length / 1024).toFixed(2) + 'kb' 98 | } 99 | 100 | function logError (e) { 101 | // console.log(e) 102 | e.message && console.log(e.message) 103 | e.stack && console.log(e.stack) 104 | } 105 | -------------------------------------------------------------------------------- /build/config.js: -------------------------------------------------------------------------------- 1 | const nodeResolve = require('rollup-plugin-node-resolve') 2 | const commonjs = require('rollup-plugin-commonjs') 3 | const path = require('path') 4 | const typescript = require('rollup-plugin-typescript') 5 | const replace = require('rollup-plugin-replace') 6 | 7 | const sites = ['douyu'] 8 | let builds = {} 9 | sites.forEach(site => { 10 | builds[`${site}-cs`] = () => genConfig( 11 | `${site}/contentScript.ts`, 12 | `${site}CS.js`, 13 | { format: 'iife' } 14 | ) 15 | builds[`${site}-inject`] = () => genConfig( 16 | `${site}/inject.ts`, 17 | `${site}Inject.js`, 18 | { format: 'iife' } 19 | ) 20 | }) 21 | 22 | function genConfig (input, output, opts) { 23 | opts.entry = path.resolve(__dirname, '../src/', input) 24 | opts.dest = path.resolve(__dirname, '../dist/', output) 25 | if (!process.env.REPLACE) { 26 | process.env.REPLACE = JSON.stringify({ 27 | DEBUG: JSON.stringify(false), 28 | USERSCRIPT: JSON.stringify(false) 29 | }) 30 | } 31 | opts.plugins = [ 32 | replace(JSON.parse(process.env.REPLACE)), 33 | nodeResolve({ 34 | skip: ['flv.js'], 35 | extensions: ['.ts', '.js'] 36 | }), 37 | commonjs(), 38 | typescript({ 39 | typescript: require('typescript') 40 | }) 41 | ] 42 | opts.context = 'window' 43 | opts.globals = { 44 | 'flv.js': 'flvjs' 45 | } 46 | opts.indent = ' ' 47 | return opts 48 | } 49 | 50 | if (process.env.TARGET) { 51 | module.exports = builds[process.env.TARGET]() 52 | } else { 53 | exports.getAllBuilds = () => Object.keys(builds).map(name => builds[name]()) 54 | exports.builds = builds 55 | } 56 | -------------------------------------------------------------------------------- /build/pack.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var archiver = require('archiver') 3 | 4 | // 同步manifest的版本 5 | console.log('sync version...') 6 | var pkg = require('../package') 7 | var manifest = fs.readFileSync('manifest.json', { 8 | encoding: 'utf-8' 9 | }) 10 | manifest = manifest.replace(/("version"\s*:\s*)"(\d+\.\d+\.\d+)"/, function (_, v) { 11 | return v + '"' + pkg.version + '"' 12 | }) 13 | fs.writeFileSync('manifest.json', manifest) 14 | 15 | // 压缩成zip 16 | function zip (manifest, filename) { 17 | console.log('ziping...', manifest) 18 | try { 19 | fs.mkdirSync('versions') 20 | } catch (e) {} 21 | var archive = archiver.create('zip', {}) 22 | var output = fs.createWriteStream(filename) 23 | var zipDirs = ['dist'] 24 | var zipFiles = ['icon.png'] 25 | 26 | archive.pipe(output) 27 | 28 | zipDirs.forEach(function (dir) { 29 | archive.directory(dir, dir) 30 | }) 31 | zipFiles.forEach(function (file) { 32 | archive.file(file) 33 | }) 34 | archive.file(manifest, { 35 | name: 'manifest.json' 36 | }) 37 | archive.finalize() 38 | } 39 | 40 | zip('manifest.json', 'versions/dh5p-' + pkg.version + '.zip') 41 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemeowx2/DouyuHTML5Player/8ee6d938db9db9d59f3069e4ee8c10184c197f42/icon.png -------------------------------------------------------------------------------- /icon.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemeowx2/DouyuHTML5Player/8ee6d938db9db9d59f3069e4ee8c10184c197f42/icon.pxd -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "minimum_chrome_version": "49", 4 | 5 | "name": "斗鱼HTML5播放器", 6 | "description": "基于 flv.js 的斗鱼HTML5播放器.", 7 | "version": "0.8.11", 8 | 9 | "page_action": { 10 | "default_icon": "icon.png" 11 | }, 12 | 13 | "icons": { 14 | "256": "icon.png" 15 | }, 16 | 17 | "background": { 18 | "scripts": ["dist/flashemu.js", "dist/background.js"], 19 | "persistent": false 20 | }, 21 | 22 | "permissions": [ 23 | "cookies", 24 | "tabs", 25 | "storage", 26 | "*://*.douyu.com/*", 27 | "*://*.douyucdn.cn/*", 28 | "*://*/*" 29 | ], 30 | 31 | "web_accessible_resources": [ 32 | "icon.png", 33 | "libs/JSocket.js", 34 | "libs/md5.js", 35 | "libs/less.min.js", 36 | "src/img/*", 37 | "src/*", 38 | "src/sharedWorker/sharedWorker.html", 39 | "dist/*", 40 | "*://*/*" 41 | ], 42 | 43 | "content_scripts": [ 44 | { 45 | "matches": [ 46 | "*://*.douyu.com/*" 47 | ], 48 | "js": ["dist/flv.min.js", "dist/douyuCS.js"], 49 | "run_at": "document_end" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "douyu-html5-player", 3 | "version": "0.8.11", 4 | "description": "斗鱼HTML5播放器", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev:douyuc": "rollup --environment TARGET:douyu-cs -w -c build/config.js", 8 | "dev:douyui": "rollup --environment TARGET:douyu-inject -w -c build/config.js", 9 | "build": "node build/build.js", 10 | "pack": "node build/pack.js", 11 | "dev": "tsc -w -p .", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/spacemeowx2/DouyuHTML5Player" 17 | }, 18 | "keywords": [ 19 | "douyu", 20 | "html5", 21 | "player" 22 | ], 23 | "author": "spacemeowx2", 24 | "license": "MIT", 25 | "dependencies": { 26 | "flv.js": "^1.4.0", 27 | "tslib": "^1.6.0" 28 | }, 29 | "devDependencies": { 30 | "@types/chrome": "0.0.44", 31 | "@types/sharedworker": "0.0.27", 32 | "@types/text-encoding": "0.0.32", 33 | "archiver": "^1.3.0", 34 | "copy": "^0.3.0", 35 | "eslint": "^4.5.0", 36 | "eslint-config-standard": "^10.2.1", 37 | "eslint-plugin-import": "^2.7.0", 38 | "eslint-plugin-node": "^5.1.1", 39 | "eslint-plugin-promise": "^3.5.0", 40 | "eslint-plugin-standard": "^3.0.1", 41 | "less": "^2.7.1", 42 | "rollup": "^0.36.3", 43 | "rollup-plugin-commonjs": "^5.0.5", 44 | "rollup-plugin-node-resolve": "^2.0.0", 45 | "rollup-plugin-replace": "^1.2.1", 46 | "rollup-plugin-typescript": "^0.8.1", 47 | "rollup-watch": "^2.5.0", 48 | "typescript": "^2.2.2", 49 | "typestate": "^1.0.4", 50 | "uglify-js": "^2.7.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/JSocket.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Jsocket - Socket on Javascript 3 | * Author: Masahiro Chiba 4 | * Depends: 5 | * - jQuery: http://jquery.com/ 6 | * - jQuery TOOLS - Flashembed: http://flowplayer.org/tools/flashembed.html 7 | * SYNOPSIS: 8 | * JSocket.init('/static/JSocket.swf', function () { 9 | * socket = new JSocket({ 10 | * connectHandler: connectHandler, 11 | * dataHandler: dataHandler, 12 | * closeHandler: closeHandler, 13 | * errorHandler: errorHandler 14 | * }); 15 | * socket.connect(location.hostname, location.port || 80); 16 | * }); 17 | * function connectHandler() { 18 | * socket.writeFlush("GET / HTTP/1.0\x0D\x0A"); 19 | * socket.write("Host: " + location.hostname + "\x0D\x0A\x0D\x0A"); 20 | * socket.flush(); 21 | * } 22 | * function dataHandler(data) { 23 | * alert(data); 24 | * socket.close(); 25 | * } 26 | * function closeHandler() { 27 | * alert('lost connection') 28 | * } 29 | * function errorHandler(errorstr) { 30 | * alert(errorstr); 31 | * } 32 | * 33 | * */ 34 | declare var window: { 35 | [key: string]: any 36 | } & Window 37 | export class Handlers { 38 | connectHandler () {} 39 | dataHandler (data: string) {} 40 | closeHandler () {} 41 | errorHandler (err: string) {} 42 | } 43 | export class JSocket { 44 | socid: number 45 | private static swfloadedcb: Function 46 | static VERSION = '0.1' 47 | static el: HTMLDivElement 48 | static flashapi: any 49 | static async init () { 50 | // const src = 'https://imspace.applinzi.com/player/JSocket.swf' 51 | const src = 'https://imspace.nos-eastchina1.126.net/JSocket2.swf' 52 | const flash = ['', '', '', '', '', '', '', '', '', ""].join("") 53 | let div = document.createElement('div') 54 | div.className = 'jsocket-cls' // 防止Chrome屏蔽小块的 Flash 55 | // div.style.width = '1px' 56 | // div.style.height = '1px' 57 | document.body.appendChild(div) 58 | JSocket.el = div 59 | div.innerHTML = flash 60 | var api = document.querySelector('#jsocket') 61 | console.log(div, api) 62 | JSocket.flashapi = api; 63 | 64 | if ( JSocket.flashapi.newsocket ) { 65 | return 66 | } else { 67 | return new Promise((res, rej) => { 68 | const id = setTimeout(rej, 10 * 1000) 69 | JSocket.swfloadedcb = () => { 70 | clearTimeout(id) 71 | res() 72 | } 73 | }) 74 | } 75 | } 76 | static swfloaded () { 77 | JSocket.swfloadedcb() 78 | } 79 | static handlers: Handlers[] = [] 80 | static connectHandler (socid: number) { 81 | JSocket.handlers[socid].connectHandler() 82 | } 83 | static dataHandler (socid: number, data: string) { 84 | try { 85 | JSocket.handlers[socid].dataHandler(atob(data)) 86 | } catch (e) { 87 | console.error(e) 88 | } 89 | } 90 | static closeHandler (socid: number) { 91 | JSocket.handlers[socid].closeHandler() 92 | } 93 | static errorHandler (socid: number, str: string) { 94 | JSocket.handlers[socid].errorHandler(str) 95 | } 96 | init (handlers: Handlers, newsocketopt: any) { 97 | this.socid = JSocket.flashapi.newsocket(newsocketopt) 98 | JSocket.handlers[this.socid] = handlers 99 | } 100 | connect (host: string, port: number) { 101 | JSocket.flashapi.connect(this.socid, host, port) 102 | } 103 | write (data: string) { 104 | JSocket.flashapi.write(this.socid, btoa(data)) 105 | } 106 | writeFlush (data: string) { 107 | JSocket.flashapi.writeFlush(this.socid, btoa(data)) 108 | } 109 | close () { 110 | JSocket.flashapi.close(this.socid) 111 | } 112 | flush () { 113 | JSocket.flashapi.flush(this.socid) 114 | } 115 | } 116 | window.JSocket = JSocket 117 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | function uint8ToBase64 (buffer) { 2 | let binary = '' 3 | let len = buffer.byteLength 4 | for (let i = 0; i < len; i++) { 5 | binary += String.fromCharCode(buffer[i]) 6 | } 7 | return btoa(binary) 8 | } 9 | function convertHeader (headers) { 10 | let out = {} 11 | for (let key of headers.keys()) { 12 | out[key] = headers.get(key) 13 | } 14 | return out 15 | } 16 | class Fetch { 17 | constructor (port) { 18 | this.reader = null 19 | this.response = null 20 | this.port = port 21 | } 22 | onDisconnect () { 23 | if (this.reader) { 24 | this.reader.cancel() 25 | } 26 | } 27 | onMessage (msg) { 28 | // console.log('fetch new msg', msg) 29 | let chain = Promise.resolve() 30 | if (msg.method === 'fetch') { 31 | chain = chain.then(() => fetch.apply(null, msg.args)).then(r => { 32 | this.response = r 33 | console.log('response', r) 34 | return { 35 | bodyUsed: r.bodyUsed, 36 | ok: r.ok, 37 | status: r.status, 38 | statusText: r.statusText, 39 | type: r.type, 40 | url: r.url, 41 | headers: convertHeader(r.headers) 42 | } 43 | }) 44 | } else if (msg.method === 'json') { 45 | chain = chain.then(() => this.response.json()) 46 | } else if (msg.method === 'arrayBuffer') { 47 | chain = chain.then(() => this.response.arrayBuffer()).then(buf => { 48 | return Array.from(new Uint8Array(buf)) 49 | }) 50 | } else if (msg.method === 'body.getReader') { 51 | chain = chain.then(() => { 52 | this.reader = this.response.body.getReader() 53 | console.log('reader', this.reader) 54 | }) 55 | } else if (msg.method === 'reader.read') { 56 | chain = chain.then(() => this.reader.read()).then(r => { 57 | // console.log('read', r) 58 | if (r.done === false) { 59 | r.value = uint8ToBase64(r.value) 60 | } 61 | return r 62 | }) 63 | } else if (msg.method === 'reader.cancel') { 64 | chain = chain.then(() => this.reader.cancel()) 65 | } else { 66 | this.port.disconnect() 67 | return 68 | } 69 | chain.then((...args) => { 70 | const outMsg = { 71 | method: msg.method, 72 | args: args 73 | } 74 | // console.log('fetch send msg', outMsg) 75 | this.port.postMessage(outMsg) 76 | }).catch(e => { 77 | console.log(e) 78 | this.port.postMessage({ 79 | method: msg.method, 80 | err: { 81 | name: e.name, 82 | message: e.message, 83 | stack: e.stack, 84 | string: e.toString() 85 | } 86 | }) 87 | }) 88 | } 89 | } 90 | FlashEmu.BUILTIN = 'dist/builtin.abc' 91 | FlashEmu.PLAYERGLOBAL = 'dist/playerglobal.abc' 92 | FlashEmu.setGlobalFlags({ 93 | enableDebug: false, 94 | enableLog: false, 95 | enableWarn: false, 96 | enableError: false 97 | }) 98 | class Signer { 99 | static init () { 100 | if (!Signer.emu) { 101 | const emu = new FlashEmu({ 102 | readFile (filename) { 103 | return fetch(filename) 104 | .then(res => res.arrayBuffer()) 105 | .then(buf => new Uint8Array(buf).buffer) 106 | } 107 | }) 108 | Signer.emu = emu 109 | return emu.runSWF('dist/douyu.swf', false).then(() => { 110 | const CModule = emu.getProperty('sample.mp', 'CModule') 111 | const xx = emu.getPublicClass('mp') 112 | Signer.CModule = CModule 113 | Signer.xx = xx 114 | CModule.callProperty('startAsync') 115 | Signer.ready = true 116 | }) 117 | } 118 | } 119 | constructor (port) { 120 | this.port = port 121 | Signer.init() 122 | } 123 | douyuSign (roomId, time, did) { 124 | const CModule = Signer.CModule 125 | const xx = Signer.xx 126 | 127 | let StreamSignDataPtr = CModule.callProperty('malloc', 4) 128 | let outptr1 = CModule.callProperty('malloc', 4) 129 | 130 | let datalen = xx.callProperty('sub_2', parseInt(roomId), parseInt(time), did.toString(), outptr1, StreamSignDataPtr) 131 | 132 | let pSign = CModule.callProperty('read32', StreamSignDataPtr) 133 | let sign = CModule.callProperty('readString', pSign, datalen) 134 | let pOut = CModule.callProperty('read32', outptr1) 135 | let out = CModule.callProperty('readString', pOut, 4) 136 | CModule.callProperty('free', StreamSignDataPtr) 137 | CModule.callProperty('free', outptr1) 138 | console.log('sign result', sign) 139 | return { 140 | sign, 141 | cptl: out 142 | } 143 | } 144 | onDisconnect () { 145 | 146 | } 147 | onMessage (msg) { 148 | let args = [] 149 | if (msg.method === 'query') { 150 | args.push(!!Signer.ready) 151 | } else if (msg.method === 'sign') { 152 | args.push(this.douyuSign(...msg.args)) 153 | } 154 | this.port.postMessage({ 155 | method: msg.method, 156 | args: args 157 | }) 158 | } 159 | } 160 | Signer.init() 161 | chrome.runtime.onConnect.addListener(port => { 162 | let handler 163 | if (port.name === 'fetch') { 164 | console.log('new fetch port', port) 165 | handler = new Fetch(port) 166 | } else if (port.name === 'signer') { 167 | console.log('new signer port', port) 168 | handler = new Signer(port) 169 | } 170 | port.onDisconnect.addListener(() => handler.onDisconnect()) 171 | port.onMessage.addListener(msg => handler.onMessage(msg)) 172 | }) 173 | chrome.pageAction.onClicked.addListener(tab => { 174 | chrome.tabs.sendMessage(tab.id, { 175 | type: 'toggle' 176 | }) 177 | }) 178 | chrome.tabs.onUpdated.addListener((id, x, tab) => { 179 | if (/https?:\/\/[^\/]*\.douyu\.com(\/|$)/.test(tab.url)) { 180 | chrome.pageAction.show(tab.id) 181 | } else { 182 | chrome.pageAction.hide(tab.id) 183 | } 184 | }) 185 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 186 | switch (request.type) { 187 | case 'disable': 188 | chrome.pageAction.setIcon({ 189 | tabId: sender.tab.id, 190 | path: 'dist/img/disabled.png' 191 | }) 192 | break 193 | } 194 | }) 195 | -------------------------------------------------------------------------------- /src/chrome.ts: -------------------------------------------------------------------------------- 1 | const pageAction = { 2 | setIcon (details: chrome.pageAction.IconDetails) { 3 | return new Promise((res, rej) => { 4 | chrome.pageAction.setIcon(details, res) 5 | }) 6 | } 7 | } 8 | const runtime = { 9 | sendMessage (message: any) { 10 | return chrome.runtime.sendMessage(message) 11 | }, 12 | connect (connectInfo?: chrome.runtime.ConnectInfo) { 13 | return chrome.runtime.connect(connectInfo) 14 | } 15 | } 16 | export function hasChrome () { 17 | return typeof chrome !== 'undefined' 18 | } 19 | export { 20 | pageAction, 21 | runtime 22 | } 23 | -------------------------------------------------------------------------------- /src/danmu.less: -------------------------------------------------------------------------------- 1 | @color: #5a5a5a; 2 | @background: #fafafa; 3 | 4 | .jsocket-cls, .big-flash-cls { 5 | width: 80vw; 6 | height: 80vh; 7 | position: absolute; 8 | top: -100vh; 9 | left: 0; 10 | } 11 | 12 | .donate-dialog { 13 | position: fixed; 14 | z-index: 100002; 15 | left: 0; 16 | top: 0; 17 | right: 0; 18 | bottom: 0; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | .donate-title { 23 | font-size: 20px; 24 | } 25 | .donate-content { 26 | margin-bottom: 10px; 27 | } 28 | .donate-wrap { 29 | width: 400px; 30 | padding: 10px; 31 | background: #fff; 32 | border-radius: 5px; 33 | border: 1px solid #aaa; 34 | } 35 | .donate-qrcode-bar { 36 | display: flex; 37 | justify-content: space-between; 38 | img { 39 | height: 168px; 40 | } 41 | } 42 | .donate-qrcode-desc { 43 | text-align: center; 44 | } 45 | .donate-close-btn { 46 | margin-top: 10px; 47 | &:before { 48 | content: '关闭'; 49 | text-align: center; 50 | display: block; 51 | } 52 | } 53 | } 54 | 55 | .player-menu { 56 | position: fixed; 57 | top: 0; 58 | left: 0; 59 | right: 0; 60 | bottom: 0; 61 | z-index: 100001; 62 | .menu { 63 | position: absolute; 64 | background-color: #fff; 65 | min-width: 200px; 66 | border: 1px solid #aaa; 67 | box-shadow: 2px 2px 5px rgba(0,0,0,0.5); 68 | -webkit-user-select: none; 69 | -moz-user-select: none; 70 | .menu-dash { 71 | height: 1px; 72 | background-color: #aaa; 73 | } 74 | .menu-item { 75 | color: #000; 76 | padding: 5px; 77 | // border-bottom: 1px solid #aaa; 78 | cursor: default; 79 | &:last-child { 80 | border-bottom: 0; 81 | } 82 | &:hover { 83 | background-color: #ddd; 84 | } 85 | } 86 | } 87 | } 88 | 89 | .danmu-container { 90 | position: absolute; 91 | top: 0; 92 | left: 0; 93 | width: 100%; 94 | height: 100%; 95 | border: 1px solid #e5e4e4; 96 | box-sizing: border-box; 97 | } 98 | 99 | .danmu-wrap { 100 | width: 100%; 101 | height: 100%; 102 | position: relative; 103 | background-color: #000; 104 | 105 | .danmu-input { 106 | display: none; 107 | } 108 | 109 | .danmu-video { 110 | width: 100%; 111 | height: calc(~"100% - 42px"); 112 | & > video { 113 | position: inherit !important; 114 | } 115 | } 116 | 117 | .danmu-ctrl { 118 | box-sizing: border-box; 119 | border: 1px solid @background; 120 | border-left: 0; 121 | border-right: 0; 122 | width: 100%; 123 | height: 42px; 124 | padding: 5px; 125 | color: @color; 126 | background: @background; 127 | & > a { 128 | float: left; 129 | cursor: pointer; 130 | } 131 | .danmu-btn { 132 | box-sizing: border-box; 133 | display: inline-block; 134 | height: 30px; 135 | } 136 | .danmu-mute { 137 | float: right; 138 | width: 30px; 139 | height: 30px; 140 | padding: 5px; 141 | transition: all .3s ease; 142 | background: url(img/volume.svg) no-repeat center; 143 | &[muted] { 144 | background: url(img/muted.svg) no-repeat center; 145 | } 146 | } 147 | .danmu-volume { 148 | @bgcolor: #9f9f9f; 149 | @fcolor: #4285f4; 150 | box-sizing: border-box; 151 | margin: 5px; 152 | height: 20px; 153 | float: right; 154 | position: relative; 155 | .progress { 156 | position: absolute; 157 | pointer-events: none; 158 | top: 9px; 159 | left: 0; 160 | width: 0; 161 | height: 2px; 162 | background-color: @fcolor; 163 | } 164 | &>input[type="range"] { 165 | cursor: pointer; 166 | height: 20px; 167 | outline: none; 168 | background-color: transparent; 169 | -webkit-appearance: none; 170 | -moz-appearance: none; 171 | .thumb { 172 | -webkit-appearance: none; 173 | -moz-appearance: none; 174 | height: 12px; 175 | width: 12px; 176 | margin-top: -5px; 177 | border-radius: 50%; 178 | background-color: @fcolor; 179 | position: relative; 180 | box-shadow: 0px 0px 0px #000000, 0px 0px 0px #0d0d0d; 181 | } 182 | &::-webkit-slider-thumb { 183 | .thumb 184 | } 185 | &::-moz-range-thumb { 186 | .thumb 187 | } 188 | .track { 189 | height: 2px; 190 | background-color: @bgcolor; 191 | } 192 | &::-webkit-slider-runnable-track { 193 | .track 194 | } 195 | &::-moz-range-track { 196 | .track 197 | } 198 | } 199 | } 200 | .danmu-fullpage { 201 | float: right; 202 | width: 30px; 203 | height: 30px; 204 | padding: 5px; 205 | &::before { 206 | content: " "; 207 | display: block; 208 | width: 20px; 209 | height: 16px; 210 | border: 2px solid @color; 211 | box-sizing: border-box; 212 | margin-top: 2px; 213 | } 214 | } 215 | .danmu-fullscreen { 216 | float: right; 217 | width: 30px; 218 | height: 30px; 219 | padding: 5px; 220 | background: url(img/fullscreen.svg); 221 | background-repeat: no-repeat; 222 | background-position: center; 223 | } 224 | .danmu-switch { 225 | float: right; 226 | display: inline-block; 227 | height: 30px; 228 | line-height: 30px; 229 | padding: 0 5px; 230 | } 231 | .danmu-tip { 232 | float: right; 233 | display: inline-block; 234 | height: 30px; 235 | line-height: 30px; 236 | padding: 0 5px; 237 | cursor: default; 238 | } 239 | .danmu-reload { 240 | width: 30px; 241 | height: 30px; 242 | padding: 5px; 243 | background: url(img/reload.svg); 244 | background-repeat: no-repeat; 245 | background-position: center; 246 | } 247 | .danmu-playpause { 248 | width: 30px; 249 | height: 30px; 250 | padding: 5px; 251 | &::before { 252 | transition: all .3s ease; 253 | border-color: transparent; 254 | content: " "; 255 | display: block; 256 | } 257 | &:not([pause]) { 258 | &::before { 259 | width: 0; 260 | height: 0; 261 | border-top: 10px solid transparent; 262 | border-left: 20px solid @color; 263 | border-bottom: 10px solid transparent; 264 | } 265 | } 266 | &[pause] { 267 | &::before { 268 | box-sizing: border-box; 269 | width: 15px; 270 | height: 20px; 271 | margin-left: 2.5px; 272 | border-left: 5px solid @color; 273 | border-right: 5px solid @color; 274 | } 275 | } 276 | } 277 | } 278 | .danmu-layout { 279 | position: absolute; 280 | top: 0; 281 | left: 0; 282 | right: 0; 283 | bottom: 0; 284 | pointer-events: none; 285 | 286 | color: #fff; 287 | font-size: 25px; 288 | font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif; 289 | text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px; 290 | line-height: 1.25; 291 | font-weight: bold; 292 | overflow: hidden; 293 | 294 | opacity: 0.5; 295 | & > div { 296 | display: none; 297 | position: absolute; 298 | white-space: pre; 299 | } 300 | .danmu-self { 301 | outline: 2px solid #fff; 302 | } 303 | } 304 | } 305 | 306 | .danmu-wrap[fullpage] { 307 | top: 0; 308 | left: 0; 309 | width: 100%; 310 | height: 100%; 311 | position: fixed; 312 | z-index: 100000; 313 | cursor: none; 314 | 315 | .danmu-input { 316 | position: absolute; 317 | top: 75%; 318 | width: 100%; 319 | display: block; 320 | transition: all .3s ease; 321 | transform: translateY(50px); 322 | opacity: 0; 323 | &>input { 324 | outline: 0; 325 | @bgcolor: rgba(255, 255, 255, 0.8); 326 | box-shadow: 0 0 10px 1px @bgcolor; 327 | width: 300px; 328 | margin: 0 auto; 329 | display: block; 330 | border: 0; 331 | background: @bgcolor; 332 | padding: 5px; 333 | color: #000; 334 | cursor: default; 335 | &::-webkit-input-placeholder { 336 | color: #888; 337 | } 338 | } 339 | } 340 | &[inputing] .danmu-input { 341 | transform: translateY(0); 342 | opacity: 1; 343 | &>input { 344 | cursor: text; 345 | } 346 | } 347 | 348 | .danmu-video { 349 | height: 100%; 350 | transition: all .3s ease; 351 | } 352 | .danmu-ctrl { 353 | position: absolute; 354 | bottom: 0; 355 | opacity: 0; 356 | transition: all .3s ease; 357 | } 358 | &[hover] { 359 | cursor: default; 360 | .danmu-video { 361 | // height: calc(~"100% - 42px"); 362 | } 363 | } 364 | &[hover] .danmu-ctrl { // , .danmu-ctrl:hover 365 | cursor: default; 366 | opacity: 0.75; 367 | } 368 | } 369 | 370 | // fixing bug 371 | .flash-version-tips { 372 | display: none !important; 373 | } -------------------------------------------------------------------------------- /src/danmuPlayer.ts: -------------------------------------------------------------------------------- 1 | import flvjs, * as FlvJs from 'flv.js' 2 | import {requestFullScreen, exitFullscreen, LocalStorage, Timer, CountByTime} from './utils' 3 | import {TypeState} from 'typestate' 4 | const storage = new LocalStorage('h5plr') 5 | 6 | function findInParent (node: HTMLElement, toFind: HTMLElement) { 7 | while ((node !== toFind) && (node !== null)) { 8 | node = node.parentElement 9 | } 10 | return node !== null 11 | } 12 | 13 | export interface DanmuPlayerListener { 14 | getSrc (): Promise 15 | onSendDanmu (txt: string): void 16 | } 17 | 18 | export enum PlayerState { 19 | Stopped, 20 | Playing, 21 | Paused, 22 | Buffering 23 | } 24 | export enum SizeState { 25 | Normal, 26 | FullPage, 27 | FullScreen, 28 | ExitFullScreen 29 | } 30 | 31 | export interface PlayerUIEventListener { 32 | onReload (): void 33 | onSendDanmu (content: string): void 34 | onStop (): void 35 | onTryPlay (): void 36 | onVolumeChange (percent: number): void 37 | onMute (muted: boolean): void 38 | onHideDanmu (hide: boolean): void 39 | onUnload (): void 40 | } 41 | class SizeStateFSM extends TypeState.FiniteStateMachine { 42 | constructor () { 43 | super(SizeState.Normal) 44 | this.fromAny(SizeState).to(SizeState.Normal) 45 | this.fromAny(SizeState).to(SizeState.FullPage) 46 | this.fromAny(SizeState).to(SizeState.FullScreen) 47 | this.from(SizeState.FullScreen).to(SizeState.ExitFullScreen) 48 | } 49 | onTransition (from: SizeState, to: SizeState) { 50 | console.log('SizeFSM', from, to) 51 | } 52 | } 53 | class PlayerStateFSM extends TypeState.FiniteStateMachine { 54 | constructor () { 55 | super(PlayerState.Stopped) 56 | this.fromAny(PlayerState).to(PlayerState.Stopped) 57 | this.fromAny(PlayerState).to(PlayerState.Playing) 58 | this.from(PlayerState.Playing).to(PlayerState.Buffering) 59 | this.from(PlayerState.Playing).to(PlayerState.Paused) 60 | this.from(PlayerState.Buffering).to(PlayerState.Paused) 61 | } 62 | onTransition (from: PlayerState, to: PlayerState) { 63 | console.log('PlayerFSM', from, to) 64 | } 65 | } 66 | export class PlayerUI { 67 | dmLayout: HTMLElement 68 | wrap: HTMLElement 69 | video: HTMLVideoElement 70 | el: HTMLElement 71 | playerCtrl: HTMLElement 72 | tipEl: HTMLElement 73 | playPause: HTMLElement 74 | inputing = false 75 | hideDanmu = false 76 | _muted = false 77 | private _fullscreen = false 78 | private _lastY: number = -1 79 | private muteEl: HTMLElement 80 | private sizeState: SizeStateFSM 81 | private volumeInput: HTMLInputElement 82 | 83 | constructor ( 84 | private listener: PlayerUIEventListener, 85 | private state: TypeState.FiniteStateMachine 86 | ) { 87 | const playerContainer = document.createElement('div') 88 | const playerWrap = document.createElement('div') 89 | const playerCtrl = document.createElement('div') 90 | const danmuLayout = document.createElement('div') 91 | const videoBox = document.createElement('div') 92 | const msgBox = document.createElement('div') 93 | const msgInput = document.createElement('input') 94 | const videoEl = document.createElement('video') 95 | 96 | this.sizeState = new SizeStateFSM() 97 | 98 | let lastState: SizeState 99 | this.sizeState 100 | .on(SizeState.Normal, from => { 101 | switch (from) { 102 | case SizeState.FullPage: 103 | this._exitFullPage() 104 | break 105 | case SizeState.ExitFullScreen: 106 | this._exitFullScreen() 107 | break 108 | } 109 | }) 110 | .on(SizeState.FullPage, from => { 111 | switch (from) { 112 | case SizeState.Normal: 113 | this._enterFullPage() 114 | break 115 | case SizeState.ExitFullScreen: 116 | this._enterFullPage() 117 | break 118 | } 119 | }) 120 | .on(SizeState.FullScreen, from => { 121 | if (from == SizeState.FullScreen) return 122 | lastState = from 123 | switch (from) { 124 | case SizeState.Normal: 125 | this._enterFullScreen() 126 | break 127 | case SizeState.FullPage: 128 | this._enterFullScreen() 129 | break 130 | } 131 | }) 132 | .on(SizeState.ExitFullScreen, from => { 133 | this._exitFullScreen() 134 | this.sizeState.go(lastState) 135 | }) 136 | 137 | videoEl.style.width = videoEl.style.height = '100%' 138 | 139 | msgInput.type = 'text' 140 | msgInput.placeholder = '发送弹幕...' 141 | 142 | msgBox.className = 'danmu-input' 143 | videoBox.className = 'danmu-video' 144 | playerCtrl.className = 'danmu-ctrl' 145 | danmuLayout.className = 'danmu-layout' 146 | playerWrap.className = 'danmu-wrap' 147 | playerContainer.className = 'danmu-container' 148 | 149 | videoBox.appendChild(videoEl) 150 | msgBox.appendChild(msgInput) 151 | playerWrap.appendChild(videoBox) 152 | playerWrap.appendChild(playerCtrl) 153 | playerWrap.appendChild(danmuLayout) 154 | playerWrap.appendChild(msgBox) 155 | playerContainer.appendChild(playerWrap) 156 | 157 | let timer = new Timer(1000) 158 | timer.onTimer = () => playerWrap.removeAttribute('hover') 159 | playerWrap.addEventListener('mousemove', event => { 160 | // const hoverCtl = event.path.indexOf(playerCtrl) !== -1 161 | const hoverCtl = findInParent(event.target as any, playerCtrl) 162 | if (event.offsetY - this._lastY == 0) return 163 | this._lastY = event.offsetY 164 | let height = playerWrap.getBoundingClientRect().height 165 | if (event.offsetY > 0) { 166 | playerWrap.setAttribute('hover', '') 167 | timer.reset() 168 | } else { 169 | playerWrap.removeAttribute('hover') 170 | } 171 | }) 172 | playerWrap.addEventListener('click', event => { 173 | // if (event.path.indexOf(msgBox) !== -1) return 174 | if (findInParent(event.target as any, msgBox)) return 175 | playerWrap.removeAttribute('inputing') 176 | this.inputing = false 177 | }) 178 | document.addEventListener('keydown', event => { 179 | if (event.keyCode == 13) { // enter 180 | if (this.sizeState.is(SizeState.Normal)) return 181 | if ((event.target as any).nodeName.toUpperCase() === 'TEXTAREA') return 182 | this.inputing = !this.inputing 183 | if (this.inputing) { 184 | msgInput.value = '' 185 | playerWrap.setAttribute('inputing', '') 186 | msgInput.focus() 187 | } else { 188 | if (msgInput.value.length > 0) { 189 | listener.onSendDanmu(msgInput.value) 190 | } 191 | playerWrap.removeAttribute('inputing') 192 | } 193 | } else if (event.keyCode == 27) { // esc 194 | if (this.sizeState.is(SizeState.FullPage)) { 195 | this.sizeState.go(SizeState.Normal) 196 | } 197 | if (this.sizeState.is(SizeState.FullScreen)) { 198 | this.sizeState.go(SizeState.ExitFullScreen) 199 | } 200 | } else if (event.keyCode == 38 || event.keyCode == 40) { //up down arrow 201 | if(this.sizeState.is(SizeState.Normal)) return 202 | let input = this.volumeInput 203 | input.value = (parseInt(input.value) + (event.keyCode == 38 ? 10 : -10)).toString() 204 | let fireEvent = document.createEvent('HTMLEvents') 205 | fireEvent.initEvent("input", true, true); 206 | input.dispatchEvent(fireEvent) 207 | } 208 | }) 209 | 210 | const onFullscreenChange = () => { 211 | this._fullscreen = !this._fullscreen 212 | if (!this._fullscreen) { 213 | if (this.sizeState.is(SizeState.FullScreen)) { 214 | this.sizeState.go(SizeState.ExitFullScreen) 215 | } 216 | } 217 | } 218 | 219 | document.addEventListener('mozfullscreenchange',onFullscreenChange) 220 | document.addEventListener('webkitfullscreenchange', onFullscreenChange) 221 | 222 | window.addEventListener('unload', event => { 223 | listener.onStop() 224 | listener.onUnload() 225 | }) 226 | 227 | this.video = videoEl 228 | this.el = playerContainer 229 | this.wrap = playerWrap 230 | this.dmLayout = danmuLayout 231 | this.playerCtrl = playerCtrl 232 | this.transparent = this.transparent 233 | } 234 | protected _exitFullScreen () { 235 | exitFullscreen() 236 | this.wrap.removeAttribute('fullpage') 237 | this.el.appendChild(this.wrap) 238 | document.body.style.overflow = document.body.parentElement.style.overflow = 'auto' 239 | this.listener.onTryPlay() 240 | } 241 | protected _enterFullScreen () { 242 | requestFullScreen() 243 | this.wrap.setAttribute('fullpage', '') 244 | document.body.appendChild(this.wrap) 245 | document.body.style.overflow = document.body.parentElement.style.overflow = 'hidden' 246 | this.listener.onTryPlay() 247 | } 248 | protected _exitFullPage () { 249 | this.wrap.removeAttribute('fullpage') 250 | this.el.appendChild(this.wrap) 251 | document.body.style.overflow = document.body.parentElement.style.overflow = 'auto' 252 | this.listener.onTryPlay() 253 | } 254 | protected _enterFullPage () { 255 | this.wrap.setAttribute('fullpage', '') 256 | document.body.appendChild(this.wrap) 257 | document.body.style.overflow = document.body.parentElement.style.overflow = 'hidden' 258 | this.listener.onTryPlay() 259 | } 260 | get transparent () { 261 | return parseInt(storage.getItem('transparent', '0')) 262 | } 263 | set transparent (val: number) { 264 | storage.setItem('transparent', val.toString()) 265 | this.dmLayout.style.opacity = (1 - val / 100).toString() 266 | } 267 | get playing () { 268 | return this.state.is(PlayerState.Playing) || this.state.is(PlayerState.Buffering) 269 | } 270 | set playing (val: boolean) { 271 | if (val) { 272 | this.state.go(PlayerState.Playing) 273 | } else { 274 | this.state.go(PlayerState.Paused) 275 | } 276 | } 277 | get muted () { 278 | return this._muted 279 | } 280 | set muted (v) { 281 | this.listener.onMute(v) 282 | if (v) { 283 | this.muteEl.setAttribute('muted', '') 284 | } else { 285 | this.muteEl.removeAttribute('muted') 286 | } 287 | this._muted = v 288 | } 289 | notifyStateChange () { 290 | if (this.playing) { 291 | this.playPause.setAttribute('pause', '') 292 | } else { 293 | this.playPause.removeAttribute('pause') 294 | } 295 | } 296 | initControls () { 297 | if (this.tipEl) return 298 | let bar = this.playerCtrl 299 | const now = () => new Date().getTime() 300 | const addBtn = (cls: string, cb: () => void) => { 301 | const btn = document.createElement('a') 302 | btn.className = ['danmu-btn', 'danmu-'+cls].join(' ') 303 | btn.addEventListener('click', cb) 304 | bar.appendChild(btn) 305 | return btn 306 | } 307 | this.video.addEventListener('dblclick', event => { 308 | switch (this.sizeState.currentState) { 309 | case SizeState.Normal: 310 | this.sizeState.go(SizeState.FullPage) 311 | break 312 | case SizeState.FullPage: 313 | this.sizeState.go(SizeState.Normal) 314 | break 315 | case SizeState.FullScreen: 316 | this.sizeState.go(SizeState.ExitFullScreen) 317 | break 318 | } 319 | event.preventDefault() 320 | event.stopPropagation() 321 | }) 322 | this.playPause = addBtn('playpause', () => { 323 | this.playing = !this.playing 324 | this.notifyStateChange() 325 | }) 326 | this.playPause.setAttribute('pause', '') 327 | 328 | const reload = addBtn('reload', () => { 329 | this.listener.onReload() 330 | }) 331 | 332 | const fullscreen = addBtn('fullscreen', () => { 333 | if (this.sizeState.is(SizeState.FullScreen)) { 334 | this.sizeState.go(SizeState.ExitFullScreen) 335 | } else { 336 | this.sizeState.go(SizeState.FullScreen) 337 | } 338 | }) 339 | 340 | const fullpage = addBtn('fullpage', () => { 341 | switch (this.sizeState.currentState) { 342 | case SizeState.Normal: 343 | this.sizeState.go(SizeState.FullPage) 344 | break 345 | case SizeState.FullPage: 346 | this.sizeState.go(SizeState.Normal) 347 | break 348 | case SizeState.FullScreen: 349 | this.sizeState.go(SizeState.ExitFullScreen) 350 | this.sizeState.go(SizeState.FullPage) 351 | break 352 | } 353 | }) 354 | 355 | const volume = this.createVolume(percent => { 356 | // volume 357 | // this.player.volume = percent 358 | this.listener.onVolumeChange(percent) 359 | }) 360 | bar.appendChild(volume) 361 | 362 | this.muteEl = addBtn('mute', () => { 363 | this.muted = !this.muted 364 | }) 365 | 366 | const danmuSwitch = addBtn('switch', () => { 367 | this.hideDanmu = !this.hideDanmu 368 | this.listener.onHideDanmu(this.hideDanmu) 369 | danmuSwitch.innerText = this.hideDanmu ? '开启弹幕' : '关闭弹幕' 370 | this.dmLayout.style.display = this.hideDanmu ? 'none' : 'block' 371 | }) 372 | danmuSwitch.innerText = this.hideDanmu ? '开启弹幕' : '关闭弹幕' 373 | 374 | const tip = document.createElement('div') 375 | tip.className = 'danmu-tip' 376 | bar.appendChild(tip) 377 | this.tipEl = tip 378 | } 379 | createVolume (cb: (v: number) => void) { 380 | const volume = document.createElement('div') 381 | const progress = document.createElement('div') 382 | const input = document.createElement('input') 383 | volume.className = 'danmu-volume' 384 | progress.className = 'progress' 385 | input.type = 'range' 386 | volume.appendChild(input) 387 | volume.appendChild(progress) 388 | 389 | input.value = storage.getItem('volume') || '100' 390 | cb(parseInt(input.value) / 100) 391 | input.addEventListener('input', event => { 392 | progress.style.width = `${input.value}%` 393 | cb(parseInt(input.value) / 100) 394 | storage.setItem('volume', input.value) 395 | }) 396 | progress.style.width = `${input.value}%` 397 | this.volumeInput = input 398 | return volume 399 | } 400 | setTip (tip: string) { 401 | this.tipEl.innerText = tip 402 | } 403 | } 404 | 405 | class PlayerBufferMonitor { 406 | private intId: number 407 | private bufTime: number 408 | constructor (protected dmPlayer: DanmuPlayer) { 409 | this.intId = window.setInterval(() => { 410 | try { 411 | this.handler() 412 | } catch (e) { 413 | console.error(e) 414 | } 415 | }, 200) 416 | this.reset() 417 | } 418 | unload () { 419 | window.clearInterval(this.intId) 420 | } 421 | reset () { 422 | this.bufTime = 1 423 | } 424 | get player () { 425 | return this.dmPlayer.player 426 | } 427 | handler () { 428 | if (this.player) { 429 | const buffered = this.player.buffered 430 | if (buffered.length === 0) return 431 | const buf = buffered.end(buffered.length - 1) - this.player.currentTime 432 | const state = this.dmPlayer.state 433 | // console.log(buffered.end(buffered.length - 1), this.player.currentTime, buf) 434 | if (state.is(PlayerState.Playing)) { 435 | if (buf <= 1) { 436 | state.go(PlayerState.Buffering) 437 | this.dmPlayer.ui.notifyStateChange() 438 | this.bufTime *= 2 439 | if (this.bufTime > 8) { 440 | console.warn('网络不佳') 441 | this.bufTime = 8 442 | } 443 | } 444 | } else if (state.is(PlayerState.Buffering)) { 445 | if (buf > this.bufTime) { 446 | state.go(PlayerState.Playing) 447 | this.dmPlayer.player.currentTime -= 0.5 448 | this.dmPlayer.ui.notifyStateChange() 449 | } 450 | } 451 | } 452 | } 453 | } 454 | 455 | export class DanmuPlayer implements PlayerUIEventListener { 456 | inputing: boolean = false 457 | listener: DanmuPlayerListener 458 | player: FlvJs.Player 459 | ui: PlayerUI 460 | state: PlayerStateFSM 461 | mgr: DanmuManager 462 | 463 | private _src: string = '' 464 | private _moveId: number 465 | private lastVolume: number 466 | private bufferMonitor: PlayerBufferMonitor 467 | private onLogBind: (type: string, str: string) => void 468 | private magicCounter = new CountByTime(5 * 1000) 469 | 470 | private onLog (type: string, str: string) { 471 | if (this.player && str.includes('Large audio timestamp gap detected, may cause AV sync to drift.')) { 472 | this.magicCounter.add() 473 | if (this.magicCounter.count() > 5 * 5) { 474 | this.magicFlvJSDisableFillAudioGap() 475 | console.warn('Too much fill, disable it.') 476 | } 477 | } 478 | } 479 | onVolumeChange (vol: number) { 480 | this.player.volume = vol 481 | } 482 | onReload () { 483 | this.stop() 484 | this.load() 485 | } 486 | onSendDanmu (txt: string) { 487 | this.listener.onSendDanmu(txt) 488 | } 489 | onStop () { 490 | this.stop() 491 | } 492 | onUnload () { 493 | this.bufferMonitor.unload() 494 | } 495 | onTryPlay () { 496 | this.tryPlay() 497 | } 498 | onMute (muted: boolean) { 499 | if (muted) { 500 | this.lastVolume = this.player.volume 501 | this.player.volume = 0 502 | } else { 503 | this.player.volume = this.lastVolume 504 | 505 | } 506 | } 507 | onHideDanmu (hide: boolean) { 508 | this.mgr.hideDanmu = hide 509 | } 510 | onStat (e: {speed: number}) { 511 | this.ui.setTip(Math.round(e.speed*10)/10 + 'KB/s') 512 | } 513 | async load () { 514 | this.src = await this.listener.getSrc() 515 | } 516 | createFlvjs () { 517 | const sourceConfig = { 518 | isLive: true, 519 | type: 'flv', 520 | url: this.src 521 | } 522 | const playerConfig: FlvJs.Config = { 523 | enableWorker: false, 524 | deferLoadAfterSourceOpen: true, 525 | stashInitialSize: 512 * 1024, 526 | enableStashBuffer: true, 527 | autoCleanupMinBackwardDuration: 20, 528 | autoCleanupMaxBackwardDuration: 40, 529 | autoCleanupSourceBuffer: true 530 | } 531 | const player = flvjs.createPlayer(sourceConfig, playerConfig) 532 | player.on(flvjs.Events.ERROR, (e: any, t: any) => { 533 | console.error('播放器发生错误:' + e + ' - ' + t) 534 | player.unload() 535 | }) 536 | player.on(flvjs.Events.STATISTICS_INFO, this.onStat.bind(this)) 537 | 538 | player.attachMediaElement(this.ui.video) 539 | player.load() 540 | player.play() 541 | return player 542 | } 543 | stop () { 544 | this.state.go(PlayerState.Stopped) 545 | } 546 | set src (val) { 547 | this._src = val 548 | this.stop() 549 | let player = this.createFlvjs() 550 | this.player = player 551 | this.ui.initControls() 552 | this.state.go(PlayerState.Playing) 553 | } 554 | get src () { 555 | return this._src 556 | } 557 | constructor (listener: DanmuPlayerListener, ui?: PlayerUI) { 558 | this.onLogBind = (type, str) => this.onLog(type, str) 559 | flvjs.LoggingControl.addLogListener(this.onLogBind) 560 | 561 | this.bufferMonitor = new PlayerBufferMonitor(this) 562 | this.state = new PlayerStateFSM() 563 | 564 | const now = () => new Date().getTime() 565 | let beginTime = 0 566 | this.state 567 | .on(PlayerState.Stopped, () => { 568 | beginTime = 0 569 | this.mgr.deferTime = 0 570 | this.bufferMonitor.reset() 571 | if (this.player) { 572 | this.player.unload() 573 | this.player.detachMediaElement() 574 | this.player = null 575 | } 576 | }) 577 | .on(PlayerState.Paused, from => { 578 | beginTime = now() 579 | this.player.pause() 580 | }) 581 | .on(PlayerState.Playing, from => { 582 | if (beginTime !== 0) { 583 | this.mgr.deferTime += now() - beginTime 584 | } 585 | this.player.play() 586 | }) 587 | .on(PlayerState.Buffering, from => { 588 | beginTime = 0 589 | this.player.pause() 590 | }) 591 | 592 | this.initUI() 593 | this.mgr = new DanmuManager(this.ui.dmLayout, this.state) 594 | 595 | this.listener = listener 596 | } 597 | initUI () { 598 | this.ui = new PlayerUI(this, this.state) 599 | } 600 | tryPlay () { 601 | if (this.state.is(PlayerState.Playing)) { 602 | try { 603 | this.ui.video.play() 604 | } catch (e) {} 605 | } 606 | } 607 | fireDanmu (text: string, color: string, cls: (string | string[])) { 608 | return this.mgr.fireDanmu(text, color, cls) 609 | } 610 | private magicFlvJSDisableFillAudioGap () { 611 | try { 612 | const player = this.player as any 613 | if (player) { 614 | player._transmuxer._controller._remuxer._fillAudioTimestampGap = false 615 | } 616 | } catch (e) {} 617 | } 618 | } 619 | 620 | class DanmuManager { 621 | private pool: { 622 | el: HTMLDivElement, 623 | using: boolean 624 | }[] = [] 625 | private rows: { 626 | duration: number, 627 | beginTime: number, 628 | endTime: number, 629 | width: number 630 | }[] = [] 631 | private _deferTime = 0 // 暂停时间 632 | maxRow = 10 633 | baseTop = 10 634 | deferId: number = null 635 | deferQueue: { 636 | oriTime: number, 637 | run: () => void 638 | }[] = [] 639 | hideDanmu = false 640 | parsePic = (i: string) => i 641 | get playing () { 642 | return this.state.is(PlayerState.Playing) 643 | } 644 | set deferTime (v) { 645 | this._deferTime = v 646 | this.defering = v !== 0 647 | } 648 | get deferTime () { 649 | return this._deferTime 650 | } 651 | constructor (private danmuLayout: HTMLElement, private state: TypeState.FiniteStateMachine) { 652 | const poolSize = 100 653 | for (let i = 0; i < poolSize; i++) { 654 | let dm = document.createElement('div') 655 | danmuLayout.appendChild(dm) 656 | this.pool.push({ 657 | el: dm, 658 | using: false 659 | }) 660 | } 661 | } 662 | calcRect () { 663 | return this.danmuLayout.getBoundingClientRect() 664 | } 665 | calcRow (width: number, duration: number) { 666 | let rect = this.calcRect() 667 | const now = new Date().getTime() 668 | const check = (idx: number) => { 669 | let row = this.rows[idx] 670 | if (!row) return true 671 | if (row.endTime <= now) { 672 | this.rows[idx] = null 673 | return true 674 | } else { 675 | const distance = rect.width + row.width 676 | const percent = (now - row.beginTime) / row.duration 677 | const left = rect.width - distance * percent 678 | if (left + row.width >= rect.width) { 679 | return false 680 | } 681 | const remainTime = row.endTime - now 682 | const myDistance = rect.width + width 683 | const leftX = rect.width - (myDistance) * (remainTime / duration) 684 | if (leftX < 0) { 685 | return false 686 | } 687 | } 688 | return true 689 | } 690 | let i = 0 691 | while(true) { 692 | if (check(i)) { 693 | this.rows[i] = { 694 | duration: duration, 695 | beginTime: now, 696 | endTime: now + duration, 697 | width: width 698 | } 699 | return i % this.maxRow 700 | } 701 | i++ 702 | } 703 | } 704 | doDefer () { 705 | if (this.deferQueue.length === 0) return 706 | const top = this.deferQueue[0] 707 | const now = new Date().getTime() 708 | if (this.playing && ((top.oriTime + this.deferTime) <= now)) { 709 | // console.log(top.oriTime, this.deferTime, now) 710 | top.run() 711 | this.deferQueue.shift() 712 | } 713 | } 714 | set defering (v: boolean) { 715 | if (this.deferId === null) { 716 | if (v) { 717 | this.deferId = window.setInterval(() => this.doDefer(), 100) 718 | } 719 | } else { 720 | if (v === false) { 721 | window.clearInterval(this.deferId) 722 | this.deferId = null 723 | } 724 | } 725 | } 726 | fireDanmu (text: string, color: string, cls: (string | string[])) { 727 | const fire = () => { 728 | let rect = this.calcRect() 729 | const duration = rect.width * 7 730 | let {el: dm} = this.pool.shift() 731 | setTimeout(() => { 732 | dm.removeAttribute('style') 733 | this.pool.push({ 734 | el: dm, 735 | using: false 736 | }) 737 | }, duration) 738 | dm.innerText = text 739 | dm.innerHTML = this.parsePic(dm.innerHTML) 740 | if (Array.isArray(cls)) cls = cls.join(' ') 741 | dm.className = cls || '' 742 | dm.style.left = `${rect.width}px` 743 | dm.style.display = 'inline-block' 744 | dm.style.color = color 745 | setTimeout(() => { 746 | let dmRect = dm.getBoundingClientRect() 747 | // console.log(dmRect) 748 | const row = this.calcRow(dmRect.width, duration) 749 | // console.log('row', text, row) 750 | dm.style.top = `${this.baseTop + row * dmRect.height}px` 751 | dm.style.transition = `transform ${duration/1000}s linear` 752 | dm.style.transform = `translateX(-${rect.width+dmRect.width}px)` 753 | }, 0) 754 | } 755 | const now = new Date().getTime() 756 | if (!this.playing || this.deferTime > 0) { 757 | // if (this.deferQueue.length === 0) setTimeout(() => this.doDefer(), 100) 758 | this.deferQueue.push({ 759 | oriTime: now, 760 | run: () => fire() 761 | }) 762 | return 763 | } 764 | if (this.hideDanmu) return 765 | if (this.pool.length == 0) return 766 | fire() 767 | } 768 | } 769 | -------------------------------------------------------------------------------- /src/donate.ts: -------------------------------------------------------------------------------- 1 | let dialog: HTMLDivElement = null 2 | export function getDialog (title: string, content: string, qrcodes: {src: string, desc: string}[]) { 3 | if (dialog) { 4 | return dialog 5 | } 6 | dialog = document.createElement('div') 7 | dialog.className = 'donate-dialog' 8 | const wrap = document.createElement('div') 9 | wrap.className = 'donate-wrap' 10 | const titleEl = document.createElement('h3') 11 | titleEl.className = 'donate-title' 12 | titleEl.innerText = title 13 | const contentEl = document.createElement('div') 14 | contentEl.className = 'donate-content' 15 | contentEl.innerText = content 16 | const qrcodeEl = document.createElement('div') 17 | qrcodeEl.className = 'donate-qrcode-bar' 18 | for (let i of qrcodes) { 19 | const qrcodeBox = document.createElement('div') 20 | qrcodeBox.className = 'donate-qrcode-box' 21 | const qrcode = document.createElement('img') 22 | qrcode.className = 'donate-qrcode-img' 23 | qrcode.src = i.src 24 | const qrcodeDesc = document.createElement('div') 25 | qrcodeDesc.className = 'donate-qrcode-desc' 26 | qrcodeDesc.innerText = i.desc 27 | qrcodeBox.appendChild(qrcode) 28 | qrcodeBox.appendChild(qrcodeDesc) 29 | qrcodeEl.appendChild(qrcodeBox) 30 | } 31 | const closeEl = document.createElement('div') 32 | closeEl.className = 'donate-close-btn' 33 | const close = () => { 34 | dialog.style.display = 'none' 35 | } 36 | closeEl.addEventListener('click', close) 37 | wrap.appendChild(titleEl) 38 | wrap.appendChild(contentEl) 39 | wrap.appendChild(qrcodeEl) 40 | wrap.appendChild(closeEl) 41 | dialog.appendChild(wrap) 42 | dialog.style.display = 'none' 43 | return dialog 44 | } 45 | -------------------------------------------------------------------------------- /src/douyu/api.ts: -------------------------------------------------------------------------------- 1 | import {postMessage, randInt, DelayNotify, LocalStorage} from '../utils' 2 | import md5 from '../md5' 3 | const storage = new LocalStorage('h5plr') 4 | const PUREMODE = 'pureMode' 5 | 6 | export function isPureMode () { 7 | return storage.getItem(PUREMODE, '0') === '1' 8 | } 9 | 10 | export function setPureMode (val: boolean) { 11 | storage.setItem(PUREMODE, val ? '1' : '0') 12 | } 13 | 14 | declare var window: { 15 | _ACJ_ (args: any[]): void, 16 | [key: string]: any 17 | } & Window 18 | declare function escape(s:string): string; 19 | 20 | export const getACF = (key: string) => { 21 | try { 22 | return new RegExp(`acf_${key}=(.*?)(;|$)`).exec(document.cookie)[1] 23 | } catch (e) { 24 | return '' 25 | } 26 | } 27 | 28 | interface DouyuPackage { 29 | type: string, 30 | [key: string]: any 31 | } 32 | function filterEnc (s: string) { 33 | s = s.toString() 34 | s = s.replace(/@/g, '@A') 35 | return s.replace(/\//g, '@S') 36 | } 37 | function filterDec (s: string) { 38 | s = s.toString() 39 | s = s.replace(/@S/g, '/') 40 | return s.replace(/@A/g, '@') 41 | } 42 | function douyuEncode (data: DouyuPackage) { 43 | return Object.keys(data).map(key => `${key}@=${filterEnc(data[key])}`).join('/') + '/' 44 | } 45 | function douyuDecode (data: string) { 46 | let out: DouyuPackage = { 47 | type: '!!missing!!' 48 | } 49 | try { 50 | data.split('/').filter(i => i.length > 2).forEach(i => { 51 | let e = i.split('@=') 52 | out[e[0]] = filterDec(e[1]) 53 | }) 54 | } catch (e) { 55 | console.error(e) 56 | console.log(data) 57 | } 58 | return out 59 | } 60 | function douyuDecodeList (list: string) { 61 | return list.split('/').filter(i => i.length > 2).map(filterDec).map(douyuDecode) 62 | } 63 | export function ACJ (id: string, data: any | string) { 64 | if (typeof data == 'object') { 65 | data = douyuEncode(data) 66 | } 67 | try { 68 | window._ACJ_([id, data]) 69 | } catch (e) { 70 | console.error(id, data, e) 71 | } 72 | } 73 | function abConcat(buffer1: ArrayBuffer, buffer2: ArrayBuffer) { 74 | let tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength) 75 | tmp.set(new Uint8Array(buffer1), 0) 76 | tmp.set(new Uint8Array(buffer2), buffer1.byteLength) 77 | return tmp.buffer 78 | }; 79 | 80 | interface DouyuListener { 81 | onReconnect (): void 82 | onPackage (pkg: DouyuPackage, pkgStr: string): void 83 | onClose (): void 84 | onError (e: string): void 85 | } 86 | 87 | class DouyuProtocol { 88 | buffer: ArrayBuffer 89 | connectHandler: () => void = () => null 90 | ws: WebSocket 91 | decoder = new TextDecoder('utf-8') 92 | encoder = new TextEncoder() 93 | constructor (public listener: DouyuListener) { 94 | this.buffer = new ArrayBuffer(0) 95 | } 96 | connectAsync (url: string) { 97 | return new Promise((res, rej) => { 98 | const ws = new WebSocket(url) 99 | ws.binaryType = 'arraybuffer' 100 | ws.onopen = () => { 101 | this.ws = ws 102 | ws.onmessage = e => { 103 | const buf: ArrayBuffer = e.data 104 | this.dataHandler(buf) 105 | } 106 | ws.onclose = () => this.closeHandler() 107 | ws.onerror = (e) => this.errorHandler('Connection error(ws)') 108 | res() 109 | } 110 | ws.onerror = () => rej() 111 | }) 112 | } 113 | dataHandler (data: ArrayBuffer) { 114 | this.buffer = abConcat(this.buffer, data) 115 | while (this.buffer.byteLength >= 4) { 116 | const buffer = this.buffer 117 | const view = new DataView(buffer) 118 | let size = view.getUint32(0, true) 119 | if (buffer.byteLength - 4 >= size) { 120 | const u8 = new Uint8Array(buffer) 121 | let pkgStr = '' 122 | try { 123 | pkgStr = this.decoder.decode(u8.slice(12, 4 + size - 1)) 124 | // pkgStr = ascii_to_utf8(buffer.substr(12, size-8)) 125 | } catch (e) { 126 | console.log('decode fail', u8) 127 | } 128 | this.buffer = u8.slice(size + 4).buffer 129 | if (pkgStr.length === 0) continue 130 | try { 131 | let pkg = douyuDecode(pkgStr) 132 | this.listener && this.listener.onPackage(pkg, pkgStr) 133 | } catch (e) { 134 | console.error('call map', e) 135 | } 136 | } else { 137 | break 138 | } 139 | } 140 | } 141 | closeHandler () { 142 | console.error('lost connection') 143 | this.listener && this.listener.onClose() 144 | } 145 | errorHandler (err: string) { 146 | console.error(err) 147 | this.listener && this.listener.onError(err) 148 | } 149 | send (data: DouyuPackage) { 150 | let msg = douyuEncode(data) 151 | let msgu8 = this.encoder.encode(msg) 152 | msgu8 = new Uint8Array(abConcat(msgu8.buffer, new ArrayBuffer(1))) 153 | 154 | let buf = new ArrayBuffer(msgu8.length + 12) 155 | const headerView = new DataView(buf) 156 | const hLen = msgu8.length + 8 157 | headerView.setUint32(0, hLen, true) 158 | headerView.setUint32(4, hLen, true) 159 | headerView.setUint32(8, 689, true) 160 | 161 | new Uint8Array(buf).set(msgu8, 12) 162 | this.ws.send(buf) 163 | } 164 | } 165 | function Type (type: string) { 166 | return (target: { 167 | map: { [key: string]: Function }, 168 | [key: string]: any 169 | }, propertyKey: string, descriptor: PropertyDescriptor) => { 170 | if (!target.map) { 171 | target.map = {} 172 | } 173 | target.map[type] = target[propertyKey] 174 | } 175 | } 176 | 177 | abstract class DouyuBaseClient implements DouyuListener { 178 | private prot: DouyuProtocol 179 | private lastIP: string = null 180 | private lastPort: string = null 181 | private keepaliveId: number = null 182 | private reconnectDelay: number = 1000 183 | private queue = Promise.resolve() 184 | redirect: { 185 | [key: string]: string 186 | } = {} 187 | map: { 188 | [key: string]: Function 189 | } 190 | static getRoomArgs () { 191 | if (window._room_args) return window._room_args 192 | if (window.room_args) { 193 | return window.room_args 194 | } else { 195 | return window.$ROOM.args 196 | } 197 | } 198 | async reconnect () { 199 | console.log('reconnect') 200 | this.prot.listener = null 201 | this.prot = new DouyuProtocol(this) 202 | try { 203 | await this.connectAsync(this.lastIP, this.lastPort) 204 | this.onReconnect() 205 | } catch (e) { 206 | // 连接失败 207 | this.onError() 208 | } 209 | } 210 | onClose () { 211 | setTimeout(() => this.reconnect(), this.reconnectDelay) 212 | if (this.reconnectDelay < 16000) { 213 | this.reconnectDelay *= 2 214 | } 215 | } 216 | onError () { 217 | this.onClose() 218 | } 219 | onPackage (pkg: DouyuPackage, pkgStr: string) { 220 | const type = pkg.type 221 | if (this.redirect[type]) { 222 | ACJ(this.redirect[type], pkg) 223 | return 224 | } 225 | if (this.map[type]) { 226 | this.queue = this.queue.then(() => this.map[type].call(this, pkg, pkgStr)) 227 | return 228 | } 229 | this.onDefault(pkg) 230 | } 231 | abstract onDefault (pkg: DouyuPackage): void 232 | abstract onReconnect (): void 233 | send (pkg: DouyuPackage) { 234 | this.prot.send(pkg) 235 | } 236 | async connectAsync (ip: string, port: string) { 237 | this.lastIP = ip 238 | this.lastPort = port 239 | const url = `wss://${ip}:${port}/` 240 | await this.prot.connectAsync(url) 241 | this.send(this.loginreq()) 242 | } 243 | keepalivePkg (): DouyuPackage { 244 | return { 245 | type: 'keeplive', 246 | tick: Math.round(new Date().getTime() / 1000).toString() 247 | } 248 | } 249 | loginreq () { 250 | const rt = Math.round(new Date().getTime() / 1000) 251 | const devid = getACF('did') // md5(Math.random()).toUpperCase() 252 | const username = getACF('username') 253 | console.log('username', username, devid) 254 | return { 255 | type: 'loginreq', 256 | username: username, 257 | ct: 0, 258 | password: '', 259 | roomid: this.roomId, 260 | devid: devid, 261 | rt: rt, 262 | pt: 2, 263 | vk: md5(`${rt}r5*^5;}2#\${XF[h+;'./.Q'1;,-]f'p[${devid}`), 264 | ver: '20150929', 265 | aver: 'H5_2018021001beta', 266 | biz: getACF('biz'), 267 | stk: getACF('stk'), 268 | ltkid: getACF('ltkid') 269 | } 270 | } 271 | startKeepalive () { 272 | this.send(this.keepalivePkg()) 273 | if (this.keepaliveId) { 274 | window.clearInterval(this.keepaliveId) 275 | } 276 | this.keepaliveId = window.setInterval(() => this.send(this.keepalivePkg()), 30 * 1000) 277 | } 278 | constructor (public roomId: string) { 279 | this.prot = new DouyuProtocol(this) 280 | } 281 | } 282 | 283 | let blacklist: string[] = [] 284 | function onChatMsg (data: DouyuPackage) { 285 | if (blacklist.indexOf(data.uid) !== -1) { 286 | console.log('black') 287 | return 288 | } 289 | try { 290 | postMessage('DANMU', data) 291 | } catch (e) { 292 | console.error('wtf', e) 293 | } 294 | ACJ('room_data_chat2', data) 295 | if (window.BarrageReturn) { 296 | window.BarrageReturn(douyuEncode(data)) 297 | } 298 | } 299 | class DouyuClient extends DouyuBaseClient { 300 | uid: string 301 | rg: number 302 | pg: number 303 | danmuClient: DouyuDanmuClient 304 | serverList: { 305 | ip: string, 306 | port: string, 307 | nr: string 308 | }[] 309 | constructor (roomId: string) { 310 | super(roomId) 311 | this.redirect = { 312 | qtlr: 'room_data_tasklis', 313 | initcl: 'room_data_chatinit', 314 | memberinfores: 'room_data_info', 315 | ranklist: 'room_data_cqrank', 316 | rsm: 'room_data_brocast', 317 | qausrespond: 'data_rank_score', 318 | frank: 'room_data_handler', 319 | online_noble_list: 'room_data_handler', 320 | } 321 | } 322 | reqOnlineGift (loginres: DouyuPackage) { 323 | return { 324 | type: 'reqog', 325 | uid: loginres.userid 326 | } 327 | } 328 | @Type('chatmsg') 329 | chatmsg (data: DouyuPackage) { 330 | if (this.rg > 1 || this.pg > 1) { 331 | return 332 | } 333 | onChatMsg(data) 334 | } 335 | @Type('resog') 336 | resog (data: DouyuPackage) { 337 | ACJ('room_data_chest', { 338 | lev: data.lv, 339 | lack_time: data.t, 340 | dl: data.dl 341 | }) 342 | } 343 | @Type('loginres') 344 | loginres (data: DouyuPackage) { 345 | console.log('loginres ms', data) 346 | this.uid = data.userid 347 | this.rg = data.roomgroup 348 | this.pg = data.pg 349 | this.send(this.reqOnlineGift(data)) 350 | this.startKeepalive() 351 | ACJ('room_data_login', data) 352 | ACJ('room_data_getdid', { 353 | devid: getACF('devid') 354 | }) 355 | } 356 | @Type('msgrepeaterproxylist') 357 | async msgrepeaterproxylist (data: DouyuPackage) { 358 | this.serverList = douyuDecodeList(data.list) as any 359 | if (this.danmuClient !== undefined) { 360 | console.warn('skip connect dm') 361 | return 362 | } 363 | const list = this.serverList 364 | const serverAddr = list[randInt(0, list.length)] 365 | this.danmuClient = new DouyuDanmuClient(this.roomId) 366 | window.dm = this.danmuClient 367 | await this.danmuClient.connectAsync(serverAddr.ip, serverAddr.port) 368 | } 369 | @Type('keeplive') 370 | keeplive (data: DouyuPackage, rawString: string) { 371 | ACJ('room_data_userc', data.uc) 372 | ACJ('room_data_tbredpacket', rawString) 373 | } 374 | @Type('setmsggroup') 375 | setmsggroup (data: DouyuPackage) { 376 | console.log('joingroup', data) 377 | this.danmuClient.joingroup(data.rid, data.gid) 378 | } 379 | onDefault (data: DouyuPackage) { 380 | ACJ('room_data_handler', data) 381 | console.log('ms', data.type, data) 382 | } 383 | onReconnect () { 384 | 385 | } 386 | } 387 | 388 | class DouyuDanmuClient extends DouyuBaseClient { 389 | gid: string 390 | rid: string 391 | hasReconnect: boolean = false 392 | constructor (roomId: string) { 393 | super(roomId) 394 | this.redirect = { 395 | chatres: 'room_data_chat2', 396 | initcl: 'room_data_chatinit', 397 | dgb: 'room_data_giftbat1', 398 | dgn: 'room_data_giftbat1', 399 | spbc: 'room_data_giftbat1', 400 | uenter: 'room_data_nstip2', 401 | upgrade: 'room_data_ulgrow', 402 | newblackres: 'room_data_sys', 403 | ranklist: 'room_data_cqrank', 404 | rankup: 'room_data_ulgrow', 405 | gift_title: 'room_data_schat', 406 | rss: 'room_data_state', 407 | srres: 'room_data_wbsharesuc', 408 | onlinegift: 'room_data_olyw', 409 | gpbc: 'room_data_handler', 410 | synexp: 'room_data_handler', 411 | frank: 'room_data_handler', 412 | ggbb: 'room_data_sabonusget', 413 | online_noble_list: 'room_data_handler', 414 | } 415 | } 416 | joingroup (rid: string, gid: string) { 417 | this.rid = rid 418 | this.gid = gid 419 | this.send({ 420 | type: 'joingroup', 421 | rid: rid, 422 | gid: gid 423 | }) 424 | } 425 | @Type('chatmsg') 426 | chatmsg (pkg: DouyuPackage) { 427 | onChatMsg(pkg) 428 | } 429 | @Type('loginres') 430 | loginres (data: DouyuPackage) { 431 | console.log('loginres dm', data) 432 | this.startKeepalive() 433 | if (this.hasReconnect) { 434 | this.hasReconnect = false 435 | if (this.rid && this.gid) { 436 | this.joingroup(this.rid, this.gid) 437 | } 438 | } 439 | } 440 | onDefault (data: DouyuPackage) { 441 | ACJ('room_data_handler', data) 442 | console.log('dm', data.type, data) 443 | } 444 | onReconnect () { 445 | this.hasReconnect = true 446 | } 447 | } 448 | 449 | function hookDouyu (roomId: string, miscClient: DouyuClient, loginNotify: DelayNotify) { 450 | let oldExe: Function 451 | const repeatPacket = (text: string) => douyuDecode(text) 452 | const jsMap: any = { 453 | js_getRankScore: repeatPacket, 454 | js_getnoble: repeatPacket, 455 | js_rewardList: { 456 | type: 'qrl', 457 | rid: roomId 458 | }, 459 | js_queryTask: { 460 | type: 'qtlnq' 461 | }, 462 | js_newQueryTask: { 463 | type: 'qtlq' 464 | }, 465 | js_sendmsg (msg: string) { 466 | let pkg = douyuDecode(msg) 467 | pkg.type = 'chatmessage' 468 | return pkg 469 | }, 470 | js_giveGift (gift: string) { 471 | let pkg = douyuDecode(gift) 472 | if (pkg.type === 'dn_s_gf') { 473 | pkg.type = 'sgq' 474 | pkg.bat = 0 475 | } 476 | console.log('giveGift', gift) 477 | return gift 478 | }, 479 | js_GetHongbao: repeatPacket, 480 | js_UserHaveHandle () {}, 481 | js_myblacklist (list: string) { 482 | console.log('add blacklist', list) 483 | blacklist = list.split('|') 484 | }, 485 | js_medal_opera (opt: string) { 486 | let pkg = douyuDecode(opt) 487 | return pkg 488 | } 489 | } 490 | const api: any = window['require']('douyu/page/room/base/api') 491 | const hookd = function hookd (...args: any[]) { 492 | let req = jsMap[args[0]] 493 | if (args[0] == 'js_userlogin') { 494 | console.log('user login') 495 | loginNotify.notify(undefined) 496 | } 497 | if (req) { 498 | if (typeof req == 'function') { 499 | req = req.apply(null, args.slice(1)) 500 | } 501 | req && miscClient.send(req) 502 | } else { 503 | console.log('exe', args) 504 | try { 505 | return oldExe.apply(api, args) 506 | } catch (e) {} 507 | } 508 | } 509 | if (api) { 510 | if (api.exe !== hookd) { 511 | oldExe = api.exe 512 | api.exe = hookd 513 | } 514 | } else if (window.thisMovie) { 515 | window.thisMovie = () => new Proxy({}, { 516 | get (target: any, key: PropertyKey, receiver: any) { 517 | return (...args: any[]) => hookd.apply(null, [key].concat(args)) 518 | } 519 | }) 520 | } 521 | } 522 | 523 | export interface DouyuAPI { 524 | sendDanmu (content: string): void 525 | serverSend (pkg: DouyuPackage): void 526 | } 527 | export async function douyuApi (roomId: string): Promise { 528 | ACJ('room_bus_login', '') 529 | const res = await fetch('/swf_api/getProxyServer') 530 | const args = await res.json() 531 | const servers = args.servers 532 | const mserver = servers[Math.floor(Math.random() * servers.length)] 533 | 534 | let miscClient = new DouyuClient(roomId) 535 | const df = new DelayNotify(undefined) 536 | hookDouyu(roomId, miscClient, df) 537 | await df.wait() 538 | await miscClient.connectAsync(mserver.ip, mserver.port) 539 | return { 540 | sendDanmu (content: string) { 541 | // type@=chatmessage/receiver@=0/content@=${内容}/scope@=/col@=0/pid@=/p2p@=0/nc@=0/rev@=0/hg@=0/ifs@=0/sid@=/lid@=0/ 542 | miscClient.send({ 543 | nc: '0', 544 | rev: '0', 545 | hg: '0', 546 | ifs: '0', 547 | lid: '0', 548 | col: '0', 549 | p2p: '0', 550 | receiver: '0', 551 | content: content, 552 | sid: '', 553 | pid: '', 554 | scope: '', 555 | type: 'chatmessage' 556 | }) 557 | }, 558 | serverSend (pkg: DouyuPackage) { 559 | return miscClient.send(pkg) 560 | } 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /src/douyu/contentScript.ts: -------------------------------------------------------------------------------- 1 | import '../hookfetch' 2 | import flvjs from 'flv.js' 3 | import { DanmuPlayer, PlayerUI, PlayerUIEventListener, PlayerState } from '../danmuPlayer' 4 | import { bindMenu } from '../playerMenu' 5 | import { DouyuSource } from './source' 6 | import { getACF, setPureMode, isPureMode } from './api' 7 | import { getURL, addScript, addCss, createBlobURL, onMessage, postMessage, sendMessage, getSetting, setSetting, setBgListener, DelayNotify } from '../utils' 8 | import { TypeState } from 'TypeState' 9 | import { Signer, SignerState } from './signer' 10 | import { getDialog } from '../donate' 11 | import { runtime } from '../chrome' 12 | 13 | declare var window: { 14 | __space_inject: { 15 | script: string, 16 | css: string 17 | }, 18 | [key: string]: any 19 | } & Window 20 | const onload = () => { 21 | if (window.__space_inject) { 22 | const {script, css} = window.__space_inject 23 | addCss(createBlobURL(css, 'text/css')) 24 | addScript(createBlobURL(script, 'text/javascript')) 25 | window.__space_inject = null 26 | } else { 27 | addCss('dist/danmu.css') 28 | addScript('dist/douyuInject.js') 29 | } 30 | // addScript('libs/less.min.js') 31 | 32 | const uid = getACF('uid') 33 | 34 | flvjs.LoggingControl.forceGlobalTag = true 35 | flvjs.LoggingControl.enableAll = true 36 | 37 | class DouyuPlayerUI extends PlayerUI { 38 | private douyuFullpage = false 39 | constructor (listener: PlayerUIEventListener, state: TypeState.FiniteStateMachine) { 40 | super(listener, state) 41 | this.wrap.style.position = 'inherit' 42 | this.wrap.style.zIndex = 'inherit' 43 | } 44 | protected _enterFullScreen () { 45 | this.wrap.style.position = '' 46 | this.wrap.style.zIndex = '' 47 | super._enterFullScreen() 48 | } 49 | protected _exitFullScreen () { 50 | this.wrap.style.position = 'inherit' 51 | this.wrap.style.zIndex = 'inherit' 52 | super._exitFullScreen() 53 | } 54 | protected _enterFullPage () { 55 | if (isPureMode()) { 56 | this.wrap.style.position = '' 57 | this.wrap.style.zIndex = '' 58 | return super._enterFullPage() 59 | } 60 | this.wrap.setAttribute('fullpage', '') 61 | this.el.style.border = '0' 62 | 63 | if (!this.douyuFullpage) { 64 | this.douyuFullpage = true 65 | postMessage('ACJ', { 66 | id: 'room_bus_pagescr' 67 | }) 68 | } 69 | } 70 | protected _exitFullPage () { 71 | if (isPureMode()) { 72 | this.wrap.style.position = 'inherit' 73 | this.wrap.style.zIndex = 'inherit' 74 | return super._exitFullPage() 75 | } 76 | this.wrap.removeAttribute('fullpage') 77 | this.el.style.border = '' 78 | 79 | if (this.douyuFullpage) { 80 | this.douyuFullpage = false 81 | postMessage('ACJ', { 82 | id: 'room_bus_pagescr' 83 | }) 84 | } 85 | } 86 | } 87 | class DouyuDanmuPlayer extends DanmuPlayer { 88 | source: DouyuSource 89 | constructor (roomId: string) { 90 | const source = new DouyuSource(roomId, async (rid, tt, did) => { 91 | let sign = await Signer.sign(roomId, tt, did) 92 | return sign 93 | }) 94 | source.onChange = videoUrl => { 95 | this.src = videoUrl 96 | } 97 | super({ 98 | getSrc: () => source.getUrl(), 99 | onSendDanmu (txt) { 100 | window.postMessage({ 101 | type: "SENDANMU", 102 | data: txt 103 | }, "*") 104 | } 105 | }) 106 | this.source = source 107 | } 108 | initUI () { 109 | this.ui = new DouyuPlayerUI(this, this.state) 110 | } 111 | onDanmuPkg (pkg: any) { 112 | if (DEBUG) { 113 | const example = { 114 | "type": "chatmsg", 115 | "rid": "510541", 116 | "ct": "1", // 酬勤 117 | "uid": "59839409", 118 | "nn": "登辛", 119 | "txt": "3ds没有鼓棒先生吗", 120 | "cid": "ce554df5bf2841e41459070000000000", 121 | "ic": "avatar/face/201607/27/12d23d30a9a7790e955d7affc54335ad", 122 | "level": "17", 123 | "gt": "2", // 124 | "rg": "4", // 125 | "el": "eid@A=1500000005@Setp@A=1@Ssc@A=1@Sef@A=0@S/" 126 | } 127 | } 128 | const getColor = (c: number) => ['#ff0000', '#1e87f0', '#7ac84b', '#ff7f00', '#9b39f4', '#ff69b4'][c-1] 129 | if (pkg.txt.length > 0) { 130 | let cls = [] 131 | let color = getColor(pkg.col) || '#ffffff' 132 | if (pkg.uid === uid) cls.push('danmu-self') 133 | this.fireDanmu(pkg.txt, color, cls) 134 | } 135 | } 136 | } 137 | 138 | const makeMenu = (player: DouyuDanmuPlayer, source: DouyuSource) => { 139 | const cdnMenu = () => source.cdnsWithName.map((i: any) => { 140 | let suffix = '' 141 | if (i.cdn == source.cdn) suffix = ' √' 142 | return { 143 | text: i.name + suffix, 144 | cb () { 145 | source.cdn = i.cdn 146 | } 147 | } 148 | }) 149 | const rateMenu = () => { 150 | const rates = [{ 151 | text: '超清', 152 | rate: '0' 153 | }, { 154 | text: '高清', 155 | rate: '2' 156 | }, { 157 | text: '普清', 158 | rate: '1' 159 | }] 160 | return rates.map(i => { 161 | let suffix = '' 162 | if (i.rate == source.rate) suffix = ' √' 163 | return { 164 | text: i.text + suffix, 165 | cb () { 166 | source.rate = i.rate 167 | } 168 | } 169 | }) 170 | } 171 | 172 | const transparentMenu = () => { 173 | const opts = [{ 174 | text: '0%', 175 | transparent: 0 176 | }, { 177 | text: '25%', 178 | transparent: 25 179 | }, { 180 | text: '50%', 181 | transparent: 50 182 | }] 183 | return [{ 184 | label: '弹幕透明度:' 185 | }].concat(opts.map(i => { 186 | let suffix = '' 187 | if (i.transparent == player.ui.transparent) suffix = ' √' 188 | return { 189 | text: i.text + suffix, 190 | cb () { 191 | player.ui.transparent = i.transparent 192 | }, 193 | label: null 194 | } 195 | })) 196 | } 197 | const disableDanmu = () => [{ 198 | text: `纯净模式${isPureMode() ? ' √' : ''}`, 199 | cb () { 200 | setPureMode(!isPureMode()) 201 | location.reload() 202 | } 203 | }] 204 | let mGetURL: (file: string) => string 205 | if (USERSCRIPT) { 206 | mGetURL = file => 'https://imspace.nos-eastchina1.126.net/img/' + file 207 | } else { 208 | mGetURL = file => getURL('dist/img/' + file) 209 | } 210 | const dialog = getDialog('捐赠', '你的支持是我最大的动力.', [{ 211 | src: mGetURL('alipay.png'), 212 | desc: '支付宝' 213 | }, { 214 | src: mGetURL('wechat.png'), 215 | desc: '微信' 216 | }]) 217 | const donate = () => { 218 | return [{ 219 | text: '捐赠', 220 | cb () { 221 | document.body.appendChild(dialog) 222 | dialog.style.display = 'flex' 223 | } 224 | }] 225 | } 226 | const dash = {} 227 | bindMenu(player.ui.video, () => [].concat(cdnMenu(), dash, rateMenu(), dash, transparentMenu(), dash, disableDanmu(), dash, donate())) 228 | } 229 | 230 | const loadVideo = (roomId: string, replace: (el: Element) => void) => { 231 | const danmuPlayer = new DouyuDanmuPlayer(roomId) 232 | 233 | danmuPlayer.mgr.parsePic = s => s.replace( 234 | /\[emot:dy(.*?)\]/g, 235 | (_, i) => ``// `
` 236 | ) 237 | 238 | replace(danmuPlayer.ui.el) 239 | 240 | makeMenu(danmuPlayer, danmuPlayer.source) 241 | 242 | window.danmu = danmuPlayer 243 | 244 | return danmuPlayer.source.getUrl().then(() => danmuPlayer) 245 | } 246 | 247 | 248 | let danmuPlayer: DouyuDanmuPlayer = null 249 | let signerLoaded = new DelayNotify(false) 250 | 251 | Signer.init().then(() => true).catch(() => false).then((data: boolean) => { 252 | console.log('SIGNER_READY', data) 253 | signerLoaded.notify(data) 254 | }) 255 | onMessage('DANMU', data => { 256 | danmuPlayer && danmuPlayer.onDanmuPkg(data) 257 | }) 258 | onMessage('VIDEOID', async data => { 259 | console.log('onVideoId', data) 260 | const roomId = data.roomId 261 | setBgListener(async req => { 262 | switch (req.type) { 263 | case 'toggle': 264 | let setting = await getSetting() 265 | const id = setting.blacklist.indexOf(roomId) 266 | if (id === -1) { 267 | setting.blacklist.push(roomId) 268 | } else { 269 | setting.blacklist.splice(id, 1) 270 | } 271 | await setSetting(setting) 272 | location.reload() 273 | } 274 | }) 275 | 276 | console.log('wait signer') 277 | if (!await signerLoaded.wait()) { 278 | console.warn('加载签名程序失败, 无法获取视频地址') 279 | return 280 | } 281 | 282 | console.log('start replace') 283 | try { 284 | const setting = await getSetting() 285 | if (setting.blacklist.indexOf(roomId) !== -1) { // 存在黑名单 286 | if (runtime.sendMessage) { 287 | runtime.sendMessage({ 288 | type: 'disable' 289 | }) 290 | } 291 | await postMessage('CONTINUE_ORIGIN', {}) 292 | return 293 | } 294 | } catch (e) { 295 | console.warn(e) 296 | } 297 | let ctr = document.querySelector(`#${data.id}`) 298 | if (!isPureMode()) { 299 | await postMessage('BEGINAPI', { 300 | roomId 301 | }) 302 | } 303 | danmuPlayer = await loadVideo(roomId, el => { 304 | ctr.parentNode.replaceChild(el, ctr) 305 | }) 306 | }) 307 | 308 | } 309 | //document.addEventListener('DOMContentLoaded', onload) 310 | onload() -------------------------------------------------------------------------------- /src/douyu/inject.ts: -------------------------------------------------------------------------------- 1 | import { douyuApi, DouyuAPI, ACJ } from './api' 2 | import {onMessage, postMessage} from '../utils' 3 | 4 | const emptyFunc = () => {} 5 | let originUse = emptyFunc 6 | let useOrigin = false 7 | declare var window: { 8 | [key: string]: any 9 | } & Window 10 | function hookFunc (obj: any, funcName: string, newFunc: (func: Function, args: any[]) => any) { 11 | var old = obj[funcName] 12 | obj[funcName] = function () { 13 | return newFunc.call(this, old.bind(this), Array.from(arguments)) 14 | } 15 | } 16 | function getParam(flash: any, name: string) { 17 | const children = flash.children 18 | for (let i=0; i i.substr(0,6) == 'RoomId')[0].split('=')[1] 28 | } 29 | function hookH5() { 30 | const player = window['require']('douyu-liveH5/live/js/player') 31 | const postReady = (player: any) => { 32 | const roomId = player.flashvars.RoomId 33 | console.log('RoomId', roomId) 34 | const ctr = document.getElementById(player.root.id) 35 | const box = document.createElement('div') 36 | box.id = `space_douyu_html5_player` 37 | ctr.appendChild(box) 38 | postMessage('VIDEOID', { 39 | roomId: roomId, 40 | id: box.id 41 | }) 42 | } 43 | const fakePlayer = { 44 | init (root: HTMLElement, param: any) { 45 | console.log('fake init', param) 46 | postReady(param) 47 | }, 48 | load (param: any) { 49 | console.log('fake load', param) 50 | postReady(param) 51 | } 52 | } 53 | if (player === null) { 54 | console.log('player null, hook `require.use`') 55 | const oldUse = window.require.use 56 | hookFunc(window, 'require', (old, args) => { 57 | const name = args[0] 58 | if (name === 'douyu-liveH5/live/js/h5') { 59 | return fakePlayer 60 | } 61 | let ret = old.apply(null, args) 62 | return ret 63 | }) 64 | window.require.use = oldUse 65 | hookFunc(window.require, 'use', (old, args) => { 66 | const name: string = args[0][0] 67 | 68 | if (!useOrigin && name.indexOf('douyu-liveH5/live/js') !== -1) { 69 | const cb: Function = args[1] 70 | 71 | console.log('require.use', name) 72 | originUse = () => { 73 | old.apply(null, args) 74 | } 75 | cb(fakePlayer) 76 | } else { 77 | let ret = old.apply(null, args) 78 | return ret 79 | } 80 | }) 81 | } else { 82 | if (player.h5player) { 83 | player.h5player.destroy() 84 | postReady(player.params) 85 | } else { 86 | player.init = fakePlayer.init 87 | player.load = fakePlayer.load 88 | console.error('TODO player: 1 h5player: 0') 89 | } 90 | } 91 | } 92 | hookFunc(document, 'createElement', (old, args) => { 93 | let ret = old.apply(null, args) 94 | if (args[0] == 'object') { 95 | hookFunc(ret, 'setAttribute', (old, args) => { 96 | // console.log(args) 97 | if (args[0] == 'data') { 98 | if (/WebRoom/.test(args[1])) { 99 | // args[1] = '' 100 | setTimeout(() => { 101 | let roomId = getRoomIdFromFlash(getParam(ret, 'flashvars')) 102 | console.log('RoomId', roomId) 103 | postMessage('VIDEOID', { 104 | roomId: roomId, 105 | id: ret.id 106 | }) 107 | }, 1) 108 | } 109 | } 110 | return old.apply(null, args) 111 | }) 112 | } 113 | return ret 114 | }) 115 | hookH5() 116 | let api: DouyuAPI 117 | onMessage('BEGINAPI', async data => { 118 | // await retry(() => JSocket.init(), 3) 119 | api = await douyuApi(data.roomId) 120 | window.api = api 121 | }) 122 | onMessage('SENDANMU', data => { 123 | api.sendDanmu(data) 124 | }) 125 | onMessage('ACJ', data => { 126 | ACJ(data.id, data.data) 127 | }) 128 | onMessage('CONTINUE_ORIGIN', data => { 129 | console.log('...continue') 130 | useOrigin = true 131 | originUse() 132 | originUse = emptyFunc 133 | }) -------------------------------------------------------------------------------- /src/douyu/signer.ts: -------------------------------------------------------------------------------- 1 | import { ISignerResult } from './source' 2 | import { runtime } from '../chrome' 3 | export enum SignerState { 4 | None, 5 | Loaded, 6 | Ready, 7 | Timeout 8 | } 9 | type WrapPort = (method: string, ...args: any[]) => Promise 10 | function wrapPort (port: chrome.runtime.Port) { 11 | let curMethod = '' 12 | let curResolve: (value?: any[] | PromiseLike) => void = null 13 | let curReject:(reason?: any) => void = null 14 | let stack = new Error().stack 15 | port.onMessage.addListener((msg: any) => { 16 | if (msg.method === curMethod) { 17 | curResolve(msg.args[0]) 18 | } else { 19 | curReject('wtf') 20 | console.error('wtf?') 21 | } 22 | }) 23 | return function (method: string, ...args: any[]) { 24 | return new Promise((resolve, reject) => { 25 | curMethod = method 26 | curResolve = resolve 27 | curReject = reject 28 | port.postMessage({ 29 | method: method, 30 | args: args 31 | }) 32 | }) 33 | } 34 | } 35 | interface ISigner { 36 | // onStateChanged: SignerStateListener 37 | state: SignerState 38 | sign (rid: string, tt: number, did: string): Promise 39 | init (): Promise 40 | } 41 | class BackgroundSigner { 42 | private static _inited: boolean = false 43 | private static _port: WrapPort 44 | private static _state: SignerState = SignerState.None 45 | private static _resolve: Function 46 | private static _reject: Function 47 | private static _clean () { 48 | this._resolve = null 49 | this._reject = null 50 | } 51 | private static onStateChanged (state: SignerState) { 52 | if (state === SignerState.Ready) { 53 | this._inited = true 54 | this._resolve() 55 | this._clean() 56 | } else if (state === SignerState.Timeout) { 57 | this._reject() 58 | this._clean() 59 | } else { 60 | return 61 | } 62 | } 63 | private static setState (val: SignerState) { 64 | if (this._state === SignerState.Timeout) { // timeout 时不再加载 65 | return 66 | } 67 | if (val !== this._state) { 68 | this._state = val 69 | this.onStateChanged(this.state) 70 | } else { 71 | this._state = val 72 | } 73 | } 74 | static async sign (rid: string, tt: number, did: string): Promise { 75 | return await this._port('sign', rid, tt, did) 76 | } 77 | static get state () { 78 | return this._state 79 | } 80 | static init (): Promise { 81 | if (this._inited) { 82 | return Promise.resolve() 83 | } 84 | return new Promise((resolve, reject) => { 85 | this._resolve = resolve 86 | this._reject = reject 87 | const port = wrapPort(runtime.connect({name: "signer"})) 88 | this._port = port 89 | let iid = window.setInterval(async () => { 90 | let ret = await port('query') 91 | console.log('query', ret) 92 | if (ret) { 93 | this.setState(SignerState.Loaded) 94 | this.setState(SignerState.Ready) 95 | if (iid !== null) { 96 | window.clearInterval(iid) 97 | iid = null 98 | } 99 | } 100 | }, 100) 101 | window.setTimeout(() => { 102 | if (this.state !== SignerState.Ready) { 103 | this.setState(SignerState.Timeout) 104 | } 105 | if (iid !== null) { 106 | window.clearInterval(iid) 107 | iid = null 108 | } 109 | }, 15 * 1000) 110 | }) 111 | } 112 | } 113 | 114 | let Signer: ISigner = BackgroundSigner 115 | export { 116 | Signer 117 | } 118 | -------------------------------------------------------------------------------- /src/douyu/source.ts: -------------------------------------------------------------------------------- 1 | import md5 from '../md5' 2 | import {BaseSource} from '../source' 3 | 4 | export interface ISignerResult { 5 | cptl: string, 6 | sign: string 7 | } 8 | type SignFunc = (rid: string, tt: number, did: string) => Promise 9 | let m_signer: SignFunc = null 10 | 11 | async function getSourceURL (rid: string, cdn: string, rate: string) { 12 | const tt = Math.round(new Date().getTime() / 60 / 1000) 13 | const did = md5(Math.random().toString()).toUpperCase() 14 | if (m_signer === null) { 15 | throw new Error('Signer is not defined.') 16 | } 17 | const sign: ISignerResult = await m_signer(rid, tt, did) 18 | let body: any = { 19 | 'cdn': cdn, 20 | 'rate': rate, 21 | 'ver': 'Douyu_h5_2017080201beta', 22 | 'tt': tt, 23 | 'did': did, 24 | 'sign': sign.sign, 25 | 'cptl': sign.cptl, 26 | 'ct': 'webh5' 27 | } 28 | body = Object.keys(body).map(key => `${key}=${encodeURIComponent(body[key])}`).join('&') 29 | const res = await fetch(`https://www.douyu.com/lapi/live/getPlay/${rid}`, { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/x-www-form-urlencoded' 33 | }, 34 | body: body 35 | }) 36 | const videoInfo = await res.json() 37 | const baseUrl = videoInfo.data.rtmp_url 38 | const livePath = videoInfo.data.rtmp_live 39 | if (baseUrl && livePath) { 40 | const videoUrl = `${baseUrl}/${livePath}` 41 | console.log('RoomId', rid, 'SourceURL:', videoUrl) 42 | return videoUrl 43 | } else { 44 | throw new Error('未开播或获取失败') 45 | } 46 | } 47 | 48 | async function getSwfApi (rid: string) { 49 | const API_KEY = 'bLFlashflowlad92' 50 | const tt = Math.round(new Date().getTime() / 60 / 1000) 51 | const signContent = [rid, API_KEY, tt].join('') 52 | const sign = md5(signContent) 53 | const res = await fetch(`https://www.douyu.com/swf_api/room/${rid}?cdn=&nofan=yes&_t=${tt}&sign=${sign}`) 54 | const obj = await res.json() 55 | return await obj.data 56 | } 57 | 58 | export class DouyuSource extends BaseSource { 59 | roomId: string 60 | swfApi: any 61 | private _cdn: string 62 | private _rate: string 63 | constructor (roomId: string, signer: SignFunc) { 64 | super() 65 | m_signer = signer 66 | this._cdn = 'ws' 67 | this._rate = '0' 68 | this.url = '' 69 | this.roomId = roomId 70 | this.swfApi = null 71 | } 72 | set cdn (val) { 73 | this._cdn = val 74 | this.getUrl() 75 | } 76 | get cdn () { 77 | return this._cdn 78 | } 79 | set rate (val) { 80 | this._rate = val 81 | this.getUrl() 82 | } 83 | get rate () { 84 | return this._rate 85 | } 86 | get cdnsWithName () { 87 | if (this.swfApi) { 88 | return this.swfApi.cdnsWithName 89 | } else { 90 | return [{ 91 | name: '主要线路', 92 | cdn: 'ws' 93 | }] 94 | } 95 | } 96 | async getUrl () { 97 | if (!this.swfApi) { 98 | this.swfApi = await getSwfApi(this.roomId) 99 | this._cdn = this.swfApi.cdns[0] 100 | } 101 | let url = await getSourceURL(this.roomId, this.cdn, this.rate) 102 | this.url = url 103 | return url 104 | } 105 | } -------------------------------------------------------------------------------- /src/embedSWF.ts: -------------------------------------------------------------------------------- 1 | export function embedSWF (id: string, src: string) { 2 | const flash = [ 3 | '', 14 | ``, 15 | '', 16 | '', 17 | '', 18 | '', 19 | '', 20 | '', 21 | '', 22 | '' 23 | ].join('') 24 | let div = document.createElement('div') 25 | div.className = 'big-flash-cls' // 防止Chrome屏蔽小块的 Flash 26 | document.body.appendChild(div) 27 | div.innerHTML = flash 28 | return div 29 | } 30 | -------------------------------------------------------------------------------- /src/flash/builtin.abc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemeowx2/DouyuHTML5Player/8ee6d938db9db9d59f3069e4ee8c10184c197f42/src/flash/builtin.abc -------------------------------------------------------------------------------- /src/flash/douyu.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemeowx2/DouyuHTML5Player/8ee6d938db9db9d59f3069e4ee8c10184c197f42/src/flash/douyu.swf -------------------------------------------------------------------------------- /src/flash/playerglobal.abc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemeowx2/DouyuHTML5Player/8ee6d938db9db9d59f3069e4ee8c10184c197f42/src/flash/playerglobal.abc -------------------------------------------------------------------------------- /src/hookfetch.js: -------------------------------------------------------------------------------- 1 | function hookFetchCode () { 2 | // let self = this 3 | const convertHeader = function convertHeader (headers) { 4 | let out = new Headers() 5 | for (let key of Object.keys(headers)) { 6 | out.set(key, headers[key]) 7 | } 8 | return out 9 | } 10 | const hideHookStack = stack => { 11 | return stack.replace(/^\s*at\s.*?hookfetch\.js:\d.*$\n/mg, '') 12 | } 13 | const base64ToUint8 = (b64) => { 14 | const s = atob(b64) 15 | const length = s.length 16 | let ret = new Uint8Array(length) 17 | for (let i = 0; i < length; i++) { 18 | ret[i] = s.charCodeAt(i) 19 | } 20 | return ret 21 | } 22 | class WrapPort { 23 | constructor (port) { 24 | this.curMethod = '' 25 | this.curResolve = null 26 | this.curReject = null 27 | this.stack = '' 28 | this.port = port 29 | this.lastDone = true 30 | 31 | port.onMessage.addListener(msg => this.onMessage(msg)) 32 | } 33 | post (method, args) { 34 | if (!this.lastDone) { 35 | throw new Error('Last post is not done') 36 | } 37 | this.stack = new Error().stack 38 | return new Promise((resolve, reject) => { 39 | this.lastDone = false 40 | this.curMethod = method 41 | this.curResolve = resolve 42 | this.curReject = reject 43 | this.port.postMessage({ 44 | method: method, 45 | args: args 46 | }) 47 | }) 48 | } 49 | onMessage (msg) { 50 | if (msg.method === this.curMethod) { 51 | if (msg.err) { 52 | let err = new Error(msg.err.message) 53 | err.oriName = msg.err.name 54 | err.stack = hideHookStack(this.stack) 55 | // console.log('fetch err', err) 56 | this.curReject.call(null, err) 57 | } else { 58 | this.curResolve.apply(null, msg.args) 59 | } 60 | this.curResolve = null 61 | this.curReject = null 62 | this.lastDone = true 63 | } else { 64 | console.error('wtf?') 65 | } 66 | } 67 | } 68 | class PortReader { 69 | constructor (port) { 70 | this.port = port 71 | this.hasReader = false 72 | } 73 | _requireReader () { 74 | if (this.hasReader) { 75 | return Promise.resolve() 76 | } else { 77 | return this.port.post('body.getReader').then(() => this.hasReader = true) 78 | } 79 | } 80 | read () { 81 | return this._requireReader() 82 | .then(() => this.port.post('reader.read')) 83 | .then(r => { 84 | if (r.done == false) { 85 | r.value = base64ToUint8(r.value) 86 | } 87 | return r 88 | }) 89 | } 90 | cancel () { 91 | return this._requireReader().then(() => this.port.post('reader.cancel')) 92 | } 93 | } 94 | class PortBody { 95 | constructor (port) { 96 | this.port = port 97 | } 98 | getReader () { 99 | return new PortReader(this.port) 100 | } 101 | } 102 | class PortFetch { 103 | constructor () { 104 | this.port = new WrapPort(chrome.runtime.connect({name: 'fetch'})) 105 | } 106 | fetch (...args) { 107 | return this.port.post('fetch', args).then(r => { 108 | r.json = () => this.port.post('json') 109 | r.arrayBuffer = () => this.port.post('arrayBuffer').then(buf => { 110 | return new Uint8Array(buf).buffer 111 | }) 112 | r.headers = convertHeader(r.headers) 113 | r.body = new PortBody(this.port) 114 | return r 115 | }) 116 | } 117 | } 118 | const bgFetch = function bgFetch (...args) { 119 | const fetch = new PortFetch() 120 | return fetch.fetch(...args) 121 | } 122 | function hookFetch () { 123 | if (fetch !== bgFetch) { 124 | fetch = bgFetch 125 | } 126 | } 127 | const oldBlob = Blob 128 | const newBlob = function newBlob(a, b) { 129 | a[0] = `(${hookFetchCode})();${a[0]}` 130 | console.log('new blob', a, b) 131 | return new oldBlob(a, b) 132 | } 133 | // if(self.document !== undefined) { 134 | // if (self.Blob !== newBlob) { 135 | // self.Blob = newBlob 136 | // } 137 | // } 138 | 139 | hookFetch() 140 | } 141 | function isFirefox () { 142 | return /Firefox/.test(navigator.userAgent) 143 | } 144 | if (!isFirefox()) { 145 | hookFetchCode() 146 | } 147 | -------------------------------------------------------------------------------- /src/img/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemeowx2/DouyuHTML5Player/8ee6d938db9db9d59f3069e4ee8c10184c197f42/src/img/alipay.png -------------------------------------------------------------------------------- /src/img/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemeowx2/DouyuHTML5Player/8ee6d938db9db9d59f3069e4ee8c10184c197f42/src/img/disabled.png -------------------------------------------------------------------------------- /src/img/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/img/muted.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/reload.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacemeowx2/DouyuHTML5Player/8ee6d938db9db9d59f3069e4ee8c10184c197f42/src/img/wechat.png -------------------------------------------------------------------------------- /src/md5.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Credit http://www.myersdaily.org/joseph/javascript/md5-text.html 3 | */ 4 | 5 | function md5cycle(x: number[], k: number[]) { 6 | var a = x[0], 7 | b = x[1], 8 | c = x[2], 9 | d = x[3]; 10 | 11 | a = ff(a, b, c, d, k[0], 7, -680876936); 12 | d = ff(d, a, b, c, k[1], 12, -389564586); 13 | c = ff(c, d, a, b, k[2], 17, 606105819); 14 | b = ff(b, c, d, a, k[3], 22, -1044525330); 15 | a = ff(a, b, c, d, k[4], 7, -176418897); 16 | d = ff(d, a, b, c, k[5], 12, 1200080426); 17 | c = ff(c, d, a, b, k[6], 17, -1473231341); 18 | b = ff(b, c, d, a, k[7], 22, -45705983); 19 | a = ff(a, b, c, d, k[8], 7, 1770035416); 20 | d = ff(d, a, b, c, k[9], 12, -1958414417); 21 | c = ff(c, d, a, b, k[10], 17, -42063); 22 | b = ff(b, c, d, a, k[11], 22, -1990404162); 23 | a = ff(a, b, c, d, k[12], 7, 1804603682); 24 | d = ff(d, a, b, c, k[13], 12, -40341101); 25 | c = ff(c, d, a, b, k[14], 17, -1502002290); 26 | b = ff(b, c, d, a, k[15], 22, 1236535329); 27 | 28 | a = gg(a, b, c, d, k[1], 5, -165796510); 29 | d = gg(d, a, b, c, k[6], 9, -1069501632); 30 | c = gg(c, d, a, b, k[11], 14, 643717713); 31 | b = gg(b, c, d, a, k[0], 20, -373897302); 32 | a = gg(a, b, c, d, k[5], 5, -701558691); 33 | d = gg(d, a, b, c, k[10], 9, 38016083); 34 | c = gg(c, d, a, b, k[15], 14, -660478335); 35 | b = gg(b, c, d, a, k[4], 20, -405537848); 36 | a = gg(a, b, c, d, k[9], 5, 568446438); 37 | d = gg(d, a, b, c, k[14], 9, -1019803690); 38 | c = gg(c, d, a, b, k[3], 14, -187363961); 39 | b = gg(b, c, d, a, k[8], 20, 1163531501); 40 | a = gg(a, b, c, d, k[13], 5, -1444681467); 41 | d = gg(d, a, b, c, k[2], 9, -51403784); 42 | c = gg(c, d, a, b, k[7], 14, 1735328473); 43 | b = gg(b, c, d, a, k[12], 20, -1926607734); 44 | 45 | a = hh(a, b, c, d, k[5], 4, -378558); 46 | d = hh(d, a, b, c, k[8], 11, -2022574463); 47 | c = hh(c, d, a, b, k[11], 16, 1839030562); 48 | b = hh(b, c, d, a, k[14], 23, -35309556); 49 | a = hh(a, b, c, d, k[1], 4, -1530992060); 50 | d = hh(d, a, b, c, k[4], 11, 1272893353); 51 | c = hh(c, d, a, b, k[7], 16, -155497632); 52 | b = hh(b, c, d, a, k[10], 23, -1094730640); 53 | a = hh(a, b, c, d, k[13], 4, 681279174); 54 | d = hh(d, a, b, c, k[0], 11, -358537222); 55 | c = hh(c, d, a, b, k[3], 16, -722521979); 56 | b = hh(b, c, d, a, k[6], 23, 76029189); 57 | a = hh(a, b, c, d, k[9], 4, -640364487); 58 | d = hh(d, a, b, c, k[12], 11, -421815835); 59 | c = hh(c, d, a, b, k[15], 16, 530742520); 60 | b = hh(b, c, d, a, k[2], 23, -995338651); 61 | 62 | a = ii(a, b, c, d, k[0], 6, -198630844); 63 | d = ii(d, a, b, c, k[7], 10, 1126891415); 64 | c = ii(c, d, a, b, k[14], 15, -1416354905); 65 | b = ii(b, c, d, a, k[5], 21, -57434055); 66 | a = ii(a, b, c, d, k[12], 6, 1700485571); 67 | d = ii(d, a, b, c, k[3], 10, -1894986606); 68 | c = ii(c, d, a, b, k[10], 15, -1051523); 69 | b = ii(b, c, d, a, k[1], 21, -2054922799); 70 | a = ii(a, b, c, d, k[8], 6, 1873313359); 71 | d = ii(d, a, b, c, k[15], 10, -30611744); 72 | c = ii(c, d, a, b, k[6], 15, -1560198380); 73 | b = ii(b, c, d, a, k[13], 21, 1309151649); 74 | a = ii(a, b, c, d, k[4], 6, -145523070); 75 | d = ii(d, a, b, c, k[11], 10, -1120210379); 76 | c = ii(c, d, a, b, k[2], 15, 718787259); 77 | b = ii(b, c, d, a, k[9], 21, -343485551); 78 | 79 | x[0] = add32(a, x[0]); 80 | x[1] = add32(b, x[1]); 81 | x[2] = add32(c, x[2]); 82 | x[3] = add32(d, x[3]); 83 | 84 | } 85 | 86 | function cmn(q: number, a: number, b: number, x: number, s: number, t: number) { 87 | a = add32(add32(a, q), add32(x, t)); 88 | return add32((a << s) | (a >>> (32 - s)), b); 89 | } 90 | 91 | function ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { 92 | return cmn((b & c) | ((~b) & d), a, b, x, s, t); 93 | } 94 | 95 | function gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { 96 | return cmn((b & d) | (c & (~d)), a, b, x, s, t); 97 | } 98 | 99 | function hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { 100 | return cmn(b ^ c ^ d, a, b, x, s, t); 101 | } 102 | 103 | function ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number) { 104 | return cmn(c ^ (b | (~d)), a, b, x, s, t); 105 | } 106 | 107 | function md51(s: string) { 108 | var txt = ''; 109 | var n = s.length, 110 | state = [1732584193, -271733879, -1732584194, 271733878], 111 | i; 112 | for (i = 64; i <= s.length; i += 64) { 113 | md5cycle(state, md5blk(s.substring(i - 64, i))); 114 | } 115 | s = s.substring(i - 64); 116 | var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 117 | for (i = 0; i < s.length; i++) 118 | tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); 119 | tail[i >> 2] |= 0x80 << ((i % 4) << 3); 120 | if (i > 55) { 121 | md5cycle(state, tail); 122 | for (i = 0; i < 16; i++) tail[i] = 0; 123 | } 124 | tail[14] = n * 8; 125 | md5cycle(state, tail); 126 | return state; 127 | } 128 | 129 | /* there needs to be support for Unicode here, 130 | * unless we pretend that we can redefine the MD-5 131 | * algorithm for multi-byte characters (perhaps 132 | * by adding every four 16-bit characters and 133 | * shortening the sum to 32 bits). Otherwise 134 | * I suggest performing MD-5 as if every character 135 | * was two bytes--e.g., 0040 0025 = @%--but then 136 | * how will an ordinary MD-5 sum be matched? 137 | * There is no way to standardize text to something 138 | * like UTF-8 before transformation; speed cost is 139 | * utterly prohibitive. The JavaScript standard 140 | * itself needs to look at this: it should start 141 | * providing access to strings as preformed UTF-8 142 | * 8-bit unsigned value arrays. 143 | */ 144 | function md5blk(s: string) { /* I figured global was faster. */ 145 | var md5blks = [], 146 | i; /* Andy King said do it this way. */ 147 | for (i = 0; i < 64; i += 4) { 148 | md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); 149 | } 150 | return md5blks; 151 | } 152 | 153 | var hex_chr = '0123456789abcdef'.split(''); 154 | 155 | function rhex(n: number) { 156 | var s = '', 157 | j = 0; 158 | for (; j < 4; j++) 159 | s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; 160 | return s; 161 | } 162 | 163 | function hex(x: number[]) { 164 | return x.map(rhex).join('') 165 | } 166 | 167 | export default function md5(s: string) { 168 | return hex(md51(s)); 169 | } 170 | 171 | /* this function is much faster, 172 | so if possible we use it. Some IEs 173 | are the only ones I know of that 174 | need the idiotic second function, 175 | generated by an if clause. */ 176 | 177 | var add32 = function (a: number, b: number) { 178 | return (a + b) & 0xFFFFFFFF; 179 | } 180 | 181 | if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') { 182 | add32 = function (x, y) { 183 | var lsw = (x & 0xFFFF) + (y & 0xFFFF), 184 | msw = (x >> 16) + (y >> 16) + (lsw >> 16); 185 | return (msw << 16) | (lsw & 0xFFFF); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/playerMenu.js: -------------------------------------------------------------------------------- 1 | const createMenu = (x, y) => { 2 | const wrap = document.createElement('div') 3 | const menu = document.createElement('div') 4 | wrap.className = 'player-menu' 5 | menu.className = 'menu' 6 | wrap.appendChild(menu) 7 | 8 | menu.style.left = `${x}px` 9 | menu.style.top = `${y}px` 10 | 11 | menu.close = () => document.body.removeChild(wrap) 12 | wrap.addEventListener('mousedown', event => { 13 | if (event.target === wrap) { 14 | document.body.removeChild(wrap) 15 | } 16 | }) 17 | wrap.addEventListener('contextmenu', event => event.preventDefault()) 18 | 19 | document.body.appendChild(wrap) 20 | return menu 21 | } 22 | const addTextMenu = (menu, text, cb) => { 23 | const item = document.createElement('div') 24 | item.className = 'menu-item' 25 | item.innerText = text 26 | menu.appendChild(item) 27 | 28 | item.addEventListener('click', () => { 29 | menu.close() 30 | cb() 31 | }) 32 | } 33 | const addEleMenu = (menu, ele) => { 34 | const item = document.createElement('div') 35 | item.className = 'menu-ele' 36 | item.appendChild(ele) 37 | menu.appendChild(item) 38 | } 39 | const addLabelMenu = (menu, label) => { 40 | const item = document.createElement('div') 41 | item.className = 'menu-item' 42 | item.innerText = label 43 | menu.appendChild(item) 44 | } 45 | const addDash = (menu) => { 46 | const item = document.createElement('div') 47 | item.className = 'menu-dash' 48 | menu.appendChild(item) 49 | } 50 | export function bindMenu (el, menuItems) { 51 | el.addEventListener('contextmenu', event => { 52 | const menu = createMenu(event.clientX, event.clientY) 53 | let items = menuItems 54 | if (typeof items === 'function') items = items() 55 | for (let item of items) { 56 | if (item.text) { 57 | addTextMenu(menu, item.text, item.cb) 58 | } else if (item.el) { 59 | addEleMenu(menu, item.el, item.cb) 60 | } else if (item.label) { 61 | addLabelMenu(menu, item.label) 62 | } else { 63 | addDash(menu) 64 | } 65 | } 66 | const rect = menu.getBoundingClientRect() 67 | if (menu.offsetLeft + menu.offsetWidth > document.documentElement.clientWidth) { 68 | menu.style.left = `${rect.left - rect.width}px` 69 | } 70 | if (menu.offsetTop + menu.offsetHeight > document.documentElement.clientHeight) { 71 | menu.style.top = `${rect.top - rect.height}px` 72 | } 73 | event.preventDefault() 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/shared-worker-signer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const arrayBuffer2BlobUrl = (buffer) => { 3 | const blob = new Blob([new Uint8Array(buffer)]) 4 | const url = URL.createObjectURL(blob) 5 | return url 6 | } 7 | let CModule 8 | let xx 9 | let ready = false 10 | let inited = false 11 | function init (port) { 12 | if (inited) { 13 | return 14 | } 15 | let res, rej 16 | port.addEventListener('message', ({data}) => { 17 | if (data.type === 'getArrayBuffer') { 18 | if (data.err) { 19 | rej(data.err) 20 | return 21 | } 22 | res(data.data) 23 | } 24 | }) 25 | const getArrayBuffer = (key, url) => { 26 | return new Promise((resolve, reject) => { 27 | res = resolve 28 | rej = reject 29 | port.postMessage({ 30 | type: 'getArrayBuffer', 31 | key, 32 | url 33 | }) 34 | }) 35 | } 36 | const getURL = (key, url) => getArrayBuffer(key, url).then(data => arrayBuffer2BlobUrl(data)) 37 | const importScript = (key, url) => getURL(key, url).then(url => importScripts(url)) 38 | fetch('https://douyu.coding.me/shared-worker-signer-manifest.json') 39 | .then(res => res.json()) 40 | .then((manifest) => { 41 | console.log('init FlashEmu', manifest) 42 | return importScript('FlashEmu', manifest.flashemu).then(() => manifest) 43 | }).then(manifest => { 44 | console.log('init ok', manifest) 45 | FlashEmu.BUILTIN = 'builtin' 46 | FlashEmu.PLAYERGLOBAL = 'playerglobal' 47 | FlashEmu.setGlobalFlags({ 48 | enableDebug: false, 49 | enableLog: false, 50 | enableWarn: false, 51 | enableError: false 52 | }) 53 | const emu = new FlashEmu({ 54 | readFile (key) { 55 | return getArrayBuffer(key, manifest[key]) 56 | } 57 | }) 58 | emu.runSWF('douyu', false).then(() => { 59 | CModule = emu.getProperty('sample.mp', 'CModule') 60 | xx = emu.getPublicClass('mp') 61 | CModule.callProperty('startAsync') 62 | ready = true 63 | console.log('ready') 64 | }) 65 | inited = true 66 | }).catch(e => { 67 | console.error('onerror', e) 68 | }) 69 | } 70 | 71 | function sign (roomId, time, did) { 72 | console.log('sign', roomId, time, did) 73 | if (!ready) { 74 | throw new Error('flascc is not ready') 75 | } 76 | let StreamSignDataPtr = CModule.callProperty('malloc', 4) 77 | let outptr1 = CModule.callProperty('malloc', 4) 78 | 79 | let datalen = xx.callProperty('sub_2', parseInt(roomId), parseInt(time), did.toString(), outptr1, StreamSignDataPtr) 80 | 81 | let pSign = CModule.callProperty('read32', StreamSignDataPtr) 82 | let sign = CModule.callProperty('readString', pSign, datalen) 83 | let pOut = CModule.callProperty('read32', outptr1) 84 | let out = CModule.callProperty('readString', pOut, 4) 85 | CModule.callProperty('free', StreamSignDataPtr) 86 | CModule.callProperty('free', outptr1) 87 | console.log('sign result', sign) 88 | return { 89 | sign, 90 | cptl: out 91 | } 92 | } 93 | onconnect = ({ports}) => { 94 | console.log('onconnect') 95 | for (let port of ports) { 96 | init(port) 97 | port.addEventListener('message', ({data}) => { 98 | let ret = { 99 | type: 'error', 100 | data: null 101 | } 102 | if (data.type === 'sign') { 103 | if (ready) { 104 | ret.data = sign(...data.args) 105 | } 106 | } else if (data.type === 'query') { 107 | ret.data = ready 108 | } else { 109 | return 110 | } 111 | if (ret.data !== null) { 112 | ret.type = data.type 113 | } 114 | port.postMessage(ret) 115 | }) 116 | port.start() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/sharedWorker/background.js: -------------------------------------------------------------------------------- 1 | let worker = new SharedWorker(chrome.runtime.getURL('src/sharedWorker/sharedWorker.js')) 2 | worker.port.start() 3 | worker.port.onmessage = function(event) { 4 | console.log('Received message', event.data) 5 | window.lastMessage = event.data 6 | let a = new Uint8Array([1,2,3,4]) 7 | worker.port.postMessage({ 8 | msg: 'test', 9 | data: a.buffer 10 | }, [a.buffer]) 11 | } 12 | -------------------------------------------------------------------------------- /src/sharedWorker/launcher.js: -------------------------------------------------------------------------------- 1 | var worker = new SharedWorker(chrome.runtime.getURL('src/sharedWorker/sharedWorker.js')); 2 | worker.port.start(); 3 | worker.port.onmessage = function(event) { 4 | console.log('Received message', event.data); 5 | window.lastMessage = event.data; 6 | }; 7 | worker.port.postMessage('Hello'); -------------------------------------------------------------------------------- /src/sharedWorker/sharedWorker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/sharedWorker/sharedWorker.js: -------------------------------------------------------------------------------- 1 | var ports = []; 2 | onconnect = function(event) { 3 | var port = event.ports[0]; 4 | ports.push(port); 5 | port.start(); 6 | port.onmessage = function(event) { 7 | for (var i = 0; i < ports.length; ++i) { 8 | if (ports[i] != port) { 9 | ports[i].postMessage(event.data); 10 | } 11 | } 12 | }; 13 | }; -------------------------------------------------------------------------------- /src/source.ts: -------------------------------------------------------------------------------- 1 | export class BaseSource { 2 | private _url: string 3 | onChange: (url: string) => void = () => null 4 | set url (v) { 5 | if (v === this._url) { 6 | this._url = v 7 | return 8 | } 9 | this.onChange(v) 10 | } 11 | get url () { 12 | return this._url 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | function utf8ToUtf16 (utf8_bytes: number[]) { 2 | let unicode_codes: number[] = []; 3 | let unicode_code = 0; 4 | let num_followed = 0; 5 | for (let i = 0; i < utf8_bytes.length; ++i) { 6 | let utf8_byte = utf8_bytes[i]; 7 | if (utf8_byte >= 0x100) { 8 | // Malformed utf8 byte ignored. 9 | } else if ((utf8_byte & 0xC0) == 0x80) { 10 | if (num_followed > 0) { 11 | unicode_code = (unicode_code << 6) | (utf8_byte & 0x3f); 12 | num_followed -= 1; 13 | } else { 14 | // Malformed UTF-8 sequence ignored. 15 | } 16 | } else { 17 | if (num_followed == 0) { 18 | unicode_codes.push(unicode_code); 19 | } else { 20 | // Malformed UTF-8 sequence ignored. 21 | } 22 | if (utf8_byte < 0x80){ // 1-byte 23 | unicode_code = utf8_byte; 24 | num_followed = 0; 25 | } else if ((utf8_byte & 0xE0) == 0xC0) { // 2-byte 26 | unicode_code = utf8_byte & 0x1f; 27 | num_followed = 1; 28 | } else if ((utf8_byte & 0xF0) == 0xE0) { // 3-byte 29 | unicode_code = utf8_byte & 0x0f; 30 | num_followed = 2; 31 | } else if ((utf8_byte & 0xF8) == 0xF0) { // 4-byte 32 | unicode_code = utf8_byte & 0x07; 33 | num_followed = 3; 34 | } else { 35 | // Malformed UTF-8 sequence ignored. 36 | } 37 | } 38 | } 39 | if (num_followed == 0) { 40 | unicode_codes.push(unicode_code); 41 | } else { 42 | // Malformed UTF-8 sequence ignored. 43 | } 44 | unicode_codes.shift(); // Trim the first element. 45 | 46 | let utf16_codes = []; 47 | for (var i = 0; i < unicode_codes.length; ++i) { 48 | unicode_code = unicode_codes[i]; 49 | if (unicode_code < (1 << 16)) { 50 | utf16_codes.push(unicode_code); 51 | } else { 52 | var first = ((unicode_code - (1 << 16)) / (1 << 10)) + 0xD800; 53 | var second = (unicode_code % (1 << 10)) + 0xDC00; 54 | utf16_codes.push(first) 55 | utf16_codes.push(second) 56 | } 57 | } 58 | return utf16_codes; 59 | } 60 | 61 | export function utf8_to_ascii( str: string ) { 62 | // return unescape(encodeURIComponent(str)) 63 | const char2bytes = (unicode_code: number) => { 64 | let utf8_bytes: number[] = []; 65 | if (unicode_code < 0x80) { // 1-byte 66 | utf8_bytes.push(unicode_code); 67 | } else if (unicode_code < (1 << 11)) { // 2-byte 68 | utf8_bytes.push((unicode_code >>> 6) | 0xC0); 69 | utf8_bytes.push((unicode_code & 0x3F) | 0x80); 70 | } else if (unicode_code < (1 << 16)) { // 3-byte 71 | utf8_bytes.push((unicode_code >>> 12) | 0xE0); 72 | utf8_bytes.push(((unicode_code >> 6) & 0x3f) | 0x80); 73 | utf8_bytes.push((unicode_code & 0x3F) | 0x80); 74 | } else if (unicode_code < (1 << 21)) { // 4-byte 75 | utf8_bytes.push((unicode_code >>> 18) | 0xF0); 76 | utf8_bytes.push(((unicode_code >> 12) & 0x3F) | 0x80); 77 | utf8_bytes.push(((unicode_code >> 6) & 0x3F) | 0x80); 78 | utf8_bytes.push((unicode_code & 0x3F) | 0x80); 79 | } 80 | return utf8_bytes; 81 | } 82 | let o: number[] = [] 83 | for (let i = 0; i < str.length; i++) { 84 | o = o.concat(char2bytes(str.charCodeAt(i))) 85 | } 86 | return o.map(i => String.fromCharCode(i)).join('') 87 | } 88 | export function ascii_to_utf8( str: string ) { 89 | let bytes = str.split('').map(i => i.charCodeAt(0)) 90 | return utf8ToUtf16(bytes).map(i => String.fromCharCode(i)).join('') 91 | } 92 | export function requestFullScreen () { 93 | let de: any = document.documentElement; 94 | if (de.requestFullscreen) { 95 | de.requestFullscreen(); 96 | } else if (de.mozRequestFullScreen) { 97 | de.mozRequestFullScreen(); 98 | } else if (de.webkitRequestFullScreen) { 99 | de.webkitRequestFullScreen(); 100 | } 101 | } 102 | export function exitFullscreen () { 103 | let de: any = document; 104 | if (de.exitFullscreen) { 105 | de.exitFullscreen(); 106 | } else if (de.mozCancelFullScreen) { 107 | de.mozCancelFullScreen(); 108 | } else if (de.webkitCancelFullScreen) { 109 | de.webkitCancelFullScreen(); 110 | } 111 | } 112 | export class LocalStorage { 113 | constructor (private domain: string) { 114 | 115 | } 116 | getItem (key: string, def?: string) { 117 | return window.localStorage.getItem(`${this.domain}-${key}`) || def 118 | } 119 | setItem (key: string, data: string) { 120 | window.localStorage.setItem(`${this.domain}-${key}`, data) 121 | } 122 | } 123 | export class Timer { 124 | private id: number 125 | onTimer: () => void 126 | constructor (private delay: number) { 127 | 128 | } 129 | reset () { 130 | if (this.id) { 131 | clearTimeout(this.id) 132 | } 133 | this.id = window.setTimeout(this.onTimer, this.delay) 134 | } 135 | } 136 | export function getURL (src: string) { 137 | if (src.substr(0, 5) !== 'blob:') { 138 | src = chrome.runtime.getURL(src) 139 | } 140 | return src 141 | } 142 | export function addScript (src: string) { 143 | var script = document.createElement('script') 144 | // blob: 145 | script.src = getURL(src) 146 | document.head.appendChild(script) 147 | } 148 | export function addCss (src: string, rel: string = 'stylesheet', type: string = 'text/css') { 149 | var link = document.createElement('link') 150 | link.rel = rel 151 | link.type = type 152 | link.href = getURL(src) 153 | document.head.appendChild(link) 154 | } 155 | export function createBlobURL (content: string, type: string) { 156 | var blob = new Blob([content], { type }) 157 | return URL.createObjectURL(blob) 158 | } 159 | 160 | export const p32 = (i: number) => [i, i / 256, i / 65536, i / 16777216].map(i => String.fromCharCode(Math.floor(i) % 256)).join('') 161 | export const u32 = (s: string) => s.split('').map(i => i.charCodeAt(0)).reduce((a, b) => b * 256 + a) 162 | 163 | // --------------------- 164 | 165 | let messageMap: any = {} 166 | export function onMessage (type: string, cb: (data: any) => any) { 167 | messageMap[type] = cb 168 | } 169 | export function postMessage (type: string, data: any) { 170 | window.postMessage({ 171 | type: type, 172 | data: data 173 | }, "*") 174 | } 175 | type msgCallBack = (result: any) => void 176 | let msgCallbacks: msgCallBack[] = [] 177 | let lastCbId = 0 178 | export function sendMessage (type: string, data: any) { 179 | return new Promise((res, rej) => { 180 | let curId = ++lastCbId 181 | let timeoutId = window.setTimeout(() => { 182 | delete msgCallbacks[curId] 183 | rej(new Error('sendMessage timeout')) 184 | }, 5000) 185 | msgCallbacks[curId] = (result) => { 186 | delete msgCallbacks[curId] 187 | window.clearTimeout(timeoutId) 188 | res(result) 189 | } 190 | window.postMessage({ 191 | type: type, 192 | data: data, 193 | cbId: curId++ 194 | }, '*') 195 | }) 196 | } 197 | window.addEventListener('message', async event => { 198 | const data = event.data 199 | if (data.cb) { 200 | let cb = msgCallbacks[data.cbId] 201 | if (cb && (typeof cb === 'function')) { 202 | cb(data.cbResult) 203 | } 204 | } else if (data.type) { 205 | let result: any = undefined 206 | if (typeof messageMap[data.type] === 'function') { 207 | result = messageMap[data.type](data.data) 208 | if (result instanceof Promise) { 209 | result = await result 210 | } 211 | if (data.cbId) { 212 | window.postMessage({ 213 | cb: true, 214 | cbId: data.cbId, 215 | cbResult: result 216 | }, '*') 217 | } 218 | } 219 | } 220 | }, false) 221 | export async function retry (promise: () => Promise, times: number) { 222 | let err = [] 223 | for (let i = 0; i < times; i++) { 224 | try { 225 | return await promise() 226 | } catch (e) { 227 | err.push(e) 228 | } 229 | } 230 | throw err 231 | } 232 | export function getSync () { 233 | return new Promise((res, rej) => { 234 | if (chrome && chrome.storage && chrome.storage.sync) { 235 | chrome.storage.sync.get(items => { 236 | res(items) 237 | }) 238 | } else { 239 | rej(new Error('不支持的存储方式')) 240 | } 241 | }) 242 | } 243 | export function setSync (item: any) { 244 | return new Promise((res, rej) => { 245 | if (chrome && chrome.storage && chrome.storage.sync) { 246 | chrome.storage.sync.set(item, res) 247 | } else { 248 | rej(new Error('不支持的存储方式')) 249 | } 250 | }) 251 | } 252 | interface Setting { 253 | blacklist?: string[] 254 | } 255 | export async function getSetting (): Promise { 256 | let setting: Setting 257 | try { 258 | setting = await getSync() 259 | } catch (e) { 260 | } 261 | if (!setting) { 262 | setting = {} 263 | } 264 | if (!setting.blacklist) { 265 | setting.blacklist = [] 266 | } 267 | return setting 268 | } 269 | export async function setSetting (setting: Setting) { 270 | await setSync(setting) 271 | } 272 | const defaultBgListener = async (request: any): Promise => null 273 | let bgListener = defaultBgListener 274 | export function setBgListener (listener: typeof defaultBgListener) { 275 | if (bgListener === defaultBgListener) { 276 | if ((typeof chrome !== 'undefined') && chrome.runtime && chrome.runtime.onMessage) { 277 | chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { 278 | sendResponse(await bgListener(request)) 279 | }) 280 | } 281 | } else { 282 | console.warn('多次设置BgListener') 283 | } 284 | bgListener = listener 285 | } 286 | export class DelayNotify { 287 | private notified = false 288 | private value: T 289 | private tmid: number = null 290 | private res: (value: T) => void = null 291 | constructor (private defaultValue: T) { 292 | } 293 | notify (value: T) { 294 | if (this.notified) { 295 | return 296 | } 297 | this.notified = true 298 | this.value = value 299 | if (this.res) { 300 | this.res(this.value) 301 | } 302 | } 303 | wait (timeout = 1000 * 10) { 304 | if (this.notified) { 305 | return Promise.resolve(this.value) 306 | } 307 | return new Promise((resolve, reject) => { 308 | if (timeout > 0) { 309 | window.setTimeout(() => { 310 | resolve(this.defaultValue) 311 | }, timeout) 312 | } 313 | this.res = (value: T) => { 314 | return resolve(value) 315 | } 316 | }) 317 | } 318 | reset () { 319 | this.notified = false 320 | } 321 | } 322 | export class CountByTime { 323 | private list: number[] = [] 324 | /** 325 | * 326 | * @param remember in ms 327 | */ 328 | constructor (private remember: number) { 329 | } 330 | add () { 331 | const now = this.time() 332 | this.list.push(now) 333 | this.list = this.list.filter(i => now - i < this.remember) 334 | } 335 | count (before: number = this.remember) { 336 | if (before > this.remember) { 337 | throw new Error('before should < remember') 338 | } 339 | const now = this.time() 340 | return this.list.filter(i => now - i < before).length 341 | } 342 | private time () { 343 | return +new Date() 344 | } 345 | } 346 | /** 347 | * [from, to) 348 | * @param from 349 | * @param to 350 | */ 351 | export function randInt (from: number, to: number) { 352 | return Math.floor(Math.random() * (to - from) + from) 353 | } 354 | export function delay (time: number): Promise { 355 | return new Promise(res => setTimeout(res, time)) 356 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "es6"], 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "removeComments": true, 8 | "preserveConstEnums": true, 9 | "outDir": "./dist/ts/", 10 | "sourceMap": true, 11 | "target": "es6", 12 | "experimentalDecorators": true, 13 | "allowJs": true, 14 | "importHelpers": true 15 | }, 16 | "files": [ 17 | "src/douyu/inject.ts", 18 | "src/douyu/contentScript.ts" 19 | ], 20 | "include": [ 21 | "typings/**/*.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /typings/flv.js.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Bilibili. All Rights Reserved. 3 | * 4 | * @author zheng qian 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // flv.js TypeScript definition file 20 | 21 | declare module 'flv.js' { 22 | 23 | export interface MediaSegment { 24 | duration: number, 25 | filesize?: number, 26 | url: string 27 | } 28 | 29 | export interface MediaDataSource { 30 | type: string, 31 | isLive?: boolean, 32 | cors?: boolean, 33 | withCredentials?: boolean, 34 | 35 | hasAudio?: boolean, 36 | hasVideo?: boolean, 37 | 38 | duration?: number; 39 | filesize?: number; 40 | url?: string; 41 | 42 | segments?: Array 43 | } 44 | 45 | export interface Config { 46 | enableWorker?: boolean, 47 | enableStashBuffer?: boolean, 48 | stashInitialSize?: number, 49 | 50 | isLive?: boolean, 51 | 52 | lazyLoad?: boolean, 53 | lazyLoadMaxDuration?: number, 54 | lazyLoadRecoverDuration?: number, 55 | deferLoadAfterSourceOpen?: boolean, 56 | 57 | autoCleanupSourceBuffer?: boolean, 58 | autoCleanupMaxBackwardDuration?: number, 59 | autoCleanupMinBackwardDuration?: number, 60 | 61 | statisticsInfoReportInterval?: number, 62 | 63 | fixAudioTimestampGap?: boolean, 64 | 65 | accurateSeek?: boolean, 66 | seekType?: string, // [range, param, custom] 67 | seekParamStart?: string, 68 | seekParamEnd?: string, 69 | rangeLoadZeroStart?: boolean, 70 | customSeekHandler?: any, 71 | reuseRedirectedURL?: boolean, 72 | referrerPolicy?: string 73 | } 74 | 75 | export interface FeatureList { 76 | mseFlvPlayback: boolean, 77 | mseLiveFlvPlayback: boolean, 78 | networkStreamIO: boolean, 79 | networkLoaderName: string, 80 | nativeMP4H264Playback: boolean, 81 | nativeWebmVP8Playback: boolean, 82 | nativeWebmVP9Playback: boolean 83 | } 84 | 85 | export interface PlayerConstructor { 86 | new (mediaDataSource: MediaDataSource, config?: Config): Player; 87 | } 88 | 89 | export interface Player { 90 | constructor: PlayerConstructor; 91 | destroy(): void; 92 | on(event: string, listener: Function): void; 93 | off(event: string, listener: Function): void; 94 | attachMediaElement(mediaElement: HTMLMediaElement): void; 95 | detachMediaElement(): void; 96 | load(): void; 97 | unload(): void; 98 | play(): Promise; 99 | pause(): void; 100 | type: string; 101 | buffered: TimeRanges; 102 | duration: number; 103 | volume: number; 104 | muted: boolean; 105 | currentTime: number; 106 | mediaInfo: Object; 107 | statisticsInfo: Object; 108 | } 109 | 110 | export interface FlvPlayer extends Player { 111 | } 112 | 113 | export interface NativePlayer extends Player { 114 | } 115 | 116 | export interface LoggingControl { 117 | forceGlobalTag: boolean; 118 | globalTag: string; 119 | enableAll: boolean; 120 | enableDebug: boolean; 121 | enableVerbose: boolean; 122 | enableInfo: boolean; 123 | enableWarn: boolean; 124 | enableError: boolean; 125 | getConfig(): Object; 126 | applyConfig(config: Object): void; 127 | addLogListener(listener: Function): void; 128 | removeLogListener(listener: Function): void; 129 | } 130 | 131 | export interface Events { 132 | ERROR: string, 133 | LOADING_COMPLETE: string, 134 | RECOVERED_EARLY_EOF: string, 135 | MEDIA_INFO: string, 136 | STATISTICS_INFO: string 137 | } 138 | 139 | export interface ErrorTypes { 140 | NETWORK_ERROR: string, 141 | MEDIA_ERROR: string, 142 | OTHER_ERROR: string 143 | } 144 | 145 | export interface ErrorDetails { 146 | NETWORK_EXCEPTION: string, 147 | NETWORK_STATUS_CODE_INVALID: string, 148 | NETWORK_TIMEOUT: string, 149 | NETWORK_UNRECOVERABLE_EARLY_EOF: string, 150 | 151 | MEDIA_MSE_ERROR: string, 152 | 153 | MEDIA_FORMAT_ERROR: string, 154 | MEDIA_FORMAT_UNSUPPORTED: string, 155 | MEDIA_CODEC_UNSUPPORTED: string 156 | } 157 | const flvjs: { 158 | createPlayer(mediaDataSource: MediaDataSource, config?: Config): Player, 159 | isSupported(): boolean, 160 | getFeatureList(): FeatureList, 161 | 162 | Events: Events, 163 | ErrorTypes: ErrorTypes, 164 | ErrorDetails: ErrorDetails, 165 | 166 | FlvPlayer: PlayerConstructor, 167 | NativePlayer: PlayerConstructor, 168 | LoggingControl: LoggingControl 169 | }; 170 | export default flvjs; 171 | } -------------------------------------------------------------------------------- /typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare let USERSCRIPT: boolean 2 | declare let DEBUG: boolean 3 | --------------------------------------------------------------------------------