diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index e93a8ed8b..d3d3ca7c5 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -92,6 +92,51 @@ class DialogController extends AbstractController return Base::retSuccess('success', $item); } + /** + * @api {get} api/dialog/user 16. 获取会话成员 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName user + * + * @apiParam {Number} dialog_id 会话ID + * @apiParam {Number} [getuser] 获取会员详情(1: 返回会员昵称、邮箱等基本信息,0: 默认不返回) + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function user() + { + User::auth(); + // + $dialog_id = intval(Request::input('dialog_id')); + $getuser = intval(Request::input('getuser', 0)); + // + $dialog = WebSocketDialog::checkDialog($dialog_id); + // + $data = $dialog->dialogUser->toArray(); + if ($getuser === 1) { + $array = []; + foreach ($data as $item) { + $res = User::userid2basic($item['userid']); + if ($res) { + $array[] = array_merge($item, $res->toArray()); + } + } + $data = $array; + } + // + $array = []; + foreach ($data as $item) { + if ($item['userid'] > 0) { + $array[] = $item; + } + } + return Base::retSuccess('success', $array); + } + /** * @api {get} api/dialog/msg/user 03. 打开会话 * @@ -695,44 +740,6 @@ class DialogController extends AbstractController ]); } - /** - * @api {get} api/dialog/group/user 16. 获取群成员 - * - * @apiDescription 需要token身份 - * @apiVersion 1.0.0 - * @apiGroup dialog - * @apiName group__user - * - * @apiParam {Number} dialog_id 会话ID - * @apiParam {Number} [getuser] 获取会员详情(1: 返回会员昵称、邮箱等基本信息,0: 默认不返回) - * - * @apiSuccess {Number} ret 返回状态码(1正确、0错误) - * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据 - */ - public function group__user() - { - User::auth(); - // - $dialog_id = intval(Request::input('dialog_id')); - $getuser = intval(Request::input('getuser', 0)); - // - $dialog = WebSocketDialog::checkDialog($dialog_id); - // - $data = $dialog->dialogUser->toArray(); - if ($getuser === 1) { - $array = []; - foreach ($data as $item) { - $res = User::userid2basic($item['userid']); - if ($res) { - $array[] = array_merge($item, $res->toArray()); - } - } - $data = $array; - } - return Base::retSuccess('success', $data); - } - /** * @api {get} api/dialog/group/adduser 17. 添加群成员 * diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 7c4e11c3f..5541eb7c1 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -3,11 +3,14 @@ namespace App\Http\Controllers\Api; use App\Models\AbstractModel; +use App\Models\Meeting; use App\Models\UmengAlias; use App\Models\User; use App\Models\UserEmailVerification; use App\Models\UserTransfer; use App\Models\WebSocket; +use App\Models\WebSocketDialog; +use App\Models\WebSocketDialogMsg; use App\Module\AgoraIO\AgoraTokenGenerator; use App\Module\Base; use Arr; @@ -763,27 +766,53 @@ class UsersController extends AbstractController } /** - * @api {get} api/users/agoraio/token 16. 【agoraio】获取 token + * @api {get} api/users/meeting/open 16. 【会议】新会议 * * @apiDescription 需要token身份 * @apiVersion 1.0.0 * @apiGroup users - * @apiName agoraio__token + * @apiName meeting__open * - * @apiParam {Number} dialog_id 会话ID + * @apiParam {String} [meetingid] 会议ID(不是数字,留空自动创建) + * @apiParam {String} [name] 会话ID + * @apiParam {Array} [userids] 邀请成员 * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {Object} data 返回数据 */ - public function agoraio__token() + public function meeting__open() { $user = User::auth(); // + $meetingid = trim(Request::input('meetingid')); + $name = trim(Request::input('name')); + $userids = Request::input('userids'); + $isCreate = false; + // + if ($meetingid) { + $meeting = Meeting::whereMeetingid($meetingid)->first(); + if (empty($meeting)) { + return Base::retError('会议不存在'); + } + } else { + $meetingid = strtoupper(Base::generatePassword()); + $name = $name ?: "{$user->nickname} 发起的会议"; + $channel = "DooTask:" . substr(md5($meetingid . env("APP_KEY")), 16); + $meeting = Meeting::createInstance([ + 'meetingid' => $meetingid, + 'name' => $name, + 'channel' => $channel, + 'userid' => $user->userid + ]); + $meeting->save(); + $isCreate = true; + } + // 创建令牌 $appid = '342c604542484b0d9659527f79aefcdb'; $app_certificate = '920eb911c1f549948366e44d6dcabcbe'; - $channel = "DooTask:" . md5(env("APP_KEY")); - $uid = $user->userid; + $channel = $meeting->channel; + $uid = $user->userid . '_' . Request::header('fd'); try { $service = new AgoraTokenGenerator($appid, $app_certificate, $channel, $uid); } catch (\Exception $e) { @@ -791,13 +820,30 @@ class UsersController extends AbstractController } $token = $service->buildToken(); if (empty($token)) { - return Base::retError('Generated token failed'); + return Base::retError('会议令牌创建失败'); } - return Base::retSuccess('success', [ - 'appid' => $appid, - 'channel' => $channel, - 'uid' => $uid, - 'token' => $token - ]); + // 发送给邀请人 + $msgs = []; + if ($isCreate) { + foreach ($userids as $userid) { + if (!User::whereUserid($userid)->exists()) { + continue; + } + $dialog = WebSocketDialog::checkUserDialog($user->userid, $userid); + if ($dialog) { + $res = WebSocketDialogMsg::sendMsg($dialog->id, 'meeting', $meeting, $user->userid); + if (Base::isSuccess($res)) { + $msgs[] = $res['data']; + } + } + } + } + // + $data = $meeting->toArray(); + $data['appid'] = $appid; + $data['uid'] = $uid; + $data['token'] = $token; + $data['msgs'] = $msgs; + return Base::retSuccess('success', $data); } } diff --git a/app/Models/Meeting.php b/app/Models/Meeting.php new file mode 100644 index 000000000..202530770 --- /dev/null +++ b/app/Models/Meeting.php @@ -0,0 +1,34 @@ +previewTextMsg($this->msg['text'], $preserveHtml); case 'record': return "[语音]"; + case 'meeting': + return "[会议] ${$this->msg['name']}"; case 'file': if ($this->msg['type'] == 'img') { return "[图片]"; diff --git a/database/migrations/2022_06_01_142823_create_meetings_table.php b/database/migrations/2022_06_01_142823_create_meetings_table.php new file mode 100644 index 000000000..f1aa13979 --- /dev/null +++ b/database/migrations/2022_06_01_142823_create_meetings_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->string('meetingid')->nullable()->default('')->unique()->comment('会议ID,不是数字'); + $table->string('name')->nullable()->default('')->comment('会议主题'); + $table->string('channel')->nullable()->default('')->comment('频道'); + $table->bigInteger('userid')->nullable()->default(0)->comment('创建人'); + $table->timestamps(); + $table->timestamp('end_at')->nullable(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('meetings'); + } +} diff --git a/public/css/fonts/taskfont.ttf b/public/css/fonts/taskfont.ttf index 4701aab06..869bf9377 100644 Binary files a/public/css/fonts/taskfont.ttf and b/public/css/fonts/taskfont.ttf differ diff --git a/public/css/fonts/taskfont.woff b/public/css/fonts/taskfont.woff index 4829fa215..9e456a627 100644 Binary files a/public/css/fonts/taskfont.woff and b/public/css/fonts/taskfont.woff differ diff --git a/public/css/fonts/taskfont.woff2 b/public/css/fonts/taskfont.woff2 index ed2d0c544..6ca9667f3 100644 Binary files a/public/css/fonts/taskfont.woff2 and b/public/css/fonts/taskfont.woff2 differ diff --git a/resources/assets/js/pages/manage/components/ChatInput/index.vue b/resources/assets/js/pages/manage/components/ChatInput/index.vue index e1ed99a70..5fac66b89 100755 --- a/resources/assets/js/pages/manage/components/ChatInput/index.vue +++ b/resources/assets/js/pages/manage/components/ChatInput/index.vue @@ -788,8 +788,9 @@ export default { case 'meeting': Store.set('addMeeting', { - userids: [this.userId] - }); // todo 加入当前会话人员 + dialog_id: this.dialogId, + userids: [this.userId], + }); break; case 'image': @@ -872,7 +873,7 @@ export default { if (this.dialogId > 0) { // 根据会话ID获取成员 this.$store.dispatch("call", { - url: 'dialog/group/user', + url: 'dialog/user', data: { dialog_id: this.dialogId, getuser: 1 diff --git a/resources/assets/js/pages/manage/components/DialogGroupInfo.vue b/resources/assets/js/pages/manage/components/DialogGroupInfo.vue index ee0fc0474..de4202972 100644 --- a/resources/assets/js/pages/manage/components/DialogGroupInfo.vue +++ b/resources/assets/js/pages/manage/components/DialogGroupInfo.vue @@ -154,7 +154,7 @@ export default { } this.loadIng++; this.$store.dispatch("call", { - url: 'dialog/group/user', + url: 'dialog/user', data: { dialog_id: this.dialogId } diff --git a/resources/assets/js/pages/manage/components/DialogView.vue b/resources/assets/js/pages/manage/components/DialogView.vue index 30e40a156..b8292f78f 100644 --- a/resources/assets/js/pages/manage/components/DialogView.vue +++ b/resources/assets/js/pages/manage/components/DialogView.vue @@ -32,6 +32,23 @@
+ +
+ +
@@ -314,6 +331,10 @@ export default { }); }, + openMeeting() { + Store.set('addMeeting', this.msgData.msg); + }, + withdraw() { $A.modalConfirm({ content: `确定撤回此信息吗?`, diff --git a/resources/assets/js/pages/manage/components/MeetingManager.vue b/resources/assets/js/pages/manage/components/MeetingManager.vue index 721db9eb4..68c4a703b 100644 --- a/resources/assets/js/pages/manage/components/MeetingManager.vue +++ b/resources/assets/js/pages/manage/components/MeetingManager.vue @@ -2,42 +2,68 @@
-
- - - - - - {{$L('开启')}} - {{$L('关闭')}} - + + + + + + + {{$L('麦克风')}} + + + {{$L('摄像头')}} + +
- +
    -
  • - +
  • +
  • -
  • - +
  • +
-
+
+ +
@@ -61,17 +87,17 @@ export default { addShow: false, addData: { userids: [], - video: 'close' + tracks: ['audio'] }, - addRule: {}, meetingShow: false, + audioLoad: false, + videoLoad: false, agoraClient: null, remoteUsers: [], - localTracks: { + localUser: { uid: null, - mediaType: null, audioTrack: null, videoTrack: null, }, @@ -95,9 +121,35 @@ export default { methods: { onAdd(data) { - this.addData = Object.assign({}, this.addData, $A.isJson(data) ? data : { - 'userids': [this.userId], - }); + data = $A.isJson(data) ? data : {}; + // 获取会话成员 + if (!data.meetingid && /\d+/.test(data.dialog_id)) { + this.loadIng++; + this.$store.dispatch("call", { + url: 'dialog/user', + data: { + dialog_id: data.dialog_id + } + }).then(({data}) => { + this.$set(this.addData, 'userids', data.map(item => item.userid)) + }).finally(_ => { + this.loadIng--; + }); + delete data.dialog_id; + } + // 加上自己 + if (!$A.isArray(data.userids)) { + data.userids = [this.userId] + } else if (!data.userids.includes(this.userId)) { + data.userids.push(this.userId) + } + // 加上音频 + if (!$A.isArray(data.tracks)) { + data.tracks = ['audio'] + } else if (!data.tracks.includes('audio')) { + data.tracks.push('audio') + } + this.addData = data; this.addShow = true; }, @@ -106,19 +158,21 @@ export default { if (valid) { this.loadIng++; this.$store.dispatch("call", { - url: 'users/agoraio/token', + url: 'users/meeting/open', + data: this.addData }).then(({data}) => { + this.$set(this.addData, 'name', data.name); + this.$store.dispatch("saveDialogMsg", data.msgs); + delete data.name; + delete data.msgs; + // $A.loadScript('//download.agora.io/sdk/release/AgoraRTC_N.js', e => { if (e !== null || typeof AgoraRTC !== 'object') { - this.loadIng--; $A.modalError("会议组件加载失败!"); - return; + } else { + this.join(data) } - this.join(data).then(_ => { - this.loadIng--; - this.addShow = false; - this.meetingShow = true; - }) + this.loadIng--; }); }).catch(({msg}) => { this.loadIng--; @@ -129,96 +183,149 @@ export default { }); }, + onAudio() { + if (this.localUser.audioTrack) { + this.closeAudio(); + } else { + this.openAudio(); + } + }, + + onVideo() { + if (this.localUser.videoTrack) { + this.closeVideo(); + } else { + this.openVideo(); + } + }, + onClose() { - $A.modalConfirm({ - content: '确定要退出会议吗?', - cancelText: '继续', - okText: '退出', - onOk: () => { - this.loadIng++; - this.leave().then(_ => { - this.loadIng--; - this.meetingShow = false; - }) - } - }); - }, - - join(options) { - return new Promise(async resolve => { - AgoraRTC.onAutoplayFailed = () => { - // alert("click to start autoplay!") - } - AgoraRTC.onMicrophoneChanged = async (changedDevice) => { - // When plugging in a device, switch to a device that is newly plugged in. - if (changedDevice.state === "ACTIVE") { - this.localTracks.audioTrack.setDevice(changedDevice.device.deviceId); - // Switch to an existing device when the current device is unplugged. - } else if (changedDevice.device.label === this.localTracks.audioTrack.getTrackLabel()) { - const oldMicrophones = await AgoraRTC.getMicrophones(); - oldMicrophones[0] && this.localTracks.audioTrack.setDevice(oldMicrophones[0].deviceId); + return new Promise(resolve => { + $A.modalConfirm({ + content: '确定要退出会议吗?', + cancelText: '继续', + okText: '退出', + onOk: async _ => { + await this.leave() + resolve() } - } - AgoraRTC.onCameraChanged = async (changedDevice) => { - // When plugging in a device, switch to a device that is newly plugged in. - if (changedDevice.state === "ACTIVE") { - this.localTracks.videoTrack.setDevice(changedDevice.device.deviceId); - // Switch to an existing device when the current device is unplugged. - } else if (changedDevice.device.label === this.localTracks.videoTrack.getTrackLabel()) { - const oldCameras = await AgoraRTC.getCameras(); - oldCameras[0] && this.localTracks.videoTrack.setDevice(oldCameras[0].deviceId); - } - } - // - this.agoraClient = AgoraRTC.createClient({ - mode: "rtc", - codec: "vp8" }); - // Add an event listener to play remote tracks when remote user publishes. - this.agoraClient.on("user-published", this.handleUserPublished); - this.agoraClient.on("user-unpublished", this.handleUserUnpublished); - // Join a channel and create local tracks. Best practice is to use Promise.all and run them concurrently. - [options.uid, this.localTracks.audioTrack, this.localTracks.videoTrack] = await Promise.all([ - // Join the channel. - this.agoraClient.join(options.appid, options.channel, options.token || null, options.uid || null), - // Create tracks to the local microphone and camera. - AgoraRTC.createMicrophoneAudioTrack(), - AgoraRTC.createCameraVideoTrack() - ]); - // Play the local video track to the local browser and update the UI with the user ID. - this.localTracks.uid = options.uid; - this.localTracks.mediaType = 'video'; - // Publish the local video and audio tracks to the channel. - await this.agoraClient.publish([this.localTracks.audioTrack, this.localTracks.videoTrack]); - resolve() }) }, - leave() { - return new Promise(async resolve => { - for (let trackName in this.localTracks) { - const track = this.localTracks[trackName]; - if (track) { - if (['audioTrack', 'videoTrack'].includes(trackName)) { - track.stop(); - track.close(); - } - this.localTracks[trackName] = null; - } + async join(options) { + this.loadIng++; + // 音频采集设备状态变化回调 + AgoraRTC.onMicrophoneChanged = async (changedDevice) => { + // When plugging in a device, switch to a device that is newly plugged in. + if (changedDevice.state === "ACTIVE") { + this.localUser.audioTrack.setDevice(changedDevice.device.deviceId); + // Switch to an existing device when the current device is unplugged. + } else if (changedDevice.device.label === this.localUser.audioTrack.getTrackLabel()) { + const oldMicrophones = await AgoraRTC.getMicrophones(); + oldMicrophones[0] && this.localUser.audioTrack.setDevice(oldMicrophones[0].deviceId); } - // Remove remote users and player views. - this.remoteUsers = []; - // leave the channel - await this.agoraClient.leave(); - resolve(); - }) + } + // 视频采集设备状态变化回调 + AgoraRTC.onCameraChanged = async (changedDevice) => { + // When plugging in a device, switch to a device that is newly plugged in. + if (changedDevice.state === "ACTIVE") { + this.localUser.videoTrack.setDevice(changedDevice.device.deviceId); + // Switch to an existing device when the current device is unplugged. + } else if (changedDevice.device.label === this.localUser.videoTrack.getTrackLabel()) { + const oldCameras = await AgoraRTC.getCameras(); + oldCameras[0] && this.localUser.videoTrack.setDevice(oldCameras[0].deviceId); + } + } + // 音频或视频轨道自动播放失败回调 + AgoraRTC.onAutoplayFailed = () => { + // + } + + // 创建客户端 + this.agoraClient = AgoraRTC.createClient({mode: "rtc", codec: "vp8"}); + // 添加事件侦听器 + this.agoraClient.on("user-joined", this.handleUserJoined); + this.agoraClient.on("user-left", this.handleUserLeft); + this.agoraClient.on("user-published", this.handleUserPublished); + this.agoraClient.on("user-unpublished", this.handleUserUnpublished); + // 加入频道、开启音视频 + const localTracks = []; + this.localUser.uid = await this.agoraClient.join(options.appid, options.channel, options.token, options.uid) + if (this.addData.tracks.includes("audio")) { + localTracks.push(this.localUser.audioTrack = await AgoraRTC.createMicrophoneAudioTrack()) + } + if (this.addData.tracks.includes("video")) { + localTracks.push(this.localUser.videoTrack = await AgoraRTC.createCameraVideoTrack()) + this.$refs[`meeting_${this.localUser.uid}`].play('video') + } + // 将本地视频曲目播放到本地浏览器、将本地音频和视频发布到频道。 + await this.agoraClient.publish(localTracks); + // + this.loadIng--; + this.addShow = false; + this.meetingShow = true; }, - async handleUserPublished(user, mediaType) { - // subscribe to a remote user - await this.agoraClient.subscribe(user, mediaType); - // add remote - user.mediaType = mediaType + async leave() { + this.loadIng++; + // 删除本地用户和播放器视图。 + ['audioTrack', 'videoTrack'].some(trackName => { + this.localUser[trackName]?.stop(); + this.localUser[trackName]?.close(); + }) + this.localUser = { + uid: null, + audioTrack: null, + videoTrack: null, + } + // 删除远程用户和播放器视图。 + this.remoteUsers = []; + // 离开频道 + await this.agoraClient.leave(); + // + this.loadIng--; + this.meetingShow = false; + }, + + async openAudio() { + if (this.audioLoad || this.localUser.audioTrack) return; + this.audioLoad = true; + this.localUser.audioTrack = await AgoraRTC.createMicrophoneAudioTrack() + await this.agoraClient.publish([this.localUser.audioTrack]); + this.audioLoad = false; + }, + + async closeAudio() { + if (this.audioLoad || !this.localUser.audioTrack) return; + this.audioLoad = true; + await this.agoraClient.unpublish([this.localUser.audioTrack]); + this.localUser.audioTrack.stop(); + this.localUser.audioTrack.close(); + this.localUser.audioTrack = null; + this.audioLoad = false; + }, + + async openVideo() { + if (this.videoLoad || this.localUser.videoTrack) return; + this.videoLoad = true; + this.localUser.videoTrack = await AgoraRTC.createCameraVideoTrack() + this.$refs[`meeting_${this.localUser.uid}`].play('video'); + await this.agoraClient.publish([this.localUser.videoTrack]); + this.videoLoad = false; + }, + + async closeVideo() { + if (this.videoLoad || !this.localUser.videoTrack) return; + this.videoLoad = true; + await this.agoraClient.unpublish([this.localUser.videoTrack]); + this.localUser.videoTrack.stop(); + this.localUser.videoTrack.close(); + this.localUser.videoTrack = null; + this.videoLoad = false; + }, + + async handleUserJoined(user) { const index = this.remoteUsers.findIndex(item => item.uid == user.uid) if (index > -1) { this.remoteUsers.splice(index, 1, user) @@ -227,11 +334,26 @@ export default { } }, - handleUserUnpublished(user) { + async handleUserLeft(user) { const index = this.remoteUsers.findIndex(item => item.uid == user.uid) if (index > -1) { this.remoteUsers.splice(index, 1) } + }, + + async handleUserPublished(user, mediaType) { + const index = this.remoteUsers.findIndex(item => item.uid == user.uid) + if (index > -1) { + await this.agoraClient.subscribe(user, mediaType); + this.$refs[`meeting_${user.uid}`][0].play(mediaType) + } + }, + + async handleUserUnpublished(user, mediaType) { + const index = this.remoteUsers.findIndex(item => item.uid == user.uid) + if (index > -1) { + await this.agoraClient.unsubscribe(user, mediaType); + } } } } diff --git a/resources/assets/js/pages/manage/components/MeetingPlayer.vue b/resources/assets/js/pages/manage/components/MeetingPlayer.vue index 3fc8601bb..1a343be8e 100644 --- a/resources/assets/js/pages/manage/components/MeetingPlayer.vue +++ b/resources/assets/js/pages/manage/components/MeetingPlayer.vue @@ -1,12 +1,18 @@