| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- <template>
- <div>
- <el-dialog v-dialogDrag top="5vh" custom-class="rtc-private-video-dialog" :title="title" :width="width"
- :visible.sync="showRoom" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="onQuit">
- <div class="rtc-private-video">
- <div v-show="isVideo" class="rtc-video-box">
- <div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..."
- element-loading-background="rgba(0, 0, 0, 0.1)">
- <head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName"
- :url="friend.headImage" :isShowUserInfo="false" radius="0">
- </head-image>
- <video ref="remoteVideo" autoplay=""></video>
- </div>
- <div class="rtc-video-mine">
- <video ref="localVideo" autoplay=""></video>
- </div>
- </div>
- <div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..."
- element-loading-background="rgba(0, 0, 0, 0.1)">
- <head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName"
- :url="friend.headImage" :isShowUserInfo="false">
- <div class="rtc-voice-name">{{ friend.nickName }}</div>
- </head-image>
- </div>
- <div class="rtc-control-bar">
- <div title="取消" class="icon iconfont icon-phone-reject reject" style="color: red;" @click="onQuit()"></div>
- </div>
- </div>
- </el-dialog>
- <rtc-private-acceptor v-if="!isHost && isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept"
- @reject="onReject"></rtc-private-acceptor>
- </div>
- </template>
- <script>
- import HeadImage from '../common/HeadImage.vue';
- import RtcPrivateAcceptor from './RtcPrivateAcceptor.vue';
- import ImWebRtc from '@/api/webrtc';
- import ImCamera from '@/api/camera';
- import RtcPrivateApi from '@/api/rtcPrivateApi'
- export default {
- name: 'rtcPrivateVideo',
- components: {
- HeadImage,
- RtcPrivateAcceptor
- },
- data() {
- return {
- camera: new ImCamera(), // 摄像头和麦克风
- webrtc: new ImWebRtc(), // webrtc相关
- API: new RtcPrivateApi(), // API
- audio: new Audio(), // 呼叫音频
- showRoom: false,
- friend: {},
- isHost: false, // 是否发起人
- state: "CLOSE", // CLOSE:关闭 WAITING:等待呼叫或接听 CHATING:聊天中 ERROR:出现异常
- mode: 'video', // 模式 video:视频聊 voice:语音聊天
- localStream: null, // 本地视频流
- remoteStream: null, // 对方视频流
- videoTime: 0,
- videoTimer: null,
- heartbeatTimer: null,
- candidates: [],
- }
- },
- methods: {
- open(rtcInfo) {
- this.showRoom = true;
- this.mode = rtcInfo.mode;
- this.isHost = rtcInfo.isHost;
- this.friend = rtcInfo.friend;
- if (this.isHost) {
- this.onCall();
- }
- },
- initAudio() {
- let url = require(`@/assets/audio/call.wav`);
- this.audio.src = url;
- this.audio.loop = true;
- },
- initRtc() {
- this.webrtc.init(this.configuration)
- this.webrtc.setupPeerConnection((stream) => {
- this.$refs.remoteVideo.srcObject = stream;
- this.remoteStream = stream;
- })
- // 监听候选信息
- this.webrtc.onIcecandidate((candidate) => {
- if (this.state == "CHATING") {
- // 连接已就绪,直接发送
- this.API.sendCandidate(this.friend.id, candidate);
- } else {
- // 连接未就绪,缓存起来,连接后再发送
- this.candidates.push(candidate)
- }
- })
- // 监听连接成功状态
- this.webrtc.onStateChange((state) => {
- if (state == "connected") {
- console.log("webrtc连接成功")
- } else if (state == "disconnected") {
- console.log("webrtc连接断开")
- }
- })
- },
- onCall() {
- if (!this.checkDevEnable()) {
- this.close();
- }
- // 初始化webrtc
- this.initRtc();
- // 启动心跳
- this.startHeartBeat();
- // 打开摄像头
- this.openStream().then(() => {
- this.webrtc.setStream(this.localStream);
- this.webrtc.createOffer().then((offer) => {
- // 发起呼叫
- this.API.call(this.friend.id, this.mode, offer).then(() => {
- // 直接进入聊天状态
- this.state = "WAITING";
- // 播放呼叫铃声
- this.audio.play();
- }).catch(() => {
- this.close();
- })
- })
- }).catch(() => {
- // 呼叫方必须能打开摄像头,否则无法正常建立连接
- this.close();
- })
- },
- onAccept() {
- if (!this.checkDevEnable()) {
- this.API.failed(this.friend.id, "对方设备不支持通话")
- this.close();
- return;
- }
- // 进入房间
- this.showRoom = true;
- this.state = "CHATING";
- // 停止呼叫铃声
- this.audio.pause();
- // 初始化webrtc
- this.initRtc();
- // 打开摄像头
- this.openStream().finally(() => {
- this.webrtc.setStream(this.localStream);
- this.webrtc.createAnswer(this.offer).then((answer) => {
- this.API.accept(this.friend.id, answer);
- // 记录时长
- this.startChatTime();
- // 清理定时器
- this.waitTimer && clearTimeout(this.waitTimer);
- })
- })
- },
- onReject() {
- // 退出通话
- this.API.reject(this.friend.id);
- // 退出
- this.close();
- },
- onHandup() {
- this.API.handup(this.friend.id)
- this.$message.success("您已挂断,通话结束")
- this.close();
- },
- onCancel() {
- this.API.cancel(this.friend.id)
- this.$message.success("已取消呼叫,通话结束")
- this.close();
- },
- onRTCMessage(msg) {
- // 除了发起通话,如果在关闭状态就无需处理
- if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
- msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO &&
- this.isClose) {
- return;
- }
- // RTC信令处理
- switch (msg.type) {
- case this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE:
- this.onRTCCall(msg, 'voice')
- break;
- case this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO:
- this.onRTCCall(msg, 'video')
- break;
- case this.$enums.MESSAGE_TYPE.RTC_ACCEPT:
- this.onRTCAccept(msg)
- break;
- case this.$enums.MESSAGE_TYPE.RTC_REJECT:
- this.onRTCReject(msg)
- break;
- case this.$enums.MESSAGE_TYPE.RTC_CANCEL:
- this.onRTCCancel(msg)
- break;
- case this.$enums.MESSAGE_TYPE.RTC_FAILED:
- this.onRTCFailed(msg)
- break;
- case this.$enums.MESSAGE_TYPE.RTC_HANDUP:
- this.onRTCHandup(msg)
- break;
- case this.$enums.MESSAGE_TYPE.RTC_CANDIDATE:
- this.onRTCCandidate(msg)
- break;
- }
- },
- onRTCCall(msg, mode) {
- this.offer = JSON.parse(msg.content);
- this.isHost = false;
- this.mode = mode;
- this.$http({
- url: `/friend/find/${msg.sendId}`,
- method: 'get'
- }).then((friend) => {
- this.friend = friend;
- this.state = "WAITING";
- this.audio.play();
- this.startHeartBeat();
- // 30s未接听自动挂掉
- this.waitTimer = setTimeout(() => {
- this.API.failed(this.friend.id, "对方无应答");
- this.$message.error("您未接听");
- this.close();
- }, 30000)
- })
- },
- onRTCAccept(msg) {
- if (msg.selfSend) {
- // 在其他设备接听
- this.$message.success("已在其他设备接听");
- this.close();
- } else {
- // 对方接受了的通话
- let offer = JSON.parse(msg.content);
- this.webrtc.setRemoteDescription(offer);
- // 状态为聊天中
- this.state = 'CHATING'
- // 停止播放语音
- this.audio.pause();
- // 发送candidate
- this.candidates.forEach((candidate) => {
- this.API.sendCandidate(this.friend.id, candidate);
- })
- // 开始计时
- this.startChatTime()
- }
- },
- onRTCReject(msg) {
- if (msg.selfSend) {
- this.$message.success("已在其他设备拒绝");
- this.close();
- } else {
- this.$message.error("对方拒绝了您的通话请求");
- this.close();
- }
- },
- onRTCFailed(msg) {
- // 呼叫失败
- this.$message.error(msg.content)
- this.close();
- },
- onRTCCancel() {
- // 对方取消通话
- this.$message.success("对方取消了呼叫");
- this.close();
- },
- onRTCHandup() {
- // 对方挂断
- this.$message.success("对方已挂断");
- this.close();
- },
- onRTCCandidate(msg) {
- let candidate = JSON.parse(msg.content);
- this.webrtc.addIceCandidate(candidate);
- },
- openStream() {
- return new Promise((resolve, reject) => {
- if (this.isVideo) {
- // 打开摄像头+麦克风
- this.camera.openVideo().then((stream) => {
- this.localStream = stream;
- this.$nextTick(() => {
- this.$refs.localVideo.srcObject = stream;
- this.$refs.localVideo.muted = true;
- })
- resolve(stream);
- }).catch((e) => {
- this.$message.error("打开摄像头失败")
- console.log("本摄像头打开失败:" + e.message)
- reject(e);
- })
- } else {
- // 打开麦克风
- this.camera.openAudio().then((stream) => {
- this.localStream = stream;
- this.$refs.localVideo.srcObject = stream;
- this.$refs.localVideo.muted = true;
- resolve(stream);
- }).catch((e) => {
- this.$message.error("打开麦克风失败")
- console.log("打开麦克风失败:" + e.message)
- reject(e);
- })
- }
- })
- },
- startChatTime() {
- this.videoTime = 0;
- this.videoTimer && clearInterval(this.videoTimer);
- this.videoTimer = setInterval(() => {
- this.videoTime++;
- }, 1000)
- },
- checkDevEnable() {
- // 检测摄像头
- if (!this.camera.isEnable()) {
- this.message.error("访问摄像头失败");
- return false;
- }
- // 检测webrtc
- if (!this.webrtc.isEnable()) {
- this.message.error("初始化RTC失败,原因可能是: 1.服务器缺少ssl证书 2.您的设备不支持WebRTC");
- return false;
- }
- return true;
- },
- startHeartBeat() {
- // 每15s推送一次心跳
- this.heartbeatTimer && clearInterval(this.heartbeatTimer);
- this.heartbeatTimer = setInterval(() => {
- this.API.heartbeat(this.friend.id);
- }, 15000)
- },
- close() {
- this.showRoom = false;
- this.camera.close();
- this.webrtc.close();
- this.audio.pause();
- this.videoTime = 0;
- this.videoTimer && clearInterval(this.videoTimer);
- this.heartbeatTimer && clearInterval(this.heartbeatTimer);
- this.waitTimer && clearTimeout(this.waitTimer);
- this.videoTimer = null;
- this.heartbeatTimer = null;
- this.waitTimer = null;
- this.state = 'CLOSE';
- this.candidates = [];
- },
- onQuit() {
- if (this.isChating) {
- this.onHandup()
- } else if (this.isWaiting) {
- this.onCancel();
- } else {
- this.close();
- }
- }
- },
- computed: {
- width() {
- return this.isVideo ? '960px' : '360px'
- },
- title() {
- let strTitle = `${this.modeText}通话-${this.friend.nickName}`;
- if (this.isChating) {
- strTitle += `(${this.currentTime})`;
- } else if (this.isWaiting) {
- strTitle += `(呼叫中)`;
- }
- return strTitle;
- },
- currentTime() {
- let min = Math.floor(this.videoTime / 60);
- let sec = this.videoTime % 60;
- let strTime = min < 10 ? "0" : "";
- strTime += min;
- strTime += ":"
- strTime += sec < 10 ? "0" : "";
- strTime += sec;
- return strTime;
- },
- configuration() {
- const iceServers = this.configStore.webrtc.iceServers;
- return {
- iceServers: iceServers
- }
- },
- isVideo() {
- return this.mode == "video"
- },
- modeText() {
- return this.isVideo ? "视频" : "语音";
- },
- isChating() {
- return this.state == "CHATING";
- },
- isWaiting() {
- return this.state == "WAITING";
- },
- isClose() {
- return this.state == "CLOSE";
- }
- },
- mounted() {
- // 初始化音频文件
- this.initAudio();
- },
- created() {
- // 监听页面刷新事件
- window.addEventListener('beforeunload', () => {
- this.onQuit();
- });
- },
- beforeUnmount() {
- this.onQuit();
- }
- }
- </script>
- <style lang="scss" scoped>
- .rtc-private-video {
- position: relative;
- .el-loading-text {
- color: white !important;
- font-size: 16px !important;
- }
- .path {
- stroke: white !important;
- }
- .rtc-video-box {
- position: relative;
- background-color: #eeeeee;
- .rtc-video-friend {
- height: 70vh;
- .friend-head-image {
- position: absolute;
- }
- video {
- width: 100%;
- height: 100%;
- object-fit: cover;
- transform: rotateY(180deg);
- }
- }
- .rtc-video-mine {
- position: absolute;
- z-index: 99999;
- width: 25vh;
- right: 0;
- bottom: -1px;
- video {
- width: 100%;
- object-fit: cover;
- transform: rotateY(180deg);
- }
- }
- }
- .rtc-voice-box {
- position: relative;
- display: flex;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: 300px;
- background-color: var(--im-color-primary-light-9);
- .rtc-voice-name {
- text-align: center;
- font-size: 20px;
- font-weight: 600;
- }
- }
- .rtc-control-bar {
- display: flex;
- justify-content: space-around;
- padding: 10px;
- .icon {
- font-size: 50px;
- cursor: pointer;
- }
- }
- }
- </style>
|