├── .editorconfig ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ └── hello-world.js ├── assets │ └── logo.png ├── components │ ├── RemoteControl.vue │ └── js │ │ ├── config.js │ │ └── utils.js ├── main.js └── utils │ └── request.js └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV = development 2 | ENV = 'development' 3 | 4 | VUE_APP_BASE_API = '/api' 5 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | ENV = 'production' 3 | 4 | VUE_APP_BASE_API = '/api' 5 | VUE_APP_BASE = '/' 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | package-lock.json 6 | editTree.vue 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | 'vue/script-setup-uses-vars': 'off', 18 | "vue/max-attributes-per-line": [2, { 19 | "singleline": 10, 20 | "multiline": { 21 | "max": 1, 22 | "allowFirstLine": false 23 | } 24 | }], 25 | "vue/singleline-html-element-content-newline": "off", 26 | "vue/multiline-html-element-content-newline": "off", 27 | "vue/name-property-casing": ["error", "PascalCase"], 28 | "vue/no-v-html": "off", 29 | 'accessor-pairs': 2, 30 | 'arrow-spacing': [2, { 31 | 'before': true, 32 | 'after': true 33 | }], 34 | 'block-spacing': [2, 'always'], 35 | 'brace-style': [2, '1tbs', { 36 | 'allowSingleLine': true 37 | }], 38 | 'camelcase': [0, { 39 | 'properties': 'always' 40 | }], 41 | 'comma-dangle': [2, 'never'], 42 | 'comma-spacing': [2, { 43 | 'before': false, 44 | 'after': true 45 | }], 46 | 'comma-style': [2, 'last'], 47 | 'constructor-super': 2, 48 | 'curly': [2, 'multi-line'], 49 | 'dot-location': [2, 'property'], 50 | 'eol-last': 2, 51 | 'eqeqeq': ["error", "always", { "null": "ignore" }], 52 | 'generator-star-spacing': [2, { 53 | 'before': true, 54 | 'after': true 55 | }], 56 | 'handle-callback-err': [2, '^(err|error)$'], 57 | 'indent': [2, 2, { 58 | 'SwitchCase': 1 59 | }], 60 | 'jsx-quotes': [2, 'prefer-single'], 61 | 'key-spacing': [2, { 62 | 'beforeColon': false, 63 | 'afterColon': true 64 | }], 65 | 'keyword-spacing': [2, { 66 | 'before': true, 67 | 'after': true 68 | }], 69 | 'new-cap': [2, { 70 | 'newIsCap': true, 71 | 'capIsNew': false 72 | }], 73 | 'new-parens': 2, 74 | 'no-array-constructor': 2, 75 | 'no-caller': 2, 76 | 'no-console': 'off', 77 | 'no-class-assign': 2, 78 | 'no-cond-assign': 2, 79 | 'no-const-assign': 2, 80 | 'no-control-regex': 0, 81 | 'no-delete-var': 2, 82 | 'no-dupe-args': 2, 83 | 'no-dupe-class-members': 2, 84 | 'no-dupe-keys': 2, 85 | 'no-duplicate-case': 2, 86 | 'no-empty-character-class': 2, 87 | 'no-empty-pattern': 2, 88 | 'no-eval': 2, 89 | 'no-ex-assign': 2, 90 | 'no-extend-native': 2, 91 | 'no-extra-bind': 2, 92 | 'no-extra-boolean-cast': 2, 93 | 'no-extra-parens': [2, 'functions'], 94 | 'no-fallthrough': 2, 95 | 'no-floating-decimal': 2, 96 | 'no-func-assign': 2, 97 | 'no-implied-eval': 2, 98 | 'no-inner-declarations': [2, 'functions'], 99 | 'no-invalid-regexp': 2, 100 | 'no-irregular-whitespace': 2, 101 | 'no-iterator': 2, 102 | 'no-label-var': 2, 103 | 'no-labels': [2, { 104 | 'allowLoop': false, 105 | 'allowSwitch': false 106 | }], 107 | 'no-lone-blocks': 2, 108 | 'no-mixed-spaces-and-tabs': 2, 109 | 'no-multi-spaces': 2, 110 | 'no-multi-str': 2, 111 | 'no-multiple-empty-lines': [2, { 112 | 'max': 1 113 | }], 114 | 'no-native-reassign': 2, 115 | 'no-negated-in-lhs': 2, 116 | 'no-new-object': 2, 117 | 'no-new-require': 2, 118 | 'no-new-symbol': 2, 119 | 'no-new-wrappers': 2, 120 | 'no-obj-calls': 2, 121 | 'no-octal': 2, 122 | 'no-octal-escape': 2, 123 | 'no-path-concat': 2, 124 | 'no-proto': 2, 125 | 'no-redeclare': 2, 126 | 'no-regex-spaces': 2, 127 | 'no-return-assign': [2, 'except-parens'], 128 | 'no-self-assign': 2, 129 | 'no-self-compare': 2, 130 | 'no-sequences': 2, 131 | 'no-shadow-restricted-names': 2, 132 | 'no-spaced-func': 2, 133 | 'no-sparse-arrays': 2, 134 | 'no-this-before-super': 2, 135 | 'no-throw-literal': 2, 136 | 'no-trailing-spaces': 2, 137 | 'no-undef': 2, 138 | 'no-undef-init': 2, 139 | 'no-unexpected-multiline': 2, 140 | 'no-unmodified-loop-condition': 2, 141 | 'no-unneeded-ternary': [2, { 142 | 'defaultAssignment': false 143 | }], 144 | 'no-unreachable': 2, 145 | 'no-unsafe-finally': 2, 146 | 'no-unused-vars': [2, { 147 | 'vars': 'all', 148 | 'args': 'none' 149 | }], 150 | 'no-useless-call': 2, 151 | 'no-useless-computed-key': 2, 152 | 'no-useless-constructor': 2, 153 | 'no-useless-escape': 0, 154 | 'no-whitespace-before-property': 2, 155 | 'no-with': 2, 156 | 'one-var': [2, { 157 | 'initialized': 'never' 158 | }], 159 | 'operator-linebreak': [2, 'after', { 160 | 'overrides': { 161 | '?': 'before', 162 | ':': 'before' 163 | } 164 | }], 165 | 'padded-blocks': [2, 'never'], 166 | 'quotes': [2, 'single', { 167 | 'avoidEscape': true, 168 | 'allowTemplateLiterals': true 169 | }], 170 | 'semi': [2, 'never'], 171 | 'semi-spacing': [2, { 172 | 'before': false, 173 | 'after': true 174 | }], 175 | 'space-before-blocks': [2, 'always'], 176 | 'space-before-function-paren': [2, 'never'], 177 | 'space-in-parens': [2, 'never'], 178 | 'space-infix-ops': 2, 179 | 'space-unary-ops': [2, { 180 | 'words': true, 181 | 'nonwords': false 182 | }], 183 | 'spaced-comment': [2, 'always', { 184 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 185 | }], 186 | 'template-curly-spacing': [2, 'never'], 187 | 'use-isnan': 2, 188 | 'valid-typeof': 2, 189 | 'wrap-iife': [2, 'any'], 190 | 'yield-star-spacing': [2, 'both'], 191 | 'yoda': [2, 'never'], 192 | 'prefer-const': 2, 193 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 194 | 'object-curly-spacing': [2, 'always', { 195 | objectsInObjects: false 196 | }], 197 | 'array-bracket-spacing': [2, 'never'] 198 | } 199 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue2-element-ui 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-element-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.3.4", 13 | "core-js": "^3.6.5", 14 | "element-ui": "^2.15.13", 15 | "vue": "^2.6.11" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "~4.5.19", 19 | "@vue/cli-plugin-eslint": "~4.5.19", 20 | "@vue/cli-service": "~4.5.19", 21 | "@vue/eslint-config-airbnb": "^5.0.2", 22 | "babel-eslint": "^10.1.0", 23 | "eslint": "^6.7.2", 24 | "eslint-plugin-import": "^2.20.2", 25 | "eslint-plugin-vue": "^6.2.2", 26 | "node-sass": "^4.14.1", 27 | "sass-loader": "^7.3.1", 28 | "vue-template-compiler": "^2.6.11" 29 | }, 30 | "eslintConfig": { 31 | "root": true, 32 | "env": { 33 | "node": true 34 | }, 35 | "extends": [ 36 | "plugin:vue/essential", 37 | "@vue/airbnb" 38 | ], 39 | "parserOptions": { 40 | "parser": "babel-eslint" 41 | }, 42 | "rules": {} 43 | }, 44 | "browserslist": [ 45 | "> 1%", 46 | "last 2 versions", 47 | "not dead" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xmengling/remote-control-webrtc/fd66ebe2085c8324841b0c7164a1bbc61d2a8425/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | 46 | -------------------------------------------------------------------------------- /src/api/hello-world.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function apiTest() { 4 | return request({ 5 | url: '/meeting/rooms/area', 6 | method: 'get' 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xmengling/remote-control-webrtc/fd66ebe2085c8324841b0c7164a1bbc61d2a8425/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/RemoteControl.vue: -------------------------------------------------------------------------------- 1 | 2 | 29 | 30 | 451 | 452 | 476 | 485 | -------------------------------------------------------------------------------- /src/components/js/config.js: -------------------------------------------------------------------------------- 1 | export const SERVERS = { 2 | iceServers: [ 3 | { url: 'stun:203.135.97.160:3478' }, 4 | { url: 'turn:203.135.97.160:3478', username: 'crh', credential: '123456' } 5 | ] 6 | // iceServers: [ 7 | // { urls: 'stun:dss-coturn.cvte.com:3478' }, 8 | // { urls: 'turn:dss-coturn.cvte.com:3478', username: 'test', credential: '123456' } 9 | // ] 10 | } 11 | 12 | export const PC_OPTIONS = { 13 | optional: [ 14 | { DtlsSrtpKeyAgreement: true } 15 | ] 16 | } 17 | 18 | export const OFFER_OPTIONS = { 19 | offerToReceiveAudio: 0, 20 | offerToReceiveVideo: 1, 21 | voiceActivityDetection: false, 22 | iceRestart: true 23 | } 24 | 25 | export const SOCKET_BASE_URL = 'ws://websocket-dev.intcloud.h3c.com/websocket/starter/' 26 | -------------------------------------------------------------------------------- /src/components/js/utils.js: -------------------------------------------------------------------------------- 1 | export function parseIce(candidateString) { 2 | // token = 1*(alphanum / "-" / "." / "!" / "%" / "*" 3 | // / "_" / "+" / "`" / "'" / "~" ) 4 | const token_re = '[0-9a-zA-Z\\-\\.!\\%\\*_\\+\\`\\\'\\~]+' 5 | // ice-char = ALPHA / DIGIT / "+" / "/" 6 | const ice_char_re = '[a-zA-Z0-9\\+\\/]+' 7 | // foundation = 1*32ice-char 8 | const foundation_re = ice_char_re 9 | // component-id = 1*5DIGIT 10 | const component_id_re = '[0-9]{1,5}' 11 | // transport = "UDP" / transport-extension 12 | // transport-extension = token ; from RFC 3261 13 | const transport_re = token_re 14 | // priority = 1*10DIGIT 15 | const priority_re = '[0-9]{1,10}' 16 | // connection-address SP ; from RFC 4566 17 | const connection_address_v4_re = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}' 18 | const connection_address_v6_re = '\\:?(?:[0-9a-fA-F]{0,4}\\:?)+' // fde8:cd2d:634c:6b00:6deb:9894:734:f75f 19 | const connection_address_re = '(?:' + connection_address_v4_re + ')|(?:' + connection_address_v6_re + ')' 20 | // port ; port from RFC 4566 21 | const port_re = '[0-9]{1,5}' 22 | // cand-type = "typ" SP candidate-types 23 | // candidate-types = "host" / "srflx" / "prflx" / "relay" / token 24 | const cand_type_re = token_re 25 | const ICE_RE = '(?:a=)?candidate:(' + foundation_re + ')' + // candidate:599991555 // 'a=' not passed for Firefox (and now for Chrome too) 26 | '\\s' + '(' + component_id_re + ')' + // 2 27 | '\\s' + '(' + transport_re + ')' + // udp 28 | '\\s' + '(' + priority_re + ')' + // 2122260222 29 | '\\s' + '(' + connection_address_re + ')' + // 192.168.1.32 || fde8:cd2d:634c:6b00:6deb:9894:734:f75f 30 | '\\s' + '(' + port_re + ')' + // 49827 31 | '\\s' + 'typ' + // typ 32 | '\\s' + '(' + cand_type_re + ')' + // host 33 | '(?:' + 34 | '\\s' + 'raddr' + 35 | '\\s' + '(' + connection_address_re + ')' + 36 | '\\s' + 'rport' + 37 | '\\s' + '(' + port_re + ')' + 38 | ')?' + 39 | '(?:' + 40 | '\\s' + 'generation' + // generation 41 | '\\s' + '(' + '\\d+' + ')' + // 0 42 | ')?' + 43 | '(?:' + 44 | '\\s' + 'ufrag' + // ufrag 45 | '\\s' + '(' + ice_char_re + ')' + // WreAYwhmkiw6SPvs 46 | ')?' 47 | const pattern = new RegExp(ICE_RE) 48 | const parsed = candidateString.match(pattern) 49 | // Check if the string was successfully parsed 50 | if (!parsed) { 51 | console.warn('parseIceCandidate(): parsed is empty: \'' + parsed + '\'') 52 | return null 53 | } 54 | const propNames = [ 55 | 'foundation', 56 | 'component_id', 57 | 'transport', 58 | 'priority', 59 | 'localIP', 60 | 'localPort', 61 | 'type', 62 | 'remoteIP', 63 | 'remotePort', 64 | 'generation', 65 | 'ufrag' 66 | ] 67 | const candObj = {} 68 | for (let i = 0; i < propNames.length; i++) { 69 | candObj[propNames[i]] = parsed[i + 1] 70 | } 71 | return candObj 72 | } 73 | 74 | // 解析视频流信息 75 | export function formatStat(o) { 76 | var s = '' 77 | if (o !== undefined && o !== null) { 78 | s += o.type + ': ' + new Date(o.timestamp).toISOString() + '
' 79 | if (o.ssrc) s += 'SSRC: ' + o.ssrc + ' ' 80 | if (o.packetsReceived !== undefined) { 81 | s += 'Recvd: ' + o.packetsReceived + ' packets (' + 82 | (o.bytesReceived / 1000000).toFixed(2) + ' MB)' + ' Lost: ' + o.packetsLost 83 | } else if (o.packetsSent !== undefined) { 84 | s += 'Sent: ' + o.packetsSent + ' packets (' + (o.bytesSent / 1000000).toFixed(2) + ' MB)' 85 | } 86 | 87 | if (o.bitrateMean !== undefined) { 88 | s += '
Avg. bitrate: ' + (o.bitrateMean / 1000000).toFixed(2) + ' Mbps (' + 89 | (o.bitrateStdDev / 1000000).toFixed(2) + ' StdDev)' 90 | if (o.discardedPackets !== undefined) { 91 | s += ' Discarded packts: ' + o.discardedPackets 92 | } 93 | } 94 | if (o.framerateMean !== undefined) { 95 | s += '
Avg. framerate: ' + (o.framerateMean).toFixed(2) + ' fps (' + 96 | o.framerateStdDev.toFixed(2) + ' StdDev)' 97 | if (o.droppedFrames !== undefined) s += ' Dropped frames: ' + o.droppedFrames 98 | if (o.jitter !== undefined) s += ' Jitter: ' + o.jitter 99 | } 100 | if (o.framesPerSecond !== undefined) { 101 | s += '
googFrameRateReceived: ' + o.framesPerSecond + ' fps' 102 | // s += ' googJitterBufferMs: ' + o.jitter 103 | s += '
googFrameReceived: ' + o.frameWidth + 'x' + o.frameHeight 104 | // s += '
googCurrentDelayMs: ' + o.googCurrentDelayMs 105 | // s += ' googDecodeMs: ' + o.googDecodeMs 106 | } 107 | 108 | if (o.googFrameRateSent !== undefined) { 109 | s += '
googFrameRateSent: ' + o.googFrameRateSent + ' fps' 110 | s += ' googEncodeUsagePercent: ' + o.googEncodeUsagePercent + '%' 111 | s += '
googFrameSent: ' + o.googFrameWidthSent + 'x' + o.googFrameHeightSent 112 | s += ' googAvgEncodeMs: ' + o.googAvgEncodeMs 113 | } 114 | } 115 | return s 116 | } 117 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ElementUI from 'element-ui' 3 | import 'element-ui/lib/theme-chalk/index.css' 4 | import App from './App.vue' 5 | 6 | Vue.use(ElementUI) 7 | 8 | Vue.config.productionTip = false 9 | 10 | new Vue({ 11 | render: (h) => h(App) 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Message } from 'element-ui' 3 | 4 | const successCode = [200, 70002, 20003] 5 | 6 | const service = axios.create({ 7 | baseURL: process.env.VUE_APP_BASE_API, 8 | withCredentials: true, 9 | timeout: 1000000 10 | }) 11 | 12 | service.interceptors.request.use( 13 | async config => { 14 | if (!config.headers['Content-Type']) { config.headers['Content-Type'] = 'application/json;charset=UTF-8;' } 15 | config.headers['Authorization'] = 'you token' 16 | return config 17 | }, 18 | error => { 19 | return Promise.reject(error) 20 | } 21 | ) 22 | service.interceptors.response.use( 23 | response => { 24 | const res = response.data 25 | if (!successCode.includes(Number(res.resultCode))) { 26 | Message.closeAll() 27 | Message({ 28 | message: res.msg, 29 | type: 'error', 30 | duration: 3000 31 | }) 32 | return Promise.reject(res.resultCode + ': ' + res.msg || 'Error') 33 | } else { 34 | return res 35 | } 36 | }, 37 | error => { 38 | Message({ 39 | message: '网络异常', 40 | type: 'error', 41 | duration: 5 * 1000 42 | }) 43 | return Promise.reject(error) 44 | } 45 | ) 46 | 47 | export default service 48 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | productionSourceMap: false, 4 | publicPath: './', 5 | outputDir: 'dist', 6 | assetsDir: 'assets', 7 | devServer: { 8 | port: 9000, 9 | open: true, 10 | proxy: { 11 | [process.env.VUE_APP_BASE_API]: { 12 | target: `http://meeting.intcloud.h3c.com/api`, // 测试服 13 | changeOrigin: true, 14 | pathRewrite: { 15 | ['^' + process.env.VUE_APP_BASE_API]: '' 16 | } 17 | } 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------