Browse Source

[vue demo] 初始化

kevin.song 6 years ago
parent
commit
4398348bee

+ 1 - 1
examples/pureJS/README.md

@@ -20,4 +20,4 @@ npm start
 
 4. 打开页面
 
-浏览器打开 http://localhost:3000
+浏览器打开 http://localhost:3001

+ 2 - 0
examples/vue/.browserslistrc

@@ -0,0 +1,2 @@
+> 1%
+last 2 versions

+ 5 - 0
examples/vue/.editorconfig

@@ -0,0 +1,5 @@
+[*.{js,jsx,ts,tsx,vue}]
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 21 - 0
examples/vue/.eslintrc.js

@@ -0,0 +1,21 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  'extends': [
+    'plugin:vue/essential',
+    '@vue/standard'
+  ],
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+    'semi': ['error', 'always', {
+      "omitLastInOneLineBlock": true
+    }],
+    "indent": [1, 2]
+  },
+  parserOptions: {
+    parser: 'babel-eslint'
+  }
+}

+ 27 - 0
examples/vue/.gitignore

@@ -0,0 +1,27 @@
+.DS_Store
+node_modules
+/dist
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+yarn.lock
+package-lock.json
+
+# config
+src/config/index.js

+ 23 - 0
examples/vue/README.md

@@ -0,0 +1,23 @@
+# URTC-demo(Vue 版本)
+
+## 运行步骤
+
+1. 添加配置
+
+详见 config 目录下的 README
+
+2. 安装 npm 依赖包
+
+```
+npm install
+```
+
+3. 执行运行命令
+
+```
+npm start
+```
+
+4. 打开页面
+
+浏览器打开 http://localhost:8080

+ 5 - 0
examples/vue/babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 26 - 0
examples/vue/package.json

@@ -0,0 +1,26 @@
+{
+  "name": "vue",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "start": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "core-js": "^3.4.3",
+    "unique-classnames": "^1.0.6",
+    "urtc-sdk": "^1.4.5",
+    "vue": "^2.6.10"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "^4.1.0",
+    "@vue/cli-plugin-eslint": "^4.1.0",
+    "@vue/cli-service": "^4.1.0",
+    "@vue/eslint-config-standard": "^4.0.0",
+    "babel-eslint": "^10.0.3",
+    "eslint": "^5.16.0",
+    "eslint-plugin-vue": "^5.0.0",
+    "vue-template-compiler": "^2.6.10"
+  }
+}

BIN
examples/vue/public/favicon.ico


+ 17 - 0
examples/vue/public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title>vue</title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 31 - 0
examples/vue/src/App.vue

@@ -0,0 +1,31 @@
+<template>
+  <div id="app">
+    <Room/>
+    <br/>
+    <a href="https://github.com/ucloud/urtc-sdk-web" target="_blank" rel="noopener noreferrer">
+      API 文档请看这里
+    </a>
+  </div>
+</template>
+
+<script>
+import Room from './pages/Room.vue';
+
+export default {
+  name: 'app',
+  components: {
+    Room
+  }
+};
+</script>
+
+<style>
+#app {
+  margin-top: 60px;
+  font-family: 'Avenir', Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-align: center;
+  color: #2c3e50;
+}
+</style>

BIN
examples/vue/src/assets/logo.png


+ 180 - 0
examples/vue/src/components/MediaPlayer.vue

