完善会议功能

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);
}
/**
* @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. 添加群成员
*

View File

@ -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);
}
}

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);
case 'record':
return "[语音]";
case 'meeting':
return "[会议] ${$this->msg['name']}";
case 'file':
if ($this->msg['type'] == 'img') {
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':
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

View File

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

View File

@ -32,6 +32,23 @@
<div class="record-icon taskfont"></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">
<Loading/>
@ -314,6 +331,10 @@ export default {
});
},
openMeeting() {
Store.set('addMeeting', this.msgData.msg);
},
withdraw() {
$A.modalConfirm({
content: `确定撤回此信息吗?`,

View File

@ -2,42 +2,68 @@
<div v-show="false">
<Modal
v-model="addShow"
:title="$L('新会议')"
:title="$L(addData.meetingid ? '加入会议' : '新会议')"
:mask-closable="false">
<Form ref="addForm" :model="addData" :rules="addRule" label-width="auto" @submit.native.prevent>
<FormItem prop="userids" :label="$L('会议成员')">
<UserInput v-model="addData.userids" :multiple-max="10" :placeholder="$L('选择会议成员')"/>
</FormItem>
<FormItem prop="video" :label="$L('开启视频')">
<RadioGroup v-model="addData.video">
<Radio label="open">{{$L('开启')}}</Radio>
<Radio label="close">{{$L('关闭')}}</Radio>
</RadioGroup>
<Form ref="addForm" :model="addData" label-width="auto" @submit.native.prevent>
<template v-if="addData.meetingid">
<!-- 加入会议 -->
<FormItem prop="userids" :label="$L('会议主题')">
<Input v-model="addData.name" disabled/>
</FormItem>
<FormItem prop="meetingid" :label="$L('会议频道')">
<Input v-model="addData.meetingid" disabled/>
</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>
</Form>
<div slot="footer" class="adaption">
<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>
</Modal>
<Modal
v-model="meetingShow"
:title="$L('会议中')"
:title="addData.name"
:mask="false"
:mask-closable="false"
:closable="false"
:transition-names="['', '']"
:beforeClose="onClose"
class-name="meeting-manager"
fullscreen>
<ul>
<li v-if="localTracks.uid">
<MeetingPlayer :player="localTracks"/>
<li v-if="localUser.uid">
<MeetingPlayer :ref="`meeting_${localUser.uid}`" :player="localUser"/>
</li>
<li v-for="item in remoteUsers">
<MeetingPlayer :player="item"/>
<li v-for="user in remoteUsers">
<MeetingPlayer :ref="`meeting_${user.uid}`" :player="user"/>
</li>
</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>
</div>
</Modal>
@ -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);
}
}
}
}

View File

@ -1,12 +1,18 @@
<template>
<div class="meeting-player">
<div :id="id" class="player"></div>
<UserAvatar :userid="player.uid" show-name/>
<div v-if="userid" class="meeting-player">
<div :id="id" class="player" :style="playerStyle"></div>
<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>
</template>
<script>
import {mapState} from "vuex";
export default {
name: "MeetingPlayer",
props: {
@ -25,21 +31,37 @@ export default {
}
},
watch: {
player: {
handler(e) {
this.$nextTick(_ => {
switch (e.mediaType) {
case 'video':
e.videoTrack.play(this.id);
break;
case 'audio':
e.audioTrack.play();
break;
}
})
},
immediate: true
computed: {
...mapState(['cacheUserBasic']),
userid() {
if (this.player.uid) {
return parseInt($A.getMiddle(this.player.uid, null, '-'))
}
return 0
},
playerStyle() {
const user = this.cacheUserBasic.find(({userid}) => userid == this.userid);
if (user) {
return {
backgroundImage: `url("${user.userimg}")`
}
}
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)
case 'record':
return `[${this.$L('语音')}]`
case 'meeting':
return `[${this.$L('会议')}] ${data.msg.name}`
case 'file':
if (data.msg.type == 'img') {
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 {
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 {

View File

@ -4,33 +4,55 @@ body {
.ivu-modal {
.ivu-modal-content {
.ivu-modal-body {
padding: 16px 12px 0;
padding: 16px 24px 0;
> ul {
display: grid;
justify-content: center;
grid-template-columns: repeat(auto-fill, 180px);
grid-gap: 16px;
justify-content: space-between;
grid-template-columns: repeat(auto-fill, 220px);
grid-gap: 24px;
> li {
list-style: none;
width: 180px;
height: 230px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
.meeting-player {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.player {
flex: 1;
width: 100%;
background-color: #f1f1f1;
width: 220px;
height: 220px;;
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 {
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;
}
}
}