ChatPrivateVideo.vue 8.3 KB

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