..._vue startremotevideo">
当前位置:   article > 正文

2021/5/12 vue制作实时音视频_vue startremotevideo

vue startremotevideo

需求: 视频通话

想法: 不会做,笑死   (采用腾讯云第三方,或者使用vue + webRtc + websocket) 暂时只做到一对一,而且还有一些问题,求指教

有想法的可以加个好友互相讨论一下

 

1. 接入腾讯云 - 直接在mounted里引入createClient方法就可以了

  1. <template>
  2. <!-- 视频会诊 -->
  3. <div class="videoCon">
  4. <div class="topBar">
  5. <el-form
  6. :inline="true"
  7. :model="formInline"
  8. class="demo-form-inline"
  9. :label-position="labelPosition"
  10. label-width="100px"
  11. >
  12. <el-form-item label="申请医生:">
  13. <el-input v-model="formInline.doctor" size="mini"></el-input>
  14. </el-form-item>
  15. <el-form-item label="手机号:">
  16. <el-input v-model="formInline.phone" size="mini"></el-input>
  17. </el-form-item>
  18. <el-form-item label="请求时间:">
  19. <el-input v-model="formInline.time" size="mini"></el-input>
  20. </el-form-item>
  21. <el-form-item label="申请医院:">
  22. <el-input v-model="formInline.hospital" size="mini"></el-input>
  23. </el-form-item>
  24. </el-form>
  25. <div class="rightBox">
  26. <div class="recording">
  27. <i></i>
  28. <span>录制中 :</span>
  29. <span>1:20:02</span>
  30. </div>
  31. <el-button type="primary" @click="signOut">退出</el-button>
  32. </div>
  33. </div>
  34. <div class="main">
  35. <div class="video">
  36. <div class="bigScreen">
  37. <span class="iconfont iconicon-03-17"></span>
  38. </div>
  39. <div style="width: 22px"></div>
  40. <div class="smallScreem">
  41. <div>
  42. <span class="iconfont iconicon-03-17"></span>
  43. <span class="iconfont iconicon-03-16"></span>
  44. <div class="center-page">
  45. <div v-html="remoteStream"
  46. :class="remoteStream?'distant-stream':''">
  47. </div>
  48. </div>
  49. </div>
  50. <!-- <div>
  51. <span class="iconfont iconicon-03-17"></span>
  52. <span class="iconfont iconicon-03-16"></span>
  53. </div>
  54. <div>
  55. <span class="iconfont iconicon-03-17"></span>
  56. <span class="iconfont iconicon-03-16"></span>
  57. </div>
  58. <div>
  59. <span class="iconfont iconicon-03-16"></span>
  60. <span class="iconfont iconicon-03-17"></span>
  61. </div>
  62. <div>
  63. <span class="iconfont iconicon-03-16"></span>
  64. <span class="iconfont iconicon-03-17"></span>
  65. </div>
  66. <div>
  67. <span class="iconfont iconicon-03-17"></span>
  68. <span class="iconfont iconicon-03-16"></span>
  69. </div>
  70. <div>
  71. <span class="iconfont iconicon-03-17"></span>
  72. <span class="iconfont iconicon-03-16"></span>
  73. </div>
  74. <div>
  75. <span class="iconfont iconicon-03-17"></span>
  76. <span class="iconfont iconicon-03-16"></span>
  77. </div>
  78. <div>
  79. <span class="iconfont iconicon-03-17"></span>
  80. <span class="iconfont iconicon-03-16"></span>
  81. </div> -->
  82. <div style="marginTop: 10px;">
  83. <span class="iconfont iconicon-03-17"></span>
  84. <span class="iconfont iconicon-03-16" @click="snapshot"></span>
  85. <div id='local_stream'
  86. class="local-stream">
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. <div class="operation">
  92. <div>
  93. <span class="iconfont iconicon-03-31"></span>
  94. <p>控制</p>
  95. </div>
  96. <div>
  97. <span class="iconfont iconicon-03-30"></span>
  98. <p>测量</p>
  99. </div>
  100. <div>
  101. <span class="iconfont iconicon-03-42"></span>
  102. <p>注释</p>
  103. </div>
  104. <div>
  105. <span class="iconfont iconicon-03-34"></span>
  106. <p>截图</p>
  107. </div>
  108. <div>
  109. <span class="iconfont iconicon-03-32"></span>
  110. <p>截图附件</p>
  111. </div>
  112. <div>
  113. <span class="iconfont iconicon-03-33"></span>
  114. <p>患者查看详情</p>
  115. </div>
  116. <div>
  117. <span class="iconfont iconicon-03-15"></span>
  118. <p>邀请</p>
  119. </div>
  120. <div>
  121. <span class="iconfont iconicon-03-29"></span>
  122. <p>聊天</p>
  123. </div>
  124. <div>
  125. <span class="iconfont iconicon-03-28"></span>
  126. <p>声音</p>
  127. </div>
  128. <div>
  129. <span class="iconfont iconicon-03-14"></span>
  130. <p>麦克风</p>
  131. </div>
  132. <div>
  133. <span class="iconfont iconicon-03-10"></span>
  134. <p>视频</p>
  135. </div>
  136. <div>
  137. <span class="iconfont iconicon-03-18"></span>
  138. <p>录制</p>
  139. </div>
  140. </div>
  141. </div>
  142. <img src="" alt="" ref="picture">
  143. </div>
  144. </template>
  145. <script>
  146. // 一定要引入这个-去下个demo里面就有
  147. import LibGenerateTestUserSig from '@/utils/lib-generate-test-usersig.min';
  148. import TRTC from 'trtc-js-sdk';
  149. export default {
  150. data() {
  151. return {
  152. formInline: {
  153. doctor: "",
  154. phone: "",
  155. time: "",
  156. hospital: "",
  157. },
  158. labelPosition: "right",
  159. userId: 'user_' + parseInt(Math.random() * 100000000),//用户id --可更改
  160. roomId: 88888,//房间号--加入相同房间才能聊
  161. client: '',//客户端服务
  162. remoteStream: '',//远方播放流
  163. localStream: '',//本地流
  164. };
  165. },
  166. methods: {
  167. // 退出
  168. signOut() {
  169. Bus.$emit('code', "true");
  170. this.$router.go(-1);
  171. },
  172. //创建链接
  173. createClient (userId) {
  174. //获取签名
  175. const config = this.genTestUserSig(userId);
  176. const sdkAppId = config.sdkAppId;
  177. const userSig = config.userSig;
  178. this.client = TRTC.createClient({
  179. mode: 'videoCall',
  180. sdkAppId,
  181. userId,
  182. userSig
  183. });
  184. //注册远程监听,要放在加入房间前--这里用了发布订阅模式
  185. this.subscribeStream(this.client);
  186. //初始化后才能加入房间
  187. this.joinRoom(this.client, this.roomId);
  188. },
  189. //加入房间
  190. joinRoom (client, roomId) {
  191. client.join({ roomId }).then(() => {
  192. console.log('进房成功');
  193. //创建本地流
  194. this.createStream(this.userId);
  195. //播放远端流
  196. this.playStream(this.client);
  197. }).catch(error => {
  198. console.error('进房失败 ' + error);
  199. })
  200. },
  201. //创建本地音视频流
  202. createStream (userId) {
  203. const localStream = TRTC.createStream({ userId, audio: false, video: true });
  204. this.localStream =localStream;
  205. localStream
  206. .initialize().then(() => {
  207. console.log('初始化本地流成功');
  208. // 创建好后才能播放 本地流播放 local_stream 是div的id
  209. localStream.play('local_stream');
  210. //创建好后才能发布
  211. // this.$nextTick(() => {
  212. this.publishStream(localStream, this.client);
  213. // });
  214. }).catch(error => {
  215. console.error('初始化本地流失败 ' + error);
  216. })
  217. },
  218. //发布本地音视频流
  219. publishStream (localStream, client) {
  220. client.publish(localStream).then(() => {
  221. console.log('本地流发布成功');
  222. }).catch(error => {
  223. console.error('本地流发布失败 ' + error);
  224. });
  225. },
  226. //订阅远端流--加入房间之前
  227. subscribeStream (client) {
  228. client.on('stream-added', event => {
  229. console.log(client,'client9696');
  230. const remoteStream = event.stream;
  231. console.log('远端流增加: ' + remoteStream.getId());
  232. //订阅远端流
  233. client.subscribe(remoteStream);
  234. });
  235. },
  236. //播放远端流
  237. playStream (client) {
  238. client.on('stream-subscribed', event => {
  239. const remoteStream = event.stream;
  240. console.log('远端流订阅成功:' + remoteStream.getId());
  241. // 创建远端流标签,因为id是动态的,所以动态创建,用了v-html
  242. this.remoteStream = `<view id="${'remote_stream-' + remoteStream.getId()}" ></view>`;
  243. //做了dom操作 需要使用$nextTick(),否则找不到创建的标签无法进行播放
  244. this.$nextTick(() => {
  245. //播放
  246. remoteStream.play('remote_stream-' + remoteStream.getId());
  247. })
  248. });
  249. },
  250. //退出音视频
  251. leaveRoom (client) {
  252. client
  253. .leave()
  254. .then(() => {
  255. console.log('退房成功')
  256. // 停止本地流,关闭本地流内部的音视频播放器
  257. this.localStream.stop();
  258. // 关闭本地流,释放摄像头和麦克风访问权限
  259. this.localStream.close();
  260. this.localStream = null;
  261. this.client = null
  262. // 退房成功,可再次调用client.join重新进房开启新的通话。
  263. })
  264. .catch(error => {
  265. console.error('退房失败 ' + error);
  266. // 错误不可恢复,需要刷新页面。
  267. });
  268. },
  269. //获取用户签名--前端测试用
  270. genTestUserSig (userID) {
  271. /**
  272. * 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
  273. *
  274. * 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId,
  275. * 它是腾讯云用于区分客户的唯一标识。
  276. */
  277. const SDKAPPID = 1400371155;
  278. /**
  279. * 签名过期时间,建议不要设置的过短
  280. * <p>
  281. * 时间单位:秒
  282. * 默认时间:7 x 24 x 60 x 60 = 604800 = 7
  283. */
  284. const EXPIRETIME = 604800;
  285. /**
  286. * 计算签名用的加密密钥,获取步骤如下:
  287. *
  288. * step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个,
  289. * step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
  290. * step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
  291. *
  292. * 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
  293. * 文档:https://cloud.tencent.com/document/product/647/17275#Server
  294. */
  295. const SECRETKEY = "41b790cbcc2e6672ebd921abb4a2ad8a75e1c80705f787926ba5a77425f772af";
  296. // a soft reminder to guide developer to configure sdkAppId/secretKey
  297. if (SDKAPPID === "" || SECRETKEY === "") {
  298. alert(
  299. "请先配置好您的账号信息: SDKAPPID 及 SECRETKEY " +
  300. "\r\n\r\nPlease configure your SDKAPPID/SECRETKEY in js/debug/GenerateTestUserSig.js"
  301. );
  302. }
  303. const generator = new LibGenerateTestUserSig(SDKAPPID, SECRETKEY, EXPIRETIME);
  304. const userSig = generator.genTestUserSig(userID);
  305. return {
  306. sdkAppId: SDKAPPID,
  307. userSig: userSig
  308. };
  309. },
  310. // 获取详情
  311. async getDetails() {
  312. // this.roomId = this.$route.query.id;
  313. let id = this.$route.query.id;
  314. let res = await meetApi.meetInfoApply(id);
  315. if(res.code != 200) return;
  316. let s = res.data;
  317. this.formInline = {
  318. doctor: s.applyDoctorDicName,
  319. phone: s.applyDoctorPhone,
  320. time: s.applyDateTime,
  321. hospital: s.applyHospitalDicName
  322. };
  323. this.createClient(this.userId)
  324. },
  325. //截图
  326. snapshot() {
  327. let canvas = this.$refs.picture;
  328. canvas.width = 400;
  329. canvas.height = 300;
  330. canvas.getContext("2d").drawImage(this.localVideo, 0, 0, canvas.width, canvas.height);
  331. },
  332. },
  333. mounted() {
  334. //测试用,所以直接创建了,其他需求可自行更改
  335. this.getDetails();
  336. }
  337. };
  338. </script>
  339. <style lang="scss" scoped>
  340. .videoCon::-webkit-scrollbar {
  341. display: none;
  342. }
  343. .videoCon {
  344. height: 100%;
  345. width: 100%;
  346. overflow-y: scroll;
  347. .topBar {
  348. width: 100%;
  349. height: 135px;
  350. // opacity: 0.25;
  351. background: #42464d;
  352. box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.25);
  353. padding: 30px 32px;
  354. display: flex;
  355. justify-content: space-between;
  356. .demo-form-inline {
  357. width: 65%;
  358. .el-form-item {
  359. margin: 0 15px;
  360. width: 375px;
  361. .el-input__inner {
  362. background-color: #42464d;
  363. color: #fff;
  364. }
  365. }
  366. }
  367. .rightBox {
  368. width: 35%;
  369. display: flex;
  370. justify-content: space-between;
  371. .recording {
  372. font-size: 20px;
  373. color: #fff;
  374. font-weight: 700;
  375. margin-top: 52px;
  376. i {
  377. display: inline-block;
  378. width: 15px;
  379. height: 15px;
  380. background: #e60012;
  381. border-radius: 15px;
  382. margin-right: 16px;
  383. }
  384. span {
  385. margin-right: 5px;
  386. }
  387. }
  388. }
  389. }
  390. .main {
  391. // height: calc(100% - 135px);
  392. // height: 100%;
  393. width: 100%;
  394. background-color: #272e43;
  395. padding: 30px;
  396. .video {
  397. padding: 0 15px;
  398. display: flex;
  399. // justify-content: space-between;
  400. margin-bottom: 40px;
  401. .bigScreen {
  402. width: 1290px;
  403. height: 720px;
  404. background-color: #ccc;
  405. span {
  406. color: #a0a0a0;
  407. font-size: 48px;
  408. cursor: pointer;
  409. }
  410. }
  411. .smallScreem {
  412. width: 492px;
  413. height: 720px;
  414. display: flex;
  415. flex-wrap: wrap;
  416. justify-content: space-between;
  417. // align-content: space-between;
  418. div {
  419. width: 492px;
  420. height: 355px;
  421. background-color: #ccc;
  422. position: relative;
  423. span {
  424. color: #a0a0a0;
  425. font-size: 24px;
  426. cursor: pointer;
  427. float: right;
  428. z-index: 2;
  429. }
  430. //远端流
  431. .distant-stream {
  432. width: 100%;
  433. height: 100%;
  434. position: absolute;
  435. top: 0;
  436. left: 0;
  437. z-index: 1;
  438. }
  439. .iconicon-03-17 {
  440. position: absolute;
  441. z-index: 2;
  442. }
  443. .iconicon-03-16 {
  444. position: absolute;
  445. z-index: 2;
  446. left: 30px;
  447. }
  448. //本地流
  449. .local-stream {
  450. width: 100%;
  451. height: 100%;
  452. position: absolute;
  453. top: 0;
  454. left: 0;
  455. z-index: 1;
  456. }
  457. }
  458. }
  459. }
  460. .operation {
  461. height: 106px;
  462. width: 100%;
  463. background-color: #1d2335;
  464. border-radius: 20px;
  465. display: flex;
  466. align-items: center;
  467. justify-content: space-around;
  468. font-size: 20px;
  469. font-weight: 400;
  470. color: #fff;
  471. text-align: center;
  472. div {
  473. cursor: pointer;
  474. span {
  475. font-size: 36px;
  476. }
  477. p {
  478. margin-top: 12px;
  479. }
  480. }
  481. }
  482. }
  483. }
  484. </style>
  485. <style lang="scss">
  486. .videoCon {
  487. .demo-form-inline {
  488. .el-form-item {
  489. .el-form-item__label {
  490. color: #fff;
  491. font-size: 20px;
  492. }
  493. .el-input__inner {
  494. background-color: #42464d;
  495. border-color: #42464d;
  496. color: #fff;
  497. font-size: 20px;
  498. height: 40px;
  499. }
  500. }
  501. }
  502. .rightBox {
  503. .el-button--primary {
  504. background-color: #d33594;
  505. border-color: #d33594;
  506. width: 116px;
  507. height: 55px;
  508. margin-top: 10px;
  509. padding-top: 10px;
  510. span {
  511. font-size: 20px;
  512. color: #e6e6e6;
  513. font-weight: 700;
  514. }
  515. }
  516. }
  517. }
  518. </style>

