Commit 954c43df by wxl

完善视频通话

parent 1c345c2e
......@@ -4,6 +4,7 @@ import { useState } from "vue-vulcan";
/** 当前客户端用户的基本信息(含token) */
export function useAuthData() {
const [authData, setAuth] = useState<UserData>(null, {storage: 'custome', key: 'auth'});
return {
authData,
......
import { UserData } from "any-hooks/types/user";
import { reactive } from "vue";
import { useInjector, useRequest } from "vue-vulcan";
import { useAuthData } from "./useAuthData";
interface UserData {
id: string;
company_id: string;
nickname: string;
avatar: string;
permission: string;
token: string;
}
export function useLogin() {
const loginData = reactive({
login_id: '',
......@@ -19,15 +11,16 @@ export function useLogin() {
const [, request] = useRequest<UserData>('/loginIn', { auto: false });
const { setAuth } = useInjector(useAuthData);
const peer_id = new Date().getTime().toString().substr(7,).replace('0', '1');
console.log('init peer_id', peer_id)
const submitLogin = () => {
return request(loginData)
.then( data => setAuth(data) )
return request({...loginData, peer_id})
.then( data => {setAuth(data); return data} )
}
return {
loginData,
submitLogin
}
}
\ No newline at end of file
import { AgoraAppStore, StreamType } from "any-hooks/types/agora";
import { AgoraAppStore, AgoraSdkClient, StreamType } from "any-hooks/types/agora";
import { onMounted, watch } from "vue";
import { useInjector, useState } from "vue-vulcan";
import { AGORA_APP_SDK } from "../communication/token";
interface AgoraStreamItem {
export interface AgoraStreamItem {
url?: string;
uid: number; //agora仅支持类型为number的uid
videoPause?: boolean;
audioPause?: boolean;
}
/** 基于声网sdk封装,由于多平台特性,请在应用根组件将对应平台的声网SDK对象以及appid通过useProvider提供; */
export function useAgoraClient() {
const [agoraErr, setError] = useState(null);
export function useAgoraClient() {
const [streamList, setStream] = useState<AgoraStreamItem[]>([]);
const { appid, client } = useInjector<AgoraAppStore>(AGORA_APP_SDK);
const { appid, sdk } = useInjector<AgoraAppStore>(AGORA_APP_SDK);
onMounted(() => {
client.init(appid, () => {}, setError);
})
let client : AgoraSdkClient;
const joinChannelWithAgora = (token: string, cid: string, uid: number) => {
console.log('准备加入视频频道 cid:',cid)
client.join(
token,
cid,
uid,
() => {pushVideoStream(); subscribeRemoteStream },
setError
);
console.log('准备初始化agora');
console.log('token', token);
console.log('cid', cid);
console.log('uid', uid);
client = new sdk.Client() as AgoraSdkClient;
subscribeRemoteStream();
client.init(appid, () => {
client.join(
token,
cid,
uid,
pushVideoStream,
err => console.error('加入频道', err)
);
}, err => console.error('初始化', err));
}
//推送本地视频流
const [localPushurl, setPushUrl] = useState('');
const pushVideoStream = () => {
client.publish( url => {setPushUrl(url);console.log('成功生成推流地址'+url)});
client.publish( url => {setPushUrl(url);console.log('成功生成推流地址'+url)}, console.error);
}
//监听远程视频流
const subscribeRemoteStream = () => {
client.on('stream-added', evt => {
const subscribeRemoteStream = () => {
console.log('订阅远端流')
client.on('stream-added', evt => {
console.log('远端流加入')
client.subscribe(
evt.uid,
url => {updateStremList('add', {uid: evt.uid, url}); console.log(`用户${evt.uid}加入频道${url}`)},
setError
console.log
)
});
client.on('update-url', evt => updateStremList('add', evt));
client.on('stream-removed', evt => updateStremList('remove', evt.uid));
client.on('mute-video', evt => updateStremList('mute-video', evt.uid));
client.on('unmute-video', evt => updateStremList('unmute-video', evt.uid));
client.on('error', err => console.log('agora err', err))
}
watch(streamList, val => console.log(`远端流变化val`, val));
// 暂停本地流推送
const muteLocalStream = (type: StreamType) => {
return new Promise (resolve => {
client.muteLocal(type, () => resolve(null))
client.muteLocal(type, () => resolve({}))
})
}
// 恢复本地流推送
const unmuteLocalStream = (type: StreamType) => {
return new Promise (resolve => {
client.unmuteLocal(type, () => resolve(null))
client.unmuteLocal(type, () => resolve({}))
})
}
// 暂停远程流接收
const muteRemoteStream = (uid: number, type: StreamType) => {
return new Promise( resolve => {
client.mute(uid, type, () => resolve(null) )
client.mute(uid, type, () => resolve({}) )
})
}
// 恢复远程流接收
const unmuteRemoteStream = (uid: number, type: StreamType) => {
return new Promise( resolve => {
client.unmute(uid, type , () => resolve(null))
client.unmute(uid, type , () => resolve({}))
})
}
// 根据uid来新增或移除远端视频流列表
function updateStremList(act: 'add', input: {uid: number, url: string}): void;
function updateStremList(act: 'remove', input: number[]): void;
function updateStremList (action, input) {
// 根据uid来新增或移除远端视频流列表
function updateStremList (action: string, input: any) {
switch(action) {
case 'add':
setStream(streamList.value.concat(input));
......@@ -91,20 +101,22 @@ export function useAgoraClient() {
});
setStream(res);
break;
case 'mute-video':
case 'unmute-video':
const arr = streamList.value;
const index = arr.findIndex( item => item.uid === input );
arr[index] && (arr[index].videoPause = action === 'mute-video' ? true: false);
setStream(arr);
break;
}
}
// 退出频道
const leaveChannelWithAgora = () => {
return new Promise( resolve => {
client.leave(() => resolve(''), setError);
})
}
// 监听来自Agora Client的错误提示
watch(agoraErr, err => {
console.error(err)
})
const leaveChannelWithAgora = () => {
client.leave(() => setStream([]));
setPushUrl(null);
client = null;
}
return {
localPushurl,
......
......@@ -21,6 +21,7 @@ export function useSocket<S = any>() {
}
ws.addEventListener('message', event => setMsg(event.data));
ws.addEventListener('open', () => {
console.log('ws链接成功')
setStatus('opening');
brokenTime.value = 0;
socketSettings.heartData && startHeartConnect();
......@@ -41,6 +42,7 @@ export function useSocket<S = any>() {
// 重连功能
const limit = socketSettings.retryLimit || 5;
const reconnect = (url: string) => {
console.log('开始重连')
brokenTime.value ++;
if(brokenTime.value > limit) {
ws.close();
......
import { useAuthData } from "any-hooks/auth/useAuthData";
import { AnyRemoteMainFlag, AnyRemoteSubFlag } from "any-hooks/types/socket";
import { UserData } from "any-hooks/types/user";
import { watch } from "vue";
import { useInjector, useState } from "vue-vulcan";
import { useInjector, useRequest, useState } from "vue-vulcan";
import { useChannelStore } from "./useChannelStore";
import { useNetSocketStore } from "./useNetSocketStore";
type CallingState = 'net_error' | 'calling' | 'call_successed' | 'being_called' | 'call_accepted' | 'free';
type CallingState = 'net_error' | 'calling' | 'call_successed' | 'being_called' | 'call_accepted' | 'connecting' | 'free';
interface Caller extends UserData {
action: AnyRemoteSubFlag | 'none';
......@@ -17,26 +18,31 @@ interface Caller extends UserData {
export function useCallCenter() {
const { sendMsg, currentMsg } = useInjector(useNetSocketStore);
const { authData } = useInjector(useAuthData);
/** 主动呼叫功能 */
const [target, setTarget] = useState<Caller>(null);
const [myCallState, setCallState] = useState<CallingState>('free');
const {currentChannel, channelMembers, createChannel, getTokenByChannel, updateMembers, clearChannel} = useInjector(useChannelStore);
const {currentChannel, channelMembers, isEmpty, createChannel, getTokenByChannel, updateMembers, clearChannel} = useInjector(useChannelStore);
const callContact = (user: UserData) => {
setTarget({...user, action: 'none'}); //保存呼叫目标的信息
setTarget({...user, action: 'none'}); //保存呼叫目标的信息
setCallState('calling'); //主动呼叫别人时将自己的呼叫状态更改为‘calling’
createChannel().then( _ => {
createChannel().then( data => {
sendMsg({
toID: user.id,
toName: user.nickname,
channelID: currentChannel.value.channel_id,
channelID: data.channel_id,
msgMainFlag: 'CallOffer',
msgSubFlag: 'Request',
msgData: {
members: channelMembers.value
}
})
})
})
//12S后若本地用户仍处于主动呼叫状态,则自动挂断
setTimeout(() => {
if(myCallState.value === 'calling') hangup();
}, 12000)
}
/** 回应呼叫功能 */
......@@ -55,14 +61,21 @@ export function useCallCenter() {
}
/** 呼叫监听功能 */
const [caller, setCaller] = useState<Caller>(null);
const [caller, setCaller] = useState<Caller>(null);
// 监听来自其他用户的呼叫请求
watch(currentMsg, msg => {
if(msg.msgMainFlag !== 'CallOffer') return;
switch(msg.msgSubFlag) {
case 'Request':
// 本地呼叫状态为free时,变更为being_called;不为free,则发送繁忙的回应。
myCallState.value === 'free' ? setCallState('being_called') : answerCaller('Busying');
myCallState.value === 'free' ? setCallState('being_called') : answerCaller('Busying');
//10S秒后若本地用户仍然处于被呼叫状态,则自动发出繁忙恢复,且将状态恢复为free
setTimeout( () => {
if(myCallState.value === 'being_called'){
answerCaller('Busying');
setCallState('free');
}
}, 10000)
break;
case 'Hangup':
//对方中断呼叫,则将呼叫状态改为free
......@@ -81,28 +94,28 @@ export function useCallCenter() {
// 监听呼叫目标的回应
watch(currentMsg, msg => {
if(msg.msgMainFlag !== 'CallAnswer') return;
setTarget({...target.value, action: msg.msgSubFlag});
switch(msg.msgSubFlag) {
case 'Connect':
setCallState('call_successed');
updateMembers([msg.fromID], 'join');
break;
case 'Busying':
case 'Hangup':
console.log('对方拒绝了呼叫请求')
setCallState('free');
setTarget({...target.value, action: msg.msgSubFlag});
case 'Hangup':
setCallState(isEmpty.value ? 'free' : 'connecting');
break;
}
})
/** 呼叫挂断功能 */
const hangup = () => {
const [, changeCallingState] = useRequest('/updateUsersCallState', {auto: false});
const hangup = (type?: 'leave' | 'cancel') => {
let flag: AnyRemoteMainFlag, toID: string;
switch(myCallState.value) {
default:
flag = 'ChannelChat';
toID = '-2'; // '-2'表示向频道发送消息
break;
break;
case 'calling':
flag = 'CallOffer';
toID = target.value.id;
......@@ -119,12 +132,24 @@ export function useCallCenter() {
msgSubFlag: 'Hangup',
toID
})
setCallState('free');
isEmpty.value && setCallState('free');
type === 'leave' && setCallState('free');
setTarget(null);
setCaller(null);
}
/** 监听呼叫状态,当状态变回free时,重置channel的信息 */
/** 监听呼叫状态,当状态变回free时,重置channel的信息,同时向服务器广播 */
watch(myCallState, state => {
if(state === 'free') clearChannel()
console.log('state', state)
if(state === 'free') {
clearChannel();
sendMsg({
msgMainFlag: 'NotifyUpdateUserList',
msgSubFlag: "UpdateUserList",
toID: '0'
})
}
changeCallingState({userIDs: JSON.stringify([{userID: authData.value.id, callState: state==='free' ? 'idle' : 'calling'}])});
})
return {
......@@ -136,8 +161,10 @@ export function useCallCenter() {
hangup,
/** 被本地用户呼叫目标联系人 */
target,
setTarget,
/** 向本地用户发起呼叫的远程联系人 */
caller,
setCaller,
/** 本地用户的呼叫状态 */
myCallState,
}
......
import { useAuthData } from "any-hooks/auth/useAuthData";
import { watch } from "vue";
import { computed, watch } from "vue";
import { useInjector, useRequest, useState } from "vue-vulcan";
import { useNetSocketStore } from "./useNetSocketStore";
......@@ -12,17 +12,26 @@ interface ChannelData {
export function useChannelStore() {
const [currentChannel, setChannel] = useState<ChannelData>(null);
const [channelMembers, setMembers] = useState<string[]>([]);
const [channelInfo, request] = useRequest<ChannelData>('/getAgoraToken', {auto: false});
const [channelInfo, request] = useRequest<ChannelData>('/getAgoraToken', {auto: false});
const { authData } = useInjector(useAuthData);
const { currentMsg } = useInjector(useNetSocketStore);
const { currentMsg } = useInjector(useNetSocketStore);
const createChannel = () => {
return request();
const isEmpty = computed( () => channelMembers.value.length < 2); //频道剩余不到1人时,表示为空
/* 创建新的频道(如果已存在一个频道则不调用创建接口,直接返回一个promise) */
const createChannel = () => {
updateMembers([authData.value.id], 'join');
console.log('创建频道前判断是否已存在频道', currentChannel.value);
if(!currentChannel.value) return request();
return new Promise<ChannelData>(resolve => resolve(channelInfo.value));
}
const getTokenByChannel = (cid: string) => {
request({channel_id: cid});
}
console.log('根据对方呼叫人的频道获取token');
updateMembers([authData.value.id], 'join');
request({channel_id: cid});
}
const updateMembers = (ids: string[], type: 'join' | 'leave') => {
const current = channelMembers.value;
......@@ -57,6 +66,7 @@ export function useChannelStore() {
return {
currentChannel,
channelMembers,
isEmpty,
getTokenByChannel,
createChannel,
updateMembers,
......
import { setKeepScreenOn } from "@tarojs/taro";
import { useAuthData } from "any-hooks/auth/useAuthData";
import { useAgoraClient } from "any-hooks/common/useAgoraClient";
import { useContacts } from "any-hooks/contacts/useContacts";
import { UserData } from "any-hooks/types/user";
import { onMounted, watch } from "vue";
import { useInjector, useState } from "vue-vulcan";
import { useCallCenter } from "./useCallCenter";
import { useChannelStore } from "./useChannelStore";
import { useVideoConference } from "./useVideoConference";
/** 会议中心功能, 该功能在呼叫功能生效后使用,通过调用agoraSDK实现多人视频会议、语音会议的功能 */
export function useMeetingCenter() {
const { currentChannel } = useInjector(useChannelStore);
const { authData } = useInjector(useAuthData);
const { hangup, myCallState } = useInjector(useCallCenter);
const {
joinChannelWithAgora,
leaveChannelWithAgora,
muteLocalStream,
unmuteLocalStream,
streamList,
localPushurl,
} = useAgoraClient();
interface MeetingMember {
id: string;
user?: UserData;
self?: boolean;
video?: {
loaded?: boolean,
url?: string
};
chat?: {
loaded: boolean
}
}
/** 监听频道信息,有值或变更后将agora通讯功能加入频道 */
watch(currentChannel, channel => {
if(!channel) return;
joinChannelWithAgora(
channel.agora_token,
channel.channel_id,
parseInt(authData.value.id)
)
})
/** 会议中心功能, 该功能在呼叫功能生效后使用,此模块会将其他即时交互模块集中到一起,如:音视频通讯模块、文本聊天模块、白板标注模块等 */
export function useMeetingCenter() {
const { hangup, myCallState } = useInjector(useCallCenter);
const {streamList, localPushurl, voiceMute, mode, stopVideoConference, switchLocalMicState, switchVideoOrAudio} = useInjector(useVideoConference);
/** 离开会议(分两步:1.挂断当前呼叫, 2.退出agora音视频流) */
/** 离开会议(分两步:1.挂断当前呼叫, 2.停止音视频功能) */
const leave = () => {
hangup();
leaveChannelWithAgora();
hangup('leave');
stopVideoConference();
}
/** 保持屏幕常亮 */
onMounted(() => setKeepScreenOn({keepScreenOn: true}));
/** 监听频道人数,人数为1时,自动退出频道 (目前直接用agora stream替代)*/
/** 监听频道成员信息 */
const { channelMembers } = useInjector(useChannelStore);
const [meetingMembers, setMembers] = useState<MeetingMember[]>([]);
const { getContactById } = useInjector(useContacts);
watch(channelMembers, cmembers => {
const arr = cmembers.map( id => {
const user = getContactById(id, 'uid');
return {
id,
user,
video: {
loaded: false
}
}
})
setMembers(arr);
})
/** 监听视频流列表,长度为1时,自动退出频道 (目前直接用agora stream替代)*/
watch(streamList, (current, last) => {
if(myCallState.value === 'free') return;
if(current.length > last.length) return;
if(current.length > last.length) {
return
};
if(current.length < 2) {
setTimeout(leave, 3000)
setTimeout(leave, 1000)
}
})
/** 切换麦克风使用状态 */
const [voiceMute, setVoiceMute] = useState(false);
const switchLocalMicState = () => {
if(!voiceMute.value) {
setVoiceMute(true)
muteLocalStream('audio');
} else {
setVoiceMute(false);
unmuteLocalStream('audio');
}
}
})
return {
streamList,
meetingMembers,
localPushurl,
voiceMute,
mode,
leave,
switchLocalMicState,
switchVideoOrAudio
}
}
\ No newline at end of file
import { useAuthData } from "any-hooks/auth/useAuthData";
import { useSocket } from "any-hooks/common/useSocket";
import { AnyRemoteSocketMessage, SocketSettings } from "any-hooks/types/socket";
import { watch } from "vue";
import { UserData } from "any-hooks/types/user";
import { useInjector, useState } from "vue-vulcan";
import { SOCKET_SETTINGS } from "./token";
......@@ -19,11 +19,9 @@ export function useNetSocketStore() {
const { authData } = useInjector(useAuthData);
const [ netState, setNetState ] = useState<NetStates>(0);
const { baseUrl } = useInjector<SocketSettings>(SOCKET_SETTINGS, 'optional');
const { connect, send, currentMsg, startHeartConnect, status } = useSocket<AnyRemoteSocketMessage>();
const { connect, send, currentMsg, startHeartConnect, status } = useSocket<AnyRemoteSocketMessage>();
watch(authData, data => {
console.log('authData', data)
if(!data) return;
const initConnect = (data: UserData) => {
connect(
`${baseUrl}?fromID=${data.id}&fromName=${data.nickname}&signID=SA&companyID=${data.company_id}`
)
......@@ -33,7 +31,7 @@ export function useNetSocketStore() {
toID: '0',
msgMainFlag: 'Heart'
})
})
}
const sendMsg = (data: AnyRemoteSocketMessage) => {
send({
......@@ -47,6 +45,7 @@ export function useNetSocketStore() {
return {
sendMsg,
setNetState,
initConnect,
currentMsg,
netState,
status
......
export function useTextChat() {
}
\ No newline at end of file
import { useAuthData } from "any-hooks/auth/useAuthData";
import { useAgoraClient } from "any-hooks/common/useAgoraClient";
import { watch } from "vue";
import { useInjector, useState } from "vue-vulcan";
import { useChannelStore } from "./useChannelStore";
/** 视频通讯中心,用于音视频聊天功能,可集成到会议中心模块(不推荐在页面组件中直接使用该hook模块) */
export function useVideoConference() {
const { currentChannel, clearChannel } = useInjector(useChannelStore);
const { authData } = useInjector(useAuthData);
const {
joinChannelWithAgora,
leaveChannelWithAgora,
muteLocalStream,
unmuteLocalStream,
muteRemoteStream,
unmuteRemoteStream,
streamList,
localPushurl,
} = useAgoraClient();
/** 监听频道模块,频道被创建后自动将agora的音视频通讯功能载入频道 */
watch(currentChannel, channel => {
if(!channel) return;
console.log('频道变化', channel, authData.value.peer_id)
joinChannelWithAgora(
channel.agora_token,
channel.channel_id,
parseInt(authData.value.peer_id)
)
})
/** 切换麦克风使用状态 */
const [voiceMute, setVoiceMute] = useState(false);
const switchLocalMicState = () => {
if(!voiceMute.value) {
setVoiceMute(true)
muteLocalStream('audio');
} else {
setVoiceMute(false);
unmuteLocalStream('audio');
}
}
/** 切换视频与音频通讯 */
const [mode, setMode] = useState<'video' | 'audio'>('video')
const switchVideoOrAudio = () => {
streamList.value.forEach( stream => {
if(mode.value === 'video'){
muteRemoteStream(stream.uid, 'video');
muteLocalStream('video')
} else {
unmuteRemoteStream(stream.uid, 'video');
unmuteLocalStream('video');
}
})
setMode(mode.value === 'video' ? 'audio' : 'video');
}
const stopVideoConference = () => {
setVoiceMute(false);
setMode('video');
clearChannel();
leaveChannelWithAgora();
}
return {
streamList,
localPushurl,
voiceMute,
mode,
switchLocalMicState,
switchVideoOrAudio,
stopVideoConference
}
}
\ No newline at end of file
......@@ -15,14 +15,31 @@ export function useContacts() {
.filter( item => item.nickname.includes(keyword.value))
});
const getContactById = (id: string) => {
const index = userList.value.findIndex( user => user.id === id );
const roleContacts = computed( () => {
const contactHome = {} as {[props: string]: UserData[]}
contacts.value?.forEach( item => {
if(!contactHome[item.group_name]) contactHome[item.group_name] = [];
contactHome[item.group_name].push(item);
})
return contactHome
})
const freeContacts = computed( () => contacts.value.filter( item => item.is_calling === '0' && item.is_signin === '1'))
const getContactById = (pid: string, type?: 'uid'|'pid') => {
const index = userList.value.findIndex( user => user[type==='uid' ? 'id' : 'peer_id'] === pid );
console.log(`根据${type === 'uid' ?'id': 'peer_id'}${pid}查找`, index)
return userList.value[index];
}
const getContactsByGroup = (groupId: string) => {
return userList.value.filter( item => item.diy_group === groupId )
}
return userList.value.filter( item => item.diy_group === groupId );
}
const getContactsOfFree = () => {
return userList.value.filter( item => item.is_calling === '0' && item.is_signin === '1');
}
// 监听socket的home类型消息 实时更新contacts联系人数据
watch(currentMsg, msg => {
......@@ -32,10 +49,13 @@ export function useContacts() {
return {
contacts,
freeContacts,
roleContacts,
setKeyWord,
keyword,
getUserList,
getContactById,
getContactsByGroup
getContactsByGroup,
getContactsOfFree
}
}
\ No newline at end of file
......@@ -7,20 +7,34 @@ export interface AgoraSdkClient {
publish(onSuccess: (url: string) => void, onFailure?: (err: { code: number; reason: string }) => void): void;
on(event: "error", callback: (evt: { code: number; reason: string }) => void): void;
on(event: "stream-added", callback: (evt: { uid: number, url?: string }) => void): void;
on(event: "update-url", callback: (evt: { uid: number, url?: string }) => void): void;
on(event: "stream-removed", callback: (evt: { uid: number[] }) => void): void;
off: (cb: any) => void;
on(event: "mute-video", callback?: (evt: { uid: number }) => void): void;
on(event: "unmute-video", callback: (evt: { uid: number }) => void): void
off: (evt: any) => void;
subscribe(uid: number, onSuccess: (url: string) => void, onFailure?: (err: any) => void): void;
mute(uid: number, target: string, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
unmute(uid: number, target: string, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
muteLocal(target: StreamType, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
unmuteLocal(target: string, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
leave(onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void
destory: (cb: any) => void;
leave(onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void;
unpublish(onSuccess: () => void, onFailure: (err: any) => void): void;
setRole(role: string, onSuccess?: () => void, onFailure?: (err: { code: number; reason: string }) => void): void
destory: (cb?: any) => void;
}
export interface AgoraAppStore {
appid: string;
sdk: any;
platform: 'web' | 'mini-app' | 'electron';
client: AgoraSdkClient;
log?: any;
stream?: any;
}
export interface AgoraStreamItem {
url?: string;
uid: number; //agora仅支持类型为number的uid
videoPause?: boolean;
audioPause?: boolean;
}
\ No newline at end of file
......@@ -16,7 +16,7 @@ export interface SocketSettings {
export type ConnectStatus = 'ready' | 'opening' | 'onerror' | 'closed' | 'reconnect';
export type AnyRemoteMainFlag = 'Heart' | 'Login' | 'Broadcast' | 'Home' | 'CallOffer' | 'CallAnswer' | 'ChannelChat';
export type AnyRemoteMainFlag = 'Heart' | 'Login' | 'Broadcast' | 'Home' | 'CallOffer' | 'CallAnswer' | 'ChannelChat' | 'NotifyUpdateUserList' | 'SetAtts';
export type AnyRemoteSubFlag = 'Request' | 'Busying' | 'Connect' | 'Hangup' | 'UpdateUserList';
export interface AnyRemoteSocketMessage {
......@@ -26,6 +26,7 @@ export interface AnyRemoteSocketMessage {
msgMainFlag: AnyRemoteMainFlag;
msgSubFlag?: AnyRemoteSubFlag;
msgData?: any;
UserAtts?: any;
toID: string;
toName?: string;
channelID?: string;
......
......@@ -12,6 +12,7 @@ export interface UserData {
diy_group?: string;
phone?: string;
email?: string;
peer_id?: string;
}
export interface WorkGroupData {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
### 介绍
## anyremote app 小程序版本
基于taro+vue3 实现, vue3 composition api的具体使用方法请参考官方文档 [https://www.vue3js.cn/docs/zh/guide/composition-api-introduction.html]
#### anyremote 小程序版本
基于taro+vue3 实现
\ No newline at end of file
### 主要工具库:
* vue-vulcan 由本人实现的vue3依赖注入方案,用于解决状态管理问题
* 主要思路与用法请参考文章 [https://zhuanlan.zhihu.com/p/351519484]
* github链接 [https://github.com/velley/vue-vulcan]
### 代码目录结构:
* any-hooks/ anyremote的业务逻辑代码,与小程序的界面逻辑代码解耦
* config/ taro框架的配置文件,大部分情况不用理会
* src/ 小程序的相关代码
* assets/ 静态资源图片
* componnents/ 通用组件
* contains/ 通用常量,如http请求根域名等
* hooks/ 应用内通用逻辑(根据vue3特性将通用逻辑写成组合函数,且该hook文件内的代码均与小程序平台本身特性相关,故与any-hooks内的hooks代码进行了分离)
* pages/ 应用内的页面组件
* types/ 暂时作废,请使用any-hooks中的types文件
### 注意事项:
1. any-hooks的业务代码原计划同时用于小程序和web平台,但由于中途遇到sdk开发的需求,sdk的代码需要满足框架无关的场景,更具有通用性,故使得基于vue3的any-hooks代码可能不再具有可重用性。后续会考虑基于sdk代码重构web端。any-hooks的代码目前只用于小程序端,若后期可用于web端,应考虑将any-hook发布为npm包使用。
\ No newline at end of file
......@@ -5,7 +5,9 @@ export default {
'pages/meeting/index',
'pages/mine/index',
'pages/calling/index',
'pages/contact-detail/index'
'pages/contact-detail/index',
'pages/group-contacts/index',
'pages/help/index'
],
window: {
backgroundTextStyle: 'light',
......
......@@ -22,6 +22,7 @@ view,text,input{
page, .page{
height: 100%;
overflow: hidden;
box-sizing: border-box;
// background-color: #f7f7f7;
}
......
import './app.less';
import { authorize } from '@tarojs/taro';
import { authorize, getCurrentInstance } from '@tarojs/taro';
import { createApp } from 'vue';
import { useCustomeRequest } from './hooks/http/useCustomeRequest';
import { useProviders } from 'vue-vulcan';
......@@ -18,13 +18,14 @@ import { useAgoraSDK } from './hooks/meeting/useAgoraSDK';
import { useMeetingCenter } from 'any-hooks/communication/useMeetingCenter';
import { useContacts } from 'any-hooks/contacts/useContacts';
import { useAppInitInfo } from './hooks/common/useAppInitInfo';
import { useSocketErrorHandle } from './hooks/socket/useSocketErrorHandle';
import { useSocketHandle } from './hooks/socket/useSocketHandle';
import dataEmpty from './components/data-empty.vue';
import MeetingBar from './components/mini-meeting-bar.vue';
import { useVideoConference } from 'any-hooks/communication/useVideoConference';
const App = createApp({
onShow () {
console.log('show a ')
},
setup() {
/* 提供全局配置类hook */
......@@ -47,16 +48,17 @@ const App = createApp({
useAuthData, //用户权限数据
useHttpIntercept, //http请求拦截器
useNetSocketStore, //socket连接中心
useContacts, //实时联系人数据
useChannelStore, //频道信息中心
useCallCenter, //多人呼叫中心
useMeetingCenter, //多人会议中心
useContacts //实时联系人数据
useCallCenter, //多人呼叫中心
useVideoConference, //多人视频通讯中心
useMeetingCenter, //多人会议中心
);
authorize({ scope: 'scope.camera' });
useAuthCheck();
useCallerListener();
useSocketErrorHandle();
useSocketHandle();
}
})
......
<script lang="ts" setup="">
import { navigateTo } from "@tarojs/taro";
import { defineProps } from "@vue/runtime-core";
import { useCallCenter } from "any-hooks/communication/useCallCenter";
import { UserData } from "any-hooks/types/user";
import { useInjector } from "vue-vulcan";
const {data} = defineProps<{data: UserData}>();
const { callContact } = useInjector(useCallCenter);
const gotoDetail = (id: string) => {
navigateTo({url: '/pages/contact-detail/index?id='+ id})
}
</script>
<template>
<view class="contact-item" @tap="gotoDetail(data.id)">
<image class="avatar" :src="data.avatar" />
<view class="info">
<view>{{data.nickname}}</view>
<text>{{data.group_name}}</text>
</view>
<image v-if="data.is_signin === '1'" @tap.stop="callContact(data)" class="call" src="../assets/call3x.png" mode="widthFix"/>
<image v-else class="call" src="../assets/call-no3x.png" mode="widthFix"/>
</view>
</template>
<style lang="less">
.contact-item{
margin: 30px 0;
padding: 30px 0;
border-bottom: 1px solid #f8f8f8;
display: flex;
align-items: center;
.avatar{
width: 80px;
height: 80px;
border-radius: 50%;
overflow:hidden;
}
.info{
flex: 1;
padding: 0 20px;
}
.call{
width: 65px;
height: 65px;
}
}
</style>
\ No newline at end of file
<script lang="ts" setup>
import { navigateTo } from "@tarojs/taro";
import { useMeetingCenter } from "any-hooks/communication/useMeetingCenter";
import { useCallCenter } from "any-hooks/communication/useCallCenter";
import { useInjector } from "vue-vulcan";
const { streamList } = useInjector(useMeetingCenter);
const { myCallState } = useInjector(useCallCenter);
const gotoMeetingHome = () => {
navigateTo({url: '/pages/meeting/index'})
......@@ -11,14 +11,16 @@ import { useMeetingCenter } from "any-hooks/communication/useMeetingCenter";
</script>
<template>
<cover-view class="meeting-bar" v-if="streamList.length">
<view class="meeting-bar" v-if="myCallState!=='free'">
<image @tap="gotoMeetingHome()" class="bar-icon" src="../assets/during-call3x.png" mode="widthFix"></image>
</cover-view>
</view>
</template>
<style lang="less">
.meeting-bar{
position: fixed;
width: 100px;
height: 100px;
bottom: 80px;
right: 5px;
.bar-icon{
......
<script lang="ts" setup>
import { useMeetingCenter } from 'any-hooks/communication/useMeetingCenter';
import { useContacts } from 'any-hooks/contacts/useContacts';
import { AgoraStreamItem } from 'any-hooks/types/agora';
import { UserData } from 'src/types/auth';
import { defineProps, onMounted } from 'vue';
import { useInjector, useState } from 'vue-vulcan';
const { id, url } = defineProps({id: String, url: String});
const { stream } = defineProps<{stream: AgoraStreamItem}>();
const { getContactById } = useInjector(useContacts);
const [ contact, setContact ] = useState<UserData>(null);
const { mode } = useInjector(useMeetingCenter)
onMounted( () => {
console.log('player ready')
// console.log(props)
const currentContact = getContactById(id+'');
onMounted( () => {
const currentContact = getContactById(stream.uid+'');
setContact(currentContact);
})
</script>
......@@ -20,26 +21,25 @@
<view class="player-container" >
<live-player
class="full"
:src="url"
:src="stream.url"
mode="RTC"
:autoplay="true"
:autoPauseIfNavigate="false"
:autoPauseIfOpenNative="false"
:backgroundMute="true"
waitingImage="https://webdemo.agora.io/away.png"
v-show="mode==='video' && !stream.videoPause"
/>
<view class="cover" v-if="contact">
<text class="name">{{contact.nickname}}</text>
<!-- <image class="avatar" :src="contact.avatar"/> -->
<image v-if="mode==='audio' || stream.videoPause" class="avatar" :src="contact.avatar" mode="widthFix"/>
</view>
</view>
</template>
<style lang="less">
.player-container{
position: relative;
width: 50%;
height: 280px;
position: relative;
background: #000;
.cover{
color: #fff;
......@@ -49,8 +49,11 @@
top:0;
left:0;
.avatar{
width: 100%;
height: 100%
position: absolute;
width: 50%;
height: 50%;
top:25%;
left:25%;
}
.name{
position: absolute;
......
<script lang="ts" setup>
import { useAuthData } from 'any-hooks/auth/useAuthData';;
import { useMeetingCenter } from 'any-hooks/communication/useMeetingCenter';
import { defineProps } from 'vue';
import { useInjector } from 'vue-vulcan';
const { authData } = useInjector(useAuthData);
const { url } = defineProps({url: String});
const { mode } = useInjector(useMeetingCenter);
const onError = (e) => {
console.log('pusher state',e);
}
</script>
<template>
<view class="pusher-container" >
<view class="pusher-container">
<live-pusher
class="full"
:autopush="true"
:url="url"
:local-mirror="'enable'"
:url="url"
mode="RTC"
waiting-image="https://webdemo.agora.io/away.png"
aspect="3:4"
waiting-image="https://webdemo.agora.io/away.png"
v-show="mode==='video'"
v-on:error="onError"
:debug="!0"
/>
<view class="cover" >
<text class="name">{{authData.nickname}}</text>
<!-- <image class="avatar" :src="authData.avatar"/> -->
<image class="avatar" v-if="mode==='audio'" :src="authData.avatar" mode="widthFix"/>
</view>
</view>
</template>
......@@ -28,8 +38,7 @@
<style lang="less">
.pusher-container{
position: relative;
width: 50%;
height: 280px;
width: 50%;
background: #000;
.cover{
color: #fff;
......@@ -39,8 +48,11 @@
width: 100%;
height: 100%;
.avatar{
width: 100%;
height: 100%
position: absolute;
width: 50%;
height: 50%;
top:25%;
left:25%;
}
.name{
position: absolute;
......
import { navigateBack, navigateTo, redirectTo, showToast } from "@tarojs/taro";
import { navigateTo, redirectTo, showToast } from "@tarojs/taro";
import { useCallCenter } from "any-hooks/communication/useCallCenter";
import { watch } from "vue";
import { useInjector } from "vue-vulcan";
......@@ -8,7 +8,8 @@ import { useInjector } from "vue-vulcan";
export function useCallerListener() {
const { caller, target, myCallState } = useInjector(useCallCenter);
watch(caller, current => {
watch(caller, current => {
if(!current) return;
switch(current.action) {
case 'Request':
break;
......@@ -19,7 +20,7 @@ export function useCallerListener() {
})
watch(target, val => {
switch(val.action) {
switch(val?.action) {
case 'Busying':
showToast({title: '对方繁忙,暂时无法接受呼叫', icon: 'none'});
break;
......@@ -30,19 +31,22 @@ export function useCallerListener() {
})
// 根据用户的呼叫状态变化,执行相应的页面跳转逻辑
watch(myCallState, state => {
watch(myCallState, (state, prev) => {
switch(state) {
case 'calling':
if(prev !== 'free') {
showToast({title:'已发出呼叫,等待对方回应...', icon:'none'});
}else {
navigateTo({url: '/pages/calling/index'});
}
break;
case 'being_called':
navigateTo({url: '/pages/calling/index'});
break;
case 'call_accepted':
case 'call_successed':
redirectTo({url: '/pages/meeting/index'});
break;
case 'free':
setTimeout(() => navigateBack({delta: 2}), 1000);
break;
break;
}
})
}
\ No newline at end of file
import { getMenuButtonBoundingClientRect } from "@tarojs/taro";
import { ref } from "vue";
export function useAppInitInfo() {
const rect = getMenuButtonBoundingClientRect();
const topDistance = ref(rect.height + rect.top + 10)
return {
rect
rect,
topDistance
}
}
\ No newline at end of file
......@@ -4,9 +4,10 @@ import { AgoraAppStore } from 'any-hooks/types/agora';
const appid = '0a67966ff4ae4eb3b32446df0151e16a';
const client = new AgoraMiniappSDK.Client();
const log = AgoraMiniappSDK.LOG;
export function useAgoraSDK(): AgoraAppStore {
return { appid, client, platform: 'mini-app' }
return { appid, client, sdk: AgoraMiniappSDK, log, platform: 'mini-app' }
}
useAgoraSDK.token = AGORA_APP_SDK;
\ No newline at end of file
import { watch } from "@vue/runtime-core";
import { useNetSocketStore } from "any-hooks/communication/useNetSocketStore";
import { useInjector } from "vue-vulcan";
export function useSocketErrorHandle() {
const { status } = useInjector(useNetSocketStore);
watch(status, val => {
console.log('socket status', val)
})
}
\ No newline at end of file
import { navigateTo, showModal } from "@tarojs/taro";
import { watch } from "@vue/runtime-core";
import { useAuthData } from "any-hooks/auth/useAuthData";
import { useLogOut } from "any-hooks/auth/useLogOut";
import { useNetSocketStore } from "any-hooks/communication/useNetSocketStore";
import { useInjector, useRequest } from "vue-vulcan";
export function useSocketHandle() {
const { status, initConnect } = useInjector(useNetSocketStore);
const { authData } = useInjector(useAuthData);
const [,requestAuthState] = useRequest<{is_signin: '1' | '0'}>('/getUserCallStatus', {auto: false});
const { submitLogOut } = useLogOut();
watch(authData, data => {
if(!data) return;
requestAuthState({user_id: data.id})
.then(res => {
if(res.is_signin === '0') {
initConnect(data)
} else {
showModal({
title:'当前账号已在线,是否确认登陆?',
success: e => {
if(e.confirm){
initConnect(data);
} else if(e.cancel){
submitLogOut({login_id: data.id}).then(_ => navigateTo({url: '/pages/login/index'}));
}
}
})
}
})
})
watch(status, val => {
console.log('socket status', val)
})
}
\ No newline at end of file
// import { useAuthData } from "any-hooks/auth/useAuthData";
import { SOCKET_SETTINGS } from "any-hooks/communication/token";
import { SocketSettings } from "any-hooks/types/socket";
// import { useInjector } from "vue-vulcan";
export function useSocketSetting(): SocketSettings {
......
<script lang="ts" setup>
import { useInjector } from 'vue-vulcan';
import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { navigateBack } from '@tarojs/taro';
import { watch } from '@vue/runtime-core';
const { target, caller, myCallState, answerCaller, hangup } = useInjector(useCallCenter);
const { target, caller, myCallState, answerCaller, hangup } = useInjector(useCallCenter);
const onTapHangup = () => {
hangup();
navigateBack();
}
watch(myCallState, state => {
if(state === 'free') {
setTimeout(navigateBack, 1000)
};
})
</script>
<template>
<view class="page">
<view class="page call-container">
<view class="call-box caller" v-if="myCallState==='being_called'">
<image class="avatar" :src="caller?.avatar"></image>
<text class="tips" >{{caller?.nickname}}向您发起呼叫</text>
......@@ -14,12 +27,12 @@
<view class="call-box target" v-if="myCallState==='calling'">
<image class="avatar" :src="target?.avatar"></image>
<text class="tips">正在呼叫{{target?.nickname}}...</text>
<text class="tips">正在呼叫{{target?.nickname}}...</text>
</view>
<view class="operators" v-if="myCallState!=='free'">
<image v-if="myCallState==='being_called'" @tap="answerCaller('Connect')" class="op-icon" src="../../assets/answer3x.png"></image>
<image @tap="hangup()" class="op-icon" src="../../assets/hangup.png"></image>
<image @tap="onTapHangup()" class="op-icon" src="../../assets/hangup.png"></image>
</view>
</view>
</template>
......@@ -29,10 +42,13 @@ page{
width: 100%;
height: 100%;
}
.page{
.call-container{
background:#333;
width:100%;
height:100%;
background-image: url(https://platserver.anyremote.cn/static/img/small_routine/1.gif);
background-size: 100% 100%;
background-repeat: no-repeat;
.call-box{
position: absolute;
text-align: center;
......
......@@ -14,7 +14,7 @@ const [currentContact, setContact] = useState<UserData>(null)
onMounted( () => {
const ins = getCurrentInstance();
const id = ins.router.params.id;
const current = getContactById(id);
const current = getContactById(id, 'uid');
setContact(current);
})
</script>
......
export default {
navigationBarTitleText: '群组联系人'
}
\ No newline at end of file
<script lang="ts" setup="">
import { getCurrentInstance } from "@tarojs/taro";
import { onMounted } from "@vue/runtime-core";
import { useContacts } from "any-hooks/contacts/useContacts";
import { useInjector, useState } from "vue-vulcan";
import ContactItem from '../../components/contact-item.vue';
const { getContactsByGroup } = useInjector(useContacts);
const [contactList, setList] = useState([]);
onMounted( () => {
const ins = getCurrentInstance();
const id = ins.router.params.id;
const res = getContactsByGroup(id);
setList(res);
console.log(res, id)
})
</script>
<template>
<view class="group-contacts">
<contact-item v-for="(item, key) in contactList" :key="key" :data="item"></contact-item>
<data-empty v-if="!contactList.length"></data-empty>
</view>
</template>
<style>
.group-contacts{
padding: 15px;
}
</style>
<template>
<web-view src="https://help.anyremote.cn/"></web-view>
</template>
......@@ -5,7 +5,7 @@
import { navigateTo } from '@tarojs/taro';
const { callContact } = useInjector(useCallCenter);
const { contacts } = useInjector(useContacts);
const { contacts, roleContacts } = useInjector(useContacts);
const gotoDetail = (id: string) => {
navigateTo({url: '/pages/contact-detail/index?id='+ id})
......@@ -13,8 +13,8 @@
</script>
<template>
<view class="contact-list pd-1">
<view class="contact-item" v-for="(item, key) in contacts" :key="key" @tap="gotoDetail(item.id)">
<view class="contact-list">
<!-- <view class="contact-item" v-for="(item, key) in contacts" :key="key" @tap="gotoDetail(item.id)">
<image class="avatar" :src="item.avatar" />
<view class="info">
<view>{{item.nickname}}</view>
......@@ -22,7 +22,32 @@
</view>
<image v-if="item.is_signin === '1'" @tap.stop="callContact(item)" class="call" src="../../assets/call3x.png" mode="widthFix"/>
<image v-else class="call" src="../../assets/call-no3x.png" mode="widthFix"/>
</view> -->
<view class="contact-role-group" v-for="(group, key, index) in roleContacts" :key="index">
<view class="role-name pd-2">{{key}}</view>
<view class="white-box">
<view class="contact-item pd-2" v-for="(item, i) in group" :key="i">
<image class="avatar" :src="item.avatar" />
<view class="info">
<view>{{item.nickname}}</view>
<text>{{item.group_name}}</text>
</view>
<image v-if="item.is_signin === '1'" @tap.stop="callContact(item)" class="call" src="../../assets/call3x.png" mode="widthFix"/>
<image v-else class="call" src="../../assets/call-no3x.png" mode="widthFix"/>
</view>
</view>
</view>
<data-empty v-if="!contacts?.length"></data-empty>
</view>
</template>
\ No newline at end of file
</template>
<style lang="less">
.contact-list{
.contact-role-group{
background-color: #fff;
.role-name{
background-color: #f5f5f5;
}
}
}
</style>
\ No newline at end of file
......@@ -6,7 +6,7 @@
import { useAppInitInfo } from 'src/hooks/common/useAppInitInfo';
import { ref } from '@vue/reactivity';
import { useThrottle } from 'any-hooks/common/useThrottle';
import { useContacts } from 'any-hooks/contacts/useContacts';
import { useContacts } from 'any-hooks/contacts/useContacts';
const [showLogo, setShowLogo] = useState(true);
const [viewMode, setViewMode] = useState<'items' | 'groups'>('items');
......@@ -37,7 +37,7 @@ import { useContacts } from 'any-hooks/contacts/useContacts';
<text class="tab-item contacts" :class="{active: viewMode==='items'}" @tap="setViewMode('items')">联系人</text>
<text class="tab-item group" :class="{active: viewMode==='groups'}" @tap="setViewMode('groups')">群组</text>
</view>
<view class="list white-box">
<view class="list">
<contacts-list v-if="viewMode === 'items'"></contacts-list>
<work-groups-list v-else></work-groups-list>
</view>
......
<script lang="ts" setup>
import { useWorkGroups } from 'any-hooks/contacts/useWorkGroups';
import { navigateTo } from '@tarojs/taro';
import { useWorkGroups } from 'any-hooks/contacts/useWorkGroups';
const { groups } = useWorkGroups();
const gotoGroupContacts = (id: string) => {
navigateTo({url: `/pages/group-contacts/index?id=${id}`})
}
</script>
<template>
<view class="work-group-list">
<view class="group-item pd-1" v-for="(item, key) in groups" :key="key">
<view class="work-group-list white-box">
<view class="group-item pd-1" v-for="(item, key) in groups" :key="key" @tap="gotoGroupContacts(item.id)">
<image class="group-icon" src="../../assets/group3x.png" mode="widthFix"/>
<text class="group-name">{{item.title}}</text>
</view>
......@@ -17,6 +21,7 @@
<style lang="less">
.work-group-list{
min-height: 300px;
.group-item{
display: flex;
align-items: center;
......
<script setup>
import { computed, watch } from 'vue';
import { navigateTo, navigateBack } from '@tarojs/taro';
import { navigateTo, navigateBack, showModal } from '@tarojs/taro';
import { useLogin } from '../../../any-hooks/auth/useLogin';
import { useInjector } from 'vue-vulcan';
import { useNetSocketStore } from 'any-hooks/communication/useNetSocketStore';
import { useLogOut } from 'any-hooks/auth/useLogOut';
const { loginData, submitLogin } = useLogin();
const allowSubmit = computed(() => !!loginData.login_id && loginData.login_password)
const { submitLogOut } = useLogOut();
const { initConnect } = useInjector(useNetSocketStore);
const allowSubmit = computed(() => !!loginData.login_id && loginData.login_password);
const onSubmit = () => {
submitLogin().then( _ => navigateBack())
submitLogin().then( _ => {
navigateBack();
})
}
</script>
......@@ -19,7 +26,7 @@
</view>
<view class="login-box">
<input class="login-item" placeholder="请输入您的账号" v-model="loginData.login_id">
<input class="login-item" placeholder="请输入密码" v-model="loginData.login_password">
<input class="login-item" type="password" placeholder="请输入密码" v-model="loginData.login_password">
<button class="login-btn" @tap="onSubmit()" :disabled="!allowSubmit" :class="{allow: allowSubmit}">登录</button>
</view>
</view>
......
export default {
navigationStyle: 'custom'
}
}
\ No newline at end of file
<script lang="ts" setup>
import { useMeetingCenter } from 'any-hooks/communication/useMeetingCenter';
import { useInjector } from 'vue-vulcan';
import { useInjector, useState } from 'vue-vulcan';
import StreamPlayer from 'src/components/stream-player.vue';
import StreamPusher from 'src/components/stream-pusher.vue';
import { createLivePusherContext, navigateBack, showModal, showToast } from '@tarojs/taro';
import { watch } from '@vue/runtime-core';
import { useAppInitInfo } from 'src/hooks/common/useAppInitInfo';
import Invite from './invite.vue';
import { useCallCenter } from 'any-hooks/communication/useCallCenter';
import { UserData } from 'any-hooks/types/user';
import { useChannelStore } from 'any-hooks/communication/useChannelStore';
const { streamList, localPushurl, voiceMute, switchLocalMicState, leave } = useInjector(useMeetingCenter);
const { target } = useInjector(useCallCenter);
const { currentChannel } = useInjector(useChannelStore);
const { streamList, localPushurl, voiceMute, mode, switchLocalMicState, switchVideoOrAudio, leave } = useInjector(useMeetingCenter);
const { topDistance } = useAppInitInfo();
/* 监听流的数量,自动退出会议 */
watch(streamList, (current, last) => {
if(current.length > last.length) return;
if(current.length < 2) {
showToast({title: '当前频道已无其他联系人,即将自动退出...', icon: 'none'})
showToast({title: '当前频道已无其他联系人,即将自动退出...', icon: 'none'});
setTimeout(() => navigateBack({delta: 2}), 1000);
}
})
const context = createLivePusherContext()
watch(localPushurl, url => {
url && context.start()
})
/* 用户主动退出会议 */
const onTapLeave = () => {
showModal({
title: '是否退出此次通讯?',
success: res => {
if(res.confirm) leave();
if(res.confirm) {
leave();
navigateBack({delta: 2});
}
}
})
}
/** 切换摄像头 */
const context = createLivePusherContext();
const switchCamera = () => {
context.switchCamera();
}
......@@ -36,49 +55,133 @@
navigateBack({delta: 2})
}
/** 切换通讯画面 0-n表示最大化远程画面 -1表示最大化本地画面 -2表示取消最大化,所有画面均分显示 */
const [activeVideo, setActive] = useState(-2);
const toggleActive = (index: number) => {
if(activeVideo.value === -2) {
setActive(index);
} else {
setActive(-2)
}
}
/** 邀请联系人功能 */
const [inviting, setInviting] = useState(false);
const { callContact } = useInjector(useCallCenter);
const onInvite = (user: UserData) => {
callContact(user);
setInviting(false);
}
</script>
<template>
<view class ="meeting-container page col-page">
<view class ="meeting-container page col-page" :style="{paddingTop: topDistance+'px'}">
<invite v-if="inviting" @choose="onInvite($event)" @cancel="setInviting(false)"></invite>
<view class="c-info" style="color:#fff">
{{currentChannel?.channel_id}}
</view>
<view class="videos">
<stream-player v-for="(item, key) in streamList" :key="key" :id="item.uid.toString()" :url="item.url"></stream-player>
<stream-pusher v-if="localPushurl" :url="localPushurl"></stream-pusher>
<stream-player
class="video-item"
v-for="(item, key) in streamList"
:key="key"
:stream="item"
:class="{active: activeVideo===key, hide: activeVideo!==-2}"
@tap="toggleActive(key)"
>
</stream-player>
<stream-pusher
class="video-item"
v-if="localPushurl"
:url="localPushurl"
:class="{active: activeVideo===-1, hide: activeVideo!==-2}"
@tap="toggleActive(-1)"
>
</stream-pusher>
<view class="waiting video-item" v-if="target?.action === 'none'">
<text>等待中...</text>
</view>
<view class="add-operator" @tap="setInviting(true)">
<image class="add-icon" src="../../assets/add.png" mode="widthFix"></image>
<text>添加联系人</text>
</view>
</view>
<view class="operators pd-2">
<view class="op-item" @tap="backToIndex()">
<!-- <image class="s-o-icon" src="../../assets/minimize3x.png" mode="heightFix"></image> -->
</view>
<view class="op-item" @tap="switchCamera()">
<image class="s-o-icon" src="../../assets/switch-camera3x.png" mode="heightFix"></image>
</view>
<view class="op-item" @tap="onTapLeave()">
<!-- <image class="x-o-icon" src="../../assets/hang-up-big2x.png"></image> -->
</view>
<view class="op-item">
<image v-if="voiceMute" @tap="switchLocalMicState()" class="x-o-icon" src="../../assets/Microphone-on3x.png" />
<image v-else @tap="switchLocalMicState()" class="x-o-icon" src="../../assets/microphone3x.png"/>
<text>麦克风</text>
</view>
<view class="op-item">
<image class="x-o-icon" src="../../assets/recording3x.png"></image>
<text>录制</text>
</view>
<view class="op-item">
<image class="x-o-icon" src="../../assets/voice3x.png"></image>
<text>语音通话</text>
</view>
<view class="op-item" @tap="backToIndex()">
<image class="s-o-icon" src="../../assets/minimize3x.png" mode="heightFix"></image>
</view>
<view class="op-item" @tap="onTapLeave()">
<image class="x-o-icon" src="../../assets/hang-up-big2x.png"></image>
</view>
<view class="op-item" @tap="switchCamera()">
<image class="s-o-icon" src="../../assets/switch-camera3x.png" mode="heightFix"></image>
<image class="x-o-icon" src="../../assets/hang-up-big2x.png"></image>
<text>挂断</text>
</view>
<view class="op-item" @tap="switchVideoOrAudio()">
<image v-if="mode === 'video'" class="x-o-icon" src="../../assets/voice3x.png"></image>
<image v-else class="x-o-icon" src="../../assets/video3x.png"></image>
<text>{{mode === 'video' ? '语音通话' : '视频通话'}}</text>
</view>
</view>
</view>
</template>
<style lang="less">
.meeting-container{
background: #333;
.videos{
display: flex;
flex: 1;
flex-wrap: wrap;
.videos{
flex:1;
text-align: center;
.video-item{
position: relative;
box-sizing: border-box;
border: 1px solid #333;
vertical-align: top;
width:50%;
height: 300px;
display: inline-block;
background: #000;
color: #fff;
&.waiting{
line-height: 300px;
}
&.active{
width: 100%;
height: 600px;
display: block !important;
}
&.hide{
display: none;
}
}
.add-operator{
width: 100%;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
padding: 15px 0;
.add-icon{
width: 30px;
height: 30px;
margin-right: 20px;
}
}
}
}
.operators{
......
<script lang="ts" setup>
import { useContacts } from "any-hooks/contacts/useContacts";
import { UserData } from "any-hooks/types/user";
import { useAppInitInfo } from "src/hooks/common/useAppInitInfo";
import { defineEmit } from "vue";
import { useInjector } from "vue-vulcan";
const { topDistance } = useAppInitInfo();
/* 获取在线且空闲的联系人 */
const { freeContacts } = useInjector(useContacts);
const emit = defineEmit();
const onChoose = (user: UserData) => {
emit('choose', user);
}
const cancelChoose = () => {
emit('cancel');
}
</script>
<template>
<view class="invite-container page" :style="{paddingTop: topDistance+'px'}" @tap="cancelChoose()">
<view class="contacts">
<view class="contact-item" v-for="(item, key) in freeContacts" :key="key" @tap.stop="onChoose(item)">
<image class="avatar" :src="item.avatar"></image>
<view class="name">{{item.nickname}}</view>
</view>
</view>
<data-empty v-if="!freeContacts?.length"></data-empty>
</view>
</template>
<style lang="less">
.invite-container{
position: absolute;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.65);
.contacts{
display: flex;
// justify-content: space-between;
padding: 0 30px;
flex-wrap: wrap;;
.contact-item{
color: #fff;
margin: 0 15px;
width: 175px;
}
}
}
</style>
\ No newline at end of file
......@@ -18,6 +18,10 @@
res!== null && navigateTo({url: '/pages/login/index'});
})
}
const gotoHelp = () => {
navigateTo({url: '/pages/help/index'})
}
</script>
<template>
<view class="mine page" v-if="authData">
......@@ -33,7 +37,7 @@
</view>
<view class="menus pd-2">
<view class="menu-item pd-2">
<view class="menu-item pd-2" @tap="gotoHelp()">
<text>帮助文档</text>
<image class="arrow icon" src="../../assets/arrow-right2x.png"></image>
</view>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment