完善会议功能

This commit is contained in:
kuaifan 2022-06-01 16:32:34 +08:00
parent 0f8cfa72b6
commit bf59fae173
19 changed files with 590 additions and 205 deletions

View File

@ -92,6 +92,51 @@ class DialogController extends AbstractController
return Base::retSuccess('success', $item); 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. 打开会话 * @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. 添加群成员 * @api {get} api/dialog/group/adduser 17. 添加群成员
* *

View File

@ -3,11 +3,14 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Models\AbstractModel; use App\Models\AbstractModel;
use App\Models\Meeting;
use App\Models\UmengAlias; use App\Models\UmengAlias;
use App\Models\User; use App\Models\User;
use App\Models\UserEmailVerification; use App\Models\UserEmailVerification;
use App\Models\UserTransfer; use App\Models\UserTransfer;
use App\Models\WebSocket; use App\Models\WebSocket;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\AgoraIO\AgoraTokenGenerator; use App\Module\AgoraIO\AgoraTokenGenerator;
use App\Module\Base; use App\Module\Base;
use Arr; 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身份 * @apiDescription 需要token身份
* @apiVersion 1.0.0 * @apiVersion 1.0.0
* @apiGroup users * @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 {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据 * @apiSuccess {Object} data 返回数据
*/ */
public function agoraio__token() public function meeting__open()
{ {
$user = User::auth(); $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'; $appid = '342c604542484b0d9659527f79aefcdb';
$app_certificate = '920eb911c1f549948366e44d6dcabcbe'; $app_certificate = '920eb911c1f549948366e44d6dcabcbe';
$channel = "DooTask:" . md5(env("APP_KEY")); $channel = $meeting->channel;
$uid = $user->userid; $uid = $user->userid . '_' . Request::header('fd');
try { try {
$service = new AgoraTokenGenerator($appid, $app_certificate, $channel, $uid); $service = new AgoraTokenGenerator($appid, $app_certificate, $channel, $uid);
} catch (\Exception $e) { } catch (\Exception $e) {
@ -791,13 +820,30 @@ class UsersController extends AbstractController
} }
$token = $service->buildToken(); $token = $service->buildToken();
if (empty($token)) { if (empty($token)) {
return Base::retError('Generated token failed'); return Base::retError('会议令牌创建失败');
} }
return Base::retSuccess('success', [ // 发送给邀请人
'appid' => $appid, $msgs = [];
'channel' => $channel, if ($isCreate) {
'uid' => $uid, foreach ($userids as $userid) {
'token' => $token 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);
} }
} }

34
app/Models/Meeting.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
/**
* App\Models\Meeting
*
* @property int $id
* @property string|null $meetingid 会议ID不是数字
* @property string|null $name 会议主题
* @property string|null $channel 频道
* @property int|null $userid 创建人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $end_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting query()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereChannel($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereEndAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereMeetingid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUserid($value)
* @mixin \Eloquent
*/
class Meeting extends AbstractModel
{
}

View File

@ -214,6 +214,8 @@ class WebSocketDialogMsg extends AbstractModel
return $this->previewTextMsg($this->msg['text'], $preserveHtml); return $this->previewTextMsg($this->msg['text'], $preserveHtml);
case 'record': case 'record':
return "[语音]"; return "[语音]";
case 'meeting':
return "[会议] ${$this->msg['name']}";
case 'file': case 'file':
if ($this->msg['type'] == 'img') { if ($this->msg['type'] == 'img') {
return "[图片]"; return "[图片]";

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateMeetingsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('meetings', function (Blueprint $table) {
$table->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');
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -788,8 +788,9 @@ export default {
case 'meeting': case 'meeting':
Store.set('addMeeting', { Store.set('addMeeting', {
userids: [this.userId] dialog_id: this.dialogId,
}); // todo userids: [this.userId],
});
break; break;
case 'image': case 'image':
@ -872,7 +873,7 @@ export default {
if (this.dialogId > 0) { if (this.dialogId > 0) {
// ID // ID
this.$store.dispatch("call", { this.$store.dispatch("call", {
url: 'dialog/group/user', url: 'dialog/user',
data: { data: {
dialog_id: this.dialogId, dialog_id: this.dialogId,
getuser: 1 getuser: 1

View File

@ -154,7 +154,7 @@ export default {
} }
this.loadIng++; this.loadIng++;
this.$store.dispatch("call", { this.$store.dispatch("call", {
url: 'dialog/group/user', url: 'dialog/user',
data: { data: {
dialog_id: this.dialogId dialog_id: this.dialogId
} }

View File

@ -32,6 +32,23 @@
<div class="record-icon taskfont"></div> <div class="record-icon taskfont"></div>
</div> </div>
</div> </div>
<!--会议-->
<div v-else-if="msgData.type === 'meeting'" class="content-meeting no-dark-content">
<ul class="dialog-meeting" @click="openMeeting">
<li>
<em>{{$L('会议主题')}}</em>
{{msgData.msg.name}}
</li>
<li>
<em>{{$L('频道ID')}}</em>
{{msgData.msg.meetingid}}
</li>
<li class="meeting-operation">
{{$L('点击进入会议')}}
<i class="taskfont">&#xe68b;</i>
</li>
</ul>
</div>
<!--等待--> <!--等待-->
<div v-else-if="msgData.type === 'loading'" class="content-loading"> <div v-else-if="msgData.type === 'loading'" class="content-loading">
<Loading/> <Loading/>
@ -314,6 +331,10 @@ export default {
}); });
}, },
openMeeting() {
Store.set('addMeeting', this.msgData.msg);
},
withdraw() { withdraw() {
$A.modalConfirm({ $A.modalConfirm({
content: `确定撤回此信息吗?`, content: `确定撤回此信息吗?`,

View File

@ -2,42 +2,68 @@
<div v-show="false"> <div v-show="false">
<Modal <Modal
v-model="addShow" v-model="addShow"
:title="$L('新会议')" :title="$L(addData.meetingid ? '加入会议' : '新会议')"
:mask-closable="false"> :mask-closable="false">
<Form ref="addForm" :model="addData" :rules="addRule" label-width="auto" @submit.native.prevent> <Form ref="addForm" :model="addData" label-width="auto" @submit.native.prevent>
<FormItem prop="userids" :label="$L('会议成员')"> <template v-if="addData.meetingid">
<UserInput v-model="addData.userids" :multiple-max="10" :placeholder="$L('选择会议成员')"/> <!-- 加入会议 -->
</FormItem> <FormItem prop="userids" :label="$L('会议主题')">
<FormItem prop="video" :label="$L('开启视频')"> <Input v-model="addData.name" disabled/>
<RadioGroup v-model="addData.video"> </FormItem>
<Radio label="open">{{$L('开启')}}</Radio> <FormItem prop="meetingid" :label="$L('会议频道')">
<Radio label="close">{{$L('关闭')}}</Radio> <Input v-model="addData.meetingid" disabled/>
</RadioGroup> </FormItem>
</template>
<template v-else>
<!-- 新会议 -->
<FormItem prop="name" :label="$L('会议主题')">
<Input v-model="addData.name" :maxlength="50" :placeholder="$L('选填')"/>
</FormItem>
<FormItem prop="userids" :label="$L('邀请成员')">
<UserInput v-model="addData.userids" :uncancelable="[userId]" :multiple-max="20" :placeholder="$L('选择邀请成员')"/>
</FormItem>
</template>
<FormItem prop="tracks">
<CheckboxGroup v-model="addData.tracks">
<Checkbox label="audio">
<span>{{$L('麦克风')}}</span>
</Checkbox>
<Checkbox label="video">
<span>{{$L('摄像头')}}</span>
</Checkbox>
</CheckboxGroup>
</FormItem> </FormItem>
</Form> </Form>
<div slot="footer" class="adaption"> <div slot="footer" class="adaption">
<Button type="default" @click="addShow=false">{{$L('取消')}}</Button> <Button type="default" @click="addShow=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="loadIng > 0" @click="onSubmit">{{$L('开始会议')}}</Button> <Button type="primary" :loading="loadIng > 0" @click="onSubmit">{{$L(addData.meetingid ? '进入会议' : '开始会议')}}</Button>
</div> </div>
</Modal> </Modal>
<Modal <Modal
v-model="meetingShow" v-model="meetingShow"
:title="$L('会议中')" :title="addData.name"
:mask="false" :mask="false"
:mask-closable="false" :mask-closable="false"
:closable="false" :closable="false"
:transition-names="['', '']" :transition-names="['', '']"
:beforeClose="onClose"
class-name="meeting-manager" class-name="meeting-manager"
fullscreen> fullscreen>
<ul> <ul>
<li v-if="localTracks.uid"> <li v-if="localUser.uid">
<MeetingPlayer :player="localTracks"/> <MeetingPlayer :ref="`meeting_${localUser.uid}`" :player="localUser"/>
</li> </li>
<li v-for="item in remoteUsers"> <li v-for="user in remoteUsers">
<MeetingPlayer :player="item"/> <MeetingPlayer :ref="`meeting_${user.uid}`" :player="user"/>
</li> </li>
</ul> </ul>
<div slot="footer" class="adaption"> <div slot="footer" class="adaption meeting-button-group">
<Button type="primary" :loading="audioLoad" @click="onAudio">
<i class="taskfont" v-html="localUser.audioTrack ? '&#xe7c3;' : '&#xe7c7;'"></i>
</Button>
<Button type="primary" :loading="videoLoad" @click="onVideo">
<i class="taskfont" v-html="localUser.videoTrack ? '&#xe7c1;' : '&#xe7c8;'"></i>
</Button>
<Button type="warning" :loading="loadIng > 0" @click="onClose">{{$L('退出会议')}}</Button> <Button type="warning" :loading="loadIng > 0" @click="onClose">{{$L('退出会议')}}</Button>
</div> </div>
</Modal> </Modal>
@ -61,17 +87,17 @@ export default {
addShow: false, addShow: false,
addData: { addData: {
userids: [], userids: [],
video: 'close' tracks: ['audio']
}, },
addRule: {},
meetingShow: false, meetingShow: false,
audioLoad: false,
videoLoad: false,
agoraClient: null, agoraClient: null,
remoteUsers: [], remoteUsers: [],
localTracks: { localUser: {
uid: null, uid: null,
mediaType: null,
audioTrack: null, audioTrack: null,
videoTrack: null, videoTrack: null,
}, },
@ -95,9 +121,35 @@ export default {
methods: { methods: {
onAdd(data) { onAdd(data) {
this.addData = Object.assign({}, this.addData, $A.isJson(data) ? data : { data = $A.isJson(data) ? data : {};
'userids': [this.userId], //
}); 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; this.addShow = true;
}, },
@ -106,19 +158,21 @@ export default {
if (valid) { if (valid) {
this.loadIng++; this.loadIng++;
this.$store.dispatch("call", { this.$store.dispatch("call", {
url: 'users/agoraio/token', url: 'users/meeting/open',
data: this.addData
}).then(({data}) => { }).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 => { $A.loadScript('//download.agora.io/sdk/release/AgoraRTC_N.js', e => {
if (e !== null || typeof AgoraRTC !== 'object') { if (e !== null || typeof AgoraRTC !== 'object') {
this.loadIng--;
$A.modalError("会议组件加载失败!"); $A.modalError("会议组件加载失败!");
return; } else {
this.join(data)
} }
this.join(data).then(_ => { this.loadIng--;
this.loadIng--;
this.addShow = false;
this.meetingShow = true;
})
}); });
}).catch(({msg}) => { }).catch(({msg}) => {
this.loadIng--; 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() { onClose() {
$A.modalConfirm({ return new Promise(resolve => {
content: '确定要退出会议吗?', $A.modalConfirm({
cancelText: '继续', content: '确定要退出会议吗?',
okText: '退出', cancelText: '继续',
onOk: () => { okText: '退出',
this.loadIng++; onOk: async _ => {
this.leave().then(_ => { await this.leave()
this.loadIng--; resolve()
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);
} }
}
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() { async join(options) {
return new Promise(async resolve => { this.loadIng++;
for (let trackName in this.localTracks) { //
const track = this.localTracks[trackName]; AgoraRTC.onMicrophoneChanged = async (changedDevice) => {
if (track) { // When plugging in a device, switch to a device that is newly plugged in.
if (['audioTrack', 'videoTrack'].includes(trackName)) { if (changedDevice.state === "ACTIVE") {
track.stop(); this.localUser.audioTrack.setDevice(changedDevice.device.deviceId);
track.close(); // Switch to an existing device when the current device is unplugged.
} } else if (changedDevice.device.label === this.localUser.audioTrack.getTrackLabel()) {
this.localTracks[trackName] = null; 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 AgoraRTC.onCameraChanged = async (changedDevice) => {
await this.agoraClient.leave(); // When plugging in a device, switch to a device that is newly plugged in.
resolve(); 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) { async leave() {
// subscribe to a remote user this.loadIng++;
await this.agoraClient.subscribe(user, mediaType); //
// add remote ['audioTrack', 'videoTrack'].some(trackName => {
user.mediaType = mediaType 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) const index = this.remoteUsers.findIndex(item => item.uid == user.uid)
if (index > -1) { if (index > -1) {
this.remoteUsers.splice(index, 1, user) 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) const index = this.remoteUsers.findIndex(item => item.uid == user.uid)
if (index > -1) { if (index > -1) {
this.remoteUsers.splice(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);
}
} }
} }
} }

View File

@ -1,12 +1,18 @@
<template> <template>
<div class="meeting-player"> <div v-if="userid" class="meeting-player">
<div :id="id" class="player"></div> <div :id="id" class="player" :style="playerStyle"></div>
<UserAvatar :userid="player.uid" show-name/> <UserAvatar :userid="userid" :size="36" :borderWitdh="2"/>
<div class="player-state">
<i v-if="!audio" class="taskfont">&#xe7c7;</i>
<i v-if="!video" class="taskfont">&#xe7c8;</i>
</div>
</div> </div>
</template> </template>
<script> <script>
import {mapState} from "vuex";
export default { export default {
name: "MeetingPlayer", name: "MeetingPlayer",
props: { props: {
@ -25,21 +31,37 @@ export default {
} }
}, },
watch: { computed: {
player: { ...mapState(['cacheUserBasic']),
handler(e) { userid() {
this.$nextTick(_ => { if (this.player.uid) {
switch (e.mediaType) { return parseInt($A.getMiddle(this.player.uid, null, '-'))
case 'video': }
e.videoTrack.play(this.id); return 0
break; },
case 'audio': playerStyle() {
e.audioTrack.play(); const user = this.cacheUserBasic.find(({userid}) => userid == this.userid);
break; if (user) {
} return {
}) backgroundImage: `url("${user.userimg}")`
}, }
immediate: true }
return null;
},
audio() {
return !!this.player.audioTrack
},
video() {
return !!this.player.videoTrack
}
},
methods: {
play(type) {
if (type === 'audio') {
this.player.audioTrack?.play();
} else if (type === 'video') {
this.player.videoTrack?.play(this.id);
}
} }
} }
} }

View File

@ -490,6 +490,8 @@ export default {
return $A.getMsgTextPreview(data.msg.text) return $A.getMsgTextPreview(data.msg.text)
case 'record': case 'record':
return `[${this.$L('语音')}]` return `[${this.$L('语音')}]`
case 'meeting':
return `[${this.$L('会议')}] ${data.msg.name}`
case 'file': case 'file':
if (data.msg.type == 'img') { if (data.msg.type == 'img') {
return `[${this.$L('图片')}]` return `[${this.$L('图片')}]`

View File

@ -498,6 +498,41 @@
} }
} }
.content-meeting {
.dialog-meeting {
cursor: pointer;
min-width: 200px;
color: $primary-title-color;
> li {
list-style: none;
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 12px;
&.meeting-operation {
border-top: 1px solid #cccccc;
margin-bottom: 0;
padding: 8px 0 4px;
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
opacity: 0.8;
.taskfont {
font-size: 12px;
padding-left: 2px;
transform: scale(0.6);
}
}
> em {
font-style: normal;
font-weight: 500;
padding-bottom: 2px;
}
}
}
}
.content-loading { .content-loading {
display: flex; display: flex;
@ -708,6 +743,19 @@
} }
} }
} }
.content-meeting {
.dialog-meeting {
min-width: 200px;
color: #ffffff;
> li {
&.meeting-operation {
border-top: 1px solid #ffffff;
opacity: 1;
}
}
}
}
} }
.dialog-menu { .dialog-menu {

View File

@ -4,33 +4,55 @@ body {
.ivu-modal { .ivu-modal {
.ivu-modal-content { .ivu-modal-content {
.ivu-modal-body { .ivu-modal-body {
padding: 16px 12px 0; padding: 16px 24px 0;
> ul { > ul {
display: grid; display: grid;
justify-content: center; justify-content: space-between;
grid-template-columns: repeat(auto-fill, 180px); grid-template-columns: repeat(auto-fill, 220px);
grid-gap: 16px; grid-gap: 24px;
> li { > li {
list-style: none; list-style: none;
width: 180px;
height: 230px;
position: relative; position: relative;
display: flex;
flex-direction: column;
align-items: center;
.meeting-player { .meeting-player {
flex: 1; position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
.player { .player {
flex: 1; width: 220px;
width: 100%; height: 220px;;
background-color: #f1f1f1; border-radius: 12px;
overflow: hidden;
background-color: #e1e1e1;
background-size: 136%;
background-position: center;
background-repeat: no-repeat
}
.player-state {
position: absolute;
top: 4px;
right: 8px;
z-index: 2;
.taskfont {
color: #ff0000;
font-size: 22px;
margin-left: 6px;
}
} }
.common-avatar { .common-avatar {
margin-top: 12px; position: absolute;
bottom: -8px;
right: -8px;
z-index: 2;
}
}
}
@media (max-width: 768px) {
grid-template-columns: repeat(auto-fill, 176px);
grid-gap: 12px;
> li {
.meeting-player {
.player {
width: 176px;
height: 176px;;
}
} }
} }
} }
@ -41,3 +63,24 @@ body {
} }
} }
} }
.meeting-button-group {
display: flex;
justify-content: flex-end;
.taskfont {
font-size: 20px;
}
.ivu-btn {
padding: 0;
display: flex;
align-items: center;
justify-content: center;
> span {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px !important;
}
}
}