mirror of
https://github.com/kuaifan/dootask.git
synced 2026-03-02 06:54:19 +00:00
完善会议功能
This commit is contained in:
parent
0f8cfa72b6
commit
bf59fae173
@ -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. 添加群成员
|
||||
*
|
||||
|
||||
@ -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
34
app/Models/Meeting.php
Normal 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
|
||||
{
|
||||
|
||||
}
|
||||
@ -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 "[图片]";
|
||||
|
||||
@ -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.
@ -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
|
||||
|
||||
@ -154,7 +154,7 @@ export default {
|
||||
}
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'dialog/group/user',
|
||||
url: 'dialog/user',
|
||||
data: {
|
||||
dialog_id: this.dialogId
|
||||
}
|
||||
|
||||
@ -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"></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: `确定撤回此信息吗?`,
|
||||
|
||||
@ -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 ? '' : ''"></i>
|
||||
</Button>
|
||||
<Button type="primary" :loading="videoLoad" @click="onVideo">
|
||||
<i class="taskfont" v-html="localUser.videoTrack ? '' : ''"></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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"></i>
|
||||
<i v-if="!video" class="taskfont"></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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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('图片')}]`
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user