RtcPrivateVideo.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <template>
  2. <div>
  3. <el-dialog v-dialogDrag top="5vh" custom-class="rtc-private-video-dialog" :title="title" :width="width"
  4. :visible.sync="showRoom" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="onQuit">
  5. <div class="rtc-private-video">
  6. <div v-show="isVideo" class="rtc-video-box">
  7. <div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..."
  8. element-loading-background="rgba(0, 0, 0, 0.1)">
  9. <head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName"
  10. :url="friend.headImage" :isShowUserInfo="false" radius="0">
  11. </head-image>
  12. <video ref="remoteVideo" autoplay=""></video>
  13. </div>
  14. <div class="rtc-video-mine">
  15. <video ref="localVideo" autoplay=""></video>
  16. </div>
  17. </div>
  18. <div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..."
  19. element-loading-background="rgba(0, 0, 0, 0.1)">
  20. <head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName"
  21. :url="friend.headImage" :isShowUserInfo="false">
  22. <div class="rtc-voice-name">{{ friend.nickName }}</div>
  23. </head-image>
  24. </div>
  25. <div class="rtc-control-bar">
  26. <div title="取消" class="icon iconfont icon-phone-reject reject" style="color: red;" @click="onQuit()"></div>
  27. </div>
  28. </div>
  29. </el-dialog>
  30. <rtc-private-acceptor v-if="!isHost && isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept"
  31. @reject="onReject"></rtc-private-acceptor>
  32. </div>
  33. </template>
  34. <script>
  35. import HeadImage from '../common/HeadImage.vue';
  36. import RtcPrivateAcceptor from './RtcPrivateAcceptor.vue';
  37. import ImWebRtc from '@/api/webrtc';
  38. import ImCamera from '@/api/camera';
  39. import RtcPrivateApi from '@/api/rtcPrivateApi'
  40. export default {
  41. name: 'rtcPrivateVideo',
  42. components: {
  43. HeadImage,
  44. RtcPrivateAcceptor
  45. },
  46. data() {
  47. return {
  48. camera: new ImCamera(), // 摄像头和麦克风
  49. webrtc: new ImWebRtc(), // webrtc相关
  50. API: new RtcPrivateApi(), // API
  51. audio: new Audio(), // 呼叫音频
  52. showRoom: false,
  53. friend: {},
  54. isHost: false, // 是否发起人
  55. state: "CLOSE", // CLOSE:关闭 WAITING:等待呼叫或接听 CHATING:聊天中 ERROR:出现异常
  56. mode: 'video', // 模式 video:视频聊 voice:语音聊天
  57. localStream: null, // 本地视频流
  58. remoteStream: null, // 对方视频流
  59. videoTime: 0,
  60. videoTimer: null,
  61. heartbeatTimer: null,
  62. candidates: [],
  63. }
  64. },
  65. methods: {
  66. open(rtcInfo) {
  67. this.showRoom = true;
  68. this.mode = rtcInfo.mode;
  69. this.isHost = rtcInfo.isHost;
  70. this.friend = rtcInfo.friend;
  71. if (this.isHost) {
  72. this.onCall();
  73. }
  74. },
  75. initAudio() {
  76. let url = require(`@/assets/audio/call.wav`);
  77. this.audio.src = url;
  78. this.audio.loop = true;
  79. },
  80. initRtc() {
  81. this.webrtc.init(this.configuration)
  82. this.webrtc.setupPeerConnection((stream) => {
  83. this.$refs.remoteVideo.srcObject = stream;
  84. this.remoteStream = stream;
  85. })
  86. // 监听候选信息
  87. this.webrtc.onIcecandidate((candidate) => {
  88. if (this.state == "CHATING") {
  89. // 连接已就绪,直接发送
  90. this.API.sendCandidate(this.friend.id, candidate);
  91. } else {
  92. // 连接未就绪,缓存起来,连接后再发送
  93. this.candidates.push(candidate)
  94. }
  95. })
  96. // 监听连接成功状态
  97. this.webrtc.onStateChange((state) => {
  98. if (state == "connected") {
  99. console.log("webrtc连接成功")
  100. } else if (state == "disconnected") {
  101. console.log("webrtc连接断开")
  102. }
  103. })
  104. },
  105. onCall() {
  106. if (!this.checkDevEnable()) {
  107. this.close();
  108. }
  109. // 初始化webrtc
  110. this.initRtc();
  111. // 启动心跳
  112. this.startHeartBeat();
  113. // 打开摄像头
  114. this.openStream().then(() => {
  115. this.webrtc.setStream(this.localStream);
  116. this.webrtc.createOffer().then((offer) => {
  117. // 发起呼叫
  118. this.API.call(this.friend.id, this.mode, offer).then(() => {
  119. // 直接进入聊天状态
  120. this.state = "WAITING";
  121. // 播放呼叫铃声
  122. this.audio.play();
  123. }).catch(() => {
  124. this.close();
  125. })
  126. })
  127. }).catch(() => {
  128. // 呼叫方必须能打开摄像头,否则无法正常建立连接
  129. this.close();
  130. })
  131. },
  132. onAccept() {
  133. if (!this.checkDevEnable()) {
  134. this.API.failed(this.friend.id, "对方设备不支持通话")
  135. this.close();
  136. return;
  137. }
  138. // 进入房间
  139. this.showRoom = true;
  140. this.state = "CHATING";
  141. // 停止呼叫铃声
  142. this.audio.pause();
  143. // 初始化webrtc
  144. this.initRtc();
  145. // 打开摄像头
  146. this.openStream().finally(() => {
  147. this.webrtc.setStream(this.localStream);
  148. this.webrtc.createAnswer(this.offer).then((answer) => {
  149. this.API.accept(this.friend.id, answer);
  150. // 记录时长
  151. this.startChatTime();
  152. // 清理定时器
  153. this.waitTimer && clearTimeout(this.waitTimer);
  154. })
  155. })
  156. },
  157. onReject() {
  158. // 退出通话
  159. this.API.reject(this.friend.id);
  160. // 退出
  161. this.close();
  162. },
  163. onHandup() {
  164. this.API.handup(this.friend.id)
  165. this.$message.success("您已挂断,通话结束")
  166. this.close();
  167. },
  168. onCancel() {
  169. this.API.cancel(this.friend.id)
  170. this.$message.success("已取消呼叫,通话结束")
  171. this.close();
  172. },
  173. onRTCMessage(msg) {
  174. // 除了发起通话,如果在关闭状态就无需处理
  175. if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
  176. msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO &&
  177. this.isClose) {
  178. return;
  179. }
  180. // RTC信令处理
  181. switch (msg.type) {
  182. case this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE:
  183. this.onRTCCall(msg, 'voice')
  184. break;
  185. case this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO:
  186. this.onRTCCall(msg, 'video')
  187. break;
  188. case this.$enums.MESSAGE_TYPE.RTC_ACCEPT:
  189. this.onRTCAccept(msg)
  190. break;
  191. case this.$enums.MESSAGE_TYPE.RTC_REJECT:
  192. this.onRTCReject(msg)
  193. break;
  194. case this.$enums.MESSAGE_TYPE.RTC_CANCEL:
  195. this.onRTCCancel(msg)
  196. break;
  197. case this.$enums.MESSAGE_TYPE.RTC_FAILED:
  198. this.onRTCFailed(msg)
  199. break;
  200. case this.$enums.MESSAGE_TYPE.RTC_HANDUP:
  201. this.onRTCHandup(msg)
  202. break;
  203. case this.$enums.MESSAGE_TYPE.RTC_CANDIDATE:
  204. this.onRTCCandidate(msg)
  205. break;
  206. }
  207. },
  208. onRTCCall(msg, mode) {
  209. this.offer = JSON.parse(msg.content);
  210. this.isHost = false;
  211. this.mode = mode;
  212. this.$http({
  213. url: `/friend/find/${msg.sendId}`,
  214. method: 'get'
  215. }).then((friend) => {
  216. this.friend = friend;
  217. this.state = "WAITING";
  218. this.audio.play();
  219. this.startHeartBeat();
  220. // 30s未接听自动挂掉
  221. this.waitTimer = setTimeout(() => {
  222. this.API.failed(this.friend.id, "对方无应答");
  223. this.$message.error("您未接听");
  224. this.close();
  225. }, 30000)
  226. })
  227. },
  228. onRTCAccept(msg) {
  229. if (msg.selfSend) {
  230. // 在其他设备接听
  231. this.$message.success("已在其他设备接听");
  232. this.close();
  233. } else {
  234. // 对方接受了的通话
  235. let offer = JSON.parse(msg.content);
  236. this.webrtc.setRemoteDescription(offer);
  237. // 状态为聊天中
  238. this.state = 'CHATING'
  239. // 停止播放语音
  240. this.audio.pause();
  241. // 发送candidate
  242. this.candidates.forEach((candidate) => {
  243. this.API.sendCandidate(this.friend.id, candidate);
  244. })
  245. // 开始计时
  246. this.startChatTime()
  247. }
  248. },
  249. onRTCReject(msg) {
  250. if (msg.selfSend) {
  251. this.$message.success("已在其他设备拒绝");
  252. this.close();
  253. } else {
  254. this.$message.error("对方拒绝了您的通话请求");
  255. this.close();
  256. }
  257. },
  258. onRTCFailed(msg) {
  259. // 呼叫失败
  260. this.$message.error(msg.content)
  261. this.close();
  262. },
  263. onRTCCancel() {
  264. // 对方取消通话
  265. this.$message.success("对方取消了呼叫");
  266. this.close();
  267. },
  268. onRTCHandup() {
  269. // 对方挂断
  270. this.$message.success("对方已挂断");
  271. this.close();
  272. },
  273. onRTCCandidate(msg) {
  274. let candidate = JSON.parse(msg.content);
  275. this.webrtc.addIceCandidate(candidate);
  276. },
  277. openStream() {
  278. return new Promise((resolve, reject) => {
  279. if (this.isVideo) {
  280. // 打开摄像头+麦克风
  281. this.camera.openVideo().then((stream) => {
  282. this.localStream = stream;
  283. this.$nextTick(() => {
  284. this.$refs.localVideo.srcObject = stream;
  285. this.$refs.localVideo.muted = true;
  286. })
  287. resolve(stream);
  288. }).catch((e) => {
  289. this.$message.error("打开摄像头失败")
  290. console.log("本摄像头打开失败:" + e.message)
  291. reject(e);
  292. })
  293. } else {
  294. // 打开麦克风
  295. this.camera.openAudio().then((stream) => {
  296. this.localStream = stream;
  297. this.$refs.localVideo.srcObject = stream;
  298. this.$refs.localVideo.muted = true;
  299. resolve(stream);
  300. }).catch((e) => {
  301. this.$message.error("打开麦克风失败")
  302. console.log("打开麦克风失败:" + e.message)
  303. reject(e);
  304. })
  305. }
  306. })
  307. },
  308. startChatTime() {
  309. this.videoTime = 0;
  310. this.videoTimer && clearInterval(this.videoTimer);
  311. this.videoTimer = setInterval(() => {
  312. this.videoTime++;
  313. }, 1000)
  314. },
  315. checkDevEnable() {
  316. // 检测摄像头
  317. if (!this.camera.isEnable()) {
  318. this.message.error("访问摄像头失败");
  319. return false;
  320. }
  321. // 检测webrtc
  322. if (!this.webrtc.isEnable()) {
  323. this.message.error("初始化RTC失败,原因可能是: 1.服务器缺少ssl证书 2.您的设备不支持WebRTC");
  324. return false;
  325. }
  326. return true;
  327. },
  328. startHeartBeat() {
  329. // 每15s推送一次心跳
  330. this.heartbeatTimer && clearInterval(this.heartbeatTimer);
  331. this.heartbeatTimer = setInterval(() => {
  332. this.API.heartbeat(this.friend.id);
  333. }, 15000)
  334. },
  335. close() {
  336. this.showRoom = false;
  337. this.camera.close();
  338. this.webrtc.close();
  339. this.audio.pause();
  340. this.videoTime = 0;
  341. this.videoTimer && clearInterval(this.videoTimer);
  342. this.heartbeatTimer && clearInterval(this.heartbeatTimer);
  343. this.waitTimer && clearTimeout(this.waitTimer);
  344. this.videoTimer = null;
  345. this.heartbeatTimer = null;
  346. this.waitTimer = null;
  347. this.state = 'CLOSE';
  348. this.candidates = [];
  349. },
  350. onQuit() {
  351. if (this.isChating) {
  352. this.onHandup()
  353. } else if (this.isWaiting) {
  354. this.onCancel();
  355. } else {
  356. this.close();
  357. }
  358. }
  359. },
  360. computed: {
  361. width() {
  362. return this.isVideo ? '960px' : '360px'
  363. },
  364. title() {
  365. let strTitle = `${this.modeText}通话-${this.friend.nickName}`;
  366. if (this.isChating) {
  367. strTitle += `(${this.currentTime})`;
  368. } else if (this.isWaiting) {
  369. strTitle += `(呼叫中)`;
  370. }
  371. return strTitle;
  372. },
  373. currentTime() {
  374. let min = Math.floor(this.videoTime / 60);
  375. let sec = this.videoTime % 60;
  376. let strTime = min < 10 ? "0" : "";
  377. strTime += min;
  378. strTime += ":"
  379. strTime += sec < 10 ? "0" : "";
  380. strTime += sec;
  381. return strTime;
  382. },
  383. configuration() {
  384. const iceServers = this.configStore.webrtc.iceServers;
  385. return {
  386. iceServers: iceServers
  387. }
  388. },
  389. isVideo() {
  390. return this.mode == "video"
  391. },
  392. modeText() {
  393. return this.isVideo ? "视频" : "语音";
  394. },
  395. isChating() {
  396. return this.state == "CHATING";
  397. },
  398. isWaiting() {
  399. return this.state == "WAITING";
  400. },
  401. isClose() {
  402. return this.state == "CLOSE";
  403. }
  404. },
  405. mounted() {
  406. // 初始化音频文件
  407. this.initAudio();
  408. },
  409. created() {
  410. // 监听页面刷新事件
  411. window.addEventListener('beforeunload', () => {
  412. this.onQuit();
  413. });
  414. },
  415. beforeUnmount() {
  416. this.onQuit();
  417. }
  418. }
  419. </script>
  420. <style lang="scss" scoped>
  421. .rtc-private-video {
  422. position: relative;
  423. .el-loading-text {
  424. color: white !important;
  425. font-size: 16px !important;
  426. }
  427. .path {
  428. stroke: white !important;
  429. }
  430. .rtc-video-box {
  431. position: relative;
  432. background-color: #eeeeee;
  433. .rtc-video-friend {
  434. height: 70vh;
  435. .friend-head-image {
  436. position: absolute;
  437. }
  438. video {
  439. width: 100%;
  440. height: 100%;
  441. object-fit: cover;
  442. transform: rotateY(180deg);
  443. }
  444. }
  445. .rtc-video-mine {
  446. position: absolute;
  447. z-index: 99999;
  448. width: 25vh;
  449. right: 0;
  450. bottom: -1px;
  451. video {
  452. width: 100%;
  453. object-fit: cover;
  454. transform: rotateY(180deg);
  455. }
  456. }
  457. }
  458. .rtc-voice-box {
  459. position: relative;
  460. display: flex;
  461. justify-content: center;
  462. align-items: center;
  463. width: 100%;
  464. height: 300px;
  465. background-color: var(--im-color-primary-light-9);
  466. .rtc-voice-name {
  467. text-align: center;
  468. font-size: 20px;
  469. font-weight: 600;
  470. }
  471. }
  472. .rtc-control-bar {
  473. display: flex;
  474. justify-content: space-around;
  475. padding: 10px;
  476. .icon {
  477. font-size: 50px;
  478. cursor: pointer;
  479. }
  480. }
  481. }
  482. </style>