2. 采用vue + webRtc + websocket制作

  1. <template>
  2. <!-- 视频会诊 -->
  3. <div class="startConsultation" ref="imageWrapper">
  4. <div class="topBar">
  5. <el-form :inline="true" :model="formInline" class="demo-form-inline" :label-position="labelPosition" label-width="100px">
  6. <el-form-item label="申请医生:">
  7. <!-- <el-input v-model="formInline.doctor" size="mini"></el-input> -->
  8. <div>{{formInline.doctor}}</div>
  9. </el-form-item>
  10. <el-form-item label="手机号:">
  11. <!-- <el-input v-model="formInline.phone" size="mini"></el-input> -->
  12. <div>{{formInline.phone}}</div>
  13. </el-form-item>
  14. <el-form-item label="请求时间:">
  15. <!-- <el-input v-model="formInline.time" size="mini"></el-input> -->
  16. <div>{{formInline.time}}</div>
  17. </el-form-item>
  18. <el-form-item label="申请医院:">
  19. <!-- <el-input v-model="formInline.hospital" size="mini"></el-input> -->
  20. <div>{{formInline.hospital}}</div>
  21. </el-form-item>
  22. </el-form>
  23. <div class="rightBox">
  24. <div class="recording">
  25. <i></i>
  26. <span>录制中 :</span>
  27. <span>1:20:02</span>
  28. </div>
  29. <el-button type="primary" @click="signOut">退出</el-button>
  30. </div>
  31. </div>
  32. <div class="main">
  33. <div class="video">
  34. <div class="bigScreen">
  35. <span class="iconfont iconicon-03-17"></span>
  36. </div>
  37. <div style="width: 22px"></div>
  38. <div class="smallScreem">
  39. <div class="bigBox">
  40. <div class="icon">
  41. <span class="iconfont iconicon-03-16"></span>
  42. <span class="iconfont iconicon-03-17"></span>
  43. </div>
  44. <video id="remoteVideo"></video>
  45. </div>
  46. <div class="bigBox">
  47. <div class="icon">
  48. <span class="iconfont iconicon-03-16"></span>
  49. <span class="iconfont iconicon-03-17"></span>
  50. </div>
  51. <video id="localVideo" autoplay muted></video>
  52. </div>
  53. </div>
  54. </div>
  55. <div class="operation">
  56. <div>
  57. <span class="iconfont iconicon-03-31"></span>
  58. <p>控制</p>
  59. </div>
  60. <div>
  61. <span class="iconfont iconicon-03-30"></span>
  62. <p>测量</p>
  63. </div>
  64. <div>
  65. <span class="iconfont iconicon-03-42"></span>
  66. <p>注释</p>
  67. </div>
  68. <div @click="snapshot">
  69. <span class="iconfont iconicon-03-34"></span>
  70. <p>截图</p>
  71. </div>
  72. <div>
  73. <span class="iconfont iconicon-03-32"></span>
  74. <p>截图附件</p>
  75. </div>
  76. <div>
  77. <span class="iconfont iconicon-03-33"></span>
  78. <p>患者查看详情</p>
  79. </div>
  80. <div>
  81. <span class="iconfont iconicon-03-15"></span>
  82. <p>邀请</p>
  83. </div>
  84. <div>
  85. <span class="iconfont iconicon-03-29"></span>
  86. <p>聊天</p>
  87. </div>
  88. <div>
  89. <span class="iconfont iconicon-03-28"></span>
  90. <p>声音</p>
  91. </div>
  92. <div>
  93. <span class="iconfont iconicon-03-14"></span>
  94. <p>麦克风</p>
  95. </div>
  96. <div>
  97. <span class="iconfont iconicon-03-10"></span>
  98. <p>视频</p>
  99. </div>
  100. <div>
  101. <span class="iconfont iconicon-03-18"></span>
  102. <p>录制</p>
  103. </div>
  104. </div>
  105. <div class="logger"></div>
  106. </div>
  107. </div>
  108. </template>
  109. <script>
  110. export default {
  111. data() {
  112. return {
  113. formInline: {
  114. doctor: "潘远航",
  115. phone: "13066990173",
  116. time: "2021-03-08 12:00:11",
  117. hospital: "普博医院",
  118. },
  119. labelPosition: "right",
  120. //websocket
  121. socket: null,
  122. //webrtc
  123. peer: null,
  124. //send data
  125. sendData: {
  126. Id: "1",
  127. SenderId: "1",
  128. Type: "join"
  129. },
  130. target: null,
  131. // videos
  132. localVideo: {},
  133. remoteVideo: {},
  134. logger: {},
  135. //source
  136. audioSourceOption: [],
  137. audioOutputOption: [],
  138. videoSourceOption: [],
  139. //Constraints(媒体约束)
  140. videoConstraints: '',
  141. // Media config
  142. constraints: {
  143. audio: {
  144. noiseSuppression: true,
  145. echoCancellation: true
  146. },
  147. video: {
  148. width: 1920,
  149. height: 1080,
  150. frameRate: 30,
  151. facingMode: "environment"
  152. }
  153. },
  154. // local video stream
  155. localStream: undefined,
  156. isOpenMediaStream: false
  157. }
  158. },
  159. async created() {
  160. this.sendData.Id = this.$route.query.id;
  161. this.sendData.SenderId = this.$route.query.sid;
  162. if (!this.sendData.SenderId) {
  163. this.sendData.SenderId = "1";
  164. }
  165. this.target = this.$route.query.t > 1 ? "answer" : "offer";
  166. console.info("target:", this.target);
  167. await this.getUserMedia();
  168. await this.getDevices();
  169. await this.getAudioVideo();
  170. },
  171. watch: {
  172. },
  173. mounted() {
  174. this.localVideo = document.getElementById("localVideo");
  175. this.remoteVideo = document.getElementById("remoteVideo");
  176. this.logger = document.querySelector('.logger');
  177. //获取浏览器支持的连接
  178. const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
  179. if (!PeerConnection) {
  180. this.error('浏览器不支持WebRTC!');
  181. return;
  182. }
  183. // stun 服务,如果要做到 NAT 穿透,还需要 turn 中转服务
  184. let iceServer = {
  185. "iceServers": [
  186. {
  187. "url": "stun:stun.l.google.com:19302"
  188. },
  189. // {
  190. // "url": "stun:192.168.1.162:3478"
  191. // },
  192. ]
  193. };
  194. this.peer = new PeerConnection(iceServer);
  195. this.peer.ontrack = e => {
  196. if (e && e.streams) {
  197. this.log('收到对方音频/视频流数据...');
  198. remoteVideo.srcObject = e.streams[0];
  199. setTimeout(() => {
  200. remoteVideo.play();
  201. }, 150);
  202. }
  203. };
  204. this.peer.onicecandidate = e => {
  205. if (e.candidate) {
  206. this.log('搜集并发送候选人');
  207. this.sendData.Type = "send";
  208. this.sendData.Data = JSON.stringify({
  209. type: `${this.target}_ice`,
  210. iceCandidate: e.candidate
  211. });
  212. this.socket.send(JSON.stringify(this.sendData));
  213. } else {
  214. this.log('候选人收集完成!');
  215. }
  216. };
  217. this.log('信令通道(WebSocket)创建中......');
  218. var socketUrl = location.protocol + "//" + location.host + "/ws";
  219. socketUrl = socketUrl.replace("https", "wss").replace("http", "ws");
  220. this.log("连接地址:" + socketUrl);
  221. this.socket = new WebSocket(socketUrl);
  222. this.socket.onopen = () => {
  223. this.log('信令通道已打开!');
  224. this.log("当前角色: " + this.target);
  225. this.sendData.Type = "join";
  226. this.socket.send(JSON.stringify(this.sendData));
  227. }
  228. this.socket.onclose = (e) => {
  229. this.error("断开连接");
  230. console.log('断开连接', e);
  231. }
  232. this.socket.onerror = () => this.error('信令通道创建失败!');
  233. //设置别名
  234. let _this = this;
  235. this.socket.onmessage = e => {
  236. const { Type, Code, Msg, Data } = JSON.parse(e.data);
  237. if (Code != 200) {
  238. console.warn(Msg);
  239. return;
  240. }
  241. //加入
  242. if (Type === "join") {
  243. this.log("当前工作间在线人数:" + Data);
  244. if (!this.target) {
  245. this.target = Data > 1 ? "answer" : "offer";
  246. }
  247. this.startLive();
  248. return;
  249. }
  250. let resultData;
  251. if (typeof Data !== "object") {
  252. resultData = JSON.parse(Data);
  253. } else {
  254. resultData = Data;
  255. }
  256. const { type, sdp, iceCandidate } = resultData;
  257. if (type === 'answer') {
  258. _this.peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));
  259. } else if (type === 'answer_ice') {
  260. _this.peer.addIceCandidate(iceCandidate);
  261. } else if (type === 'offer') {
  262. this.startLive(new RTCSessionDescription({ type, sdp }));
  263. } else if (type === 'offer_ice') {
  264. _this.peer.addIceCandidate(iceCandidate);
  265. }
  266. };
  267. },
  268. beforeDestroy() {
  269. if (this.socket.readyState == WebSocket.OPEN) {
  270. this.sendData.Type = "leave";
  271. this.socket.send(JSON.stringify(this.sendData));
  272. this.socket.close();
  273. }
  274. if (this.isOpenMediaStream) {
  275. this.localStream.getVideoTracks()[0].stop();
  276. this.localStream.getAudioTracks()[0].stop();
  277. }
  278. },
  279. methods: {
  280. //拉起本地音视频流
  281. async startLocalMedia() {
  282. await this.getUserMedia();
  283. await this.getAudioVideo();
  284. },
  285. //拿到媒体流
  286. async getUserMedia() {
  287. // log(`Requesting video stream`);
  288. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
  289. if ("mediaDevices" in navigator) {
  290. try {
  291. const stream = await navigator.mediaDevices.getUserMedia(this.constraints);
  292. this.localVideo.srcObject = stream;
  293. this.localStream = stream;
  294. // log("Received local video stream");
  295. } catch (error) {
  296. // log(`getUserMedia error: ${error}`);
  297. }
  298. }
  299. },
  300. //拿到音视频轨道
  301. async getAudioVideo() {
  302. if (this.localStream == undefined) {
  303. await this.getUserMedia();
  304. }
  305. console.log("localStream", this.localStream);
  306. if (this.localStream !== undefined && this.localStream != null) {
  307. this.isOpenMediaStream = true;
  308. } else {
  309. this.isOpenMediaStream = false;
  310. return;
  311. }
  312. let videoTrack = this.localStream.getVideoTracks()[0];
  313. let audioTrack = this.localStream.getAudioTracks();
  314. console.log(videoTrack);
  315. console.log(audioTrack);
  316. let videoConstraintsData = videoTrack.getSettings();
  317. console.log(videoConstraintsData)
  318. this.videoConstraints = JSON.stringify(videoConstraintsData, null, 4)
  319. console.log(this.videoConstraints)
  320. },
  321. //获取设备信息
  322. getDevices() {
  323. if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
  324. console.log('不支持获取设备信息!');
  325. this.isOpenMediaStream = false;
  326. } else {
  327. navigator.mediaDevices.enumerateDevices().then(this.showDevice).catch((err) => {
  328. this.error(err.name + ":" + err.message);
  329. });
  330. }
  331. },
  332. //展示设备信息
  333. showDevice(deviceInfos) {
  334. let _this = this;
  335. deviceInfos.forEach(function (deviceinfo) {
  336. var option = {
  337. text: deviceinfo.label,
  338. value: deviceinfo.deviceId
  339. }
  340. if (deviceinfo.kind === "audioinput") {
  341. _this.audioSourceOption.push(option);
  342. } else if (deviceinfo.kind === "audiooutput") {
  343. _this.audioOutputOption.push(option);
  344. } else if (deviceinfo.kind === "videoinput") {
  345. _this.videoSourceOption.push(option);
  346. }
  347. })
  348. },
  349. // 退出
  350. signOut() {
  351. Bus.$emit('code', "true");
  352. this.$router.go(-1);
  353. },
  354. async startLive(offerSdp) {
  355. let stream = this.localStream;
  356. let _this = this;
  357. // if (!this.isOpenMediaStream) {
  358. // this.error("尚未开启媒体流!");
  359. // return;
  360. // }
  361. if (stream == undefined) {
  362. stream = await navigator.mediaDevices.getUserMedia(this.constraints);
  363. }
  364. this.log('将媒体轨道添加到轨道集');
  365. stream.getTracks().forEach(track => {
  366. //console.info("startLive", _this.peer, track, stream);
  367. _this.peer.addTrack(track, stream);
  368. });
  369. this.sendData.Type = "send";
  370. if (!offerSdp) {
  371. this.log('创建本地SDP');
  372. const offer = await this.peer.createOffer();
  373. await this.peer.setLocalDescription(offer);
  374. offer.sdp = offer.sdp;
  375. this.log(`传输本地SDP`);
  376. this.sendData.Data = offer;
  377. this.socket.send(JSON.stringify(this.sendData));
  378. } else {
  379. this.log('接收远程SDP');
  380. //设置接收到的远端offer
  381. await this.peer.setRemoteDescription(offerSdp);
  382. this.log('创建回复(应答)SDP');
  383. //创建answer并发送给对方。
  384. const answer = await this.peer.createAnswer();
  385. console.warn(answer);
  386. this.log(`传输回复接收(应答)SDP`);
  387. this.sendData.Data = answer;
  388. this.socket.send(JSON.stringify(this.sendData));
  389. await this.peer.setLocalDescription(answer);
  390. }
  391. },
  392. log(msg) {
  393. this.logger.innerHTML += `<span>${new Date().toLocaleTimeString()}:${msg}</span><br/>`;
  394. },
  395. error(msg) {
  396. this.logger.innerHTML += `<span class="error">${new Date().toLocaleTimeString()}:${msg}</span><br/>`;
  397. }
  398. }
  399. };
  400. </script>
  401. <style lang="scss" scoped>
  402. .startConsultation::-webkit-scrollbar {
  403. display: none;
  404. }
  405. .startConsultation {
  406. width: 100vw;
  407. height: 100vh;
  408. display: flex;
  409. flex-direction: column;
  410. // overflow-y: scroll;
  411. .topBar {
  412. width: 100%;
  413. height: 105px;
  414. // opacity: 0.25;
  415. background: #42464d;
  416. box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.25);
  417. padding: 15px 32px;
  418. box-sizing: border-box;
  419. display: flex;
  420. justify-content: space-between;
  421. .demo-form-inline {
  422. width: 65%;
  423. .el-form-item {
  424. margin: 0 15px;
  425. width: 375px;
  426. div {
  427. color: #fff;
  428. font-size: 20px;
  429. }
  430. // .el-input__inner {
  431. // background-color: #42464d;
  432. // color: #fff;
  433. // }
  434. }
  435. }
  436. .rightBox {
  437. width: 35%;
  438. display: flex;
  439. justify-content: space-between;
  440. .recording {
  441. font-size: 20px;
  442. color: #fff;
  443. font-weight: 700;
  444. margin-top: 52px;
  445. i {
  446. display: inline-block;
  447. width: 15px;
  448. height: 15px;
  449. background: #e60012;
  450. border-radius: 15px;
  451. margin-right: 16px;
  452. }
  453. span {
  454. margin-right: 5px;
  455. }
  456. }
  457. }
  458. }
  459. .main {
  460. // height: calc(100% - 135px);
  461. // height: 100%;
  462. width: 100%;
  463. background-color: #272e43;
  464. padding: 30px;
  465. box-sizing: border-box;
  466. flex: 1;
  467. display: flex;
  468. flex-direction: column;
  469. .video {
  470. flex: 1;
  471. padding: 0 15px;
  472. display: flex;
  473. // justify-content: space-between;
  474. margin-bottom: 40px;
  475. .bigScreen {
  476. width: 1290px;
  477. height: 100%;
  478. background-color: #ccc;
  479. span {
  480. color: #a0a0a0;
  481. font-size: 48px;
  482. cursor: pointer;
  483. }
  484. }
  485. .smallScreem {
  486. width: 492px;
  487. height: 100%;
  488. display: flex;
  489. flex-wrap: wrap;
  490. justify-content: space-between;
  491. align-content: space-between;
  492. .bigBox {
  493. width: 500px;
  494. height: 48%;
  495. background-color: #ccc;
  496. position: relative;
  497. border: 1px solid #ccc;
  498. .icon {
  499. position: absolute;
  500. right: 5px;
  501. top: 5px;
  502. }
  503. video {
  504. width: 100%;
  505. height: 100%;
  506. }
  507. span {
  508. color: #a0a0a0;
  509. font-size: 24px;
  510. cursor: pointer;
  511. }
  512. }
  513. }
  514. }
  515. .operation {
  516. // height: 106px;
  517. width: 100%;
  518. flex-basis: 106px;
  519. background-color: #1d2335;
  520. border-radius: 20px;
  521. display: flex;
  522. align-items: center;
  523. justify-content: space-around;
  524. font-size: 20px;
  525. font-weight: 400;
  526. color: #fff;
  527. text-align: center;
  528. div {
  529. cursor: pointer;
  530. span {
  531. font-size: 36px;
  532. }
  533. p {
  534. margin-top: 12px;
  535. }
  536. }
  537. }
  538. }
  539. }
  540. </style>
  541. <style lang="scss">
  542. .startConsultation {
  543. .demo-form-inline {
  544. .el-form-item {
  545. .el-form-item__label {
  546. color: #fff;
  547. font-size: 20px;
  548. }
  549. .el-input__inner {
  550. background-color: #42464d;
  551. border-color: #42464d;
  552. color: #fff;
  553. font-size: 20px;
  554. height: 40px;
  555. }
  556. }
  557. }
  558. .rightBox {
  559. .el-button--primary {
  560. background-color: #d33594;
  561. border-color: #d33594;
  562. width: 116px;
  563. height: 55px;
  564. margin-top: 10px;
  565. padding-top: 10px;
  566. span {
  567. font-size: 20px;
  568. color: #e6e6e6;
  569. font-weight: 700;
  570. }
  571. }
  572. }
  573. .logger {
  574. // width: 40%;
  575. // padding: 14px;
  576. // line-height: 1.5;
  577. // color: #4fbf40;
  578. // border-radius: 6px;
  579. // background-color: #272727;
  580. width: 400px;
  581. height: 600px;
  582. padding: 14px;
  583. line-height: 1.5;
  584. color: #4fbf40;
  585. border-radius: 6px;
  586. left: 100px;
  587. top: 160px;
  588. z-index: 1;
  589. position: absolute;
  590. // transform: translateY(-800px);
  591. background-color: #272727;
  592. }
  593. .logger .error {
  594. color: #dd4a68;
  595. }
  596. }
  597. .vue-cropper {
  598. position: absolute !important;
  599. top: 0;
  600. left: 0;
  601. z-index: -1;
  602. }
  603. </style>

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/495253
推荐阅读
相关标签