@@ -0,0 +1,180 @@
+<template>
+  <div v-bind:class="classes" v-on:click="handleClick">
+    <div style="overflow: 'hidden', textOverflow: 'ellipsis'">用户ID: {{stream.uid}}</div>
+    <div style="overflow: 'hidden', textOverflow: 'ellipsis'">流ID: {{stream.sid}}</div>
+    <div v-show="stream.mediaStream" style="overflow: 'hidden', textOverflow: 'ellipsis'">音量: {{volume}} % &nbsp;&nbsp;&nbsp;&nbsp;音频丢包率: {{stats.audioLost}} %</div>
+    <div v-show="stream.mediaStream" style="overflow: 'hidden', textOverflow: 'ellipsis'">视频丢包率: {{stats.videoLost}} % &nbsp;&nbsp;&nbsp;&nbsp;网络延时: {{stats.rtt}} ms</div>
+    <div v-show="stream.mediaStream">
+      <video
+        ref="video"
+        webkit-playsinline
+        autoplay
+        playsinline>
+      </video>
+    </div>
+    <p v-show="!stream.mediaStream">unsubscribe</p>
+  </div>
+</template>
+
+<script>
+import classnames from 'unique-classnames';
+
+export default {
+  name: 'MediaPlayer',
+  data: function () {
+    const classes = classnames('media-player', this.className);
+    return {
+      classes: classes,
+      volume: 0,
+      stats: {
+        audioLost: 0,
+        biggestAudioLost: 0,
+        videoLost: 0,
+        biggestVideoLost: 0,
+        rtt: 0,
+        biggestRTT: 0
+      }
+    };
+  },
+  props: {
+    className: {
+      type: String,
+      default: ''
+    },
+    stream: {
+      type: Object,
+      default: function () {
+        return {};
+      }
+    },
+    client: {
+      type: Object,
+      default: function () {
+        return null;
+      }
+    },
+    onClick: {
+      type: Function,
+      default: function () {}
+    }
+  },
+  created: function () {
+    this.volumeTimer = 0;
+    this.stateTimer = 0;
+  },
+  mounted: function () {
+    this.isComponentDestroyed = false;
+    if (this.stream.mediaStream) {
+      this.play(this.stream.mediaStream);
+    }
+  },
+  beforeDestroy: function () {
+    this.stop();
+  },
+  destroyed: function () {
+    this.isComponentDestroyed = true;
+  },
+  watch: {
+    'stream.mediaStream': function (val, oldVal) {
+      console.log('media stream changed: ', val, oldVal);
+      if (val) {
+        this.play(val);
+      } else {
+        this.stop();
+      }
+    }
+  },
+  methods: {
+    play: function (mediaStream) {
+      this.$refs.video.srcObject = mediaStream;
+      this.startGetVolume();
+      this.startGetState();
+    },
+    stop: function () {
+      this.stopGetVolume();
+      this.stopGetState();
+      this.$refs.video.srcObject = null;
+    },
+    startGetVolume: function () {
+      const { client, stream } = this;
+      if (!client || !stream || !stream.audio) {
+        return;
+      }
+      if (this.volumeTimer) {
+        clearInterval(this.volumeTimer);
+      }
+      this.volumeTimer = setInterval(() => {
+        this.volume = client.getAudioVolume(stream.sid);
+      }, 1000);
+    },
+    stopGetVolume: function () {
+      clearInterval(this.volumeTimer);
+    },
+    startGetState: function () {
+         const { client, stream } = this;
+        if (!client || !stream || !stream.video) {
+          return;
+        }
+        if (this.stateTimer) {
+          clearInterval(this.stateTimer);
+        }
+        this.stateTimer = setInterval(() => {
+          client.getAudioStats(stream.sid, (_stats) => {
+            if (this.isComponentDestroyed) return;
+            const { stats } = this;
+            stats.audioLost = _stats.lostpre;
+            if (stats.biggestAudioLost < _stats.lostpre) {
+              stats.biggestAudioLost = _stats.lostpre;
+            }
+          }, (e) => {
+            console.error('get video stats ', stream.sid);
+          });
+          client.getVideoStats(stream.sid, (_stats) => {
+            if (this.isComponentDestroyed) return;
+            const { stats } = this;
+            stats.videoLost = _stats.lostpre;
+            if (stats.biggestVideoLost < _stats.lostpre) {
+              stats.biggestVideoLost = _stats.lostpre;
+            }
+          }, (e) => {
+            console.error('get video stats ', stream.sid);
+          });
+          client.getNetworkStats(stream.sid, (_stats) => {
+            if (this.isComponentDestroyed) return;
+            const { stats } = this;
+            stats.rtt = _stats.rtt;
+            if (stats.biggestRTT < _stats.rtt) {
+              stats.biggestRTT = _stats.rtt;
+            }
+          }, (e) => {
+            console.error('get network stats ', stream.sid);
+          });
+        }, 1000);
+    },
+    stopGetState: function () {
+      clearInterval(this.stateTimer);
+    },
+    handleClick: function () {
+      const { stream, onClick } = this;
+      onClick && onClick(stream);
+    }
+  }
+};
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.media-player {
+  display: inline-block;
+  margin: 2px;
+  width: 300px;
+  text-align: left;
+  white-space: nowrap;
+  cursor: pointer;
+}
+
+.media-player video {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 15 - 0
examples/vue/src/config/README.md

@@ -0,0 +1,15 @@
+本目录中需要创建 index.js 文件,并配置 AppId 和 AppKey,示例代码:
+
+```
+const config = {
+  AppId: 'urtc-xxxxxxxx',
+  AppKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
+}
+
+export default config;
+```
+
+> 注:
+> 
+> 1. AppId 和 AppKey 可从 URTC 产品中获取
+> 2. AppKey 不可暴露于公网,建议生产环境时,由后端进行保存并由前端调 API 获取

+ 8 - 0
examples/vue/src/main.js

@@ -0,0 +1,8 @@
+import Vue from 'vue'
+import App from './App.vue'
+
+Vue.config.productionTip = false
+
+new Vue({
+  render: h => h(App)
+}).$mount('#app')

+ 237 - 0
examples/vue/src/pages/Room.vue

@@ -0,0 +1,237 @@
+<template>
+  <div class="room">
+    <label>房间号:{{roomId}}({{roomStatus}})</label>
+    <p>当前选中的流:{{selectedStreamStatus}}</p>
+    <h3>本地(发布)流</h3>
+    <MediaPlayer v-for="stream in localStreams" :key="stream.sid" className="local-stream" v-bind:client="client" v-bind:stream="stream" v-bind:onClick="handleSelectStream"/>
+    <h3>远端(订阅)流</h3>
+    <MediaPlayer v-for="stream in remoteStreams" :key="stream.sid" className="remote-stream" v-bind:client="client" v-bind:stream="stream" v-bind:onClick="handleSelectStream"/>
+    <h3>操作</h3>
+    <button v-on:click="handleJoinRoom">加入房间</button>
+    <button v-on:click="handlePublish">发布</button>
+    <button v-on:click="handlePublishScreen">屏幕共享</button>
+    <button v-on:click="handleUnpublish">取消发布/屏幕共享</button>
+    <button v-on:click="handleSubscribe">订阅</button>
+    <button v-on:click="handleUnsubscribe">取消订阅</button>
+    <button v-on:click="handleLeaveRoom">离开房间</button>
+  </div>
+</template>
+
+<script>
+import sdk, { Client } from 'urtc-sdk';
+
+import config from '../config';
+import MediaPlayer from '../components/MediaPlayer.vue';
+
+const { AppId, AppKey } = config;
+
+// 此处使用固定的房间号的随机的用户ID,请自行替换
+const RoomId = 'ssss02';
+const UserId = Math.floor(Math.random() * 1000000).toString();
+
+console.log('UCloudRTC sdk version: ', sdk.version);
+
+export default {
+  name: 'Room',
+  components: {
+    MediaPlayer
+  },
+  data: function () {
+    return {
+      roomId: RoomId,
+      userId: UserId,
+      isJoinedRoom: false,
+      selectedStream: null,
+      localStreams: [],
+      remoteStreams: []
+    };
+  },
+  computed: {
+    roomStatus: function () {
+      return this.isJoinedRoom ? '已加入' : '未加入';
+    },
+    selectedStreamStatus: function () {
+      return this.selectedStream ? this.selectedStream.sid : '未选择';
+    }
+  },
+  created: function () {
+    if (!AppId || !AppKey) {
+      alert('请先设置 AppId 和 AppKey');
+      return;
+    }
+    if (!RoomId) {
+      alert('请先设置 RoomId');
+      return;
+    }
+    if (!UserId) {
+      alert('请先设置 UserId');
+    }
+  },
+  mounted: function () {
+    const token = sdk.generateToken(AppId, AppKey, RoomId, UserId);
+    this.client = new Client(AppId, token);
+    this.client.on('stream-published', (localStream) => {
+      console.info('stream-published: ', localStream);
+      const { localStreams } = this;
+      localStreams.push(localStream);
+    });
+    this.client.on('stream-added', (remoteStream) => {
+      console.info('stream-added: ', remoteStream);
+      const { remoteStreams } = this;
+      remoteStreams.push(remoteStream);
+      // 自动订阅
+      this.client.subscribe(remoteStream.sid, (err) => {
+        console.error('自动订阅失败:', err);
+      });
+    });
+    this.client.on('stream-subscribed', (remoteStream) => {
+      console.info('stream-subscribed: ', remoteStream);
+      const { remoteStreams } = this;
+      const idx = remoteStreams.findIndex(item => item.sid === remoteStream.sid);
+      if (idx >= 0) {
+        remoteStreams.splice(idx, 1, remoteStream);
+      }
+    });
+    this.client.on('stream-removed', (remoteStream) => {
+      console.info('stream-removed: ', remoteStream);
+      const { remoteStreams } = this;
+      const idx = remoteStreams.findIndex(item => item.sid === remoteStream.sid);
+      if (idx >= 0) {
+        remoteStreams.splice(idx, 1);
+      }
+    });
+
+    window.addEventListener('beforeunload', this.handleLeaveRoom);
+  },
+  beforeDestroy: function () {
+    console.info('component will destroy');
+    window.removeEventListener('beforeunload', this.handleLeaveRoom);
+    this.handleLeaveRoom();
+  },
+  destroyed: function () {
+    this.isComponentDestroyed = true;
+  },
+  methods: {
+    handleJoinRoom: function () {
+      const { roomId, userId, isJoinedRoom } = this;
+      if (isJoinedRoom) {
+        alert('已经加入了房间');
+        return;
+      }
+      if (!roomId) {
+        alert('请先填写房间号');
+        return;
+      }
+      this.client.joinRoom(roomId, userId, () => {
+        console.info('加入房间成功');
+        this.isJoinedRoom = true;
+      }, (err) => {
+        console.error('加入房间失败: ', err);
+      });
+    },
+    handlePublish: function () {
+      this.client.publish(err => {
+        console.error('发布失败:', err);
+      });
+    },
+    handlePublishScreen: function () {
+      this.client.publish({ audio: true, video: false, screen: true }, (err) => {
+        console.error('发布失败:', err);
+      });
+    },
+    handleUnpublish: function () {
+      const { selectedStream } = this;
+      if (!selectedStream) {
+        alert('未选择需要取消发布的本地流');
+        return;
+      }
+      this.client.unpublish(selectedStream.sid, (stream) => {
+        console.info('取消发布本地流成功:', stream);
+        const { localStreams } = this;
+        const idx = localStreams.findIndex(item => item.sid === stream.sid);
+        if (idx >= 0) {
+          localStreams.splice(idx, 1);
+        }
+        this.selectedStream = null;
+      }, (err) => {
+        console.error('取消发布本地流失败:', err);
+      });
+    },
+    handleSubscribe: function () {
+      const { selectedStream } = this;
+      if (!selectedStream) {
+        alert('未选择需要订阅的远端流');
+        return;
+      }
+      this.client.subscribe(selectedStream.sid, (err) => {
+        console.error('订阅失败:', err);
+      });
+    },
+    handleUnsubscribe: function () {
+      const { selectedStream } = this;
+      if (!selectedStream) {
+        alert('未选择需要取消订阅的远端流');
+        return;
+      }
+      this.client.unsubscribe(selectedStream.sid, (stream) => {
+        console.info('取消订阅成功:', stream);
+        const { remoteStreams } = this;
+        const idx = remoteStreams.findIndex(item => item.sid === stream.sid);
+        if (idx >= 0) {
+          remoteStreams.splice(idx, 1, stream);
+        }
+      }, (err) => {
+        console.error('订阅失败:', err);
+      });
+    },
+    handleLeaveRoom: function () {
+      const { isJoinedRoom } = this;
+      if (!isJoinedRoom) {
+        return;
+      }
+      this.client.leaveRoom(() => {
+        console.info('离开房间成功');
+        this.selectedStream = null;
+        this.localStreams = [];
+        this.remoteStreams = [];
+        this.isJoinedRoom = null;
+      }, (err) => {
+        console.error('离开房间失败:', err);
+      });
+    },
+    handleSelectStream: function (stream) {
+      console.log('select stream: ', stream);
+      this.selectedStream = stream;
+    }
+  }
+};
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.room {
+  max-width: 640px;
+  margin: 0 auto;
+}
+
+.room .local-stream,
+.room .remote-stream {
+  text-align: left;
+}
+.room button {
+  padding: 8px 0;
+  display: inline-block;
+  width: 100%;
+  border-radius: 6px;
+  cursor: pointer;
+  text-align: center;;
+}
+
+.room input:visited,
+.room button:focus,
+.room button:visited,
+.room button:hover,
+.room button:active {
+  outline: none;
+}
+</style>