├── src
├── core
│ ├── hook.ts
│ ├── proxy.ts
│ ├── detector.ts
│ ├── view
│ │ └── panel
│ │ │ ├── panel.css
│ │ │ ├── ViewPanel
│ │ │ ├── lib
│ │ │ │ ├── recordScreen.ts
│ │ │ │ ├── shutdown.ts
│ │ │ │ ├── showFdAndId.ts
│ │ │ │ ├── useConsole.ts
│ │ │ │ ├── watchSheetPageReload.ts
│ │ │ │ ├── showRecordsList.ts
│ │ │ │ ├── emptyDom.ts
│ │ │ │ ├── handleSheetSearchTree.ts
│ │ │ │ └── showTips.ts
│ │ │ ├── component
│ │ │ │ ├── ReloadTipModel.tsx
│ │ │ │ ├── TableSearchTree.tsx
│ │ │ │ └── SheetSearchTree.tsx
│ │ │ └── index.tsx
│ │ │ ├── ModelPanel
│ │ │ ├── lib
│ │ │ │ ├── showNetworkSpeed.ts
│ │ │ │ └── showFpsAndMemory.ts
│ │ │ └── index.tsx
│ │ │ ├── PastePanel
│ │ │ ├── component
│ │ │ │ └── PasteList.tsx
│ │ │ └── index.tsx
│ │ │ ├── DebugPanel
│ │ │ └── index.tsx
│ │ │ └── panel.tsx
│ ├── backend.ts
│ ├── popup.ts
│ ├── inject.ts
│ ├── store
│ │ └── index.ts
│ ├── content.ts
│ ├── devtool.ts
│ ├── panel.ts
│ └── background.ts
├── public
│ ├── page
│ │ ├── index.html
│ │ ├── devtool.html
│ │ ├── panel.html
│ │ ├── background.html
│ │ └── popup.html
│ └── logo
│ │ ├── logo.jpeg
│ │ └── logo-128.png
├── screenshot
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ └── 6.png
├── __tests__
│ ├── sum.ts
│ ├── menu.html
│ ├── paste.html
│ ├── alpha.html
│ ├── spaceDom.html
│ ├── record.html
│ ├── expend.html
│ ├── expend2.html
│ └── MediaStreamRecorder.js
└── manifest.json
├── .gitignore
├── dist.crx
├── .gitattributes
├── .eslintignore
├── .travis.yml
├── jest.config.js
├── webpack
├── webpack.prod.js
├── webpack.dev.js
└── webpack.common.js
├── tsconfig.json
├── .eslintrc.js
├── README.md
├── package.json
├── .prettierrc.js
└── dist.pem
/src/core/hook.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/core/proxy.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/core/detector.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/core/view/panel/panel.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/public/page/index.html:
--------------------------------------------------------------------------------
1 | 24asd123
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/recordScreen.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/core/backend.ts:
--------------------------------------------------------------------------------
1 | window.postMessage({"test": '你好!'}, '*');
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules
3 | dist
4 | tmp
5 | .vscode
--------------------------------------------------------------------------------
/dist.crx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/dist.crx
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .cache
2 | .steamer
3 | config
4 | dev
5 | node_modules
6 | package.json
7 |
--------------------------------------------------------------------------------
/src/screenshot/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/src/screenshot/1.png
--------------------------------------------------------------------------------
/src/screenshot/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/src/screenshot/2.png
--------------------------------------------------------------------------------
/src/screenshot/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/src/screenshot/3.png
--------------------------------------------------------------------------------
/src/screenshot/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/src/screenshot/4.png
--------------------------------------------------------------------------------
/src/screenshot/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/src/screenshot/5.png
--------------------------------------------------------------------------------
/src/screenshot/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/src/screenshot/6.png
--------------------------------------------------------------------------------
/src/public/logo/logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/src/public/logo/logo.jpeg
--------------------------------------------------------------------------------
/src/public/logo/logo-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wscats/chrome-devtools-cli/main/src/public/logo/logo-128.png
--------------------------------------------------------------------------------
/src/public/page/devtool.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | cache:
5 | directories:
6 | - "node_modules"
7 | script:
8 | - npm build
9 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "roots": [
3 | "src"
4 | ],
5 | "transform": {
6 | "^.+\\.ts$": "ts-jest"
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/__tests__/sum.ts:
--------------------------------------------------------------------------------
1 | test('1 + 1 = 2', () => {
2 | expect(2).toBe(2);
3 | });
4 |
5 | test('1 + 2 != 2', () => {
6 | expect(2).not.toBe(2);
7 | });
8 |
--------------------------------------------------------------------------------
/src/core/popup.ts:
--------------------------------------------------------------------------------
1 | (() => {
2 | const status = document.querySelector('.status');
3 | // status.innerHTML = window.location.host === 'docs.qq.com' ? '✅' : '❌';
4 | })();
5 |
--------------------------------------------------------------------------------
/webpack/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'production'
6 | });
--------------------------------------------------------------------------------
/src/__tests__/menu.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | devtool: 'inline-source-map',
6 | mode: 'development'
7 | });
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/shutdown.ts:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.postMessage(
3 | {
4 | source: 'tencent-docs-devtools-devtool-script',
5 | payload: 'shutdown'
6 | },
7 | '*'
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/showFdAndId.ts:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.postMessage(
3 | {
4 | source: 'tencent-docs-devtools-inject',
5 | payload: {
6 | cookie: window.document.cookie
7 | }
8 | },
9 | '*'
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/src/core/inject.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | T_SHEET: any;
3 | animationFrameID: any;
4 | // getrangeSelections: any;
5 | SpreadsheetApp: any;
6 | app: any;
7 | stream: any;
8 | // 录屏
9 | recordScreen: any;
10 | __console: any;
11 | // 粘贴板定时器
12 | pasteRecord: any;
13 | networkSpeedTimer: any;
14 | }
15 |
--------------------------------------------------------------------------------
/src/public/page/panel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Panel
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/useConsole.ts:
--------------------------------------------------------------------------------
1 | export default () => {
2 | // 设置 cookie 的方法
3 | const setCookie = (name, value) => {
4 | let Days = 30;
5 | let exp = new Date();
6 | exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
7 | // @ts-ignore
8 | document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
9 | debugger;
10 | }
11 |
12 | setCookie('useConsole', true);
13 | // @ts-ignore
14 | console = __console;
15 | };
16 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/watchSheetPageReload.ts:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.onbeforeunload = function(event) {
3 | window.postMessage(
4 | {
5 | source: 'tencent-docs-devtools-inject',
6 | payload: 'sheet-reload'
7 | },
8 | '*'
9 | );
10 | event = event || window.event;
11 | if (event) {
12 | event.returnValue = '确定要关闭窗口吗?';
13 | }
14 | return '确定要关闭窗口吗?';
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "noImplicitAny": false,
6 | "sourceMap": false,
7 | "rootDir": "src",
8 | "outDir": "dist/core",
9 | "noEmitOnError": true,
10 | "typeRoots": [
11 | "node_modules/@types"
12 | ],
13 | // 给错误和消息设置样式,使用颜色和上下文
14 | "pretty": true,
15 | // 兼容 babel 模块导入的模式(不需要 * as)
16 | "esModuleInterop": true,
17 | // 支持引入 json 模块
18 | "resolveJsonModule": true,
19 | // React 项目需要配置此项
20 | "jsx": "react",
21 | }
22 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'alloy',
4 | 'alloy/typescript',
5 | ],
6 | env: {
7 | // 您的环境变量(包含多个预定义的全局变量)
8 | // Your environments (which contains several predefined global variables)
9 | //
10 | // browser: true,
11 | // node: true,
12 | // mocha: true,
13 | // jest: true,
14 | // jquery: true
15 | },
16 | globals: {
17 | // 您的全局变量(设置为 false 表示它不允许被重新赋值)
18 | // Your global variables (setting to false means it's not allowed to be reassigned)
19 | //
20 | // myGlobal: false
21 | },
22 | rules: {
23 | // 自定义您的规则
24 | // Customize your rules
25 | }
26 | };
--------------------------------------------------------------------------------
/src/core/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | const counter = (
3 | state = {
4 | rangeSelections: [],
5 | skill: ['ps', 'ts']
6 | },
7 | action
8 | ) => {
9 | switch (action.type) {
10 | case 'SET_RANGE_SELECTIONS':
11 | return {
12 | ...state,
13 | rangeSelections: action.rangeSelections
14 | };
15 | case 'SET_FD_AND_ROUTE_ID':
16 | return {
17 | ...state,
18 | fd: action.fd,
19 | routeId: action.routeId
20 | };
21 | default:
22 | return state;
23 | }
24 | };
25 | let store = createStore(counter);
26 | export default store;
27 |
--------------------------------------------------------------------------------
/src/core/content.ts:
--------------------------------------------------------------------------------
1 | const handshake = (e) => {
2 | // 断开 inject.ts 和 content.ts 的通信
3 | if (e.data.payload === 'shutdown') {
4 | window.removeEventListener('message', handshake);
5 | }
6 | try {
7 | chrome.runtime.sendMessage(e.data, function(response) {
8 | console.log('收到来自后台的回复:' + response);
9 | });
10 | } catch (error) {
11 | console.log(error);
12 | }
13 | };
14 |
15 | // inject.ts 和 content.ts 的通信
16 | window.addEventListener('message', handshake);
17 |
18 | const sendListening = () => {
19 | window.postMessage(
20 | {
21 | source: 'tencent-docs-devtools-content-script',
22 | payload: 'listening'
23 | },
24 | '*'
25 | );
26 | };
27 | sendListening();
28 |
--------------------------------------------------------------------------------
/src/core/view/panel/ModelPanel/lib/showNetworkSpeed.ts:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.networkSpeedTimer = setInterval(() => {
3 | let startTime = Date.now();
4 | let xhr = new XMLHttpRequest();
5 | xhr.open(
6 | 'GET',
7 | 'https://docs.qq.com/static/dev/sheet_static/img/brandlogo/logo-sprites-new.png'
8 | );
9 | xhr.onload = function () {
10 | let duration = (Date.now() - startTime) / 1000;
11 | // @ts-ignore
12 | let size = xhr.getResponseHeader('Content-Length') / 1024;
13 | let speed = (size / duration).toFixed(2);
14 | window.postMessage({ networkSpeed: speed }, '*');
15 | console.log(speed);
16 | };
17 | xhr.send();
18 | }, 1000);
19 | }
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/showRecordsList.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | export default () => {
3 | window.__console.log('-----start ajax-----');
4 | $.ajax({
5 | url: '/dop-api/get/history',
6 | data: {
7 | padId: window.clientVars.globalPadId,
8 | page: 1,
9 | _r: 10000 * Math.random()
10 | },
11 | success(data) {
12 | window.__console.log(data.history.items);
13 | window.postMessage(
14 | {
15 | source: 'tencent-docs-devtools-inject',
16 | payload: {
17 | history: data
18 | }
19 | },
20 | '*'
21 | );
22 | window.__console.log('-----end ajax-----');
23 | }
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/src/public/page/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 | 背景页
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/component/ReloadTipModel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal } from 'antd';
3 | export default class ReloadTipModel extends React.Component {
4 | state = {
5 | visible: true
6 | };
7 |
8 | handleOk = (e) => {
9 | this.setState({
10 | visible: false
11 | });
12 | window.location.reload();
13 | };
14 |
15 | handleCancel = (e) => {
16 | this.setState({
17 | visible: false
18 | });
19 | };
20 |
21 | render() {
22 | return (
23 |
31 | 如果刷新了表格页面,请重新刷新当前调试面板更新数据。
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 开发
2 |
3 | ```sh
4 | # 下载项目
5 | git clone https://git.code.oa.com/tencent-doc/tencent-docs-devtools
6 | # 进入项目目录
7 | cd tencent-docs-devtools
8 | # 安装依赖
9 | npm install
10 | # 调试
11 | npm run watch
12 | # 编译打包
13 | npm run build
14 | ```
15 |
16 | # 安装
17 |
18 | 将下载好的 tencent-docs-extension.zip 压缩文件解压,得到一个文件夹,里面内容如下。
19 |
20 | - tencent-docs-extension文件夹目录
21 | - dist
22 | - manifest.json
23 | - src
24 | - README.md
25 |
26 |
27 |
28 | 然后打开 Chrome 浏览器,点击右上角三个点,点击`更多工具`,选择`扩展程序`进入插件管理界面。
29 |
30 |
31 |
32 | 打开右上角的`开发者模式`,会在左边出现点击`加载已解压的扩展程序`,选取解压后的文件夹,并点击`选择`。
33 |
34 |
35 |
36 | 如果出现面板出现`Tencent Docs Dev Tools`则证明安装成功。
37 |
38 |
39 |
40 | 然后打开你的需要调试的[文档](https://docs.qq.com/desktop)进行测试。
41 |
42 | 现在的界面会比较粗糙,比如当你打开文档后会在右下角出现一个红色背景的弹窗,里面会有错误弹窗的测试按钮,点击可运行测试,后续会变更到调试控制台里面。
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/emptyDom.ts:
--------------------------------------------------------------------------------
1 | export default () => {
2 | // 查找空节点
3 | function traverseNodes(node) {
4 | // 判断是否是元素节点
5 | if (node.nodeType === 1) {
6 | // 判断该元素节点是否有子节点
7 | if (node.childNodes.length) {
8 | // 得到所有的子节点
9 | let sonnodes = node.childNodes;
10 | // 遍历所有的子节点
11 | for (let i = 0; i < sonnodes.length; i++) {
12 | // 找出有空格字符的节点
13 | if (!node.innerText && !node.childNodes.length) {
14 | window.__console.log(node);
15 | }
16 | // 得到具体的某个子节点
17 | let sonnode = sonnodes.item(i);
18 | // 递归遍历
19 | traverseNodes(sonnode);
20 | }
21 | } else {
22 | window.__console.log(node);
23 | }
24 | }
25 | }
26 | traverseNodes(document.body);
27 | };
28 |
--------------------------------------------------------------------------------
/src/public/page/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
34 |
35 |
36 |
37 | This extension is using in tencent documents applications.
38 | ✅
39 |
40 | Open the developer tools, and the Tencent Docs Dev Tools tab will appear to the right.😁
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/__tests__/paste.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/core/view/panel/ModelPanel/lib/showFpsAndMemory.ts:
--------------------------------------------------------------------------------
1 | const showFpsAndMemory = () => {
2 | (() => {
3 | let frame = 0;
4 | let lastTime = Date.now();
5 | const getMemoryInfo = () => {
6 | // @ts-ignore
7 | if (!window.performance || !(window.performance).memory) {
8 | return 0;
9 | }
10 | // @ts-ignore
11 | return Math.round((window.performance).memory.usedJSHeapSize / (1024 * 1024));
12 | };
13 | const loop = () => {
14 | const now = Date.now();
15 | // 每运行一次, frame++
16 | frame += 1;
17 | // 1s更新一次面板
18 | if (now > 1000 + lastTime) {
19 | const fps = Math.round((frame * 1000) / (now - lastTime));
20 | frame = 0;
21 | lastTime = now;
22 | window.postMessage({
23 | source: 'tencent-docs-devtools-inject',
24 | payload: {
25 | memory: getMemoryInfo(),
26 | fps
27 | }
28 | }, '*');
29 | }
30 | window.animationFrameID = window.requestAnimationFrame(loop);
31 | };
32 | window.animationFrameID = window.requestAnimationFrame(loop);
33 | })();
34 | }
35 |
36 | export default showFpsAndMemory;
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tencent-docs-devtools",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "watch": "webpack --config webpack/webpack.dev.js --watch",
6 | "build": "webpack --config webpack/webpack.prod.js",
7 | "clean": "rimraf dist",
8 | "test": "npx jest"
9 | },
10 | "author": "enoyao",
11 | "license": "MIT",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://git.code.oa.com/tencent-doc/tencent-docs-devtools"
15 | },
16 | "devDependencies": {
17 | "@types/chrome": "0.0.91",
18 | "@types/jest": "24.0.23",
19 | "@types/jquery": "3.3.31",
20 | "@typescript-eslint/eslint-plugin": "2.14.0",
21 | "@typescript-eslint/parser": "2.14.0",
22 | "copy-webpack-plugin": "5.0.5",
23 | "css-loader": "3.4.0",
24 | "eslint": "6.8.0",
25 | "eslint-config-alloy": "3.5.0",
26 | "jest": "24.9.0",
27 | "prettier": "1.19.1",
28 | "rimraf": "3.0.0",
29 | "style-loader": "1.1.2",
30 | "ts-jest": "24.2.0",
31 | "ts-loader": "6.2.1",
32 | "typescript": "3.7.4",
33 | "webpack": "~4.41.2",
34 | "webpack-cli": "~3.3.10",
35 | "webpack-merge": "~4.2.2"
36 | },
37 | "dependencies": {
38 | "antd": "3.26.5",
39 | "echarts": "4.6.0",
40 | "html2canvas": "1.0.0-rc.5",
41 | "react": "16.12.0",
42 | "react-dom": "16.12.0",
43 | "react-redux": "7.1.3",
44 | "redux": "4.0.5"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | // .prettierrc.js
2 | module.exports = {
3 | // max 100 characters per line
4 | printWidth: 100,
5 | // use 4 spaces for indentation
6 | tabWidth: 4,
7 | // use spaces instead of indentations
8 | useTabs: false,
9 | // semicolon at the end of the line
10 | semi: true,
11 | // use single quotes
12 | singleQuote: true,
13 | // object's key is quoted only when necessary
14 | quoteProps: 'as-needed',
15 | // use double quotes instead of single quotes in jsx
16 | jsxSingleQuote: false,
17 | // no comma at the end
18 | trailingComma: 'none',
19 | // spaces are required at the beginning and end of the braces
20 | bracketSpacing: true,
21 | // end tag of jsx need to wrap
22 | jsxBracketSameLine: false,
23 | // brackets are required for arrow function parameter, even when there is only one parameter
24 | arrowParens: 'always',
25 | // format the entire contents of the file
26 | rangeStart: 0,
27 | rangeEnd: Infinity,
28 | // no need to write the beginning @prettier of the file
29 | requirePragma: false,
30 | // No need to automatically insert @prettier at the beginning of the file
31 | insertPragma: false,
32 | // use default break criteria
33 | proseWrap: 'preserve',
34 | // decide whether to break the html according to the display style
35 | htmlWhitespaceSensitivity: 'css',
36 | // lf for newline
37 | endOfLine: 'lf'
38 | };
--------------------------------------------------------------------------------
/src/__tests__/alpha.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Tencent Docs Dev Tools",
3 | "manifest_version": 2,
4 | "version": "1.00",
5 | "description": "Chrome devtools extension for debugging tencent documents applications.",
6 | "icons": {
7 | "16": "./public/logo/logo-128.png",
8 | "48": "./public/logo/logo-128.png",
9 | "128": "./public/logo/logo-128.png"
10 | },
11 | "content_scripts": [
12 | {
13 | "matches": ["https://docs.qq.com/*"],
14 | "run_at": "document_end",
15 | "js": ["./core/content.js"]
16 | },
17 | {
18 | "matches": [""],
19 | "js": ["./core/hook.js"],
20 | "run_at": "document_start"
21 | },
22 | {
23 | "matches": [""],
24 | "js": ["./core/detector.js"],
25 | "run_at": "document_idle"
26 | }
27 | ],
28 | "devtools_page": "./public/page/devtool.html",
29 | "background": {
30 | "page": "./public/page/background.html",
31 | "persistent": false
32 | },
33 | "permissions": [
34 | "http://*/*",
35 | "https://*/*",
36 | "file:///*",
37 | "tabs",
38 | "contextMenus",
39 | "storage",
40 | "clipboardWrite",
41 | "clipboardRead",
42 | ""
43 | ],
44 | "web_accessible_resources": ["core/*", "public/*"],
45 | "browser_action": {
46 | "default_title": "",
47 | "default_icon": {
48 | "16": "./public/logo/logo-128.png",
49 | "48": "./public/logo/logo-128.png",
50 | "128": "./public/logo/logo-128.png"
51 | },
52 | "default_popup": "./public/page/popup.html"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/dist.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6K88EMMd8ULYE
3 | lidMk3C+Z9KqKSPunEarbyXWf48FHpi7vfIowQqf8MJcRGsee74UF9wDx/u90ry3
4 | LFUcamfgjTQN5i73ITRw6sSd+5OTy62+UA3L3+BlldFdetKqVrp3JCf+WbfhO8qH
5 | jDX2dJVgVGg6jfnEoSnM44z4cPmhMhI91lB6pYiSvDWz8turmrmHZHIhl04Wco7S
6 | /qOfhkpsJ7inMNeLXezBeHA8lL7RFn+lFH81fL9OdcmzrSgxvhXsF+nyPZNNWUHX
7 | OJZSQLOm3Aa2KS/lX2oBxhUIRfoJ4hUAPDko5vUGDL1o96CreekPYEQ4EQt2i/Et
8 | wA7PQ5qRAgMBAAECggEAB/vZ+GGJJIUnkSHJVKjcBGZaa+6wukpcY3y0QDMfNIZ/
9 | 4Uk/iczhgkNmamOKvndrCtyNFGJuotDXQ/m0OrA7fS9BBLmS2QlFAPSocuf3h7aL
10 | 4GX5eX1D18dpf5tFToXCoFpeFrPNRWz1fgOeys9sGoH0KFKvW+XXb5BwmrTpl6aF
11 | mzTdhv8YxrtyWm1s9PUxSSpYhcUv18+4Le+nbT6ysqITm1oZnXbbyazS0w02UDx4
12 | lGhgYSpTrzYb8CM628ZaoPB6qcQCX6+zbY7YrflHP5hosdUOyRFLgy8l1WNDDEFQ
13 | FA5e0KkjSPaQAIevXlt4XTzAr93f/zjaU7VNAQNv4wKBgQD/Cj9T3+IKENmSwX1m
14 | qU9OkDUy0qRDmgo0kac31JhL5TM8XV9BoHJ3lNO2oH3HWm3/u04O237PmOZei7bx
15 | fJGtHm3p/xWRb8gCm5EacEdJhbS75eQXH9q5bZe2srp2KwKNYc0lz9WOiSSCYqKI
16 | bE4s8aE4qP6I15ZnGIcIzQ0jawKBgQC63zNFTfdMPi/2KqJT9Hs3YLJlPE2SNKvB
17 | /Tk8+7IEyDGg6W4H0Oq0wAMXWYB1KxNciXRjjMy3r+5/8m7QMFlqR5L9siZmi1rE
18 | PfOzvESh1zjYejcarEeneNcmrUDIuxaytwg0XTd6yobz7EO00kqAhsJJH3CGQW0t
19 | fMbq7Vv08wKBgQCXq0Jp4NSN4+Nf8Q1gRgPR1tSsOrRRN4QmKxBklVtUlyNXHLgP
20 | URmBn9If42Wpbk/IK12KXGIXlvg49aGADvKbH2OCKp5q9Sze0CEEtgzO3mLS8y4n
21 | ylvKti/pCHT23TERXz4e5HA96bT6jSnXM4FyHOghx+5G9t6MxPtY8oiDrQKBgCp5
22 | cP3fqsX5nI0nQ54xg5AiummoOzlwH1oIZGooC15q4lzgsdG0+qP6wLkd7sSy9cOi
23 | uoGmDyHxKxSJ1gYb9zbg7dgcFf5YzlisfCfuMutWL+WEYeH+Jos6PCzeVEtLuu0E
24 | DSlZxrJyNhpbKUiAQ87S0nYsXzckx9v+8HeVW8FhAoGBAIpRi5hUEvHFNHIYRINp
25 | U6nbjsRowsJMkkxrtL24/3CZBEgugTOlUvBphTVArM3m0z44xTK65Ql6e5FU5YvD
26 | FwpoK6q2IoW1GqacP8MplzL7Lgl4IS5nvnlBiKFGipuwOCondZT/RM9WisXEyZys
27 | AdGvgVNB2jn5U5d1+ypU1r1r
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/webpack/webpack.common.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const CopyPlugin = require('copy-webpack-plugin');
4 | const coreDir = '../src/core/';
5 | const viewDir = '../src/core/view/';
6 | module.exports = {
7 | entry: {
8 | // Chrome Extension 逻辑层
9 | hook: path.join(__dirname, coreDir + 'hook.ts'),
10 | devtool: path.join(__dirname, coreDir + 'devtool.ts'),
11 | background: path.join(__dirname, coreDir + 'background.ts'),
12 | panel: path.join(__dirname, coreDir + 'panel.ts'),
13 | backend: path.join(__dirname, coreDir + 'backend.ts'),
14 | proxy: path.join(__dirname, coreDir + 'proxy.ts'),
15 | detector: path.join(__dirname, coreDir + 'detector.ts'),
16 | content: path.join(__dirname, coreDir + 'content.ts'),
17 | inject: path.join(__dirname, coreDir + 'inject.ts'),
18 | popup: path.join(__dirname, coreDir + 'popup.ts'),
19 | // React 视图层
20 | panelView: path.join(__dirname, viewDir, 'panel/panel.tsx')
21 | },
22 | output: {
23 | path: path.join(__dirname, '../dist/core'),
24 | filename: '[name].js'
25 | },
26 | optimization: {
27 | splitChunks: {
28 | name: 'vendor',
29 | chunks: 'initial'
30 | }
31 | },
32 | module: {
33 | rules: [
34 | {
35 | test: /\.tsx?$/,
36 | use: 'ts-loader',
37 | exclude: /node_modules/
38 | },
39 | {
40 | test: /\.css?$/,
41 | use: ['style-loader', 'css-loader']
42 | }
43 | ]
44 | },
45 | resolve: {
46 | extensions: ['.ts', '.tsx', '.js']
47 | },
48 | plugins: [
49 | new webpack.IgnorePlugin(/screenshot$/),
50 | new CopyPlugin([
51 | { from: '.', to: '../public', context: './src/public' },
52 | { from: './src/manifest.json', to: '../manifest.json' }
53 | ])
54 | ]
55 | };
56 |
--------------------------------------------------------------------------------
/src/__tests__/spaceDom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | | 值1 |
10 | 值2 |
11 | 值3 |
12 |
13 |
14 | | aaa |
15 | bbb |
16 | ccc |
17 |
18 |
19 | | ddd |
20 | eee |
21 | fff |
22 |
23 |
24 | | ggg |
25 | hhh |
26 | kkk |
27 |
28 |
29 | |
30 | ppp |
31 | ooo |
32 | |
33 |
34 |
35 |
36 |
37 |
38 |
64 |
65 |
--------------------------------------------------------------------------------
/src/core/view/panel/PastePanel/component/PasteList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, Typography, Button, PageHeader, Tag } from 'antd';
3 |
4 | interface State {
5 | pasteRecord: string[]
6 | }
7 | interface Props {
8 | isWatchClipboardContents: boolean
9 | }
10 | export default class SheetSearchTree extends React.Component {
11 | constructor(props) {
12 | super(props);
13 | }
14 | state: State = {
15 | pasteRecord: []
16 | }
17 | clearPasteRecord() {
18 | localStorage.removeItem('paste-info');
19 | }
20 | render() {
21 | return (
22 | {this.props.isWatchClipboardContents ? 'Stop' : 'Running'}}
30 | />}
31 | footer={}
32 | bordered
33 | dataSource={this.state.pasteRecord}
34 | renderItem={item => (
35 | item ?
36 |
37 | {item}
38 |
39 | :
40 |
41 | 空数据
42 |
43 | )}
44 | />
45 | );
46 | }
47 | componentDidMount() {
48 | // 设置粘贴板信息到列表数据里面
49 | this.setState({
50 | pasteRecord: localStorage.getItem('paste-info') ? JSON.parse(localStorage.getItem('paste-info')) : []
51 | })
52 | // 监听Storage的改变,更新面板内容
53 | window.addEventListener("storage", (e) => {
54 | this.setState({
55 | pasteRecord: JSON.parse(e.newValue)
56 | })
57 | });
58 | }
59 | }
--------------------------------------------------------------------------------
/src/core/view/panel/PastePanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Button, Input, Statistic, Card, Row, Col } from 'antd';
3 | // 通讯库
4 | import tool from '../../../panel';
5 | // 粘贴板列表
6 | import PasteList from './component/PasteList';
7 | interface State {
8 | isWatchClipboardContents: boolean;
9 | }
10 |
11 | interface Props {}
12 |
13 | export default class ModelPanel extends React.Component {
14 | state: State = {
15 | // 用于判断当前是否监听粘贴板状态状态
16 | isWatchClipboardContents: true
17 | };
18 | componentDidMount() {
19 | // 首次加载启动监听粘贴板的功能
20 | this.startWatchClipboardContents();
21 | }
22 | // 开始监听粘贴板
23 | startWatchClipboardContents() {
24 | this.setState({
25 | isWatchClipboardContents: false
26 | });
27 | tool.injectScriptStringToPage(
28 | `window.postMessage({payload:'startWatchClipboardContents'},'*')`
29 | );
30 | }
31 | // 停止监听粘贴板
32 | stopWatchClipboardContents() {
33 | this.setState({
34 | isWatchClipboardContents: true
35 | });
36 | tool.injectScriptStringToPage(
37 | `window.postMessage({payload:'stopWatchClipboardContents'},'*')`
38 | );
39 | }
40 | render() {
41 | return (
42 |
43 | {this.state.isWatchClipboardContents ? (
44 |
51 | ) : (
52 |
59 | )}
60 |
61 |
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/__tests__/record.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
17 |
18 |
19 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/__tests__/expend.html:
--------------------------------------------------------------------------------
1 |
64 |
--------------------------------------------------------------------------------
/src/core/devtool.ts:
--------------------------------------------------------------------------------
1 | import tool from './panel';
2 | // 面板创建开关
3 | let created = false;
4 | // 检查次数
5 | let checkCount = 0;
6 |
7 | const createPanelIfHasDocs = () => {
8 | if (created || checkCount++ > 10) {
9 | return;
10 | }
11 | // 根据全局变量 window.T_SHEET 判断是否是在腾讯文档页面
12 | chrome.devtools.inspectedWindow.eval('!!(window.T_SHEET)', (hasDocs) => {
13 | if (!hasDocs || created) {
14 | return;
15 | }
16 | clearInterval(checkDocsInterval);
17 | created = true;
18 | createPanels();
19 | });
20 | };
21 | // 在控制台创建调试面板 Tencent Docs Dev Tools
22 | const createPanels = () => {
23 | // alert(created);
24 | // alert(checkCount);
25 | // 创建自定义面板,同一个插件可以创建多个自定义面板
26 | // 几个参数依次为:panel 标题、图标(其实设置了也没地方显示)、要加载的页面、加载成功后的回调
27 | chrome.devtools.panels.create(
28 | 'Tencent Docs Dev Tools',
29 | '../public/logo/logo-128.png',
30 | '../public/page/panel.html',
31 | (panel) => {
32 | let _window;
33 | // 注意这个log一般看不到
34 | console.log('自定义面板创建成功!');
35 | // 监听调试面板显示
36 | panel.onShown.addListener((panelWindow) => {
37 | // alert(2);
38 | console.log(panelWindow);
39 | // 以后可以在这里对面板封装一些公共方法
40 | _window = panelWindow;
41 | // 审查窗口
42 | _window.inspectedWindow = chrome.devtools.inspectedWindow;
43 | });
44 | // 监听调试面板隐藏
45 | panel.onHidden.addListener(() => {
46 | // 断开 content.ts 文件中 inject.ts 和 content.ts 的通信
47 | // tool.injectScriptStringToPage(`
48 | // window.postMessage(
49 | // {
50 | // source: 'tencent-docs-devtools-devtool-script',
51 | // payload: 'shutdown'
52 | // },
53 | // '*'
54 | // );
55 | // `);
56 | });
57 | }
58 | );
59 | };
60 | chrome.devtools.network.onNavigated.addListener(createPanelIfHasDocs);
61 | // 使用定时器每秒检查一次
62 | const checkDocsInterval = setInterval(createPanelIfHasDocs, 1000);
63 | createPanelIfHasDocs();
64 |
--------------------------------------------------------------------------------
/src/core/view/panel/DebugPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Cascader } from 'antd';
3 | import { TreeSelect } from 'antd';
4 | // 通讯库
5 | import tool from '../../../panel';
6 | // 类型接口
7 | interface State {
8 |
9 | }
10 |
11 | export default class ModelPanel extends React.Component {
12 | state = {
13 | value: undefined,
14 | treeData: [
15 | { id: 1, pId: 0, value: '1', title: 'Expand to load' },
16 | { id: 2, pId: 0, value: '2', title: 'Expand to load' },
17 | { id: 3, pId: 0, value: '3', title: 'Tree Node', isLeaf: true },
18 | ],
19 | };
20 |
21 | genTreeNode = (parentId, isLeaf = false) => {
22 | const random = Math.random()
23 | .toString(36)
24 | .substring(2, 6);
25 | return {
26 | id: random,
27 | pId: parentId,
28 | value: random,
29 | title: isLeaf ? 'Tree Node' : 'Expand to load',
30 | isLeaf,
31 | };
32 | };
33 |
34 | onLoadData = treeNode =>
35 | new Promise(resolve => {
36 | console.log(treeNode)
37 | const { id } = treeNode.props;
38 | setTimeout(() => {
39 | const treeData = [
40 | ...this.state.treeData,
41 | this.genTreeNode(id, false),
42 | this.genTreeNode(id, false),
43 | ];
44 | this.setState({
45 | treeData,
46 | });
47 | resolve();
48 | }, 300);
49 | });
50 |
51 | onChange = value => {
52 | console.log(value);
53 | this.setState({ value });
54 | };
55 |
56 | render() {
57 | const { treeData } = this.state;
58 | return (
59 |
69 | );
70 | }
71 | }
--------------------------------------------------------------------------------
/src/core/panel.ts:
--------------------------------------------------------------------------------
1 | class Tool {
2 | public port: any;
3 | public listen: any;
4 | public send: any;
5 | constructor() {
6 | // 初始化,与 background.ts 建立连接
7 | let { port, listen, send } = this.sendMessageToBackground();
8 | this.port = port;
9 | this.listen = listen;
10 | this.send = send;
11 | }
12 | // 1.panel.ts 与 inject.ts 的通信方案,使用链接的方式
13 | injectScriptLinkToPage = (scriptName: string, cb: Function): void => {
14 | const src = `
15 | (function() {
16 | var script = document.constructor.prototype.createElement.call(document, 'script');
17 | script.src = "${chrome.runtime.getURL(scriptName)}";
18 | document.documentElement.appendChild(script);
19 | // script.parentNode.removeChild(script);
20 | })()
21 | `;
22 | chrome.devtools.inspectedWindow.eval(src, (res, err) => {
23 | if (err) {
24 | console.log(err);
25 | }
26 | cb();
27 | });
28 | };
29 | // 2.panel.ts 与 inject.ts 的通信方案,使用字符串的方式
30 | injectScriptStringToPage = (message: string, callback: Function = null) => {
31 | chrome.devtools.inspectedWindow.eval(message, (value) => {
32 | callback && callback(value);
33 | });
34 | };
35 | // 3.panel.ts 与 content.ts 的通信方案
36 | sendMessageToContent = (message: any) => {
37 | window.postMessage(message, '*');
38 | };
39 |
40 | // 4.panel.ts 与 background.ts 的通信方案,注意是一个长连接
41 | // 使用 window.postMessage({name:1}, '*')测试
42 | sendMessageToBackground = () => {
43 | // 建立长连接
44 | const port = chrome.runtime.connect({ name: 'devtools' });
45 | // 监听断开
46 | // 主动关闭使用 port.disconnect();
47 | port.onDisconnect.addListener(() => { });
48 | // 监听 background.ts 消息
49 | // port.onMessage.addListener((message) => {
50 | // console.log(`后台已传来消息:${JSON.stringify(message)}`);
51 | // return true;
52 | // });
53 | // 往 background.ts 发送消息
54 | port.postMessage({
55 | name: 'original',
56 | tabId: chrome.devtools.inspectedWindow.tabId
57 | });
58 |
59 | // 释放 port 用于在外部监听
60 | return {
61 | port,
62 | listen(fn) {
63 | port.onMessage.addListener(fn);
64 | },
65 | send(data) {
66 | port.postMessage(data);
67 | }
68 | };
69 | };
70 | }
71 | // 提供通讯手段的工具方法
72 | export default new Tool();
73 |
--------------------------------------------------------------------------------
/src/__tests__/expend2.html:
--------------------------------------------------------------------------------
1 |
59 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/handleSheetSearchTree.ts:
--------------------------------------------------------------------------------
1 | export default () => {
2 | window.SpreadsheetApp.view.canvas.normalSelectData.emitter.on(window.SpreadsheetApp.view.canvas.normalSelectData.EVENT.MODIFY_SELECTION, (e) => {
3 | // window.__console.log(e);
4 | // try {
5 | // window.clearInterval(window.getrangeSelections);
6 | // window.getrangeSelections = setInterval(() => {
7 | let rangeSelections = [...window.SpreadsheetApp.view.getSelection().rangeSelections];
8 | // 这个方法主要就是把选中表格范围的数据的每一条详情遍历取出来
9 | rangeSelections.map((item) => {
10 | // 行起始位置
11 | let startRowIndex = item.startRowIndex;
12 | // 列起始位置
13 | let startColIndex = item.startColIndex;
14 | // 初始化
15 | item.children = [];
16 | item.title = '';
17 | // 行的长度
18 | let rowLength =
19 | item.endRowIndex - startRowIndex > 0 ? item.endRowIndex - startRowIndex : 0;
20 | // 列的长度
21 | let colLength =
22 | item.endColIndex - startColIndex > 0 ? item.endColIndex - startColIndex : 0;
23 | // 显示 xxx 行 xxx 列
24 | if (item.startRowIndex - item.endRowIndex === 0) {
25 | item.title = `${item.startRowIndex + 1}行`;
26 | } else {
27 | item.title = `${item.startRowIndex + 1}-${item.endRowIndex + 1}行`;
28 | }
29 | if (item.startColIndex - item.endColIndex === 0) {
30 | item.title += `${item.startColIndex + 1}列`;
31 | } else {
32 | item.title += `${item.startColIndex + 1}-${item.endColIndex + 1}列`;
33 | }
34 | for (let i = 0; i <= rowLength; i++) {
35 | let rowData = window.SpreadsheetApp.spreadsheet.activeSheet.data.rowData;
36 | // SpreadsheetApp.spreadsheet.activeSheet.data.rowData[0].values[0]
37 | // @ts-ignore
38 | // window.__console.log(rowData, startRowIndex + i, startColIndex + i);
39 | // console.log(rowData[startRowIndex + i].values[startColIndex]);
40 | // 如果没有所有行数据,将中断循环
41 | if (!rowData) {
42 | break;
43 | }
44 | // 如果遍历的当前行没数据将不处理该行
45 | if (!rowData[startRowIndex + i]) {
46 | continue;
47 | }
48 | // e 为该单元格详细的数据信息
49 | let e = rowData[startRowIndex + i].values[startColIndex];
50 | // 设置第n行的第一个
51 | item.children.push([{ e, position: [startRowIndex + i, startColIndex] }]);
52 | for (let j = 0; j < colLength; j++) {
53 | // e 为该单元格详细的数据信息
54 | let e = rowData[startRowIndex + i].values[startColIndex + j + 1];
55 | // 设置第 n 行第二个之后的元素
56 | item.children[i].push({
57 | e,
58 | position: [startRowIndex + i, startColIndex + j + 1]
59 | });
60 | }
61 | }
62 | });
63 | // 传给面板
64 | window.postMessage(
65 | {
66 | source: 'tencent-docs-devtools-inject',
67 | payload: { rangeSelections }
68 | },
69 | '*'
70 | );
71 | // }, 1000);
72 | // } catch (error) {}
73 | });
74 | };
75 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/component/TableSearchTree.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Table, Descriptions } from 'antd';
3 |
4 | const columns = [
5 | {
6 | title: 'setSlots',
7 | dataIndex: 'setSlots',
8 | key: 'setSlots'
9 | },
10 | {
11 | title: 'effectiveValue',
12 | dataIndex: 'effectiveValue',
13 | key: 'effectiveValue'
14 | },
15 | {
16 | title: 'formattedValue',
17 | dataIndex: 'formattedValue',
18 | key: 'formattedValue'
19 | },
20 | {
21 | title: 'editValue',
22 | key: 'editValue',
23 | dataIndex: 'editValue'
24 | },
25 | {
26 | title: 'userEnteredFormat',
27 | dataIndex: 'userEnteredFormat',
28 | key: 'userEnteredFormat',
29 | render(userEnteredFormat) {
30 | console.log(userEnteredFormat);
31 | return (
32 |
33 | Cloud Database
34 | Prepaid
35 | YES
36 |
37 | );
38 | }
39 | },
40 | {
41 | title: 'effectiveFormat',
42 | dataIndex: 'effectiveFormat',
43 | key: 'effectiveFormat'
44 | },
45 | {
46 | title: 'conditionalFormatResult',
47 | dataIndex: 'conditionalFormatResult',
48 | key: 'conditionalFormatResult'
49 | },
50 | {
51 | title: 'mergeReference',
52 | dataIndex: 'mergeReference',
53 | key: 'mergeReference'
54 | },
55 | {
56 | title: 'hyperlink',
57 | dataIndex: 'hyperlink',
58 | key: 'hyperlink'
59 | },
60 | {
61 | title: 'formula',
62 | dataIndex: 'formula',
63 | key: 'formula'
64 | },
65 | {
66 | title: 'textFormatRuns',
67 | dataIndex: 'textFormatRuns',
68 | key: 'textFormatRuns'
69 | },
70 | {
71 | title: 'isDataValid',
72 | dataIndex: 'isDataValid',
73 | key: 'isDataValid'
74 | },
75 | {
76 | title: 'sheetRangeReferences',
77 | dataIndex: 'sheetRangeReferences',
78 | key: 'sheetRangeReferences'
79 | },
80 | {
81 | title: 'author',
82 | dataIndex: 'author',
83 | key: 'author'
84 | },
85 | {
86 | title: 'mentionDetails',
87 | dataIndex: 'mentionDetails',
88 | key: 'mentionDetails'
89 | }
90 | ];
91 |
92 | const data = [
93 | {
94 | setSlots: 5,
95 | effectiveValue: { type: 0, value: 33333333, originValue: null },
96 | formattedValue: {
97 | type: 'normal',
98 | value: '33333333',
99 | textColor: null
100 | },
101 | editValue: '33333333',
102 | userEnteredFormat: {
103 | setSlots: 1,
104 | numberFormat: { __type: null, __id: 0, __pattern: 'General' },
105 | dataValidation: null,
106 | backgroundColor: null,
107 | borders: null,
108 | horizontalAlign: null,
109 | verticalAlign: null,
110 | wrapStrategy: null,
111 | textFormat: null
112 | },
113 | effectiveFormat: null,
114 | conditionalFormatResult: null,
115 | mergeReference: null,
116 | hyperlink: null,
117 | formula: null,
118 | textFormatRuns: null,
119 | isDataValid: null,
120 | sheetRangeReferences: null,
121 | author: null,
122 | mentionDetails: null
123 | }
124 | ];
125 |
126 | interface Props {
127 | tableData: any[];
128 | }
129 | export default class SheetSearchTree extends React.Component {
130 | constructor(props) {
131 | super(props);
132 | }
133 | render() {
134 | console.log(this.props.tableData);
135 | return (
136 | JSON.stringify(this.props.tableData)
137 | // !this.props.tableData ||
138 | );
139 | // return {JSON.stringify(this.props.tableData)}
;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/core/view/panel/panel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { Provider } from 'react-redux';
5 | import store from '../../store';
6 |
7 | import tool from '../../panel';
8 | import 'antd/dist/antd.css';
9 | import './panel.css';
10 |
11 | // 数据层面板
12 | import ModelPanel from './ModelPanel';
13 | // 视图层面板
14 | import ViewPanel from './ViewPanel';
15 | // 视图层面板
16 | import PastePanel from './PastePanel';
17 | // debug调试面板
18 | import DebugPanel from './DebugPanel';
19 |
20 | // 提醒调试面板刷新的提示窗口
21 | import ReloadTipModel from './ViewPanel/component/ReloadTipModel';
22 |
23 | import { Layout, Menu, Icon } from 'antd';
24 |
25 | import watchSheetPageReload from './ViewPanel/lib/watchSheetPageReload';
26 |
27 | const { Content, Sider } = Layout;
28 |
29 | // 侧边栏
30 | const menus = [
31 | {
32 | key: '1',
33 | type: 'user',
34 | className: 'nav-text',
35 | text: '基本功能'
36 | },
37 | {
38 | key: '2',
39 | type: 'video-camera',
40 | className: 'nav-text',
41 | text: '性能'
42 | },
43 | {
44 | key: '3',
45 | type: 'upload',
46 | className: 'nav-text',
47 | text: '粘贴板记录'
48 | },
49 | {
50 | key: '4',
51 | type: 'upload',
52 | className: 'nav-text',
53 | text: '断点调试'
54 | }
55 | // {
56 | // key: '4',
57 | // type: 'user',
58 | // className: 'nav-text',
59 | // text: '其他'
60 | // }
61 | ];
62 | class DevtoolLayout extends React.Component {
63 | state = {
64 | visible: false,
65 | selectedKeys: '1',
66 | menus
67 | };
68 | toggle = (selectedKeys) => {
69 | this.setState({
70 | selectedKeys
71 | });
72 | };
73 | render() {
74 | return (
75 |
76 |
77 |
78 |
79 |
92 |
93 |
94 |
95 |
96 | {(() => {
97 | switch (this.state.selectedKeys) {
98 | case '1':
99 | return
;
100 | case '2':
101 | return
;
102 | case '3':
103 | return
104 | case '4':
105 | return
106 | }
107 | })()}
108 |
109 |
110 |
111 | {this.state.visible && }
112 |
113 |
114 | );
115 | }
116 | // 监听表格页的刷新操作
117 | watchSheetPageReload() {
118 | tool.listen((message) => {
119 | if (message.payload === 'sheet-reload') {
120 | this.setState({
121 | visible: true
122 | });
123 | }
124 | });
125 | tool.injectScriptStringToPage(`;(${watchSheetPageReload.toString()})();`);
126 | }
127 | componentDidMount() {
128 | this.watchSheetPageReload();
129 | }
130 | }
131 |
132 | ReactDOM.render(
133 |
134 |
135 |
,
136 | document.querySelector('#panel')
137 | );
138 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/lib/showTips.ts:
--------------------------------------------------------------------------------
1 | const showTips = () => {
2 | let i = 0;
3 | let timer = setInterval(() => {
4 | if (!window.T_SHEET) {
5 | clearInterval(timer);
6 | return;
7 | }
8 | window.console.log(window.T_SHEET);
9 | window.console.log(i);
10 | [
11 | // showSaveFrontEndErrorTip 60003 60016 60017 40002 40003 40030
12 | () => {
13 | window.T_SHEET.ErrorCodeModal.client.showSaveFrontEndErrorTip('60003');
14 | },
15 | // showFileNotExsitErrorTip
16 | () => {
17 | window.T_SHEET.ErrorCodeModal.client.showFileNotExsitErrorTip('40001');
18 | },
19 | // showLoginExpiredTip
20 | () => {
21 | window.T_SHEET.ErrorCodeModal.client.showLoginExpiredTip('60006');
22 | },
23 | // showUnAuthTip 60007 60015
24 | () => {
25 | window.T_SHEET.ErrorCodeModal.client.showUnAuthTip('60007');
26 | },
27 | // showBigDataFromFrontTip C0001 C0002
28 | () => {
29 | window.T_SHEET.ErrorCodeModal.client.showBigDataFromFrontTip(
30 | 'C0002',
31 | '数据量较大,操作不成功'
32 | );
33 | },
34 | // showSecurityTip
35 | () => {
36 | window.T_SHEET.ErrorCodeModal.client.showSecurityTip('40025');
37 | },
38 | // showSheetBarDuplicateSheetId
39 | () => {
40 | window.T_SHEET.ErrorCodeModal.client.showSheetBarDuplicateSheetId(
41 | '888888',
42 | 'insertNewSheet'
43 | );
44 | },
45 | // window.T_SHEET.ErrorCodeModal.client.showPermissionChangesTip();
46 | // showCommonErrorModal1
47 | () => {
48 | window.T_SHEET.ErrorCodeModal.client.showCommonErrorModal1('通用错误提示modal1');
49 | },
50 | // showCommonCrashAndFeedBackModal
51 | () => {
52 | window.T_SHEET.ErrorCodeModal.client.showCommonCrashAndFeedBackModal(
53 | '通用型的引导反馈的 modal'
54 | );
55 | },
56 | // checkVipToast
57 | () => {
58 | window.T_SHEET.ErrorCodeModal.server.checkVipToast('vip', () => {
59 | console.log('测试');
60 | });
61 | },
62 | // showServerErrorTip 60000 60001(有可能走这)
63 | () => {
64 | window.T_SHEET.ErrorCodeModal.server.showServerErrorTip('60000');
65 | },
66 | // showServerConnectTimeout 60002
67 | () => {
68 | window.T_SHEET.ErrorCodeModal.server.showServerConnectTimeout('60002');
69 | },
70 | // showServerTaskTimeout 服务器处理超时 60022
71 | () => {
72 | window.T_SHEET.ErrorCodeModal.server.showServerTaskTimeout('60022');
73 | },
74 | // showFullMemberTip
75 | () => {
76 | window.T_SHEET.ErrorCodeModal.server.showFullMemberTip('满员');
77 | },
78 | // showFullMemberTipWithRefresh
79 | () => {
80 | window.T_SHEET.ErrorCodeModal.server.showFullMemberTipWithRefresh('INACTIVE');
81 | },
82 | // showBigDataFromServerTip 60008 40026 40028
83 | () => {
84 | window.T_SHEET.ErrorCodeModal.server.showBigDataFromServerTip('40026');
85 | },
86 | // showFrequentEditTip 60014
87 | () => {
88 | window.T_SHEET.ErrorCodeModal.server.showFrequentEditTip('60014');
89 | },
90 | // showSaveBackEndErrorTip 40000 40005 40006 40020 40022 40023 40035 40036 40037 41001 41002 10000 10001
91 | () => {
92 | window.T_SHEET.ErrorCodeModal.server.showSaveBackEndErrorTip('40000');
93 | },
94 | // showUpgradeTip
95 | () => {
96 | window.T_SHEET.ErrorCodeModal.server.showUpgradeTip('40034');
97 | },
98 | // showUpdatePadTip
99 | // ()=>{window.T_SHEET.ErrorCodeModal.server.showUpdatePadTip()},
100 | // showApplyNewChangeErrorTip
101 | () => {
102 | window.T_SHEET.ErrorCodeModal.server.showApplyNewChangeErrorTip('aaaaaa');
103 | },
104 | // switchSheetError
105 | () => {
106 | window.T_SHEET.ErrorCodeModal.server.switchSheetError('aaaaa');
107 | },
108 | () => {
109 | clearInterval(timer);
110 | }
111 | ][i]();
112 | i += 1;
113 | }, 1000);
114 | };
115 |
116 | export default showTips;
117 |
--------------------------------------------------------------------------------
/src/core/view/panel/ModelPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col, Progress } from 'antd';
3 | // 通讯库
4 | import tool from '../../../panel';
5 | import showFpsAndMemory from './lib/showFpsAndMemory';
6 | import showNetworkSpeed from './lib/showNetworkSpeed';
7 | // 引入 echarts 绘图
8 | import echarts from 'echarts';
9 | // 类型接口
10 | interface State {
11 | memory: number;
12 | fps: number;
13 | networkSpeed: number;
14 | memoryCharts: any;
15 | fpsCharts: any;
16 | networkSpeedCharts: any;
17 | }
18 | const memoryChartsData = {
19 | title: {
20 | text: ''
21 | },
22 | tooltip: {},
23 | xAxis: {
24 | data: []
25 | },
26 | yAxis: {},
27 | series: [
28 | {
29 | name: 'memory',
30 | type: 'line',
31 | smooth: true,
32 | data: []
33 | }
34 | ]
35 | };
36 |
37 | const fpsChartsData = {
38 | title: {
39 | text: ''
40 | },
41 | tooltip: {},
42 | xAxis: {
43 | data: []
44 | },
45 | yAxis: {},
46 | series: [
47 | {
48 | name: 'fps',
49 | type: 'line',
50 | smooth: true,
51 | data: []
52 | }
53 | ]
54 | };
55 |
56 | const networkSpeedChartsData = {
57 | title: {
58 | text: ''
59 | },
60 | tooltip: {},
61 | xAxis: {
62 | data: []
63 | },
64 | yAxis: {},
65 | series: [
66 | {
67 | name: 'speed',
68 | type: 'line',
69 | smooth: true,
70 | data: []
71 | }
72 | ]
73 | };
74 |
75 | export default class ModelPanel extends React.Component {
76 | state: State = {
77 | memory: 60,
78 | fps: 60,
79 | networkSpeed: 0,
80 | memoryCharts: null,
81 | fpsCharts: null,
82 | networkSpeedCharts: null
83 | };
84 |
85 | componentDidMount() {
86 | // 显示 fps 和 memory 的功能
87 | this.showFpsAndMemory();
88 | // 网速测速
89 | // this.showNetworkSpeed();
90 | }
91 |
92 | showNetworkSpeed() {
93 | tool.listen((message) => {
94 | if (!message.networkSpeed) {
95 | return true;
96 | }
97 | console.log(message.networkSpeed);
98 | this.setState(
99 | {
100 | networkSpeed: message.networkSpeed,
101 | networkSpeedCharts: echarts.init(this.refs.renderNetworkSpeedCharts)
102 | },
103 | () => {
104 | // 设置网络速度变化的图标数据
105 | networkSpeedChartsData.series[0].data.push(message.networkSpeed);
106 | this.state.networkSpeedCharts.setOption(networkSpeedChartsData);
107 | }
108 | );
109 | return true;
110 | });
111 | tool.injectScriptStringToPage(`;(${showNetworkSpeed.toString()})();`);
112 | }
113 |
114 | showFpsAndMemory() {
115 | tool.listen((message) => {
116 | let memory = message.payload.memory;
117 | let fps = message.payload.fps;
118 | if (!memory || !fps) {
119 | return true;
120 | }
121 | if (!this.refs.renderMemoryCharts || !this.refs.renderFpsCharts) {
122 | return true;
123 | }
124 | this.setState(
125 | {
126 | memory: memory,
127 | fps: fps,
128 | memoryCharts: echarts.init(this.refs.renderMemoryCharts),
129 | fpsCharts: echarts.init(this.refs.renderFpsCharts)
130 | },
131 | () => {
132 | // 设置内存变化的图标数据
133 | memoryChartsData.series[0].data.push(memory);
134 | // 设置 fps 变化的图标数据
135 | fpsChartsData.series[0].data.push(fps);
136 | // 绘制图表
137 | this.state.memoryCharts.setOption(memoryChartsData);
138 | this.state.fpsCharts.setOption(fpsChartsData);
139 | }
140 | );
141 | return true;
142 | });
143 | tool.injectScriptStringToPage(`;(${showFpsAndMemory.toString()})();`);
144 | }
145 |
146 | componentWillUnmount() {
147 | tool.injectScriptStringToPage(`window.cancelAnimationFrame(window.animationFrameID);`);
148 | // 销毁监听网速的定时器
149 | tool.injectScriptStringToPage(`clearInterval(window.networkSpeedTimer);`);
150 | // 清空 echarts 数据队列
151 | memoryChartsData.series[0].data = [];
152 | fpsChartsData.series[0].data = [];
153 | networkSpeedChartsData.series[0].data = [];
154 | }
155 |
156 | render() {
157 | return (
158 |
159 |
160 |
161 |
174 |
175 |
176 |
189 | {/*
190 |
191 |
*/}
204 |
205 | );
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/component/SheetSearchTree.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tree, Input, Drawer, Tag } from 'antd';
3 | import TableSearchTree from './TableSearchTree';
4 | import tool from '../../../../panel';
5 | import showRecordsList from '../lib/showRecordsList';
6 |
7 | const { TreeNode } = Tree;
8 | const { Search } = Input;
9 | interface Props {
10 | rangeSelections: [
11 | {
12 | isSheet: boolean;
13 | sheetId: string;
14 | startRowIndex: number;
15 | endRowIndex: number;
16 | startColIndex: number;
17 | endColIndex: number;
18 | title: string;
19 | children?: any[];
20 | }
21 | ];
22 | }
23 |
24 | interface State {
25 | searchValue: string;
26 | autoExpandParent: boolean;
27 | tableData: any;
28 | history: any;
29 | visible: boolean;
30 | selectedKeys: string;
31 | }
32 |
33 | export default class SheetSearchTree extends React.Component {
34 | constructor(props) {
35 | super(props);
36 | }
37 | state: State = {
38 | searchValue: '',
39 | autoExpandParent: true,
40 | tableData: null,
41 | history: null,
42 | visible: false,
43 | selectedKeys: ''
44 | };
45 | num2letter(index: number) {
46 | let letter = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
47 | if (index <= 25) {
48 | return letter[index];
49 | } else {
50 | let result = index / 26;
51 | let l1 = parseInt(`${result}`) - 1;
52 | let l2 = index % 26;
53 | return letter[l1] + letter[l2];
54 | }
55 | }
56 | getSheetDetail(selectedKeys, e) {
57 | try {
58 | const tableData = e.selectedNodes[0].props.tableData;
59 | // console.log(selectedKeys, e);
60 | // 获取单行
61 | if (tableData instanceof Array) {
62 | this.showDrawer(selectedKeys, { e: tableData });
63 | }
64 | // 整列和行的所有信息
65 | else if (tableData.children instanceof Array) {
66 | this.showDrawer(selectedKeys, { e: tableData.children });
67 | }
68 | // 获取一个单元格的信息
69 | else {
70 | // 展开抽屉
71 | this.showDrawer(selectedKeys, tableData);
72 | // 获取修订记录
73 | // this.getRecordsList();
74 | let postion = tableData.position;
75 | tool.injectScriptStringToPage(`
76 | window.__console.log('-----第${postion[0] + 1}行,第${this.num2letter(
77 | postion[1]
78 | )}列-----')
79 | window.__console.log(window.SpreadsheetApp.spreadsheet.activeSheet.data.rowData[${
80 | postion[0]
81 | }].values[${postion[1]}])
82 | `);
83 | }
84 | } catch (error) {
85 | console.log(error);
86 | }
87 | }
88 | onChange() {
89 | this.setState({
90 | autoExpandParent: true
91 | });
92 | }
93 | treeNode(item) {
94 | // 渲染列
95 | let colTreeNode = (col) => {
96 | return (
97 |
102 | );
103 | };
104 | // 渲染行
105 | let rowTreeNode = (row) => {
106 | return (
107 |
112 | {row
113 | ? row.map((col) => {
114 | return colTreeNode(col);
115 | })
116 | : '加载树结构...'}
117 |
118 | );
119 | };
120 | return (
121 | // 渲染行和列总体的数据
122 |
123 | {item.children.length
124 | ? item.children.map((row) => {
125 | return rowTreeNode(row);
126 | })
127 | : '加载树结构...'}
128 |
129 | );
130 | }
131 |
132 | getRecordsList(page?: number, callBack?: Function) {
133 | tool.listen((message) => {
134 | try {
135 | const history = message.payload.history;
136 | if (history) {
137 | this.setState({
138 | history
139 | });
140 | }
141 | } catch (error) {}
142 | });
143 | tool.injectScriptStringToPage(`;(${showRecordsList.toString()})();`);
144 | }
145 | showDrawer = (selectedKeys, tableData) => {
146 | this.setState({
147 | selectedKeys,
148 | visible: true,
149 | tableData: tableData ? tableData : null
150 | });
151 | };
152 | onClose() {
153 | this.setState({
154 | visible: false
155 | });
156 | }
157 | // 把键值对渲染树状图上
158 | showKeyAndValue(data) {
159 | let arr = [];
160 | Object.keys(data).forEach((key, index) => {
161 | switch (typeof data[key]) {
162 | case 'object':
163 | // null类型
164 | if (data[key] === null) {
165 | arr.push(
166 |
170 | );
171 | }
172 | // 数组类型
173 | else if (data[key] instanceof Array) {
174 | arr.push(
175 |
176 | {/* 渲染子元素 */}
177 | {this.showKeyAndValue(data[key])}
178 |
179 | );
180 | }
181 | // 对象类型
182 | else {
183 | arr.push(
184 |
185 | {/* 渲染子元素 */}
186 | {this.showKeyAndValue(data[key])}
187 |
188 | );
189 | }
190 | break;
191 | case 'string':
192 | arr.push(
193 |
197 | );
198 | break;
199 | case 'number':
200 | arr.push(
201 |
205 | );
206 | break;
207 | case 'undefined':
208 | arr.push(
209 |
213 | );
214 | break;
215 | }
216 | });
217 | return arr;
218 | }
219 | componentDidMount() {}
220 | render() {
221 | return (
222 | <>
223 |
224 | {this.props.rangeSelections.length > 0 && (
225 |
230 | {this.props.rangeSelections.length
231 | ? this.props.rangeSelections.map((item) => {
232 | return this.treeNode(item);
233 | })
234 | : '加载树结构...'}
235 |
236 | )}
237 | {/*
*/}
238 |
239 | {/* 弹窗 */}
240 |
248 | {/* */}
253 |
254 | {this.state.tableData
255 | ? this.showKeyAndValue(this.state.tableData.e)
256 | : '加载树结构...'}
257 |
258 |
259 | >
260 | );
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/src/core/background.ts:
--------------------------------------------------------------------------------
1 | // import recordScreen from '../core/view/panel/ViewPanel/lib/recordScreen';
2 |
3 | // 作为 content.ts 与 devtool.ts 通信的桥
4 | const connections = {};
5 | // panel.ts 和 background.ts 的通信
6 | chrome.runtime.onConnect.addListener(function (port) {
7 | const extensionListener = function (message) {
8 | if (message.name == 'original') {
9 | connections[message.tabId] = port;
10 | }
11 | return true;
12 | };
13 | port.onMessage.addListener(extensionListener);
14 | port.onDisconnect.addListener(function (port) {
15 | port.onMessage.removeListener(extensionListener);
16 | const tabs = Object.keys(connections);
17 | for (let i = 0, len = tabs.length; i < len; i++) {
18 | if (connections[tabs[i]] == port) {
19 | delete connections[tabs[i]];
20 | break;
21 | }
22 | }
23 | });
24 | });
25 |
26 | declare const MediaRecorder: any;
27 |
28 | let mediaRecorder;
29 | let recordedBlobs;
30 | let sourceBuffer;
31 |
32 | let localStream;
33 |
34 | let localVideo = document.getElementById('localVideo') as HTMLVideoElement;
35 | let recordedVideo = document.getElementById('recordedVideo') as HTMLVideoElement;
36 |
37 | let recordButton = document.getElementById('record') as HTMLButtonElement;
38 | let playButton = document.getElementById('play') as HTMLButtonElement;
39 | let downloadButton = document.getElementById('download') as HTMLButtonElement;
40 | playButton.disabled = true;
41 | downloadButton.disabled = true;
42 |
43 | recordButton.onclick = toggleRecording;
44 | playButton.onclick = call;
45 | downloadButton.onclick = download;
46 |
47 | function trace(param) {
48 | let time = (window.performance.now() / 1000).toFixed(3);
49 | console.log(time + ':' + param);
50 | }
51 |
52 | function handleSuccess(stream) {
53 | console.log(stream);
54 | stream.onremovetrack = () => {
55 | // Click on browser UI stop sharing button
56 | alert('Recording has ended');
57 | };
58 | trace('getUserMedia() got stream!');
59 | recordButton.disabled = false;
60 | window.stream = stream; // 录制视频的时候要用到
61 | localStream = stream;
62 | localVideo.srcObject = stream;
63 | // 点击录制视频的按钮
64 | document.getElementById('record').click();
65 | }
66 |
67 | function handleError(error) {
68 | // 点击暂停按钮
69 | document.getElementById('record').click();
70 | // 点击下载按钮
71 | document.getElementById('download').click();
72 | // trace(error.message);
73 | }
74 |
75 | let constraints = {
76 | video: true,
77 | audio: false
78 | };
79 |
80 | let mediaSource = new MediaSource();
81 | // 通过注册事件event::sourceopen来触发当前连接之后的的回调处理
82 | mediaSource.addEventListener('sourceopen', handleSourceOpen, false);
83 |
84 | /*
85 | * 回调处理就是需要赋值视频数据的地方,
86 | * 调用MediaSourceBuffer::addSourceBuffer方法来 ==> 构建一个存放视频数据的Buffer;
87 | */
88 | function handleSourceOpen(event) {
89 | trace('MediaSource opened!');
90 | sourceBuffer = mediaSource.addSourceBuffer("video/webm;codecs='vp8'");
91 | trace(sourceBuffer);
92 | }
93 |
94 | /* 这里把每10ms获取的数据放入recordedBlobs */
95 | function handleDataAvailable(event) {
96 | if (event.data && event.data.size > 0) {
97 | recordedBlobs.push(event.data);
98 | }
99 | }
100 |
101 | /*
102 | * 录制button状态转换
103 | */
104 | function toggleRecording() {
105 | if (recordButton.textContent === 'Start Recording') {
106 | startRecording();
107 | } else {
108 | stopRecording();
109 | recordButton.textContent = 'Start Recording';
110 | playButton.disabled = false;
111 | downloadButton.disabled = false;
112 |
113 | let test = document.getElementById('test') as HTMLVideoElement;
114 | let buffer = new Blob(recordedBlobs, {
115 | type: 'video/webm'
116 | });
117 | test.src = window.URL.createObjectURL(buffer);
118 | }
119 | }
120 |
121 | /* 开始录制 */
122 | function startRecording() {
123 | recordedBlobs = [];
124 | let options = {
125 | mimeType: 'video/webm;codecs=vp9'
126 | };
127 | // isTypeSupporteed:判断是否支持要解码播放的视频文件编码和类型。
128 | if (!MediaRecorder.isTypeSupported(options.mimeType)) {
129 | console.log(options.mimeType + 'is not Supported');
130 | options = {
131 | mimeType: 'video/webm;codecs=vp8'
132 | };
133 | if (!MediaRecorder.isTypeSupported(options.mimeType)) {
134 | console.log(options.mimeType + 'is not Supported');
135 | options = {
136 | mimeType: 'video/webm;codecs=vp8'
137 | };
138 | if (!MediaRecorder.isTypeSupported(options.mimeType)) {
139 | console.log(options.mimeType + 'is not Supported');
140 | options = {
141 | mimeType: ''
142 | };
143 | }
144 | }
145 | }
146 | try {
147 | mediaRecorder = new MediaRecorder(window.stream, options); // 设置音频录入源、格式
148 | } catch (e) {
149 | console.log('Exception while creating MediaRecorder: ' + e);
150 | // alert('Exception while creating MediaRecorder: ' + e + '. mimeType: ' + options.mimeType);
151 | return;
152 | }
153 | console.log('Created MediaRecorder', mediaRecorder, 'with options', options);
154 | recordButton.textContent = 'Stop Recording';
155 | playButton.disabled = true;
156 | downloadButton.disabled = true;
157 | mediaRecorder.onstop = handleStop;
158 | mediaRecorder.ondataavailable = handleDataAvailable; // 存放获取的数据
159 | mediaRecorder.start(10); // 开始录制 collect 10ms of data
160 | console.log('MediaRecorder started', mediaRecorder);
161 | }
162 |
163 | function handleStop(event) {
164 | console.log('Recorder stopped: ', event);
165 | }
166 |
167 | /* 结束录制 */
168 | function stopRecording() {
169 | mediaRecorder.stop();
170 | console.log('Recorded Blobs: ', recordedBlobs);
171 | recordedVideo.controls = true;
172 | }
173 |
174 | /*
175 | * 录制视频添加监听事件
176 | */
177 | recordedVideo.addEventListener(
178 | 'error',
179 | function (ev) {
180 | // 点击暂停按钮
181 | document.getElementById('record').click();
182 | // 点击下载按钮
183 | document.getElementById('download').click();
184 | trace('MediaRecording.recordedMedia.error()');
185 | },
186 | true
187 | );
188 |
189 | /*
190 | * 播放录制的视频
191 | */
192 | function call() {
193 | trace('Starting call');
194 | if (localStream.getVideoTracks().length > 0) {
195 | trace('Using video device: ' + localStream.getVideoTracks()[0].label);
196 | }
197 | if (localStream.getAudioTracks().length > 0) {
198 | trace('Using audio device: ' + localStream.getAudioTracks()[0].label);
199 | }
200 | }
201 |
202 | /*
203 | * 下载视频
204 | */
205 | function download() {
206 | let blob = new Blob(recordedBlobs, {
207 | type: 'video/webm'
208 | });
209 | let url = window.URL.createObjectURL(blob);
210 | let a = document.createElement('a');
211 | a.style.display = 'none';
212 | a.href = url;
213 | a.download = 'test.webm';
214 | document.body.appendChild(a);
215 | a.click();
216 | setTimeout(function () {
217 | document.body.removeChild(a);
218 | window.URL.revokeObjectURL(url);
219 | }, 100);
220 | }
221 |
222 | // 通信线路:inject->content->background->devtool/panel->inject
223 | // 接收 content.ts 的消息,并发送到 devtool.ts/panel.ts 的消息
224 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
225 | // 开始录屏
226 | if (message.payload === 'startRecordScreen') {
227 | navigator.mediaDevices
228 | // @ts-ignore
229 | .getDisplayMedia(constraints)
230 | .then(handleSuccess)
231 | .catch(handleError);
232 | }
233 | // 暂停录屏
234 | if (message.payload === 'stopRecordScreen') {
235 | // 点击暂停按钮
236 | document.getElementById('record').click();
237 | // 点击下载按钮
238 | document.getElementById('download').click();
239 | }
240 |
241 | // 截图
242 | if (message.payload === 'startScreenShot') {
243 | chrome.tabs.captureVisibleTab(null, {}, (dataUrl) => {
244 | const a = document.createElement('a');
245 | a.download = 'screenshot.png';
246 | a.href = dataUrl;
247 | document.body.appendChild(a);
248 | a.click();
249 | setTimeout(() => {
250 | document.body.removeChild(a);
251 | }, 100);
252 | });
253 | return true;
254 | }
255 |
256 | // 监听粘贴板内容
257 | if (message.payload === 'startWatchClipboardContents') {
258 | clearInterval(window.pasteRecord);
259 | window.pasteRecord = setInterval(() => {
260 | let pasteInfoFromStorage = localStorage.getItem('paste-info')
261 | ? JSON.parse(localStorage.getItem('paste-info'))
262 | : [];
263 | // 继承原来Storage的粘贴板数据
264 | let pasteArr = [...pasteInfoFromStorage];
265 | console.log(pasteArr);
266 | let input = document.createElement('input');
267 | document.body.appendChild(input);
268 | input.focus();
269 | document.execCommand('paste');
270 | let clipboardText = input.value;
271 | if (pasteArr.length > 0) {
272 | // 当当前粘贴板的数据和上一次的数据一样,则不存进Storage里面,否则才加入
273 | !(pasteArr[pasteArr.length - 1] === clipboardText) &&
274 | (() => {
275 | pasteArr.push(clipboardText);
276 | localStorage.setItem('paste-info', JSON.stringify(pasteArr));
277 | })();
278 | }
279 | // 处理第一个放入数组的数据
280 | else if (pasteArr.length === 0) {
281 | pasteArr.push(clipboardText);
282 | localStorage.setItem('paste-info', JSON.stringify(pasteArr));
283 | }
284 | console.log(clipboardText);
285 | document.body.removeChild(input);
286 | }, 1000);
287 | }
288 | if (message.payload === 'stopWatchClipboardContents') {
289 | clearInterval(window.pasteRecord);
290 | }
291 | // 测试代码
292 | console.log('收到来自content-script的消息:');
293 | console.log(message, sender, sendResponse);
294 | if (sender.tab) {
295 | const tabId = sender.tab.id;
296 | if (tabId in connections) {
297 | console.log('sender.tab is defined.');
298 | connections[tabId].postMessage(message);
299 | } else {
300 | console.log('Tab not found in connection list.');
301 | }
302 | } else {
303 | console.log('sender.tab not defined.');
304 | }
305 | return true;
306 | });
307 |
--------------------------------------------------------------------------------
/src/core/view/panel/ViewPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Button, Input, Statistic, Card, Row, Col } from 'antd';
3 | // 通讯库
4 | import tool from '../../../panel';
5 | // 弹窗测试
6 | import showTips from './lib/showTips';
7 | import showFdAndId from './lib/showFdAndId';
8 | // 列表树组件
9 | import SheetSearchTree from './component/SheetSearchTree';
10 | import handleSheetSearchTree from './lib/handleSheetSearchTree';
11 |
12 | // 启用 console.log 方法
13 | import useConsole from './lib/useConsole';
14 |
15 | // 断开调试面板和页面的连接
16 | import shutdown from './lib/shutdown';
17 | // 查找空节点
18 | import emptyDom from './lib/emptyDom';
19 | import { connect } from 'react-redux';
20 |
21 | const { TextArea } = Input;
22 | interface State {
23 | textAreaScript: string;
24 | rangeSelections: [
25 | {
26 | isSheet: boolean;
27 | sheetId: string;
28 | startRowIndex: number;
29 | endRowIndex: number;
30 | startColIndex: number;
31 | endColIndex: number;
32 | children?: any[];
33 | title: string;
34 | }
35 | ];
36 | SheetSearchTree: any;
37 | isRecordScreen: boolean;
38 | }
39 |
40 | interface Props {
41 | setRangeSelections: (author: string) => void;
42 | setFdAndRouteId: (any) => void;
43 | rangeSelections: [
44 | {
45 | isSheet: boolean;
46 | sheetId: string;
47 | startRowIndex: number;
48 | endRowIndex: number;
49 | startColIndex: number;
50 | endColIndex: number;
51 | children?: any[];
52 | title: string;
53 | }
54 | ];
55 | fd: string;
56 | routeId: string;
57 | }
58 |
59 | class ModelPanel extends React.Component {
60 | constructor(props) {
61 | super(props);
62 | }
63 | state: State = {
64 | textAreaScript: '',
65 | rangeSelections: [
66 | {
67 | isSheet: false,
68 | sheetId: 'BB08J2',
69 | startRowIndex: 20,
70 | endRowIndex: 20,
71 | startColIndex: 2,
72 | endColIndex: 2,
73 | children: [
74 | [
75 | {
76 | e: {
77 | setSlots: 5,
78 | effectiveValue: { type: 0, value: 13123123, originValue: null },
79 | formattedValue: {
80 | type: 'normal',
81 | value: '13123123',
82 | textColor: null
83 | },
84 | editValue: '13123123',
85 | userEnteredFormat: {
86 | setSlots: 1,
87 | numberFormat: { __type: null, __id: 0, __pattern: 'General' },
88 | dataValidation: null,
89 | backgroundColor: null,
90 | borders: null,
91 | horizontalAlign: null,
92 | verticalAlign: null,
93 | wrapStrategy: null,
94 | textFormat: null
95 | },
96 | effectiveFormat: null,
97 | conditionalFormatResult: null,
98 | mergeReference: null,
99 | hyperlink: null,
100 | formula: null,
101 | textFormatRuns: null,
102 | isDataValid: null,
103 | sheetRangeReferences: null,
104 | author: null,
105 | mentionDetails: null
106 | },
107 | position: [20, 2]
108 | }
109 | ]
110 | ],
111 | title: '21行3列'
112 | }
113 | ],
114 | // 存放树组件
115 | SheetSearchTree: null,
116 | // 用于判断当前状态是否录屏状态
117 | isRecordScreen: false
118 | };
119 | componentDidMount() {
120 | this.showFdAndId();
121 | // 获取页面上用户在表格中选取范围的数据信息
122 | this.sheetSearchTree();
123 | // console.log(kscreenshot);
124 | }
125 | componentWillUnmount() {
126 | // 清空获取页面上用户在表格中选取范围的数据信息的定时器
127 | // window.clearInterval(window.getrangeSelections);
128 | }
129 | render() {
130 | return (
131 |
132 | {/* 显示 fd 和 id 信息 */}
133 |
134 |
135 |
136 |
137 |
143 |
144 |
145 |
146 |
147 |
153 |
154 |
155 |
156 |
157 | {/* 如果表格页面刷新了,那么请把调试工具面板重新刷新一次,不然无法监听最新的表格数据 */}
158 |
159 |
162 |
169 |
170 |
177 |
178 |
185 |
186 |
193 |
194 | {/*
*/}
201 | {this.state.isRecordScreen ? (
202 |
209 | ) : (
210 |
217 | )}
218 |
221 |
222 |
223 | {/*
224 |
225 |
226 |
227 |
233 |
234 |
235 |
238 |
239 |
*/}
240 | {/* 将表格数据渲染再此树组件上,要保证每一次都是新的树 */}
241 | {this.state.SheetSearchTree}
242 |
243 | );
244 | }
245 | // 获取 cookie 的方法
246 | getCookie(cookie, key) {
247 | let r = new RegExp('(?:^|;+|\\s+)' + key + '=([^;]*)');
248 | let m;
249 | if (cookie) {
250 | m = cookie.match(r);
251 | }
252 | return (!m ? '' : m[1]) || null;
253 | }
254 |
255 | debug() {
256 | tool.injectScriptStringToPage(`
257 | debug(SpreadsheetApp.messageCenter.sendMessageCenter.addUserChange);
258 | `);
259 | }
260 |
261 | unDebug() {
262 | tool.injectScriptStringToPage(`
263 | undebug(SpreadsheetApp.messageCenter.sendMessageCenter.addUserChange);
264 | `);
265 | }
266 |
267 | // 通知 background.ts 录屏幕
268 | startRecordScreen() {
269 | tool.injectScriptStringToPage(`
270 | window.postMessage({payload:'startRecordScreen'},'*')
271 | `);
272 | this.setState({
273 | isRecordScreen: true
274 | });
275 | }
276 | stopRecordScreen() {
277 | tool.injectScriptStringToPage(`
278 | window.postMessage({payload:'stopRecordScreen'},'*')
279 | `);
280 | this.setState({
281 | isRecordScreen: false
282 | });
283 | }
284 | // 显示 fd 和 id
285 | showFdAndId() {
286 | // 监听页面的 cookie 信息
287 | tool.listen((message) => {
288 | try {
289 | let cookie = message.payload.cookie;
290 | if (!cookie) {
291 | return true;
292 | }
293 | this.props.setFdAndRouteId({
294 | fd: this.getCookie(cookie, 'fd'),
295 | routeId: this.getCookie(cookie, 'dev_route_id')
296 | });
297 | return true;
298 | } catch (error) { }
299 | });
300 | tool.injectScriptStringToPage(`;(${showFdAndId.toString()})();`);
301 | }
302 | // 截图
303 | screenshot() {
304 | tool.injectScriptStringToPage(`
305 | window.postMessage({payload: 'startScreenShot'},'*')
306 | `);
307 | }
308 | reset() {
309 | window.location.reload();
310 | // 浏览器页面刷新
311 | // tool.injectScriptStringToPage(`;window.location.reload();`, () => {
312 | // // 断开 panel.ts 与 background.ts 的长通信
313 | // tool.port.disconnect();
314 | // // 断开 content.ts 文件中 inject.ts 和 content.ts 的通信
315 | // tool.injectScriptStringToPage(`;(${shutdown.toString()})();`);
316 | // setTimeout(() => {
317 | // // 面板页面刷新
318 | // window.location.reload();
319 | // }, 1000);
320 | // });
321 | }
322 | // 显示所有的提醒框
323 | showTips() {
324 | tool.injectScriptStringToPage(`;(${showTips.toString()})();`);
325 | }
326 | // 获取需要注入页面的脚本字符串
327 | getTextAreaScript(e) {
328 | this.setState({
329 | textAreaScript: e.target.value
330 | });
331 | }
332 | // 在页面注入输入的脚本
333 | execScript() {
334 | tool.injectScriptStringToPage(`;(()=>{${this.state.textAreaScript}})();`);
335 | }
336 | // 在页面注入输入的脚本
337 | sheetSearchTree() {
338 | tool.injectScriptStringToPage(`;(${handleSheetSearchTree.toString()})();`);
339 | tool.listen(async (message) => {
340 | try {
341 | this.props.setRangeSelections(message.payload.rangeSelections);
342 | let rangeSelections = message.payload.rangeSelections;
343 | if (!rangeSelections) {
344 | return true;
345 | }
346 | // 需要等待表格数据拿到之后,再渲染SheetSearchTree组件
347 | await new Promise((resolve) => {
348 | this.setState(
349 | {
350 | // 清空树组件
351 | SheetSearchTree: null
352 | },
353 | () => {
354 | resolve();
355 | }
356 | );
357 | });
358 | this.setState({
359 | // 重新构造新的树组件
360 | SheetSearchTree: (
361 |
362 | )
363 | });
364 | return true;
365 | } catch (error) { }
366 | });
367 | }
368 | // 查找空节点
369 | emptyDom() {
370 | tool.injectScriptStringToPage(`;(${emptyDom.toString()})();`);
371 | }
372 | // 启用console.log函数
373 | useConsole() {
374 | tool.injectScriptStringToPage(`;(${useConsole.toString()})();`);
375 | }
376 | }
377 | const mapStateToProps = (state) => {
378 | return { ...state };
379 | };
380 | const mapDispatchToProps = (dispatch) => {
381 | return {
382 | setRangeSelections(rangeSelections) {
383 | dispatch({
384 | type: 'SET_RANGE_SELECTIONS',
385 | rangeSelections
386 | });
387 | },
388 | setFdAndRouteId({ fd, routeId }) {
389 | dispatch({
390 | type: 'SET_FD_AND_ROUTE_ID',
391 | fd,
392 | routeId
393 | });
394 | }
395 | };
396 | };
397 | export default connect(mapStateToProps, mapDispatchToProps)(ModelPanel);
398 |
--------------------------------------------------------------------------------
/src/__tests__/MediaStreamRecorder.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Last time updated: 2017-08-31 4:03:22 AM UTC
4 |
5 | // __________________________
6 | // MediaStreamRecorder v1.3.4
7 |
8 | // Open-Sourced: https://github.com/streamproc/MediaStreamRecorder
9 |
10 | // --------------------------------------------------
11 | // Muaz Khan - www.MuazKhan.com
12 | // MIT License - www.WebRTC-Experiment.com/licence
13 | // --------------------------------------------------
14 |
15 | // ______________________
16 | // MediaStreamRecorder.js
17 |
18 | function MediaStreamRecorder(mediaStream) {
19 | if (!mediaStream) {
20 | throw 'MediaStream is mandatory.';
21 | }
22 |
23 | // void start(optional long timeSlice)
24 | // timestamp to fire "ondataavailable"
25 | this.start = function(timeSlice) {
26 | var Recorder;
27 |
28 | if (typeof MediaRecorder !== 'undefined') {
29 | Recorder = MediaRecorderWrapper;
30 | } else if (IsChrome || IsOpera || IsEdge) {
31 | if (this.mimeType.indexOf('video') !== -1) {
32 | Recorder = WhammyRecorder;
33 | } else if (this.mimeType.indexOf('audio') !== -1) {
34 | Recorder = StereoAudioRecorder;
35 | }
36 | }
37 |
38 | // video recorder (in GIF format)
39 | if (this.mimeType === 'image/gif') {
40 | Recorder = GifRecorder;
41 | }
42 |
43 | // audio/wav is supported only via StereoAudioRecorder
44 | // audio/pcm (int16) is supported only via StereoAudioRecorder
45 | if (this.mimeType === 'audio/wav' || this.mimeType === 'audio/pcm') {
46 | Recorder = StereoAudioRecorder;
47 | }
48 |
49 | // allows forcing StereoAudioRecorder.js on Edge/Firefox
50 | if (this.recorderType) {
51 | Recorder = this.recorderType;
52 | }
53 |
54 | mediaRecorder = new Recorder(mediaStream);
55 | mediaRecorder.blobs = [];
56 |
57 | var self = this;
58 | mediaRecorder.ondataavailable = function(data) {
59 | mediaRecorder.blobs.push(data);
60 | self.ondataavailable(data);
61 | };
62 | mediaRecorder.onstop = this.onstop;
63 | mediaRecorder.onStartedDrawingNonBlankFrames = this.onStartedDrawingNonBlankFrames;
64 |
65 | // Merge all data-types except "function"
66 | mediaRecorder = mergeProps(mediaRecorder, this);
67 |
68 | mediaRecorder.start(timeSlice);
69 | };
70 |
71 | this.onStartedDrawingNonBlankFrames = function() {};
72 | this.clearOldRecordedFrames = function() {
73 | if (!mediaRecorder) {
74 | return;
75 | }
76 |
77 | mediaRecorder.clearOldRecordedFrames();
78 | };
79 |
80 | this.stop = function() {
81 | if (mediaRecorder) {
82 | mediaRecorder.stop();
83 | }
84 | };
85 |
86 | this.ondataavailable = function(blob) {
87 | if (this.disableLogs) return;
88 | console.log('ondataavailable..', blob);
89 | };
90 |
91 | this.onstop = function(error) {
92 | console.warn('stopped..', error);
93 | };
94 |
95 | this.save = function(file, fileName) {
96 | if (!file) {
97 | if (!mediaRecorder) {
98 | return;
99 | }
100 |
101 | ConcatenateBlobs(mediaRecorder.blobs, mediaRecorder.blobs[0].type, function(concatenatedBlob) {
102 | invokeSaveAsDialog(concatenatedBlob);
103 | });
104 | return;
105 | }
106 | invokeSaveAsDialog(file, fileName);
107 | };
108 |
109 | this.pause = function() {
110 | if (!mediaRecorder) {
111 | return;
112 | }
113 | mediaRecorder.pause();
114 |
115 | if (this.disableLogs) return;
116 | console.log('Paused recording.', this.mimeType || mediaRecorder.mimeType);
117 | };
118 |
119 | this.resume = function() {
120 | if (!mediaRecorder) {
121 | return;
122 | }
123 | mediaRecorder.resume();
124 |
125 | if (this.disableLogs) return;
126 | console.log('Resumed recording.', this.mimeType || mediaRecorder.mimeType);
127 | };
128 |
129 | // StereoAudioRecorder || WhammyRecorder || MediaRecorderWrapper || GifRecorder
130 | this.recorderType = null;
131 |
132 | // video/webm or audio/webm or audio/ogg or audio/wav
133 | this.mimeType = 'video/webm';
134 |
135 | // logs are enabled by default
136 | this.disableLogs = false;
137 |
138 | // Reference to "MediaRecorder.js"
139 | var mediaRecorder;
140 | }
141 |
142 | // ______________________
143 | // MultiStreamRecorder.js
144 |
145 | function MultiStreamRecorder(arrayOfMediaStreams, options) {
146 | arrayOfMediaStreams = arrayOfMediaStreams || [];
147 |
148 | if (arrayOfMediaStreams instanceof MediaStream) {
149 | arrayOfMediaStreams = [arrayOfMediaStreams];
150 | }
151 |
152 | var self = this;
153 |
154 | var mixer;
155 | var mediaRecorder;
156 |
157 | options = options || {
158 | mimeType: 'video/webm',
159 | video: {
160 | width: 360,
161 | height: 240
162 | }
163 | };
164 |
165 | if (!options.frameInterval) {
166 | options.frameInterval = 10;
167 | }
168 |
169 | if (!options.video) {
170 | options.video = {};
171 | }
172 |
173 | if (!options.video.width) {
174 | options.video.width = 360;
175 | }
176 |
177 | if (!options.video.height) {
178 | options.video.height = 240;
179 | }
180 |
181 | this.start = function(timeSlice) {
182 | // github/muaz-khan/MultiStreamsMixer
183 | mixer = new MultiStreamsMixer(arrayOfMediaStreams);
184 |
185 | if (getVideoTracks().length) {
186 | mixer.frameInterval = options.frameInterval || 10;
187 | mixer.width = options.video.width || 360;
188 | mixer.height = options.video.height || 240;
189 | mixer.startDrawingFrames();
190 | }
191 |
192 | if (typeof self.previewStream === 'function') {
193 | self.previewStream(mixer.getMixedStream());
194 | }
195 |
196 | // record using MediaRecorder API
197 | mediaRecorder = new MediaStreamRecorder(mixer.getMixedStream());
198 |
199 | for (var prop in self) {
200 | if (typeof self[prop] !== 'function') {
201 | mediaRecorder[prop] = self[prop];
202 | }
203 | }
204 |
205 | mediaRecorder.ondataavailable = function(blob) {
206 | self.ondataavailable(blob);
207 | };
208 |
209 | mediaRecorder.onstop = self.onstop;
210 |
211 | mediaRecorder.start(timeSlice);
212 | };
213 |
214 | function getVideoTracks() {
215 | var tracks = [];
216 | arrayOfMediaStreams.forEach(function(stream) {
217 | stream.getVideoTracks().forEach(function(track) {
218 | tracks.push(track);
219 | });
220 | });
221 | return tracks;
222 | }
223 |
224 | this.stop = function(callback) {
225 | if (!mediaRecorder) {
226 | return;
227 | }
228 |
229 | mediaRecorder.stop(function(blob) {
230 | callback(blob);
231 | });
232 | };
233 |
234 | this.pause = function() {
235 | if (mediaRecorder) {
236 | mediaRecorder.pause();
237 | }
238 | };
239 |
240 | this.resume = function() {
241 | if (mediaRecorder) {
242 | mediaRecorder.resume();
243 | }
244 | };
245 |
246 | this.clearRecordedData = function() {
247 | if (mediaRecorder) {
248 | mediaRecorder.clearRecordedData();
249 | mediaRecorder = null;
250 | }
251 |
252 | if (mixer) {
253 | mixer.releaseStreams();
254 | mixer = null;
255 | }
256 | };
257 |
258 | this.addStreams = this.addStream = function(streams) {
259 | if (!streams) {
260 | throw 'First parameter is required.';
261 | }
262 |
263 | if (!(streams instanceof Array)) {
264 | streams = [streams];
265 | }
266 |
267 | arrayOfMediaStreams.concat(streams);
268 |
269 | if (!mediaRecorder || !mixer) {
270 | return;
271 | }
272 |
273 | mixer.appendStreams(streams);
274 | };
275 |
276 | this.resetVideoStreams = function(streams) {
277 | if (!mixer) {
278 | return;
279 | }
280 |
281 | if (streams && !(streams instanceof Array)) {
282 | streams = [streams];
283 | }
284 |
285 | mixer.resetVideoStreams(streams);
286 | };
287 |
288 | this.ondataavailable = function(blob) {
289 | if (self.disableLogs) {
290 | return;
291 | }
292 |
293 | console.log('ondataavailable', blob);
294 | };
295 |
296 | this.onstop = function() {};
297 |
298 | // for debugging
299 | this.name = 'MultiStreamRecorder';
300 | this.toString = function() {
301 | return this.name;
302 | };
303 | }
304 |
305 | if (typeof MediaStreamRecorder !== 'undefined') {
306 | MediaStreamRecorder.MultiStreamRecorder = MultiStreamRecorder;
307 | }
308 |
309 | // Last time updated: 2017-08-31 2:56:12 AM UTC
310 |
311 | // ________________________
312 | // MultiStreamsMixer v1.0.2
313 |
314 | // Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer
315 |
316 | // --------------------------------------------------
317 | // Muaz Khan - www.MuazKhan.com
318 | // MIT License - www.WebRTC-Experiment.com/licence
319 | // --------------------------------------------------
320 |
321 | function MultiStreamsMixer(arrayOfMediaStreams) {
322 |
323 | // requires: chrome://flags/#enable-experimental-web-platform-features
324 |
325 | var videos = [];
326 | var isStopDrawingFrames = false;
327 |
328 | var canvas = document.createElement('canvas');
329 | var context = canvas.getContext('2d');
330 | canvas.style = 'opacity:0;position:absolute;z-index:-1;top: -100000000;left:-1000000000; margin-top:-1000000000;margin-left:-1000000000;';
331 | (document.body || document.documentElement).appendChild(canvas);
332 |
333 | this.disableLogs = false;
334 | this.frameInterval = 10;
335 |
336 | this.width = 360;
337 | this.height = 240;
338 |
339 | // use gain node to prevent echo
340 | this.useGainNode = true;
341 |
342 | var self = this;
343 |
344 | // _____________________________
345 | // Cross-Browser-Declarations.js
346 |
347 | // WebAudio API representer
348 | var AudioContext = window.AudioContext;
349 |
350 | if (typeof AudioContext === 'undefined') {
351 | if (typeof webkitAudioContext !== 'undefined') {
352 | /*global AudioContext:true */
353 | AudioContext = webkitAudioContext;
354 | }
355 |
356 | if (typeof mozAudioContext !== 'undefined') {
357 | /*global AudioContext:true */
358 | AudioContext = mozAudioContext;
359 | }
360 | }
361 |
362 | /*jshint -W079 */
363 | var URL = window.URL;
364 |
365 | if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
366 | /*global URL:true */
367 | URL = webkitURL;
368 | }
369 |
370 | if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator?
371 | if (typeof navigator.webkitGetUserMedia !== 'undefined') {
372 | navigator.getUserMedia = navigator.webkitGetUserMedia;
373 | }
374 |
375 | if (typeof navigator.mozGetUserMedia !== 'undefined') {
376 | navigator.getUserMedia = navigator.mozGetUserMedia;
377 | }
378 | }
379 |
380 | var MediaStream = window.MediaStream;
381 |
382 | if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
383 | MediaStream = webkitMediaStream;
384 | }
385 |
386 | /*global MediaStream:true */
387 | if (typeof MediaStream !== 'undefined') {
388 | if (!('getVideoTracks' in MediaStream.prototype)) {
389 | MediaStream.prototype.getVideoTracks = function() {
390 | if (!this.getTracks) {
391 | return [];
392 | }
393 |
394 | var tracks = [];
395 | this.getTracks.forEach(function(track) {
396 | if (track.kind.toString().indexOf('video') !== -1) {
397 | tracks.push(track);
398 | }
399 | });
400 | return tracks;
401 | };
402 |
403 | MediaStream.prototype.getAudioTracks = function() {
404 | if (!this.getTracks) {
405 | return [];
406 | }
407 |
408 | var tracks = [];
409 | this.getTracks.forEach(function(track) {
410 | if (track.kind.toString().indexOf('audio') !== -1) {
411 | tracks.push(track);
412 | }
413 | });
414 | return tracks;
415 | };
416 | }
417 |
418 | // override "stop" method for all browsers
419 | if (typeof MediaStream.prototype.stop === 'undefined') {
420 | MediaStream.prototype.stop = function() {
421 | this.getTracks().forEach(function(track) {
422 | track.stop();
423 | });
424 | };
425 | }
426 | }
427 |
428 | var Storage = {};
429 |
430 | if (typeof AudioContext !== 'undefined') {
431 | Storage.AudioContext = AudioContext;
432 | } else if (typeof webkitAudioContext !== 'undefined') {
433 | Storage.AudioContext = webkitAudioContext;
434 | }
435 |
436 | this.startDrawingFrames = function() {
437 | drawVideosToCanvas();
438 | };
439 |
440 | function drawVideosToCanvas() {
441 | if (isStopDrawingFrames) {
442 | return;
443 | }
444 |
445 | var videosLength = videos.length;
446 |
447 | var fullcanvas = false;
448 | var remaining = [];
449 | videos.forEach(function(video) {
450 | if (!video.stream) {
451 | video.stream = {};
452 | }
453 |
454 | if (video.stream.fullcanvas) {
455 | fullcanvas = video;
456 | } else {
457 | remaining.push(video);
458 | }
459 | });
460 |
461 | if (fullcanvas) {
462 | canvas.width = fullcanvas.stream.width;
463 | canvas.height = fullcanvas.stream.height;
464 | } else if (remaining.length) {
465 | canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width;
466 | canvas.height = videosLength > 2 ? remaining[0].height * 2 : remaining[0].height;
467 | } else {
468 | canvas.width = self.width || 360;
469 | canvas.height = self.height || 240;
470 | }
471 |
472 | if (fullcanvas && fullcanvas instanceof HTMLVideoElement) {
473 | drawImage(fullcanvas);
474 | }
475 |
476 | remaining.forEach(function(video, idx) {
477 | drawImage(video, idx);
478 | });
479 |
480 | setTimeout(drawVideosToCanvas, self.frameInterval);
481 | }
482 |
483 | function drawImage(video, idx) {
484 | if (isStopDrawingFrames) {
485 | return;
486 | }
487 |
488 | var x = 0;
489 | var y = 0;
490 | var width = video.width;
491 | var height = video.height;
492 |
493 | if (idx === 1) {
494 | x = video.width;
495 | }
496 |
497 | if (idx === 2) {
498 | y = video.height;
499 | }
500 |
501 | if (idx === 3) {
502 | x = video.width;
503 | y = video.height;
504 | }
505 |
506 | if (typeof video.stream.left !== 'undefined') {
507 | x = video.stream.left;
508 | }
509 |
510 | if (typeof video.stream.top !== 'undefined') {
511 | y = video.stream.top;
512 | }
513 |
514 | if (typeof video.stream.width !== 'undefined') {
515 | width = video.stream.width;
516 | }
517 |
518 | if (typeof video.stream.height !== 'undefined') {
519 | height = video.stream.height;
520 | }
521 |
522 | context.drawImage(video, x, y, width, height);
523 |
524 | if (typeof video.stream.onRender === 'function') {
525 | video.stream.onRender(context, x, y, width, height, idx);
526 | }
527 | }
528 |
529 | function getMixedStream() {
530 | isStopDrawingFrames = false;
531 | var mixedVideoStream = getMixedVideoStream();
532 |
533 | var mixedAudioStream = getMixedAudioStream();
534 | if (mixedAudioStream) {
535 | mixedAudioStream.getAudioTracks().forEach(function(track) {
536 | mixedVideoStream.addTrack(track);
537 | });
538 | }
539 |
540 | var fullcanvas;
541 | arrayOfMediaStreams.forEach(function(stream) {
542 | if (stream.fullcanvas) {
543 | fullcanvas = true;
544 | }
545 | });
546 |
547 | return mixedVideoStream;
548 | }
549 |
550 | function getMixedVideoStream() {
551 | resetVideoStreams();
552 |
553 | var capturedStream;
554 |
555 | if ('captureStream' in canvas) {
556 | capturedStream = canvas.captureStream();
557 | } else if ('mozCaptureStream' in canvas) {
558 | capturedStream = canvas.mozCaptureStream();
559 | } else if (!self.disableLogs) {
560 | console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features');
561 | }
562 |
563 | var videoStream = new MediaStream();
564 |
565 | capturedStream.getVideoTracks().forEach(function(track) {
566 | videoStream.addTrack(track);
567 | });
568 |
569 | canvas.stream = videoStream;
570 |
571 | return videoStream;
572 | }
573 |
574 | function getMixedAudioStream() {
575 | // via: @pehrsons
576 | if (!Storage.AudioContextConstructor) {
577 | Storage.AudioContextConstructor = new Storage.AudioContext();
578 | }
579 |
580 | self.audioContext = Storage.AudioContextConstructor;
581 |
582 | self.audioSources = [];
583 |
584 | if (self.useGainNode === true) {
585 | self.gainNode = self.audioContext.createGain();
586 | self.gainNode.connect(self.audioContext.destination);
587 | self.gainNode.gain.value = 0; // don't hear self
588 | }
589 |
590 | var audioTracksLength = 0;
591 | arrayOfMediaStreams.forEach(function(stream) {
592 | if (!stream.getAudioTracks().length) {
593 | return;
594 | }
595 |
596 | audioTracksLength++;
597 |
598 | var audioSource = self.audioContext.createMediaStreamSource(stream);
599 |
600 | if (self.useGainNode === true) {
601 | audioSource.connect(self.gainNode);
602 | }
603 |
604 | self.audioSources.push(audioSource);
605 | });
606 |
607 | if (!audioTracksLength) {
608 | return;
609 | }
610 |
611 | self.audioDestination = self.audioContext.createMediaStreamDestination();
612 | self.audioSources.forEach(function(audioSource) {
613 | audioSource.connect(self.audioDestination);
614 | });
615 | return self.audioDestination.stream;
616 | }
617 |
618 | function getVideo(stream) {
619 | var video = document.createElement('video');
620 |
621 | if ('srcObject' in video) {
622 | video.srcObject = stream;
623 | } else {
624 | video.src = URL.createObjectURL(stream);
625 | }
626 |
627 | video.muted = true;
628 | video.volume = 0;
629 |
630 | video.width = stream.width || self.width || 360;
631 | video.height = stream.height || self.height || 240;
632 |
633 | video.play();
634 |
635 | return video;
636 | }
637 |
638 | this.appendStreams = function(streams) {
639 | if (!streams) {
640 | throw 'First parameter is required.';
641 | }
642 |
643 | if (!(streams instanceof Array)) {
644 | streams = [streams];
645 | }
646 |
647 | arrayOfMediaStreams.concat(streams);
648 |
649 | streams.forEach(function(stream) {
650 | if (stream.getVideoTracks().length) {
651 | var video = getVideo(stream);
652 | video.stream = stream;
653 | videos.push(video);
654 | }
655 |
656 | if (stream.getAudioTracks().length && self.audioContext) {
657 | var audioSource = self.audioContext.createMediaStreamSource(stream);
658 | audioSource.connect(self.audioDestination);
659 | self.audioSources.push(audioSource);
660 | }
661 | });
662 | };
663 |
664 | this.releaseStreams = function() {
665 | videos = [];
666 | isStopDrawingFrames = true;
667 |
668 | if (self.gainNode) {
669 | self.gainNode.disconnect();
670 | self.gainNode = null;
671 | }
672 |
673 | if (self.audioSources.length) {
674 | self.audioSources.forEach(function(source) {
675 | source.disconnect();
676 | });
677 | self.audioSources = [];
678 | }
679 |
680 | if (self.audioDestination) {
681 | self.audioDestination.disconnect();
682 | self.audioDestination = null;
683 | }
684 |
685 | self.audioContext = null;
686 |
687 | context.clearRect(0, 0, canvas.width, canvas.height);
688 |
689 | if (canvas.stream) {
690 | canvas.stream.stop();
691 | canvas.stream = null;
692 | }
693 | };
694 |
695 | this.resetVideoStreams = function(streams) {
696 | if (streams && !(streams instanceof Array)) {
697 | streams = [streams];
698 | }
699 |
700 | resetVideoStreams(streams);
701 | };
702 |
703 | function resetVideoStreams(streams) {
704 | videos = [];
705 | streams = streams || arrayOfMediaStreams;
706 |
707 | // via: @adrian-ber
708 | streams.forEach(function(stream) {
709 | if (!stream.getVideoTracks().length) {
710 | return;
711 | }
712 |
713 | var video = getVideo(stream);
714 | video.stream = stream;
715 | videos.push(video);
716 | });
717 | }
718 |
719 | // for debugging
720 | this.name = 'MultiStreamsMixer';
721 | this.toString = function() {
722 | return this.name;
723 | };
724 |
725 | this.getMixedStream = getMixedStream;
726 |
727 | }
728 |
729 | // _____________________________
730 | // Cross-Browser-Declarations.js
731 |
732 | var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45';
733 |
734 | (function(that) {
735 | if (typeof window !== 'undefined') {
736 | return;
737 | }
738 |
739 | if (typeof window === 'undefined' && typeof global !== 'undefined') {
740 | global.navigator = {
741 | userAgent: browserFakeUserAgent,
742 | getUserMedia: function() {}
743 | };
744 |
745 | /*global window:true */
746 | that.window = global;
747 | } else if (typeof window === 'undefined') {
748 | // window = this;
749 | }
750 |
751 | if (typeof document === 'undefined') {
752 | /*global document:true */
753 | that.document = {};
754 |
755 | document.createElement = document.captureStream = document.mozCaptureStream = function() {
756 | return {};
757 | };
758 | }
759 |
760 | if (typeof location === 'undefined') {
761 | /*global location:true */
762 | that.location = {
763 | protocol: 'file:',
764 | href: '',
765 | hash: ''
766 | };
767 | }
768 |
769 | if (typeof screen === 'undefined') {
770 | /*global screen:true */
771 | that.screen = {
772 | width: 0,
773 | height: 0
774 | };
775 | }
776 | })(typeof global !== 'undefined' ? global : window);
777 |
778 | // WebAudio API representer
779 | var AudioContext = window.AudioContext;
780 |
781 | if (typeof AudioContext === 'undefined') {
782 | if (typeof webkitAudioContext !== 'undefined') {
783 | /*global AudioContext:true */
784 | AudioContext = webkitAudioContext;
785 | }
786 |
787 | if (typeof mozAudioContext !== 'undefined') {
788 | /*global AudioContext:true */
789 | AudioContext = mozAudioContext;
790 | }
791 | }
792 |
793 | if (typeof window === 'undefined') {
794 | /*jshint -W020 */
795 | window = {};
796 | }
797 |
798 | // WebAudio API representer
799 | var AudioContext = window.AudioContext;
800 |
801 | if (typeof AudioContext === 'undefined') {
802 | if (typeof webkitAudioContext !== 'undefined') {
803 | /*global AudioContext:true */
804 | AudioContext = webkitAudioContext;
805 | }
806 |
807 | if (typeof mozAudioContext !== 'undefined') {
808 | /*global AudioContext:true */
809 | AudioContext = mozAudioContext;
810 | }
811 | }
812 |
813 | /*jshint -W079 */
814 | var URL = window.URL;
815 |
816 | if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
817 | /*global URL:true */
818 | URL = webkitURL;
819 | }
820 |
821 | if (typeof navigator !== 'undefined') {
822 | if (typeof navigator.webkitGetUserMedia !== 'undefined') {
823 | navigator.getUserMedia = navigator.webkitGetUserMedia;
824 | }
825 |
826 | if (typeof navigator.mozGetUserMedia !== 'undefined') {
827 | navigator.getUserMedia = navigator.mozGetUserMedia;
828 | }
829 | } else {
830 | navigator = {
831 | getUserMedia: function() {},
832 | userAgent: browserFakeUserAgent
833 | };
834 | }
835 |
836 | var IsEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob);
837 |
838 | var IsOpera = false;
839 | if (typeof opera !== 'undefined' && navigator.userAgent && navigator.userAgent.indexOf('OPR/') !== -1) {
840 | IsOpera = true;
841 | }
842 | var IsChrome = !IsEdge && !IsEdge && !!navigator.webkitGetUserMedia;
843 |
844 | var MediaStream = window.MediaStream;
845 |
846 | if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
847 | MediaStream = webkitMediaStream;
848 | }
849 |
850 | /*global MediaStream:true */
851 | if (typeof MediaStream !== 'undefined') {
852 | if (!('getVideoTracks' in MediaStream.prototype)) {
853 | MediaStream.prototype.getVideoTracks = function() {
854 | if (!this.getTracks) {
855 | return [];
856 | }
857 |
858 | var tracks = [];
859 | this.getTracks.forEach(function(track) {
860 | if (track.kind.toString().indexOf('video') !== -1) {
861 | tracks.push(track);
862 | }
863 | });
864 | return tracks;
865 | };
866 |
867 | MediaStream.prototype.getAudioTracks = function() {
868 | if (!this.getTracks) {
869 | return [];
870 | }
871 |
872 | var tracks = [];
873 | this.getTracks.forEach(function(track) {
874 | if (track.kind.toString().indexOf('audio') !== -1) {
875 | tracks.push(track);
876 | }
877 | });
878 | return tracks;
879 | };
880 | }
881 |
882 | if (!('stop' in MediaStream.prototype)) {
883 | MediaStream.prototype.stop = function() {
884 | this.getAudioTracks().forEach(function(track) {
885 | if (!!track.stop) {
886 | track.stop();
887 | }
888 | });
889 |
890 | this.getVideoTracks().forEach(function(track) {
891 | if (!!track.stop) {
892 | track.stop();
893 | }
894 | });
895 | };
896 | }
897 | }
898 |
899 | if (typeof location !== 'undefined') {
900 | if (location.href.indexOf('file:') === 0) {
901 | console.error('Please load this HTML file on HTTP or HTTPS.');
902 | }
903 | }
904 |
905 | // Merge all other data-types except "function"
906 |
907 | function mergeProps(mergein, mergeto) {
908 | for (var t in mergeto) {
909 | if (typeof mergeto[t] !== 'function') {
910 | mergein[t] = mergeto[t];
911 | }
912 | }
913 | return mergein;
914 | }
915 |
916 | // "dropFirstFrame" has been added by Graham Roth
917 | // https://github.com/gsroth
918 |
919 | function dropFirstFrame(arr) {
920 | arr.shift();
921 | return arr;
922 | }
923 |
924 | /**
925 | * @param {Blob} file - File or Blob object. This parameter is required.
926 | * @param {string} fileName - Optional file name e.g. "Recorded-Video.webm"
927 | * @example
928 | * invokeSaveAsDialog(blob or file, [optional] fileName);
929 | * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
930 | */
931 | function invokeSaveAsDialog(file, fileName) {
932 | if (!file) {
933 | throw 'Blob object is required.';
934 | }
935 |
936 | if (!file.type) {
937 | try {
938 | file.type = 'video/webm';
939 | } catch (e) {}
940 | }
941 |
942 | var fileExtension = (file.type || 'video/webm').split('/')[1];
943 |
944 | if (fileName && fileName.indexOf('.') !== -1) {
945 | var splitted = fileName.split('.');
946 | fileName = splitted[0];
947 | fileExtension = splitted[1];
948 | }
949 |
950 | var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension;
951 |
952 | if (typeof navigator.msSaveOrOpenBlob !== 'undefined') {
953 | return navigator.msSaveOrOpenBlob(file, fileFullName);
954 | } else if (typeof navigator.msSaveBlob !== 'undefined') {
955 | return navigator.msSaveBlob(file, fileFullName);
956 | }
957 |
958 | var hyperlink = document.createElement('a');
959 | hyperlink.href = URL.createObjectURL(file);
960 | hyperlink.target = '_blank';
961 | hyperlink.download = fileFullName;
962 |
963 | if (!!navigator.mozGetUserMedia) {
964 | hyperlink.onclick = function() {
965 | (document.body || document.documentElement).removeChild(hyperlink);
966 | };
967 | (document.body || document.documentElement).appendChild(hyperlink);
968 | }
969 |
970 | var evt = new MouseEvent('click', {
971 | view: window,
972 | bubbles: true,
973 | cancelable: true
974 | });
975 |
976 | hyperlink.dispatchEvent(evt);
977 |
978 | if (!navigator.mozGetUserMedia) {
979 | URL.revokeObjectURL(hyperlink.href);
980 | }
981 | }
982 |
983 | function bytesToSize(bytes) {
984 | var k = 1000;
985 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
986 | if (bytes === 0) {
987 | return '0 Bytes';
988 | }
989 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10);
990 | return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
991 | }
992 |
993 | // ______________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129
994 | // ObjectStore.js
995 | var ObjectStore = {
996 | AudioContext: AudioContext
997 | };
998 |
999 | function isMediaRecorderCompatible() {
1000 | var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
1001 | var isChrome = !!window.chrome && !isOpera;
1002 | var isFirefox = typeof window.InstallTrigger !== 'undefined';
1003 |
1004 | if (isFirefox) {
1005 | return true;
1006 | }
1007 |
1008 | if (!isChrome) {
1009 | return false;
1010 | }
1011 |
1012 | var nVer = navigator.appVersion;
1013 | var nAgt = navigator.userAgent;
1014 | var fullVersion = '' + parseFloat(navigator.appVersion);
1015 | var majorVersion = parseInt(navigator.appVersion, 10);
1016 | var nameOffset, verOffset, ix;
1017 |
1018 | if (isChrome) {
1019 | verOffset = nAgt.indexOf('Chrome');
1020 | fullVersion = nAgt.substring(verOffset + 7);
1021 | }
1022 |
1023 | // trim the fullVersion string at semicolon/space if present
1024 | if ((ix = fullVersion.indexOf(';')) !== -1) {
1025 | fullVersion = fullVersion.substring(0, ix);
1026 | }
1027 |
1028 | if ((ix = fullVersion.indexOf(' ')) !== -1) {
1029 | fullVersion = fullVersion.substring(0, ix);
1030 | }
1031 |
1032 | majorVersion = parseInt('' + fullVersion, 10);
1033 |
1034 | if (isNaN(majorVersion)) {
1035 | fullVersion = '' + parseFloat(navigator.appVersion);
1036 | majorVersion = parseInt(navigator.appVersion, 10);
1037 | }
1038 |
1039 | return majorVersion >= 49;
1040 | }
1041 |
1042 | // ==================
1043 | // MediaRecorder.js
1044 |
1045 | /**
1046 | * Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html
1047 | * The MediaRecorder accepts a mediaStream as input source passed from UA. When recorder starts,
1048 | * a MediaEncoder will be created and accept the mediaStream as input source.
1049 | * Encoder will get the raw data by track data changes, encode it by selected MIME Type, then store the encoded in EncodedBufferCache object.
1050 | * The encoded data will be extracted on every timeslice passed from Start function call or by RequestData function.
1051 | * Thread model:
1052 | * When the recorder starts, it creates a "Media Encoder" thread to read data from MediaEncoder object and store buffer in EncodedBufferCache object.
1053 | * Also extract the encoded data and create blobs on every timeslice passed from start function or RequestData function called by UA.
1054 | */
1055 |
1056 | function MediaRecorderWrapper(mediaStream) {
1057 | var self = this;
1058 |
1059 | /**
1060 | * This method records MediaStream.
1061 | * @method
1062 | * @memberof MediaStreamRecorder
1063 | * @example
1064 | * recorder.start(5000);
1065 | */
1066 | this.start = function(timeSlice, __disableLogs) {
1067 | this.timeSlice = timeSlice || 5000;
1068 |
1069 | if (!self.mimeType) {
1070 | self.mimeType = 'video/webm';
1071 | }
1072 |
1073 | if (self.mimeType.indexOf('audio') !== -1) {
1074 | if (mediaStream.getVideoTracks().length && mediaStream.getAudioTracks().length) {
1075 | var stream;
1076 | if (!!navigator.mozGetUserMedia) {
1077 | stream = new MediaStream();
1078 | stream.addTrack(mediaStream.getAudioTracks()[0]);
1079 | } else {
1080 | // webkitMediaStream
1081 | stream = new MediaStream(mediaStream.getAudioTracks());
1082 | }
1083 | mediaStream = stream;
1084 | }
1085 | }
1086 |
1087 | if (self.mimeType.indexOf('audio') !== -1) {
1088 | self.mimeType = IsChrome ? 'audio/webm' : 'audio/ogg';
1089 | }
1090 |
1091 | self.dontFireOnDataAvailableEvent = false;
1092 |
1093 | var recorderHints = {
1094 | mimeType: self.mimeType
1095 | };
1096 |
1097 | if (!self.disableLogs && !__disableLogs) {
1098 | console.log('Passing following params over MediaRecorder API.', recorderHints);
1099 | }
1100 |
1101 | if (mediaRecorder) {
1102 | // mandatory to make sure Firefox doesn't fails to record streams 3-4 times without reloading the page.
1103 | mediaRecorder = null;
1104 | }
1105 |
1106 | if (IsChrome && !isMediaRecorderCompatible()) {
1107 | // to support video-only recording on stable
1108 | recorderHints = 'video/vp8';
1109 | }
1110 |
1111 | // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp
1112 | // https://wiki.mozilla.org/Gecko:MediaRecorder
1113 | // https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html
1114 |
1115 | // starting a recording session; which will initiate "Reading Thread"
1116 | // "Reading Thread" are used to prevent main-thread blocking scenarios
1117 | try {
1118 | mediaRecorder = new MediaRecorder(mediaStream, recorderHints);
1119 | } catch (e) {
1120 | // if someone passed NON_supported mimeType
1121 | // or if Firefox on Android
1122 | mediaRecorder = new MediaRecorder(mediaStream);
1123 | }
1124 |
1125 | if ('canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(self.mimeType) === false) {
1126 | if (!self.disableLogs) {
1127 | console.warn('MediaRecorder API seems unable to record mimeType:', self.mimeType);
1128 | }
1129 | }
1130 |
1131 | // i.e. stop recording when