ChatPrivateVideo.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <template>
  2. <el-dialog :title="title" :visible.sync="visible" width="800px" :before-close="handleClose">
  3. <div class="chat-video">
  4. <div class="chat-video-box">
  5. <div class="chat-video-friend" v-loading="loading" element-loading-text="等待对方接听..." element-loading-spinner="el-icon-loading"
  6. element-loading-background="rgba(0, 0, 0, 0.9)">
  7. <video ref="friendVideo" autoplay=""></video>
  8. </div>
  9. <div class="chat-video-mine">
  10. <video ref="mineVideo" autoplay=""></video>
  11. </div>
  12. </div>
  13. <div class="chat-video-controllbar">
  14. <div v-show="state=='CONNECTING'" title="取消呼叫" class="icon iconfont icon-phone-reject reject" style="color: red;"
  15. @click="cancel()"></div>
  16. <div v-show="state=='CONNECTED'" title="挂断" class="icon iconfont icon-phone-reject reject" style="color: red;"
  17. @click="handup()"></div>
  18. </div>
  19. </div>
  20. </el-dialog>
  21. </template>
  22. <script>
  23. export default {
  24. name: 'chatVideo',
  25. props: {
  26. visible: {
  27. type: Boolean
  28. },
  29. friend: {
  30. type: Object
  31. },
  32. master: {
  33. type: Boolean
  34. },
  35. offer: {
  36. type: Object
  37. }
  38. },
  39. data() {
  40. return {
  41. stream: null,
  42. loading: false,
  43. peerConnection: null,
  44. state: 'NOT_CONNECTED',
  45. candidates: [],
  46. configuration: {
  47. iceServers: [{
  48. urls: 'stun:stun.l.google.com:19302'
  49. },
  50. {
  51. urls: 'turn:www.boxim.online:3478',
  52. credential: 'admin123',
  53. username: 'admin'
  54. },
  55. {
  56. urls: 'stun:www.boxim.online:3478'
  57. }
  58. ]
  59. }
  60. }
  61. },
  62. methods: {
  63. init() {
  64. if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) {
  65. this.$message.error("您的浏览器不支持WebRTC");
  66. if (!this.master) {
  67. this.sendFailed("对方浏览器不支持WebRTC")
  68. }
  69. return;
  70. }
  71. // 打开摄像头
  72. this.openCamera((stream) => {
  73. // 初始化webrtc连接
  74. this.setupPeerConnection(stream);
  75. if (this.master) {
  76. // 发起呼叫
  77. this.call();
  78. } else {
  79. // 接受呼叫
  80. this.accept(this.offer);
  81. }
  82. this.timerx && clearInterval(this.timerx);
  83. this.timerx = setInterval(() => {
  84. console.log(this.peerConnection.iceConnectionState);
  85. console.log(this.peerConnection.iceGatheringState);
  86. }, 3000)
  87. });
  88. },
  89. openCamera(callback) {
  90. navigator.getUserMedia({
  91. video: true,
  92. audio: true
  93. },
  94. (stream) => {
  95. this.stream = stream;
  96. this.$refs.mineVideo.srcObject = stream;
  97. this.$refs.mineVideo.muted = true;
  98. callback(stream)
  99. },
  100. (error) => {
  101. this.$message.error("打开摄像头失败:" + error);
  102. callback()
  103. });
  104. },
  105. closeCamera() {
  106. if (this.stream) {
  107. this.stream.getTracks().forEach((track) => {
  108. track.stop();
  109. });
  110. this.$refs.mineVideo.srcObject = null;
  111. this.stream = null;
  112. }
  113. },
  114. setupPeerConnection(stream) {
  115. this.peerConnection = new RTCPeerConnection(this.configuration);
  116. this.peerConnection.ontrack = (e) => {
  117. console.log("onaddstream")
  118. this.$refs.friendVideo.srcObject = e.streams[0];
  119. };
  120. this.peerConnection.onicegatheringstatechange = (event) => {
  121. if (this.peerConnection.iceGatheringState == 'complete') {
  122. this.candidates.forEach((candidate) => {
  123. this.sendCandidate(candidate);
  124. })
  125. }
  126. }
  127. this.peerConnection.onicecandidate = (event) => {
  128. if (event.candidate) {
  129. // if (this.state == 'CONNECTED') {
  130. // // 已连接,直接发送
  131. // this.sendCandidate(event.candidate);
  132. // } else {
  133. // 未连接,缓存起来,连接后再发送
  134. this.candidates.push(event.candidate)
  135. //}
  136. }
  137. }
  138. if (stream) {
  139. stream.getTracks().forEach((track) => {
  140. this.peerConnection.addTrack(track, stream);
  141. });
  142. }
  143. this.peerConnection.oniceconnectionstatechange = function(event) {
  144. console.log("ICE connection status changed : " + event.target.iceConnectionState)
  145. };
  146. },
  147. handleMessage(msg) {
  148. if (msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) {
  149. console.log("接收answer")
  150. console.log(msg.content)
  151. this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content)));
  152. // 关闭等待提示
  153. this.loading = false;
  154. // 状态为连接中
  155. this.state = 'CONNECTED';
  156. // 发送candidate
  157. // this.candidates.forEach((candidate) => {
  158. // this.sendCandidate(candidate);
  159. // })
  160. }
  161. if (msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) {
  162. this.$message.error("对方拒绝了您的视频请求");
  163. this.peerConnection.close();
  164. // 关闭等待提示
  165. this.loading = false;
  166. // 状态为未连接
  167. this.state = 'NOT_CONNECTED';
  168. }
  169. if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) {
  170. this.$message.error(msg.content)
  171. // 关闭等待提示
  172. this.loading = false;
  173. // 状态为未连接
  174. this.state = 'NOT_CONNECTED';
  175. }
  176. if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
  177. this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content)));
  178. }
  179. if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) {
  180. this.$message.success("对方已挂断");
  181. this.close();
  182. }
  183. },
  184. call() {
  185. this.peerConnection.createOffer((offer) => {
  186. this.peerConnection.setLocalDescription(offer);
  187. console.log("发送offer")
  188. console.log(JSON.stringify(offer))
  189. this.$http({
  190. url: `/webrtc/private/call?uid=${this.friend.id}`,
  191. method: 'post',
  192. data: JSON.stringify(offer)
  193. }).then(() => {
  194. this.loading = true;
  195. this.state = 'CONNECTING';
  196. });
  197. },
  198. (error) => {
  199. this.$message.error(error);
  200. });
  201. },
  202. accept(offer) {
  203. console.log("接收到offer")
  204. console.log(offer)
  205. this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
  206. this.peerConnection.createAnswer((answer) => {
  207. console.log("发送answer")
  208. console.log(JSON.stringify(answer))
  209. this.peerConnection.setLocalDescription(answer);
  210. this.$http({
  211. url: `/webrtc/private/accept?uid=${this.friend.id}`,
  212. method: 'post',
  213. data: JSON.stringify(answer)
  214. })
  215. this.state = 'CONNECTED';
  216. },
  217. (error) => {
  218. this.$message.error(error);
  219. });
  220. },
  221. handup() {
  222. this.$http({
  223. url: `/webrtc/private/handup?uid=${this.friend.id}`,
  224. method: 'post'
  225. })
  226. this.close();
  227. this.$message.success("已挂断视频通话")
  228. },
  229. cancel() {
  230. this.$http({
  231. url: `/webrtc/private/cancel?uid=${this.friend.id}`,
  232. method: 'post'
  233. })
  234. this.close();
  235. this.$message.success("已停止呼叫视频通话")
  236. },
  237. sendFailed(reason) {
  238. this.$http({
  239. url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`,
  240. method: 'post'
  241. })
  242. },
  243. sendCandidate(candidate) {
  244. this.$http({
  245. url: `/webrtc/private/candidate?uid=${this.friend.id}`,
  246. method: 'post',
  247. data: JSON.stringify(candidate)
  248. })
  249. },
  250. close() {
  251. this.$emit("close");
  252. this.closeCamera();
  253. this.loading = false;
  254. this.state = 'NOT_CONNECTED';
  255. this.candidates = [];
  256. this.$store.commit("setUserState", this.$enums.USER_STATE.FREE);
  257. this.$refs.friendVideo.srcObject = null;
  258. this.peerConnection.close();
  259. this.peerConnection.onicecandidate = null;
  260. this.peerConnection.onaddstream = null;
  261. },
  262. handleClose() {
  263. if (this.state == 'CONNECTED') {
  264. this.handup()
  265. } else if (this.state == 'CONNECTING') {
  266. this.cancel();
  267. } else {
  268. this.close();
  269. }
  270. },
  271. hasUserMedia() {
  272. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ||
  273. navigator.msGetUserMedia;
  274. return !!navigator.getUserMedia;
  275. },
  276. hasRTCPeerConnection() {
  277. window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
  278. window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
  279. window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
  280. return !!window.RTCPeerConnection;
  281. }
  282. },
  283. watch: {
  284. visible: {
  285. handler(newValue, oldValue) {
  286. if (newValue) {
  287. this.init();
  288. // 用户忙状态
  289. this.$store.commit("setUserState", this.$enums.USER_STATE.BUSY);
  290. console.log(this.$store.state.userStore.state)
  291. }
  292. }
  293. }
  294. },
  295. computed: {
  296. title() {
  297. return `视频聊天-${this.friend.nickName}`;
  298. }
  299. }
  300. }
  301. </script>
  302. <style lang="scss">
  303. .chat-video {
  304. .chat-video-box {
  305. position: relative;
  306. border: #2C3E50 solid 1px;
  307. background-color: #eeeeee;
  308. .chat-video-friend {
  309. height: 600px;
  310. video {
  311. width: 100%;
  312. height: 100%;
  313. }
  314. }
  315. .chat-video-mine {
  316. position: absolute;
  317. z-index: 99999;
  318. width: 200px;
  319. right: 0;
  320. bottom: 0;
  321. video {
  322. width: 100%;
  323. }
  324. }
  325. }
  326. .chat-video-controllbar {
  327. display: flex;
  328. justify-content: space-around;
  329. padding: 10px;
  330. .icon {
  331. font-size: 50px;
  332. cursor: pointer;
  333. }
  334. }
  335. }
  336. </style>