mirror of
https://github.com/Teamlinker/Teamlinker.git
synced 2025-06-03 03:00:17 +00:00
add
This commit is contained in:
parent
58e27b0717
commit
f7858d1c77
1
.gitignore
vendored
1
.gitignore
vendored
@ -87,7 +87,6 @@ package-lock.json
|
||||
|
||||
code/client/web_admin/vue-element-admin/node_modules/
|
||||
code/docker/teamlinker
|
||||
npm/
|
||||
docker/
|
||||
teamlinker.config.json
|
||||
extra.config.json
|
||||
|
144
code/client/src/business/common/component/calendar/README.md
Normal file
144
code/client/src/business/common/component/calendar/README.md
Normal file
@ -0,0 +1,144 @@
|
||||
<h1 align="center">
|
||||
TLCalendar
|
||||
</h1>
|
||||
<p align="center">
|
||||
A simple calendar component based on <b>vue3</b> and <b>typescript</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/tlcalendar">
|
||||
<img src="https://flat.badgen.net/npm/v/tlcalendar?icon=npm" alt="npm"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tlcalendar">
|
||||
<img src="https://flat.badgen.net/bundlephobia/minzip/tlcalendar?color=green" alt="Minzipped size"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## About
|
||||
It is an open-source calendar of [Teamlinker](https://team-linker.com). It provides a variety of features to help users to build their own calendar components like these below:
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
1. Customize the calendar event show dialog
|
||||
2. Switch between day,week and month
|
||||
3. Support timezone switch
|
||||
4. Calendar event freely drag and move
|
||||
5. Support cross-day event and all-day event
|
||||
6. Support multiply calendars
|
||||
|
||||
|
||||
## Demo
|
||||
[Teamlinker](https://team-linker.com)
|
||||
|
||||
Teamlinker provides a full experience of this package.Have a try!
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
npm i tlcalendar
|
||||
```
|
||||
## Usage
|
||||
|
||||
### Global
|
||||
main.ts
|
||||
```typescript
|
||||
import Calendar from "tlcalendar"
|
||||
import "tlcalendar/style.css"
|
||||
|
||||
app.use(Calendar)
|
||||
```
|
||||
.vue
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from "vue";
|
||||
import moment from "moment";
|
||||
import "moment-timezone"
|
||||
const startDay=ref(moment().startOf("weeks").format("YYYY-MM-DD"))
|
||||
const endDay=ref(moment().endOf("weeks").format("YYYY-MM-DD"))
|
||||
const month=ref(moment().format("YYYY-MM"))
|
||||
const timezone=moment.tz.guess(true)
|
||||
const content=ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 600px;height: 800px">
|
||||
<TLCalendar mode="day" :start-date="startDay" :end-date="endDay" :month="month" :event-list="[]" :utc-offset="8" :time-zone="timezone"></TLCalendar>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Sfc
|
||||
.vue
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, ref} from "vue";
|
||||
import moment from "moment";
|
||||
import "moment-timezone"
|
||||
import {TLCalendar} from "tlcalendar";
|
||||
import "tlcalendar/style.css"
|
||||
const startDay=ref(moment().startOf("weeks").format("YYYY-MM-DD"))
|
||||
const endDay=ref(moment().endOf("weeks").format("YYYY-MM-DD"))
|
||||
const month=ref(moment().format("YYYY-MM"))
|
||||
const timezone=moment.tz.guess(true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 600px;height: 800px">
|
||||
<TLCalendar mode="day" :start-date="startDay" :end-date="endDay" :month="month" :event-list="[]" :utc-offset="8" :time-zone="timezone"></TLCalendar>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Customization
|
||||
### Props
|
||||
```typescript
|
||||
eventList: {
|
||||
type: PropType<IClient_Calendar_Info[]>;
|
||||
required: true;
|
||||
};
|
||||
startDate: {
|
||||
type: PropType<string>;
|
||||
};
|
||||
endDate: {
|
||||
type: PropType<string>;
|
||||
};
|
||||
mode: {
|
||||
type: PropType<"day" | "month">;
|
||||
required: true;
|
||||
};
|
||||
month: {
|
||||
type: PropType<string>;
|
||||
};
|
||||
utcOffset: {
|
||||
type: PropType<number>;
|
||||
};
|
||||
timeZone: {
|
||||
type: PropType<string>;
|
||||
required: true;
|
||||
};
|
||||
```
|
||||
|
||||
### emits
|
||||
```typescript
|
||||
changeEventDate: (event: IClient_Calendar_Info, originalDateRange: {
|
||||
start: IClient_Calendar_Date;
|
||||
end: IClient_Calendar_Date;
|
||||
}, type: "resize" | "move") => void;
|
||||
//when user drag event or adjust event ,this event will be triggered
|
||||
blankClick: (date: moment_2.Moment, point: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => void;
|
||||
//when user click blank area of calendar,this event will be triggered
|
||||
```
|
||||
|
||||
### Slots
|
||||
```vue
|
||||
<template #shortView="{timeZone,selectedEvent,maskInfoTop,maskInfoLeft,onClose}">
|
||||
</template>
|
||||
```
|
||||
|
||||
## About Teamlinker
|
||||
Teamlinker is a cooperation platform that integrates different kind of modules.You can contact your teammates,assign your tasks,start a meeting,schedule your events,manage your files and so on with Teamlinker.
|
144
code/client/src/business/common/component/calendar/npm/README.md
Normal file
144
code/client/src/business/common/component/calendar/npm/README.md
Normal file
@ -0,0 +1,144 @@
|
||||
<h1 align="center">
|
||||
TLCalendar
|
||||
</h1>
|
||||
<p align="center">
|
||||
A simple calendar component based on <b>vue3</b> and <b>typescript</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/tlcalendar">
|
||||
<img src="https://flat.badgen.net/npm/v/tlcalendar?icon=npm" alt="npm"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tlcalendar">
|
||||
<img src="https://flat.badgen.net/bundlephobia/minzip/tlcalendar?color=green" alt="Minzipped size"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## About
|
||||
It is an open-source calendar of [Teamlinker](https://team-linker.com). It provides a variety of features to help users to build their own calendar components like these below:
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
1. Customize the calendar event show dialog
|
||||
2. Switch between day,week and month
|
||||
3. Support timezone switch
|
||||
4. Calendar event freely drag and move
|
||||
5. Support cross-day event and all-day event
|
||||
6. Support multiply calendars
|
||||
|
||||
|
||||
## Demo
|
||||
[Teamlinker](https://team-linker.com)
|
||||
|
||||
Teamlinker provides a full experience of this package.Have a try!
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
npm i tlcalendar
|
||||
```
|
||||
## Usage
|
||||
|
||||
### Global
|
||||
main.ts
|
||||
```typescript
|
||||
import Calendar from "tlcalendar"
|
||||
import "tlcalendar/style.css"
|
||||
|
||||
app.use(Calendar)
|
||||
```
|
||||
.vue
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from "vue";
|
||||
import moment from "moment";
|
||||
import "moment-timezone"
|
||||
const startDay=ref(moment().startOf("weeks").format("YYYY-MM-DD"))
|
||||
const endDay=ref(moment().endOf("weeks").format("YYYY-MM-DD"))
|
||||
const month=ref(moment().format("YYYY-MM"))
|
||||
const timezone=moment.tz.guess(true)
|
||||
const content=ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 600px;height: 800px">
|
||||
<TLCalendar mode="day" :start-date="startDay" :end-date="endDay" :month="month" :event-list="[]" :utc-offset="8" :time-zone="timezone"></TLCalendar>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Sfc
|
||||
.vue
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, ref} from "vue";
|
||||
import moment from "moment";
|
||||
import "moment-timezone"
|
||||
import {TLCalendar} from "tlcalendar";
|
||||
import "tlcalendar/style.css"
|
||||
const startDay=ref(moment().startOf("weeks").format("YYYY-MM-DD"))
|
||||
const endDay=ref(moment().endOf("weeks").format("YYYY-MM-DD"))
|
||||
const month=ref(moment().format("YYYY-MM"))
|
||||
const timezone=moment.tz.guess(true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 600px;height: 800px">
|
||||
<TLCalendar mode="day" :start-date="startDay" :end-date="endDay" :month="month" :event-list="[]" :utc-offset="8" :time-zone="timezone"></TLCalendar>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Customization
|
||||
### Props
|
||||
```typescript
|
||||
eventList: {
|
||||
type: PropType<IClient_Calendar_Info[]>;
|
||||
required: true;
|
||||
};
|
||||
startDate: {
|
||||
type: PropType<string>;
|
||||
};
|
||||
endDate: {
|
||||
type: PropType<string>;
|
||||
};
|
||||
mode: {
|
||||
type: PropType<"day" | "month">;
|
||||
required: true;
|
||||
};
|
||||
month: {
|
||||
type: PropType<string>;
|
||||
};
|
||||
utcOffset: {
|
||||
type: PropType<number>;
|
||||
};
|
||||
timeZone: {
|
||||
type: PropType<string>;
|
||||
required: true;
|
||||
};
|
||||
```
|
||||
|
||||
### emits
|
||||
```typescript
|
||||
changeEventDate: (event: IClient_Calendar_Info, originalDateRange: {
|
||||
start: IClient_Calendar_Date;
|
||||
end: IClient_Calendar_Date;
|
||||
}, type: "resize" | "move") => void;
|
||||
//when user drag event or adjust event ,this event will be triggered
|
||||
blankClick: (date: moment_2.Moment, point: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => void;
|
||||
//when user click blank area of calendar,this event will be triggered
|
||||
```
|
||||
|
||||
### Slots
|
||||
```vue
|
||||
<template #shortView="{timeZone,selectedEvent,maskInfoTop,maskInfoLeft,onClose}">
|
||||
</template>
|
||||
```
|
||||
|
||||
## About Teamlinker
|
||||
Teamlinker is a cooperation platform that integrates different kind of modules.You can contact your teammates,assign your tasks,start a meeting,schedule your events,manage your files and so on with Teamlinker.
|
100
code/client/src/business/common/component/calendar/npm/TLCalendar.d.ts
vendored
Normal file
100
code/client/src/business/common/component/calendar/npm/TLCalendar.d.ts
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
import { App } from 'vue';
|
||||
import { ComponentOptionsMixin } from 'vue';
|
||||
import { default as default_2 } from 'moment';
|
||||
import { DefineComponent } from 'vue';
|
||||
import { ExtractPropTypes } from 'vue';
|
||||
import { PropType } from 'vue';
|
||||
import { PublicProps } from 'vue';
|
||||
|
||||
declare type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
|
||||
|
||||
declare type __VLS_TypePropsToRuntimeProps<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? {
|
||||
type: PropType<__VLS_NonUndefinedable<T[K]>>;
|
||||
} : {
|
||||
type: PropType<T[K]>;
|
||||
required: true;
|
||||
};
|
||||
};
|
||||
|
||||
declare type __VLS_WithTemplateSlots<T, S> = T & {
|
||||
new (): {
|
||||
$slots: S;
|
||||
};
|
||||
};
|
||||
|
||||
declare interface IClient_Calendar_Date {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
hour: number;
|
||||
minute: number;
|
||||
}
|
||||
|
||||
declare interface IClient_Calendar_Info {
|
||||
id: string;
|
||||
name: string;
|
||||
startDate: IClient_Calendar_Date;
|
||||
endDate: IClient_Calendar_Date;
|
||||
isAllDay: boolean;
|
||||
color: string;
|
||||
resource: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
reminder?: number;
|
||||
created_by: any;
|
||||
fixed: boolean;
|
||||
extra?: any;
|
||||
}
|
||||
|
||||
declare const root: {
|
||||
install(app: App, options: any): void;
|
||||
};
|
||||
export default root;
|
||||
|
||||
export declare const TLCalendar: __VLS_WithTemplateSlots<DefineComponent<__VLS_TypePropsToRuntimeProps<{
|
||||
eventList: IClient_Calendar_Info[];
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
mode: "day" | "month";
|
||||
month?: string;
|
||||
utcOffset?: number;
|
||||
timeZone: string;
|
||||
}>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
|
||||
changeEventDate: (event: IClient_Calendar_Info, originalDateRange: {
|
||||
start: IClient_Calendar_Date;
|
||||
end: IClient_Calendar_Date;
|
||||
}, type: "resize" | "move") => void;
|
||||
blankClick: (date: default_2.Moment, point: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => void;
|
||||
}, string, PublicProps, Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
|
||||
eventList: IClient_Calendar_Info[];
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
mode: "day" | "month";
|
||||
month?: string;
|
||||
utcOffset?: number;
|
||||
timeZone: string;
|
||||
}>>> & {
|
||||
onChangeEventDate?: (event: IClient_Calendar_Info, originalDateRange: {
|
||||
start: IClient_Calendar_Date;
|
||||
end: IClient_Calendar_Date;
|
||||
}, type: "resize" | "move") => any;
|
||||
onBlankClick?: (date: default_2.Moment, point: {
|
||||
x: number;
|
||||
y: number;
|
||||
}) => any;
|
||||
}, {}, {}>, {
|
||||
shortView?(_: {
|
||||
timeZone: string;
|
||||
selectedEvent: IClient_Calendar_Info;
|
||||
maskInfoTop: string;
|
||||
maskInfoLeft: string;
|
||||
onClose: () => void;
|
||||
}): any;
|
||||
}>;
|
||||
|
||||
export { }
|
4552
code/client/src/business/common/component/calendar/npm/TLCalendar.js
Normal file
4552
code/client/src/business/common/component/calendar/npm/TLCalendar.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "tlcalendar",
|
||||
"version": "0.0.3",
|
||||
"description": "A simple calendar component based on vue3 and typescript",
|
||||
"main": "TLCalendar.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Teamlinker/TLCalendar.git"
|
||||
},
|
||||
"keywords": ["vue","calendar","date","date-picker","time-picker","teamlinker"],
|
||||
"author": "teamlinker",
|
||||
"license": "ISC",
|
||||
"types": "./TLCalendar.d.ts",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43"
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
.eventMore[data-v-88240b0c]:hover{background-color:#efefef;cursor:pointer}.svg[data-v-88240b0c]{fill:#5f6368}.svg[data-v-88240b0c]:hover{background-color:#efefef;cursor:pointer}
|
@ -1,6 +1,9 @@
|
||||
// @ts-ignore
|
||||
import {defineConfig} from "vite"
|
||||
// @ts-ignore
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import path from "path";
|
||||
// @ts-ignore
|
||||
import dts from "vite-plugin-dts"
|
||||
|
||||
export default defineConfig({
|
||||
|
303
code/client/src/business/common/component/meeting/README.md
Normal file
303
code/client/src/business/common/component/meeting/README.md
Normal file
@ -0,0 +1,303 @@
|
||||
<h1 align="center">
|
||||
TLMeetingClient
|
||||
</h1>
|
||||
<p align="center">
|
||||
A simple video meeting library based on <b>node.js</b> and <b>typescript</b>
|
||||
</p>
|
||||
<p align="center">
|
||||
This is <b>client</b> package,you can retrieve server package from <a href="https://github.com/Teamlinker/TLMeetingServer">here</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/tlmeetingclient">
|
||||
<img src="https://flat.badgen.net/npm/v/tlmeetingclient?icon=npm" alt="npm"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tlmeetingclient">
|
||||
<img src="https://flat.badgen.net/bundlephobia/minzip/tlmeetingclient?color=green" alt="Minzipped size"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## About
|
||||
It is an open-source video meeting package of [Teamlinker](https://team-linker.com). It provides a variety of features to help users to build their own meeting rooms like these below:
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
1. Very easy to complete video meeting functionality
|
||||
2. Support screen share,presenter management
|
||||
3. mute & unmute
|
||||
4. meeting chat
|
||||
5. Free and open-source based on mediasoup
|
||||
|
||||
|
||||
## Demo
|
||||
[Teamlinker](https://team-linker.com)
|
||||
|
||||
Teamlinker provides a full experience of this package.Have a try!
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
npm i tlmeetingclient
|
||||
```
|
||||
## Usage
|
||||
|
||||
TLMeetingClient is based on socket.io,you should build a socket.io connection from backend and pass the socket instance to the TLMeetingClient construct function.
|
||||
|
||||
.vue
|
||||
```typescript
|
||||
const props=defineProps<{
|
||||
meetingId:string,
|
||||
password?:string,
|
||||
inviteBusinessIds?:{
|
||||
id:string,
|
||||
type:ECommon_Model_Organization_Member_Type
|
||||
}[]
|
||||
}>()
|
||||
const loading=ref(true)
|
||||
const unReadCount=ref(0)
|
||||
const organizationUserList=ref<OrganizationUserItem[]>([])
|
||||
const tabValue=ref("participant")
|
||||
const speaker=ref<OrganizationUserItem>()
|
||||
const myOrganizationUserId=SessionStorage.get("organizationUserId")
|
||||
const me=ref<OrganizationUserItem>({
|
||||
organizationUserId:myOrganizationUserId,
|
||||
name:"",
|
||||
permission:ECommon_Meeting_Room_Permission.NORMAL,
|
||||
audioStream:null,
|
||||
videoStream:null,
|
||||
video:true,
|
||||
audio:true
|
||||
})
|
||||
const {t}=useI18n()
|
||||
const meetingChat=ref<InstanceType<typeof MeetingChat>>(null)
|
||||
const socket=SocketIOClient.get(ECommon_Socket_Type.MEETING)
|
||||
const navigator=getCurrentNavigator()
|
||||
const root=getRootNavigatorRef()
|
||||
const appContext=getCurrentInstance().appContext
|
||||
const currentMeeting=ref<DCSType<ICommon_Model_Meeting_Room>>()
|
||||
const screenShareInfo=ref<{
|
||||
video:MediaStream,
|
||||
audio:MediaStream,
|
||||
organizationUserId:string
|
||||
}>()
|
||||
let meetingClient=new MeetingClient(socket.getSocket())
|
||||
watch(tabValue,()=>{
|
||||
if(tabValue.value==="chat") {
|
||||
unReadCount.value=0
|
||||
}
|
||||
})
|
||||
meetingClient.onProducerStateChange=async (state, kind, businessId,type, stream, producerId) => {
|
||||
let objOrganizationUser=organizationUserList.value.find(value => value.organizationUserId===businessId)
|
||||
if(state=="new") {
|
||||
if(type==="camera" || type==="data") {
|
||||
if(!objOrganizationUser) {
|
||||
let obj=userTeamInfoPick.getInfos([{
|
||||
id:businessId,
|
||||
type:ECommon_IM_Message_EntityType.USER
|
||||
}])
|
||||
organizationUserList.value.push({
|
||||
organizationUserId:businessId,
|
||||
name:obj[businessId]?obj[businessId].name:"",
|
||||
permission:ECommon_Meeting_Room_Permission.NORMAL,
|
||||
audioStream:kind==="audio"?stream:null,
|
||||
videoStream:kind==="video"?stream:null,
|
||||
audio:kind==="audio"?true:false,
|
||||
video:kind==="video"?true:false,
|
||||
})
|
||||
objOrganizationUser=organizationUserList.value.at(-1);
|
||||
} else {
|
||||
if(kind=="video") {
|
||||
objOrganizationUser.videoStream=stream
|
||||
objOrganizationUser.video=true
|
||||
} else if(kind=="audio") {
|
||||
objOrganizationUser.audioStream=stream
|
||||
objOrganizationUser.audio=true
|
||||
}
|
||||
}
|
||||
if(!speaker.value) {
|
||||
speaker.value=objOrganizationUser
|
||||
}
|
||||
} else if(type==="screen") {
|
||||
if(!screenShareInfo.value) {
|
||||
screenShareInfo.value={
|
||||
organizationUserId:businessId,
|
||||
audio:kind==="audio"?stream:null,
|
||||
video:kind==="video"?stream:null,
|
||||
}
|
||||
} else {
|
||||
if(kind==="video") {
|
||||
screenShareInfo.value.video=stream
|
||||
} else if(kind==="audio") {
|
||||
screenShareInfo.value.audio=stream
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(state=="close") {
|
||||
if(type==="camera" || type==="data") {
|
||||
if(objOrganizationUser) {
|
||||
let index=organizationUserList.value.findIndex(value => value.organizationUserId===businessId)
|
||||
organizationUserList.value.splice(index,1)
|
||||
if(objOrganizationUser===speaker.value) {
|
||||
speaker.value=null
|
||||
}
|
||||
}
|
||||
} else if(type==="screen") {
|
||||
screenShareInfo.value=null;
|
||||
}
|
||||
} else if(state=="pause") {
|
||||
if(objOrganizationUser) {
|
||||
if(kind=="video") {
|
||||
objOrganizationUser.video=false
|
||||
} else if(kind=="audio") {
|
||||
objOrganizationUser.audio=false
|
||||
}
|
||||
}
|
||||
} else if(state=="resume") {
|
||||
if(objOrganizationUser) {
|
||||
if(kind=="video") {
|
||||
objOrganizationUser.video=true
|
||||
} else if(kind=="audio") {
|
||||
objOrganizationUser.audio=true
|
||||
}
|
||||
}
|
||||
}
|
||||
handleState()
|
||||
}
|
||||
meetingClient.onKick=() => {
|
||||
navigator.pop()
|
||||
}
|
||||
meetingClient.onJoinedRoom=async roomInfo => {
|
||||
getCurrentMeeting()
|
||||
meetingChat.value.getMessage()
|
||||
if(props.inviteBusinessIds) {
|
||||
socket.getSocket().emit("meeting_invite",props.inviteBusinessIds)
|
||||
}
|
||||
}
|
||||
meetingClient.onLeavedRoom=roomInfo => {
|
||||
|
||||
}
|
||||
meetingClient.onSpeaker=async businessId => {
|
||||
let obj=organizationUserList.value.find(item=>item.organizationUserId===businessId)
|
||||
if(obj && obj.organizationUserId!==myOrganizationUserId) {
|
||||
speaker.value=obj
|
||||
}
|
||||
}
|
||||
meetingClient.onLocalProducerInit=async stream => {
|
||||
let obj=userTeamInfoPick.getInfos([{
|
||||
id:myOrganizationUserId,
|
||||
type:ECommon_IM_Message_EntityType.USER
|
||||
}])
|
||||
me.value={
|
||||
organizationUserId:myOrganizationUserId,
|
||||
name:obj[myOrganizationUserId]?obj[myOrganizationUserId].name:"",
|
||||
permission:ECommon_Meeting_Room_Permission.NORMAL,
|
||||
audioStream:stream,
|
||||
videoStream:stream,
|
||||
video:true,
|
||||
audio:true
|
||||
}
|
||||
organizationUserList.value.unshift(me.value)
|
||||
}
|
||||
meetingClient.onLocalProducerStart=kind => {
|
||||
if(kind=="video" || kind=="audio") {
|
||||
handleState()
|
||||
loading.value=false
|
||||
}
|
||||
}
|
||||
const initMeeting=async ()=>{
|
||||
let password=props.password
|
||||
if(password==null) {
|
||||
password=await Dialog.input(root.value,appContext,t("placeholder.typeMeetingPassword"))
|
||||
if(!password) {
|
||||
Message.error(t("tip.joinMeetingFailed"))
|
||||
return
|
||||
}
|
||||
}
|
||||
let preview:any=await Dialog.open(root.value,appContext,t("controller.app.meeting.meetingProfile.meetingPreview"),markRaw(MeetingPreview))
|
||||
if(preview) {
|
||||
let ret=await meetingClient.join(props.meetingId,password,preview.enableVideo,preview.enableAudio,preview.cameraId)
|
||||
if(!ret?.success) {
|
||||
Message.error(ret.msg)
|
||||
navigator.pop()
|
||||
}
|
||||
} else {
|
||||
navigator.pop()
|
||||
}
|
||||
}
|
||||
|
||||
const handleState=async ()=>{
|
||||
let [retState,retPermission]=await Promise.all([
|
||||
meetingClient.states(),
|
||||
socket.getSocket().emitWithAck("meeting_get_presenters")
|
||||
])
|
||||
for(let obj of organizationUserList.value) {
|
||||
if(retPermission[obj.organizationUserId]) {
|
||||
obj.permission=retPermission[obj.organizationUserId]
|
||||
}
|
||||
}
|
||||
for(let objState of retState) {
|
||||
for(let objOrganizationUser of organizationUserList.value) {
|
||||
if (objState.businessId===objOrganizationUser.organizationUserId) {
|
||||
objOrganizationUser.video=objState.kinds["video"]
|
||||
objOrganizationUser.audio=objState.kinds["audio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserInfo = (id: string, info: {
|
||||
id: string,
|
||||
name: string,
|
||||
photo: string
|
||||
}) => {
|
||||
for(let obj of organizationUserList.value) {
|
||||
if(obj.organizationUserId==id) {
|
||||
obj.name=info.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onPresenterChange=async (organizationUserId, permission) => {
|
||||
let obj=organizationUserList.value.find(item=>item.organizationUserId===organizationUserId)
|
||||
if(obj) {
|
||||
obj.permission=permission
|
||||
}
|
||||
}
|
||||
|
||||
const onNewMessage=()=>{
|
||||
if(tabValue.value!=="chat") {
|
||||
unReadCount.value=1
|
||||
}
|
||||
}
|
||||
|
||||
const getCurrentMeeting=async ()=>{
|
||||
let res=await apiMeeting.getCurrentRoom()
|
||||
if(res?.code==0) {
|
||||
currentMeeting.value=res.data
|
||||
}
|
||||
}
|
||||
|
||||
const handleLeaveMeeting=async ()=>{
|
||||
await meetingClient.leave()
|
||||
navigator.pop()
|
||||
}
|
||||
|
||||
onBeforeMount(()=>{
|
||||
eventBus.on(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
|
||||
eventBus.on(EClient_EVENTBUS_TYPE.LEAVE_MEETING, handleLeaveMeeting)
|
||||
socket.getSocket().on("meeting_presenter_change", onPresenterChange)
|
||||
initMeeting()
|
||||
})
|
||||
|
||||
onBeforeUnmount(()=>{
|
||||
eventBus.off(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
|
||||
eventBus.off(EClient_EVENTBUS_TYPE.LEAVE_MEETING, handleLeaveMeeting)
|
||||
socket.getSocket().off("meeting_presenter_change", onPresenterChange)
|
||||
if(meetingClient.getRoomInfo()) {
|
||||
meetingClient.leave()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## About Teamlinker
|
||||
Teamlinker is a cooperation platform that integrates different kind of modules.You can contact your teammates,assign your tasks,start a meeting,schedule your events,manage your files and so on with Teamlinker.
|
@ -0,0 +1 @@
|
||||
export {MeetingClient} from "./src/client";
|
303
code/client/src/business/common/component/meeting/npm/README.md
Normal file
303
code/client/src/business/common/component/meeting/npm/README.md
Normal file
@ -0,0 +1,303 @@
|
||||
<h1 align="center">
|
||||
TLMeetingClient
|
||||
</h1>
|
||||
<p align="center">
|
||||
A simple video meeting library based on <b>node.js</b> and <b>typescript</b>
|
||||
</p>
|
||||
<p align="center">
|
||||
This is <b>client</b> package,you can retrieve server package from <a href="https://github.com/Teamlinker/TLMeetingServer">here</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/tlmeetingclient">
|
||||
<img src="https://flat.badgen.net/npm/v/tlmeetingclient?icon=npm" alt="npm"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tlmeetingclient">
|
||||
<img src="https://flat.badgen.net/bundlephobia/minzip/tlmeetingclient?color=green" alt="Minzipped size"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## About
|
||||
It is an open-source video meeting package of [Teamlinker](https://team-linker.com). It provides a variety of features to help users to build their own meeting rooms like these below:
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
1. Very easy to complete video meeting functionality
|
||||
2. Support screen share,presenter management
|
||||
3. mute & unmute
|
||||
4. meeting chat
|
||||
5. Free and open-source based on mediasoup
|
||||
|
||||
|
||||
## Demo
|
||||
[Teamlinker](https://team-linker.com)
|
||||
|
||||
Teamlinker provides a full experience of this package.Have a try!
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
npm i tlmeetingclient
|
||||
```
|
||||
## Usage
|
||||
|
||||
TLMeetingClient is based on socket.io,you should build a socket.io connection from backend and pass the socket instance to the TLMeetingClient construct function.
|
||||
|
||||
.vue
|
||||
```typescript
|
||||
const props=defineProps<{
|
||||
meetingId:string,
|
||||
password?:string,
|
||||
inviteBusinessIds?:{
|
||||
id:string,
|
||||
type:ECommon_Model_Organization_Member_Type
|
||||
}[]
|
||||
}>()
|
||||
const loading=ref(true)
|
||||
const unReadCount=ref(0)
|
||||
const organizationUserList=ref<OrganizationUserItem[]>([])
|
||||
const tabValue=ref("participant")
|
||||
const speaker=ref<OrganizationUserItem>()
|
||||
const myOrganizationUserId=SessionStorage.get("organizationUserId")
|
||||
const me=ref<OrganizationUserItem>({
|
||||
organizationUserId:myOrganizationUserId,
|
||||
name:"",
|
||||
permission:ECommon_Meeting_Room_Permission.NORMAL,
|
||||
audioStream:null,
|
||||
videoStream:null,
|
||||
video:true,
|
||||
audio:true
|
||||
})
|
||||
const {t}=useI18n()
|
||||
const meetingChat=ref<InstanceType<typeof MeetingChat>>(null)
|
||||
const socket=SocketIOClient.get(ECommon_Socket_Type.MEETING)
|
||||
const navigator=getCurrentNavigator()
|
||||
const root=getRootNavigatorRef()
|
||||
const appContext=getCurrentInstance().appContext
|
||||
const currentMeeting=ref<DCSType<ICommon_Model_Meeting_Room>>()
|
||||
const screenShareInfo=ref<{
|
||||
video:MediaStream,
|
||||
audio:MediaStream,
|
||||
organizationUserId:string
|
||||
}>()
|
||||
let meetingClient=new MeetingClient(socket.getSocket())
|
||||
watch(tabValue,()=>{
|
||||
if(tabValue.value==="chat") {
|
||||
unReadCount.value=0
|
||||
}
|
||||
})
|
||||
meetingClient.onProducerStateChange=async (state, kind, businessId,type, stream, producerId) => {
|
||||
let objOrganizationUser=organizationUserList.value.find(value => value.organizationUserId===businessId)
|
||||
if(state=="new") {
|
||||
if(type==="camera" || type==="data") {
|
||||
if(!objOrganizationUser) {
|
||||
let obj=userTeamInfoPick.getInfos([{
|
||||
id:businessId,
|
||||
type:ECommon_IM_Message_EntityType.USER
|
||||
}])
|
||||
organizationUserList.value.push({
|
||||
organizationUserId:businessId,
|
||||
name:obj[businessId]?obj[businessId].name:"",
|
||||
permission:ECommon_Meeting_Room_Permission.NORMAL,
|
||||
audioStream:kind==="audio"?stream:null,
|
||||
videoStream:kind==="video"?stream:null,
|
||||
audio:kind==="audio"?true:false,
|
||||
video:kind==="video"?true:false,
|
||||
})
|
||||
objOrganizationUser=organizationUserList.value.at(-1);
|
||||
} else {
|
||||
if(kind=="video") {
|
||||
objOrganizationUser.videoStream=stream
|
||||
objOrganizationUser.video=true
|
||||
} else if(kind=="audio") {
|
||||
objOrganizationUser.audioStream=stream
|
||||
objOrganizationUser.audio=true
|
||||
}
|
||||
}
|
||||
if(!speaker.value) {
|
||||
speaker.value=objOrganizationUser
|
||||
}
|
||||
} else if(type==="screen") {
|
||||
if(!screenShareInfo.value) {
|
||||
screenShareInfo.value={
|
||||
organizationUserId:businessId,
|
||||
audio:kind==="audio"?stream:null,
|
||||
video:kind==="video"?stream:null,
|
||||
}
|
||||
} else {
|
||||
if(kind==="video") {
|
||||
screenShareInfo.value.video=stream
|
||||
} else if(kind==="audio") {
|
||||
screenShareInfo.value.audio=stream
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(state=="close") {
|
||||
if(type==="camera" || type==="data") {
|
||||
if(objOrganizationUser) {
|
||||
let index=organizationUserList.value.findIndex(value => value.organizationUserId===businessId)
|
||||
organizationUserList.value.splice(index,1)
|
||||
if(objOrganizationUser===speaker.value) {
|
||||
speaker.value=null
|
||||
}
|
||||
}
|
||||
} else if(type==="screen") {
|
||||
screenShareInfo.value=null;
|
||||
}
|
||||
} else if(state=="pause") {
|
||||
if(objOrganizationUser) {
|
||||
if(kind=="video") {
|
||||
objOrganizationUser.video=false
|
||||
} else if(kind=="audio") {
|
||||
objOrganizationUser.audio=false
|
||||
}
|
||||
}
|
||||
} else if(state=="resume") {
|
||||
if(objOrganizationUser) {
|
||||
if(kind=="video") {
|
||||
objOrganizationUser.video=true
|
||||
} else if(kind=="audio") {
|
||||
objOrganizationUser.audio=true
|
||||
}
|
||||
}
|
||||
}
|
||||
handleState()
|
||||
}
|
||||
meetingClient.onKick=() => {
|
||||
navigator.pop()
|
||||
}
|
||||
meetingClient.onJoinedRoom=async roomInfo => {
|
||||
getCurrentMeeting()
|
||||
meetingChat.value.getMessage()
|
||||
if(props.inviteBusinessIds) {
|
||||
socket.getSocket().emit("meeting_invite",props.inviteBusinessIds)
|
||||
}
|
||||
}
|
||||
meetingClient.onLeavedRoom=roomInfo => {
|
||||
|
||||
}
|
||||
meetingClient.onSpeaker=async businessId => {
|
||||
let obj=organizationUserList.value.find(item=>item.organizationUserId===businessId)
|
||||
if(obj && obj.organizationUserId!==myOrganizationUserId) {
|
||||
speaker.value=obj
|
||||
}
|
||||
}
|
||||
meetingClient.onLocalProducerInit=async stream => {
|
||||
let obj=userTeamInfoPick.getInfos([{
|
||||
id:myOrganizationUserId,
|
||||
type:ECommon_IM_Message_EntityType.USER
|
||||
}])
|
||||
me.value={
|
||||
organizationUserId:myOrganizationUserId,
|
||||
name:obj[myOrganizationUserId]?obj[myOrganizationUserId].name:"",
|
||||
permission:ECommon_Meeting_Room_Permission.NORMAL,
|
||||
audioStream:stream,
|
||||
videoStream:stream,
|
||||
video:true,
|
||||
audio:true
|
||||
}
|
||||
organizationUserList.value.unshift(me.value)
|
||||
}
|
||||
meetingClient.onLocalProducerStart=kind => {
|
||||
if(kind=="video" || kind=="audio") {
|
||||
handleState()
|
||||
loading.value=false
|
||||
}
|
||||
}
|
||||
const initMeeting=async ()=>{
|
||||
let password=props.password
|
||||
if(password==null) {
|
||||
password=await Dialog.input(root.value,appContext,t("placeholder.typeMeetingPassword"))
|
||||
if(!password) {
|
||||
Message.error(t("tip.joinMeetingFailed"))
|
||||
return
|
||||
}
|
||||
}
|
||||
let preview:any=await Dialog.open(root.value,appContext,t("controller.app.meeting.meetingProfile.meetingPreview"),markRaw(MeetingPreview))
|
||||
if(preview) {
|
||||
let ret=await meetingClient.join(props.meetingId,password,preview.enableVideo,preview.enableAudio,preview.cameraId)
|
||||
if(!ret?.success) {
|
||||
Message.error(ret.msg)
|
||||
navigator.pop()
|
||||
}
|
||||
} else {
|
||||
navigator.pop()
|
||||
}
|
||||
}
|
||||
|
||||
const handleState=async ()=>{
|
||||
let [retState,retPermission]=await Promise.all([
|
||||
meetingClient.states(),
|
||||
socket.getSocket().emitWithAck("meeting_get_presenters")
|
||||
])
|
||||
for(let obj of organizationUserList.value) {
|
||||
if(retPermission[obj.organizationUserId]) {
|
||||
obj.permission=retPermission[obj.organizationUserId]
|
||||
}
|
||||
}
|
||||
for(let objState of retState) {
|
||||
for(let objOrganizationUser of organizationUserList.value) {
|
||||
if (objState.businessId===objOrganizationUser.organizationUserId) {
|
||||
objOrganizationUser.video=objState.kinds["video"]
|
||||
objOrganizationUser.audio=objState.kinds["audio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleUserInfo = (id: string, info: {
|
||||
id: string,
|
||||
name: string,
|
||||
photo: string
|
||||
}) => {
|
||||
for(let obj of organizationUserList.value) {
|
||||
if(obj.organizationUserId==id) {
|
||||
obj.name=info.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onPresenterChange=async (organizationUserId, permission) => {
|
||||
let obj=organizationUserList.value.find(item=>item.organizationUserId===organizationUserId)
|
||||
if(obj) {
|
||||
obj.permission=permission
|
||||
}
|
||||
}
|
||||
|
||||
const onNewMessage=()=>{
|
||||
if(tabValue.value!=="chat") {
|
||||
unReadCount.value=1
|
||||
}
|
||||
}
|
||||
|
||||
const getCurrentMeeting=async ()=>{
|
||||
let res=await apiMeeting.getCurrentRoom()
|
||||
if(res?.code==0) {
|
||||
currentMeeting.value=res.data
|
||||
}
|
||||
}
|
||||
|
||||
const handleLeaveMeeting=async ()=>{
|
||||
await meetingClient.leave()
|
||||
navigator.pop()
|
||||
}
|
||||
|
||||
onBeforeMount(()=>{
|
||||
eventBus.on(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
|
||||
eventBus.on(EClient_EVENTBUS_TYPE.LEAVE_MEETING, handleLeaveMeeting)
|
||||
socket.getSocket().on("meeting_presenter_change", onPresenterChange)
|
||||
initMeeting()
|
||||
})
|
||||
|
||||
onBeforeUnmount(()=>{
|
||||
eventBus.off(EClient_EVENTBUS_TYPE.UPDATE_USER_INFO, handleUserInfo)
|
||||
eventBus.off(EClient_EVENTBUS_TYPE.LEAVE_MEETING, handleLeaveMeeting)
|
||||
socket.getSocket().off("meeting_presenter_change", onPresenterChange)
|
||||
if(meetingClient.getRoomInfo()) {
|
||||
meetingClient.leave()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## About Teamlinker
|
||||
Teamlinker is a cooperation platform that integrates different kind of modules.You can contact your teammates,assign your tasks,start a meeting,schedule your events,manage your files and so on with Teamlinker.
|
80
code/client/src/business/common/component/meeting/npm/TLMeetingClient.d.ts
vendored
Normal file
80
code/client/src/business/common/component/meeting/npm/TLMeetingClient.d.ts
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
import { MediaKind } from 'mediasoup-client/lib/RtpParameters';
|
||||
import * as mediaSoup from 'mediasoup-client';
|
||||
|
||||
export declare class MeetingClient {
|
||||
private device;
|
||||
private producerAudio;
|
||||
private producerVideo;
|
||||
private producerAudioScreen;
|
||||
private producerVideoScreen;
|
||||
private producerChat;
|
||||
private producerSet;
|
||||
private transportReceive;
|
||||
private transportSend;
|
||||
private transportDataSend;
|
||||
private socket;
|
||||
private roomInfo;
|
||||
private defaultVideo;
|
||||
private defaultAudio;
|
||||
private defaultCameraId;
|
||||
private defaultAudioId;
|
||||
onProducerStateChange: (state: "new" | "close" | "pause" | "resume", kind: mediaSoup.types.MediaKind, businessId: string, type: "data" | "camera" | "screen", stream?: MediaStream, producerId?: string) => void;
|
||||
onLocalProducerInit: (stream: MediaStream) => void;
|
||||
onLocalProducerStart: (kind: MediaKind) => void;
|
||||
onJoinedRoom: (roomInfo: RoomInfo) => void;
|
||||
onLeavedRoom: (roomInfo: RoomInfo) => void;
|
||||
onSpeaker: (businessId: string) => void;
|
||||
onKick: () => void;
|
||||
onMessageReceive: (data: any, businessId: string) => void;
|
||||
onMessageSend: (data: any) => void;
|
||||
onScreenStopped: () => void;
|
||||
private onDisconnect;
|
||||
constructor(socket: any);
|
||||
static enumVideoDevice(): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}[]>;
|
||||
static enumAudioDevice(): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}[]>;
|
||||
static checkVideoStream(id: string): Promise<MediaStream>;
|
||||
private _onDisconnect;
|
||||
getRoomInfo(): RoomInfo;
|
||||
join(roomId: string, extraData: any, isVideo?: boolean, isAudio?: boolean, cameraId?: string, audioId?: string, backImg?: string, blur?: boolean): Promise<{
|
||||
success: boolean;
|
||||
msg?: string;
|
||||
}>;
|
||||
leave(): Promise<boolean>;
|
||||
pause(kind: MediaKind): Promise<unknown>;
|
||||
resume(kind: MediaKind): Promise<unknown>;
|
||||
mute(kind: MediaKind, businessId: string): Promise<boolean>;
|
||||
unmute(kind: MediaKind, businessId: string): Promise<boolean>;
|
||||
kick(businessId: string): Promise<boolean>;
|
||||
end(): Promise<boolean>;
|
||||
states(): Promise<{
|
||||
businessId: string;
|
||||
kinds: {
|
||||
[kind: string]: boolean;
|
||||
};
|
||||
}[]>;
|
||||
sendMessage(message: string | Buffer): Promise<void>;
|
||||
startShare(): Promise<boolean>;
|
||||
stopShare(): void;
|
||||
private clearRoomConnection;
|
||||
private loadDevice;
|
||||
private subscribe;
|
||||
private consume;
|
||||
private subscribeData;
|
||||
private consumeData;
|
||||
private publish;
|
||||
private publishData;
|
||||
private getProducers;
|
||||
}
|
||||
|
||||
declare type RoomInfo = {
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
};
|
||||
|
||||
export { }
|
@ -0,0 +1 @@
|
||||
export { MeetingClient } from "./src/client";
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "tlmeetingclient",
|
||||
"version": "0.0.4",
|
||||
"description": "A simple video meeting library based on node.js and typescript",
|
||||
"main": "client.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Teamlinker/TLMeetingClient.git"
|
||||
},
|
||||
"keywords": ["teamlinker","meeting","video","nodejs","typescript","meeting client"],
|
||||
"author": "teamlinker",
|
||||
"license": "ISC",
|
||||
"types": "./TLMeetingClient.d.ts",
|
||||
"dependencies": {
|
||||
"mediasoup-client": "^3.7.7",
|
||||
"socket.io-client": "^4.7.2"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
export class AutoExecuteArray {
|
||||
static arr = [];
|
||||
static async push(func) {
|
||||
if (this.arr.length > 0) {
|
||||
this.arr.unshift(func);
|
||||
}
|
||||
else {
|
||||
this.arr.unshift(func);
|
||||
while (this.arr.length > 0) {
|
||||
let func = this.arr[this.arr.length - 1];
|
||||
await func();
|
||||
this.arr.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,594 @@
|
||||
import * as mediaSoup from "mediasoup-client";
|
||||
import { AutoExecuteArray } from "./AutoExecuteArray";
|
||||
export class MeetingClient {
|
||||
device;
|
||||
producerAudio;
|
||||
producerVideo;
|
||||
producerAudioScreen;
|
||||
producerVideoScreen;
|
||||
producerChat;
|
||||
producerSet = new Set();
|
||||
transportReceive;
|
||||
transportSend;
|
||||
transportDataSend;
|
||||
socket;
|
||||
roomInfo;
|
||||
defaultVideo = true;
|
||||
defaultAudio = true;
|
||||
defaultCameraId;
|
||||
defaultAudioId;
|
||||
onProducerStateChange;
|
||||
onLocalProducerInit;
|
||||
onLocalProducerStart;
|
||||
onJoinedRoom;
|
||||
onLeavedRoom;
|
||||
onSpeaker;
|
||||
onKick;
|
||||
onMessageReceive;
|
||||
onMessageSend;
|
||||
onScreenStopped;
|
||||
onDisconnect;
|
||||
constructor(socket) {
|
||||
this.onDisconnect = this._onDisconnect.bind(this);
|
||||
this.socket = socket;
|
||||
this.socket.on('newProducer', async (producerId, kind, businessId, type) => {
|
||||
if (!this.producerSet.has(producerId)) {
|
||||
this.producerSet.add(producerId);
|
||||
if (type === "data") {
|
||||
AutoExecuteArray.push(this.subscribeData.bind(this, producerId));
|
||||
}
|
||||
else if (type === "camera" || type === "screen") {
|
||||
AutoExecuteArray.push(this.subscribe.bind(this, producerId));
|
||||
}
|
||||
}
|
||||
});
|
||||
this.socket.on('producerClosed', (producerId, kind, businessId, type) => {
|
||||
if (this.onProducerStateChange) {
|
||||
this.onProducerStateChange("close", kind, businessId, type, null, producerId);
|
||||
}
|
||||
this.producerSet.delete(producerId);
|
||||
});
|
||||
this.socket.on("producerPause", (producerId, kind, businessId) => {
|
||||
if (this.onProducerStateChange) {
|
||||
this.onProducerStateChange("pause", kind, businessId, "camera", null, producerId);
|
||||
}
|
||||
});
|
||||
this.socket.on("producerResume", (producerId, kind, businessId) => {
|
||||
if (this.onProducerStateChange) {
|
||||
this.onProducerStateChange("resume", kind, businessId, "camera", null, producerId);
|
||||
}
|
||||
});
|
||||
this.socket.on("kick", () => {
|
||||
this.clearRoomConnection();
|
||||
if (this.onKick) {
|
||||
this.onKick();
|
||||
}
|
||||
});
|
||||
this.socket.on("disconnect", this.onDisconnect);
|
||||
this.socket.on("speaker", businessId => {
|
||||
if (this.onSpeaker) {
|
||||
this.onSpeaker(businessId);
|
||||
}
|
||||
});
|
||||
this.socket.on("messageReceive", (message, businessId) => {
|
||||
this.onMessageReceive?.(message, businessId);
|
||||
});
|
||||
}
|
||||
static async enumVideoDevice() {
|
||||
let ret = await navigator.mediaDevices.enumerateDevices();
|
||||
return ret.filter(item => item.kind === "videoinput").map(item => ({
|
||||
id: item.deviceId,
|
||||
name: item.label
|
||||
}));
|
||||
}
|
||||
static async enumAudioDevice() {
|
||||
let ret = await navigator.mediaDevices.enumerateDevices();
|
||||
return ret.filter(item => item.kind === "audioinput").map(item => ({
|
||||
id: item.deviceId,
|
||||
name: item.label
|
||||
}));
|
||||
}
|
||||
static async checkVideoStream(id) {
|
||||
let stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
deviceId: id
|
||||
}
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
_onDisconnect(reason) {
|
||||
this.clearRoomConnection();
|
||||
}
|
||||
getRoomInfo() {
|
||||
return this.roomInfo;
|
||||
}
|
||||
async join(roomId, extraData, isVideo = true, isAudio = true, cameraId, audioId, backImg, blur) {
|
||||
if (this.roomInfo) {
|
||||
return {
|
||||
success: false,
|
||||
msg: "you have joined a meeting"
|
||||
};
|
||||
}
|
||||
this.defaultVideo = isVideo;
|
||||
this.defaultAudio = isAudio;
|
||||
this.defaultCameraId = cameraId;
|
||||
this.defaultAudioId = audioId;
|
||||
let ret = await this.socket.emitWithAck("joinRoom", roomId, extraData);
|
||||
if (ret) {
|
||||
this.roomInfo = ret;
|
||||
}
|
||||
else {
|
||||
return {
|
||||
success: false
|
||||
};
|
||||
}
|
||||
if (!this.device) {
|
||||
const data = await this.socket.emitWithAck('getRouterRtpCapabilities');
|
||||
await this.loadDevice(data);
|
||||
}
|
||||
let retPublic = await this.publish();
|
||||
if (retPublic !== true) {
|
||||
return {
|
||||
success: false,
|
||||
msg: retPublic
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
}
|
||||
}
|
||||
async leave() {
|
||||
if (!this.roomInfo) {
|
||||
return false;
|
||||
}
|
||||
await this.socket.emitWithAck("leaveRoom");
|
||||
this.clearRoomConnection();
|
||||
return true;
|
||||
}
|
||||
async pause(kind) {
|
||||
let ret = await this.socket.emitWithAck("pauseSelf", kind);
|
||||
return ret;
|
||||
}
|
||||
async resume(kind) {
|
||||
let ret = await this.socket.emitWithAck("resumeSelf", kind);
|
||||
return ret;
|
||||
}
|
||||
async mute(kind, businessId) {
|
||||
let ret = await this.socket.emitWithAck("pauseOther", kind, businessId);
|
||||
return ret;
|
||||
}
|
||||
async unmute(kind, businessId) {
|
||||
let ret = await this.socket.emitWithAck("resumeOther", kind, businessId);
|
||||
return ret;
|
||||
}
|
||||
async kick(businessId) {
|
||||
let ret = await this.socket.emitWithAck("kick", businessId);
|
||||
return ret;
|
||||
}
|
||||
async end() {
|
||||
let ret = await this.socket.emitWithAck("end");
|
||||
return ret;
|
||||
}
|
||||
async states() {
|
||||
let ret = await this.socket.emitWithAck("states");
|
||||
return ret;
|
||||
}
|
||||
async sendMessage(message) {
|
||||
let ret = await this.socket.emitWithAck("messageSend", message);
|
||||
//this.producerChat.send(message)
|
||||
if (ret) {
|
||||
this.onMessageSend?.(message);
|
||||
}
|
||||
}
|
||||
async startShare() {
|
||||
let ret = await this.socket.emitWithAck("getScreenProducers");
|
||||
if (ret) {
|
||||
return false;
|
||||
}
|
||||
const mediaConstraints = {
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
},
|
||||
video: {
|
||||
cursor: "always"
|
||||
},
|
||||
};
|
||||
try {
|
||||
let stream = await navigator.mediaDevices.getDisplayMedia(mediaConstraints);
|
||||
if (stream) {
|
||||
let trackAudio = stream.getAudioTracks()[0];
|
||||
if (trackAudio) {
|
||||
let params = {
|
||||
track: trackAudio,
|
||||
appData: {
|
||||
screen: true
|
||||
}
|
||||
};
|
||||
params.codecOptions = {
|
||||
opusStereo: true,
|
||||
opusDtx: true
|
||||
};
|
||||
this.producerAudioScreen = await this.transportSend.produce(params);
|
||||
}
|
||||
let trackVideo = stream.getVideoTracks()[0];
|
||||
if (trackVideo) {
|
||||
trackVideo.onended = ev => {
|
||||
this.stopShare();
|
||||
};
|
||||
let params = {
|
||||
track: trackVideo,
|
||||
appData: {
|
||||
screen: true
|
||||
}
|
||||
};
|
||||
this.producerVideoScreen = await this.transportSend.produce(params);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
stopShare() {
|
||||
this.socket.emit("stopScreen");
|
||||
if (this.producerVideoScreen) {
|
||||
this.producerVideoScreen.removeAllListeners();
|
||||
this.producerVideoScreen.close();
|
||||
this.producerVideoScreen = null;
|
||||
}
|
||||
if (this.producerAudioScreen) {
|
||||
this.producerAudioScreen.removeAllListeners();
|
||||
this.producerAudioScreen.close();
|
||||
this.producerAudioScreen = null;
|
||||
}
|
||||
this.onScreenStopped?.();
|
||||
}
|
||||
clearRoomConnection() {
|
||||
if (this.roomInfo) {
|
||||
if (this.onLeavedRoom) {
|
||||
this.onLeavedRoom(Object.assign({}, this.roomInfo));
|
||||
}
|
||||
this.roomInfo = null;
|
||||
}
|
||||
this.device = null;
|
||||
this.producerSet = new Set;
|
||||
this.stopShare();
|
||||
if (this.producerAudio) {
|
||||
this.producerAudio.removeAllListeners();
|
||||
this.producerAudio.close();
|
||||
this.producerAudio = null;
|
||||
}
|
||||
if (this.producerVideo) {
|
||||
this.producerVideo.removeAllListeners();
|
||||
this.producerVideo.close();
|
||||
this.producerVideo = null;
|
||||
}
|
||||
if (this.transportReceive) {
|
||||
this.transportReceive.removeAllListeners();
|
||||
this.transportReceive.close();
|
||||
this.transportReceive = null;
|
||||
}
|
||||
if (this.transportSend) {
|
||||
this.transportSend.removeAllListeners();
|
||||
this.transportSend.close();
|
||||
this.transportSend = null;
|
||||
}
|
||||
if (this.producerChat) {
|
||||
this.producerChat.removeAllListeners();
|
||||
this.producerChat.close();
|
||||
this.producerChat = null;
|
||||
}
|
||||
if (this.transportDataSend) {
|
||||
this.transportDataSend.removeAllListeners();
|
||||
this.transportDataSend.close();
|
||||
this.transportDataSend = null;
|
||||
}
|
||||
this.socket.removeAllListeners("newProducer");
|
||||
this.socket.removeAllListeners("producerClosed");
|
||||
this.socket.removeAllListeners("producerPause");
|
||||
this.socket.removeAllListeners("producerResume");
|
||||
this.socket.removeAllListeners("kick");
|
||||
this.socket.removeAllListeners("speaker");
|
||||
this.socket.off("disconnect", this.onDisconnect);
|
||||
this.socket = null;
|
||||
}
|
||||
async loadDevice(routerRtpCapabilities) {
|
||||
try {
|
||||
this.device = new mediaSoup.Device();
|
||||
}
|
||||
catch (error) {
|
||||
if (error.name === 'UnsupportedError') {
|
||||
console.error('browser not supported');
|
||||
}
|
||||
}
|
||||
await this.device.load({ routerRtpCapabilities });
|
||||
}
|
||||
async subscribe(remoteProducerId) {
|
||||
if (!this.transportReceive) {
|
||||
const data = await this.socket.emitWithAck('createConsumerTransport');
|
||||
if (!data) {
|
||||
this.producerSet.delete(remoteProducerId);
|
||||
return;
|
||||
}
|
||||
this.transportReceive = this.device.createRecvTransport({ ...data, iceServers: [] });
|
||||
this.transportReceive.on('connect', async ({ dtlsParameters }, callback, errback) => {
|
||||
this.socket.emitWithAck('connectConsumerTransport', {
|
||||
dtlsParameters
|
||||
})
|
||||
.then(callback)
|
||||
.catch(errback);
|
||||
});
|
||||
this.transportReceive.on('connectionstatechange', async (state) => {
|
||||
switch (state) {
|
||||
case 'connecting':
|
||||
console.log("Connecting to consumer for audio, transport id: " + this.transportReceive.id);
|
||||
break;
|
||||
case 'connected':
|
||||
console.log("Connected to consumer for audio, transport id: " + this.transportReceive.id);
|
||||
break;
|
||||
case 'failed':
|
||||
case "closed":
|
||||
case "disconnected": {
|
||||
this.leave();
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.consume(this.transportReceive, remoteProducerId).then(async (value) => {
|
||||
await this.socket.emitWithAck("resume", value.consumer.id);
|
||||
if (this.onProducerStateChange) {
|
||||
this.onProducerStateChange("new", value.consumer.kind, value.businessId, value.type, value.stream, remoteProducerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
async consume(transport, remoteProducerId) {
|
||||
const { rtpCapabilities } = this.device;
|
||||
const transportId = transport.id;
|
||||
const data = await this.socket.emitWithAck('consume', { rtpCapabilities, remoteProducerId, transportId });
|
||||
const { producerId, id, kind, rtpParameters, type } = data;
|
||||
const consumer = await transport.consume({
|
||||
id,
|
||||
producerId,
|
||||
kind,
|
||||
rtpParameters,
|
||||
});
|
||||
const stream = new MediaStream();
|
||||
stream.addTrack(consumer.track);
|
||||
return { stream, consumer, businessId: data.businessId, type };
|
||||
}
|
||||
async subscribeData(remoteProducerId) {
|
||||
const data = await this.socket.emitWithAck('createDataConsumerTransport');
|
||||
if (!data) {
|
||||
this.producerSet.delete(remoteProducerId);
|
||||
return;
|
||||
}
|
||||
const transportDataReceive = this.device.createRecvTransport({ ...data, iceServers: [] });
|
||||
transportDataReceive.on('connect', async ({ dtlsParameters, }, callback, errback) => {
|
||||
this.socket.emitWithAck('connectDataConsumerTransport', {
|
||||
dtlsParameters,
|
||||
sctpParameters: data.sctpParameters
|
||||
})
|
||||
.then(callback)
|
||||
.catch(errback);
|
||||
});
|
||||
transportDataReceive.on('connectionstatechange', async (state) => {
|
||||
switch (state) {
|
||||
case 'connecting':
|
||||
console.log("Connecting to consumer for audio, transport id: " + transportDataReceive.id);
|
||||
break;
|
||||
case 'connected':
|
||||
console.log("Connected to consumer for audio, transport id: " + transportDataReceive.id);
|
||||
break;
|
||||
case 'failed':
|
||||
case "closed":
|
||||
case "disconnected": {
|
||||
this.leave();
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
});
|
||||
this.consumeData(transportDataReceive, remoteProducerId).then(async (value) => {
|
||||
if (this.onProducerStateChange) {
|
||||
this.onProducerStateChange("new", null, value.businessId, "data", null, remoteProducerId);
|
||||
}
|
||||
value.consumer.on("message", data => {
|
||||
this.onMessageReceive?.(data, value.businessId);
|
||||
});
|
||||
});
|
||||
}
|
||||
async consumeData(transport, remoteProducerId) {
|
||||
const transportId = transport.id;
|
||||
const data = await this.socket.emitWithAck('consumeData', { remoteProducerId, transportId });
|
||||
const { id, } = data;
|
||||
console.log(`producerId:${remoteProducerId} consumerId:${id}`);
|
||||
const consumer = await transport.consumeData({
|
||||
id,
|
||||
dataProducerId: remoteProducerId,
|
||||
sctpStreamParameters: {
|
||||
streamId: 0,
|
||||
ordered: true
|
||||
}
|
||||
});
|
||||
consumer.on("close", () => {
|
||||
consumer.removeAllListeners();
|
||||
});
|
||||
return { consumer, businessId: data.businessId };
|
||||
}
|
||||
async publish() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const data = await this.socket.emitWithAck('createProducerTransport');
|
||||
if (!data) {
|
||||
resolve("createProducerTransport failed");
|
||||
}
|
||||
this.transportSend = this.device.createSendTransport({ ...data, iceServers: [] });
|
||||
this.transportSend.on('connect', async ({ dtlsParameters }, callback, errback) => {
|
||||
this.socket.emitWithAck('connectProducerTransport', { dtlsParameters })
|
||||
.then(callback)
|
||||
.catch(errback);
|
||||
});
|
||||
this.transportSend.on('produce', async ({ kind, rtpParameters, appData }, callback, errback) => {
|
||||
try {
|
||||
const { id, producersExist } = await this.socket.emitWithAck('produce', {
|
||||
kind,
|
||||
rtpParameters,
|
||||
appData
|
||||
});
|
||||
if (this.onLocalProducerStart) {
|
||||
this.onLocalProducerStart(kind);
|
||||
}
|
||||
if (producersExist) {
|
||||
this.getProducers();
|
||||
}
|
||||
callback({ id });
|
||||
}
|
||||
catch (err) {
|
||||
errback(err);
|
||||
}
|
||||
});
|
||||
this.transportSend.on('connectionstatechange', (state) => {
|
||||
switch (state) {
|
||||
case 'connecting':
|
||||
console.log("Connecting to publish");
|
||||
break;
|
||||
case 'connected':
|
||||
console.log("Connected");
|
||||
if (this.onJoinedRoom) {
|
||||
this.onJoinedRoom(this.roomInfo);
|
||||
}
|
||||
break;
|
||||
case 'failed':
|
||||
this.transportSend.close();
|
||||
console.log("Failed connection");
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
});
|
||||
let ret = await navigator.mediaDevices.enumerateDevices();
|
||||
let isVideo = false;
|
||||
for (let obj of ret) {
|
||||
if (obj.kind === "videoinput") {
|
||||
isVideo = true;
|
||||
}
|
||||
}
|
||||
const mediaConstraints = {
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
...(this.defaultAudioId && {
|
||||
deviceId: this.defaultAudioId
|
||||
})
|
||||
},
|
||||
video: (isVideo && this.defaultCameraId) ? {
|
||||
deviceId: this.defaultCameraId
|
||||
} : isVideo,
|
||||
};
|
||||
navigator.mediaDevices.getUserMedia(mediaConstraints).then(async (stream) => {
|
||||
let trackAudio = stream.getAudioTracks()[0];
|
||||
if (trackAudio) {
|
||||
let params = {
|
||||
track: trackAudio,
|
||||
appData: {
|
||||
paused: !this.defaultAudio
|
||||
}
|
||||
};
|
||||
params.codecOptions = {
|
||||
opusStereo: true,
|
||||
opusDtx: true,
|
||||
};
|
||||
this.producerAudio = await this.transportSend.produce(params);
|
||||
}
|
||||
let trackVideo = stream.getVideoTracks()[0];
|
||||
if (trackVideo) {
|
||||
let params = {
|
||||
track: trackVideo,
|
||||
appData: {
|
||||
paused: !this.defaultVideo
|
||||
}
|
||||
};
|
||||
this.producerVideo = await this.transportSend.produce(params);
|
||||
}
|
||||
if (this.onLocalProducerInit) {
|
||||
const stream = new MediaStream();
|
||||
stream.addTrack(trackAudio);
|
||||
stream.addTrack(trackVideo);
|
||||
this.onLocalProducerInit(stream);
|
||||
}
|
||||
resolve(true);
|
||||
}, reason => {
|
||||
resolve(reason.message);
|
||||
}).catch(reason => {
|
||||
resolve(reason.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
async publishData() {
|
||||
const data = await this.socket.emitWithAck('createDataProducerTransport');
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
this.transportDataSend = this.device.createSendTransport({ ...data, iceServers: [] });
|
||||
this.transportDataSend.on('connect', async ({ dtlsParameters }, callback, errback) => {
|
||||
this.socket.emitWithAck('connectDataProducerTransport', { dtlsParameters })
|
||||
.then(callback)
|
||||
.catch(errback);
|
||||
});
|
||||
this.transportDataSend.on('producedata', async (_, callback, errback) => {
|
||||
try {
|
||||
const { id, producersExist } = await this.socket.emitWithAck('produceData');
|
||||
if (this.onLocalProducerStart) {
|
||||
this.onLocalProducerStart(null);
|
||||
}
|
||||
if (producersExist) {
|
||||
this.getProducers();
|
||||
}
|
||||
callback({ id });
|
||||
}
|
||||
catch (err) {
|
||||
errback(err);
|
||||
}
|
||||
});
|
||||
this.transportDataSend.on('connectionstatechange', (state) => {
|
||||
switch (state) {
|
||||
case 'connecting':
|
||||
console.log("data Connecting to publish");
|
||||
break;
|
||||
case 'connected':
|
||||
console.log("data connected");
|
||||
// if (this.onJoinedRoom) {
|
||||
// this.onJoinedRoom(this.roomInfo)
|
||||
// }
|
||||
break;
|
||||
case 'failed':
|
||||
this.transportDataSend.close();
|
||||
console.log("data Failed connection");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.producerChat = await this.transportDataSend.produceData();
|
||||
}
|
||||
getProducers() {
|
||||
this.socket.emit('getProducers', async (producerList) => {
|
||||
for (let obj of producerList) {
|
||||
if (!this.producerSet.has(obj.id)) {
|
||||
this.producerSet.add(obj.id);
|
||||
if (obj.type === "data") {
|
||||
AutoExecuteArray.push(this.subscribeData.bind(this, obj.id));
|
||||
}
|
||||
else if (obj.type === "camera" || obj.type === "screen") {
|
||||
AutoExecuteArray.push(this.subscribe.bind(this, obj.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export {};
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "meeting-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build && rm -f ./dist/*js && tsc --noEmit false && cp -rf ./dist/* ./npm",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
}
|
@ -6,8 +6,11 @@ import {Meeting_ClientToServerEvents, Meeting_ServerToClientEvents} from "./type
|
||||
import {AutoExecuteArray} from "./AutoExecuteArray";
|
||||
import {MediaKind, RtpCapabilities} from "mediasoup-client/lib/RtpParameters";
|
||||
import {ProducerOptions} from "mediasoup-client/lib/Producer";
|
||||
import {VirtualBackgroundProcessor, VirtualBackgroundProcessorOptions} from "@shiguredo/virtual-background";
|
||||
|
||||
type RoomInfo = {
|
||||
roomId:string,
|
||||
roomName:string
|
||||
}
|
||||
export class MeetingClient {
|
||||
private device:Device
|
||||
private producerAudio:mediaSoup.types.Producer<mediaSoup.types.AppData>
|
||||
@ -20,22 +23,16 @@ export class MeetingClient {
|
||||
private transportSend:Transport
|
||||
private transportDataSend:Transport
|
||||
private socket:Socket<Meeting_ServerToClientEvents,Meeting_ClientToServerEvents>
|
||||
private roomInfo:{
|
||||
roomId:string,
|
||||
roomName:string
|
||||
}
|
||||
private roomInfo:RoomInfo
|
||||
private defaultVideo=true
|
||||
private defaultAudio=true
|
||||
private defaultCameraId:string
|
||||
private defaultAudioId:string
|
||||
private backImg:string
|
||||
private blur:boolean
|
||||
private processor:VirtualBackgroundProcessor
|
||||
onProducerStateChange:(state:"new"|"close"|"pause"|"resume", kind: mediaSoup.types.MediaKind, businessId:string,type:"data"|"camera"|"screen",stream?: MediaStream,producerId?:string)=>void
|
||||
onLocalProducerInit:(stream:MediaStream)=>void
|
||||
onLocalProducerStart:(kind:MediaKind)=>void
|
||||
onJoinedRoom:(roomInfo:typeof this.roomInfo)=>void
|
||||
onLeavedRoom:(roomInfo:typeof this.roomInfo)=>void
|
||||
onJoinedRoom:(roomInfo:RoomInfo)=>void
|
||||
onLeavedRoom:(roomInfo:RoomInfo)=>void
|
||||
onSpeaker:(businessId:string)=>void
|
||||
onKick:()=>void
|
||||
onMessageReceive:(data:any,businessId:string)=>void
|
||||
@ -136,8 +133,6 @@ export class MeetingClient {
|
||||
this.defaultAudio=isAudio
|
||||
this.defaultCameraId=cameraId
|
||||
this.defaultAudioId=audioId
|
||||
this.backImg=backImg
|
||||
this.blur=blur
|
||||
let ret=await this.socket.emitWithAck("joinRoom",roomId,extraData)
|
||||
if(ret) {
|
||||
this.roomInfo=ret;
|
||||
@ -283,12 +278,6 @@ export class MeetingClient {
|
||||
this.device=null;
|
||||
this.producerSet=new Set
|
||||
this.stopShare()
|
||||
if(this.processor) {
|
||||
this.processor.getOriginalTrack().stop()
|
||||
this.processor.getProcessedTrack().stop()
|
||||
this.processor.stopProcessing()
|
||||
this.processor=null
|
||||
}
|
||||
if(this.producerAudio) {
|
||||
this.producerAudio.removeAllListeners();
|
||||
this.producerAudio.close()
|
||||
@ -549,23 +538,6 @@ export class MeetingClient {
|
||||
}
|
||||
let trackVideo=stream.getVideoTracks()[0]
|
||||
if(trackVideo) {
|
||||
if(this.backImg || this.blur) {
|
||||
if(this.processor) {
|
||||
this.processor.stopProcessing()
|
||||
}
|
||||
this.processor = new VirtualBackgroundProcessor("/");
|
||||
const options:VirtualBackgroundProcessorOptions={
|
||||
segmentationModel:"selfie-general"
|
||||
}
|
||||
if(this.backImg) {
|
||||
const backgroundImage = new Image();
|
||||
backgroundImage.src = this.backImg;
|
||||
options.backgroundImage=backgroundImage
|
||||
} else if(this.blur) {
|
||||
options.blurRadius=5
|
||||
}
|
||||
trackVideo=await this.processor.startProcessing(trackVideo, options)
|
||||
}
|
||||
let params:ProducerOptions={
|
||||
track:trackVideo,
|
||||
appData:{
|
@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,4 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noImplicitUseStrict":true,
|
||||
"sourceMap": false,
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitThis": true,
|
||||
"ignoreDeprecations": "5.0",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*.ts","index.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
// @ts-ignore
|
||||
import {defineConfig} from "vite"
|
||||
// @ts-ignore
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import path from "path";
|
||||
// @ts-ignore
|
||||
import dts from "vite-plugin-dts"
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(),dts({
|
||||
rollupTypes:true
|
||||
})],
|
||||
build: {
|
||||
outDir: "dist", //输出文件名称
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "./index.ts"), //指定组件编译入口文件
|
||||
name: "TLMeetingClient",
|
||||
fileName: "TLMeetingClient",
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["vue"],
|
||||
output: {
|
||||
globals: {
|
||||
vue: "Vue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
@ -8,12 +8,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Editor from "@/business/common/component/richEditorCore/index.vue"
|
||||
import Editor from "@/business/common/component/richEditorCore/src/index.vue"
|
||||
import {
|
||||
EEditor_Content_Line_Config_Type,
|
||||
IEditor_Content_Line,
|
||||
IEditor_Content_Line_Config
|
||||
} from "@/business/common/component/richEditorCore/types";
|
||||
} from "@/business/common/component/richEditorCore/src/types";
|
||||
import {ECommon_Content_Line_Config_Type} from "../../../../../../common/model/content";
|
||||
import {getCurrentInstance, nextTick, ref} from "vue";
|
||||
import UserShortView from "@/business/common/component/userShortView.vue";
|
||||
|
@ -0,0 +1,221 @@
|
||||
<h1 align="center">
|
||||
TLEditor
|
||||
</h1>
|
||||
<p align="center">
|
||||
A simple block-style editor based on <b>vue3</b> and <b>typescript</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/tleditor">
|
||||
<img src="https://flat.badgen.net/npm/v/tleditor?icon=npm" alt="npm"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tleditor">
|
||||
<img src="https://flat.badgen.net/bundlephobia/minzip/tleditor?color=green" alt="Minzipped size"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## About
|
||||
It is an open-source editor of [Teamlinker](https://team-linker.com). It provides a variety of features to help users to build their own text editors like these below:
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
1. Output a clean json
|
||||
2. Customize the style and behavior of pop menu,quote menu
|
||||
3. Insert your own block style content
|
||||
4. type "/" to get pop menu and "@" to get quote menu
|
||||
5. Well-designed API
|
||||
6. Free and open source
|
||||
|
||||
**It solves the cross-line selection issue that many other components can't support**
|
||||
|
||||
## Demo
|
||||
[Teamlinker](https://team-linker.com)
|
||||
|
||||
Teamlinker provides a full experience of this package.Have a try!
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
npm i tleditor
|
||||
```
|
||||
## Usage
|
||||
|
||||
### Global
|
||||
main.ts
|
||||
```typescript
|
||||
import Editor from "tleditor"
|
||||
import "tleditor/style.css"
|
||||
|
||||
app.use(Editor)
|
||||
```
|
||||
.vue
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
const content=ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 500px">
|
||||
<TLEditor v-model="content"></TLEditor>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Sfc
|
||||
.vue
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import {TLEditor} from "tleditor"
|
||||
import "tleditor/style.css"
|
||||
const content=ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 500px">
|
||||
<TLEditor v-model="content"></TLEditor>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Customization
|
||||
### Props
|
||||
```typescript
|
||||
readonly: {
|
||||
type: PropType<boolean>;
|
||||
};
|
||||
border: {
|
||||
type: PropType<boolean>;
|
||||
};
|
||||
popMenuList: { //the pop menu list when user type "/"
|
||||
type: PropType<{
|
||||
type: any;
|
||||
title: string;
|
||||
}[]>;
|
||||
};
|
||||
placeholder: {
|
||||
type: PropType<string>;
|
||||
};
|
||||
quoteType: { //a quote type should be specified when user type "@"
|
||||
type: PropType<any>;
|
||||
};
|
||||
```
|
||||
|
||||
### emits
|
||||
```typescript
|
||||
onQuoteList: (keyword: string, handleFunc: (list: {
|
||||
value: string;
|
||||
label: string;
|
||||
photo: string;
|
||||
}[]) => void) => any;
|
||||
//users use keyword to call user api and return a users' list,the call handleFunc to complete this search
|
||||
onUploadFile: (file: File, handleFunc: (fileId: string, path: string) => void) => any;
|
||||
//users use File object to process upload business,get a file id and path ,then call handleFunc to complete this upload
|
||||
onPopMenuClick: (type: any, handleFunc: (item: IEditor_Content_Line_Config) => void) => any;
|
||||
//the pop menu item click
|
||||
onCustomAnchorClick: (type: any, value: string, link: string, label: string) => any;
|
||||
//the anchor click of customized content
|
||||
onMetaEnter: () => any;
|
||||
//use press meta+enter button
|
||||
onLinkClick: (type: any, value: string, x: number, y: number) => any;
|
||||
//all anchors click
|
||||
onSetLineConfigType: (linkElement: HTMLElement, objConfig: IEditor_Content_Line_Config) => any;
|
||||
//customize the link and image to the html element
|
||||
onGetLineConfigType: (config: IEditor_Content_Line_Config, linkElement: HTMLElement) => any;
|
||||
//parse the link and image from html element
|
||||
```
|
||||
here is an example about onSetLineConfigType and onGetLineConfigType
|
||||
```typescript
|
||||
const onSetLineConfigType=(ele:HTMLElement,obj:IEditor_Content_Line_Config)=> {
|
||||
if (obj.type == ECommon_Content_Line_Config_Type.LINK) {
|
||||
ele.setAttribute("href", obj.link)
|
||||
ele.setAttribute("target", "_blank")
|
||||
ele.style.cursor = "pointer"
|
||||
ele.innerText = obj.value
|
||||
if (obj.style) {
|
||||
for (let key in obj.style) {
|
||||
ele.style[key] = obj.style[key]
|
||||
}
|
||||
}
|
||||
} else if (obj.type == ECommon_Content_Line_Config_Type.IMAGE) {
|
||||
ele.setAttribute("src", obj.link)
|
||||
ele.setAttribute("width", String(obj.width ?? 200))
|
||||
ele.setAttribute("height", "auto")
|
||||
ele.setAttribute("fileId", obj.value)
|
||||
} else if (obj.type === ECommon_Content_Line_Config_Type.FILE) {
|
||||
ele.setAttribute("href", obj.link)
|
||||
ele.setAttribute("download", obj.label)
|
||||
ele.setAttribute("fileId", obj.value)
|
||||
ele.style.margin = "0 2px 0 2px"
|
||||
ele.style.cursor = "pointer"
|
||||
ele.contentEditable = "false"
|
||||
ele.innerText = obj.label
|
||||
ele.style.color = "black"
|
||||
let icon = document.createElement("i")
|
||||
icon.className = "svg svg-file"
|
||||
icon.style.marginRight = "5px"
|
||||
icon.style.color = "gray"
|
||||
ele.prepend(icon)
|
||||
}
|
||||
}
|
||||
|
||||
const onGetLineConfigType=(obj:IEditor_Content_Line_Config,ele:HTMLElement)=>{
|
||||
if(ele.tagName=="A") {
|
||||
let fileId=ele.getAttribute("fileId")
|
||||
if(fileId) {
|
||||
obj.type=ECommon_Content_Line_Config_Type.FILE
|
||||
obj.link=ele.getAttribute("href")
|
||||
obj.value=fileId
|
||||
obj.label=ele.innerText??""
|
||||
} else {
|
||||
obj.type=ECommon_Content_Line_Config_Type.LINK
|
||||
obj.link=ele.getAttribute("href")
|
||||
obj.value=ele.innerText??""
|
||||
}
|
||||
} else if(ele.tagName=="IMG") {
|
||||
obj.type=EEditor_Content_Line_Config_Type.IMAGE
|
||||
obj.link=ele.getAttribute("src")
|
||||
obj.width=parseInt(ele.getAttribute("width"))
|
||||
obj.value=ele.getAttribute("fileId")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Methods
|
||||
```typescript
|
||||
insertConfig: (itemList: IEditor_Content_Line_Config[]) => void;
|
||||
```
|
||||
here is an example:
|
||||
```typescript
|
||||
let arrPromise=await Promise.allSettled((data.data as File[]).map(file=>{
|
||||
return apiFile.upload({
|
||||
file:file as any
|
||||
}).then(res=>{
|
||||
let ret:ICommon_Content_Line_Config
|
||||
if(res?.code==0) {
|
||||
ret={
|
||||
type:ECommon_Content_Line_Config_Type.FILE,
|
||||
value:res.data.id,
|
||||
link:res.data.path,
|
||||
label:file.name
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
})
|
||||
}))
|
||||
if(loading) {
|
||||
loading.value=false
|
||||
}
|
||||
let itemList=arrPromise.filter(item=>{
|
||||
if(item.status==="fulfilled" && item.value) {
|
||||
return true
|
||||
}
|
||||
}).map(item=>{
|
||||
return (item as any).value
|
||||
})
|
||||
objEditor.value.insertConfig(itemList)
|
||||
```
|
||||
## About Teamlinker
|
||||
Teamlinker is a cooperation platform that integrates different kind of modules.You can contact your teammates,assign your tasks,start a meeting,schedule your events,manage your files and so on with Teamlinker.
|
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="test/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,13 @@
|
||||
import TLEditor from "./src/index.vue"
|
||||
|
||||
import {App} from "vue";
|
||||
|
||||
export { TLEditor };
|
||||
export * from "./src/types"
|
||||
const root= {
|
||||
install(app:App,options:any) {
|
||||
app.component("TLEditor", TLEditor);
|
||||
},
|
||||
};
|
||||
|
||||
export default root;
|
@ -0,0 +1,221 @@
|
||||
<h1 align="center">
|
||||
TLEditor
|
||||
</h1>
|
||||
<p align="center">
|
||||
A simple block-style editor based on <b>vue3</b> and <b>typescript</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/tleditor">
|
||||
<img src="https://flat.badgen.net/npm/v/tleditor?icon=npm" alt="npm"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tleditor">
|
||||
<img src="https://flat.badgen.net/bundlephobia/minzip/tleditor?color=green" alt="Minzipped size"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## About
|
||||
It is an open-source editor of [Teamlinker](https://team-linker.com). It provides a variety of features to help users to build their own text editors like these below:
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
1. Output a clean json
|
||||
2. Customize the style and behavior of pop menu,quote menu
|
||||
3. Insert your own block style content
|
||||
4. type "/" to get pop menu and "@" to get quote menu
|
||||
5. Well-designed API
|
||||
6. Free and open source
|
||||
|
||||
**It solves the cross-line selection issue that many other components can't support**
|
||||
|
||||
## Demo
|
||||
[Teamlinker](https://team-linker.com)
|
||||
|
||||
Teamlinker provides a full experience of this package.Have a try!
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
npm i tleditor
|
||||
```
|
||||
## Usage
|
||||
|
||||
### Global
|
||||
main.ts
|
||||
```typescript
|
||||
import Editor from "tleditor"
|
||||
import "tleditor/style.css"
|
||||
|
||||
app.use(Editor)
|
||||
```
|
||||
.vue
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
const content=ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 500px">
|
||||
<TLEditor v-model="content"></TLEditor>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Sfc
|
||||
.vue
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import {TLEditor} from "tleditor"
|
||||
import "tleditor/style.css"
|
||||
const content=ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 500px">
|
||||
<TLEditor v-model="content"></TLEditor>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Customization
|
||||
### Props
|
||||
```typescript
|
||||
readonly: {
|
||||
type: PropType<boolean>;
|
||||
};
|
||||
border: {
|
||||
type: PropType<boolean>;
|
||||
};
|
||||
popMenuList: { //the pop menu list when user type "/"
|
||||
type: PropType<{
|
||||
type: any;
|
||||
title: string;
|
||||
}[]>;
|
||||
};
|
||||
placeholder: {
|
||||
type: PropType<string>;
|
||||
};
|
||||
quoteType: { //a quote type should be specified when user type "@"
|
||||
type: PropType<any>;
|
||||
};
|
||||
```
|
||||
|
||||
### emits
|
||||
```typescript
|
||||
onQuoteList: (keyword: string, handleFunc: (list: {
|
||||
value: string;
|
||||
label: string;
|
||||
photo: string;
|
||||
}[]) => void) => any;
|
||||
//users use keyword to call user api and return a users' list,the call handleFunc to complete this search
|
||||
onUploadFile: (file: File, handleFunc: (fileId: string, path: string) => void) => any;
|
||||
//users use File object to process upload business,get a file id and path ,then call handleFunc to complete this upload
|
||||
onPopMenuClick: (type: any, handleFunc: (item: IEditor_Content_Line_Config) => void) => any;
|
||||
//the pop menu item click
|
||||
onCustomAnchorClick: (type: any, value: string, link: string, label: string) => any;
|
||||
//the anchor click of customized content
|
||||
onMetaEnter: () => any;
|
||||
//use press meta+enter button
|
||||
onLinkClick: (type: any, value: string, x: number, y: number) => any;
|
||||
//all anchors click
|
||||
onSetLineConfigType: (linkElement: HTMLElement, objConfig: IEditor_Content_Line_Config) => any;
|
||||
//customize the link and image to the html element
|
||||
onGetLineConfigType: (config: IEditor_Content_Line_Config, linkElement: HTMLElement) => any;
|
||||
//parse the link and image from html element
|
||||
```
|
||||
here is an example about onSetLineConfigType and onGetLineConfigType
|
||||
```typescript
|
||||
const onSetLineConfigType=(ele:HTMLElement,obj:IEditor_Content_Line_Config)=> {
|
||||
if (obj.type == ECommon_Content_Line_Config_Type.LINK) {
|
||||
ele.setAttribute("href", obj.link)
|
||||
ele.setAttribute("target", "_blank")
|
||||
ele.style.cursor = "pointer"
|
||||
ele.innerText = obj.value
|
||||
if (obj.style) {
|
||||
for (let key in obj.style) {
|
||||
ele.style[key] = obj.style[key]
|
||||
}
|
||||
}
|
||||
} else if (obj.type == ECommon_Content_Line_Config_Type.IMAGE) {
|
||||
ele.setAttribute("src", obj.link)
|
||||
ele.setAttribute("width", String(obj.width ?? 200))
|
||||
ele.setAttribute("height", "auto")
|
||||
ele.setAttribute("fileId", obj.value)
|
||||
} else if (obj.type === ECommon_Content_Line_Config_Type.FILE) {
|
||||
ele.setAttribute("href", obj.link)
|
||||
ele.setAttribute("download", obj.label)
|
||||
ele.setAttribute("fileId", obj.value)
|
||||
ele.style.margin = "0 2px 0 2px"
|
||||
ele.style.cursor = "pointer"
|
||||
ele.contentEditable = "false"
|
||||
ele.innerText = obj.label
|
||||
ele.style.color = "black"
|
||||
let icon = document.createElement("i")
|
||||
icon.className = "svg svg-file"
|
||||
icon.style.marginRight = "5px"
|
||||
icon.style.color = "gray"
|
||||
ele.prepend(icon)
|
||||
}
|
||||
}
|
||||
|
||||
const onGetLineConfigType=(obj:IEditor_Content_Line_Config,ele:HTMLElement)=>{
|
||||
if(ele.tagName=="A") {
|
||||
let fileId=ele.getAttribute("fileId")
|
||||
if(fileId) {
|
||||
obj.type=ECommon_Content_Line_Config_Type.FILE
|
||||
obj.link=ele.getAttribute("href")
|
||||
obj.value=fileId
|
||||
obj.label=ele.innerText??""
|
||||
} else {
|
||||
obj.type=ECommon_Content_Line_Config_Type.LINK
|
||||
obj.link=ele.getAttribute("href")
|
||||
obj.value=ele.innerText??""
|
||||
}
|
||||
} else if(ele.tagName=="IMG") {
|
||||
obj.type=EEditor_Content_Line_Config_Type.IMAGE
|
||||
obj.link=ele.getAttribute("src")
|
||||
obj.width=parseInt(ele.getAttribute("width"))
|
||||
obj.value=ele.getAttribute("fileId")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Methods
|
||||
```typescript
|
||||
insertConfig: (itemList: IEditor_Content_Line_Config[]) => void;
|
||||
```
|
||||
here is an example:
|
||||
```typescript
|
||||
let arrPromise=await Promise.allSettled((data.data as File[]).map(file=>{
|
||||
return apiFile.upload({
|
||||
file:file as any
|
||||
}).then(res=>{
|
||||
let ret:ICommon_Content_Line_Config
|
||||
if(res?.code==0) {
|
||||
ret={
|
||||
type:ECommon_Content_Line_Config_Type.FILE,
|
||||
value:res.data.id,
|
||||
link:res.data.path,
|
||||
label:file.name
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
})
|
||||
}))
|
||||
if(loading) {
|
||||
loading.value=false
|
||||
}
|
||||
let itemList=arrPromise.filter(item=>{
|
||||
if(item.status==="fulfilled" && item.value) {
|
||||
return true
|
||||
}
|
||||
}).map(item=>{
|
||||
return (item as any).value
|
||||
})
|
||||
objEditor.value.insertConfig(itemList)
|
||||
```
|
||||
## About Teamlinker
|
||||
Teamlinker is a cooperation platform that integrates different kind of modules.You can contact your teammates,assign your tasks,start a meeting,schedule your events,manage your files and so on with Teamlinker.
|
106
code/client/src/business/common/component/richEditorCore/npm/TLEditor.d.ts
vendored
Normal file
106
code/client/src/business/common/component/richEditorCore/npm/TLEditor.d.ts
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
import { App } from 'vue';
|
||||
import { ComponentOptionsMixin } from 'vue';
|
||||
import { DefineComponent } from 'vue';
|
||||
import { ExtractPropTypes } from 'vue';
|
||||
import { PropType } from 'vue';
|
||||
import { PublicProps } from 'vue';
|
||||
|
||||
declare type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
|
||||
|
||||
declare type __VLS_TypePropsToRuntimeProps<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? {
|
||||
type: PropType<__VLS_NonUndefinedable<T[K]>>;
|
||||
} : {
|
||||
type: PropType<T[K]>;
|
||||
required: true;
|
||||
};
|
||||
};
|
||||
|
||||
export declare enum EEditor_Content_Line_Config_Type {
|
||||
TEXT = 0,
|
||||
LINK = 1,
|
||||
IMAGE = 2
|
||||
}
|
||||
|
||||
export declare type IEditor_Content_Line = {
|
||||
arr: IEditor_Content_Line_Config[];
|
||||
selectStartIndexPath?: number[];
|
||||
selectEndIndexPath?: number[];
|
||||
};
|
||||
|
||||
export declare type IEditor_Content_Line_Config = {
|
||||
style?: IEditor_Content_Line_Style;
|
||||
value: string;
|
||||
link?: string;
|
||||
type: any;
|
||||
width?: number;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export declare type IEditor_Content_Line_Style = {
|
||||
fontStyle?: string;
|
||||
fontWeight?: string;
|
||||
color?: string;
|
||||
backgroundColor?: string;
|
||||
textDecoration?: string;
|
||||
fontSize?: string;
|
||||
};
|
||||
|
||||
declare const root: {
|
||||
install(app: App, options: any): void;
|
||||
};
|
||||
export default root;
|
||||
|
||||
export declare const TLEditor: DefineComponent<__VLS_TypePropsToRuntimeProps<{
|
||||
readonly?: boolean;
|
||||
modelValue: IEditor_Content_Line[];
|
||||
border?: boolean;
|
||||
popMenuList?: {
|
||||
type: any;
|
||||
title: string;
|
||||
}[];
|
||||
placeholder?: string;
|
||||
quoteType?: any;
|
||||
}>, {
|
||||
insertConfig: (itemList: IEditor_Content_Line_Config[]) => void;
|
||||
}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
|
||||
"update:modelValue": (value: IEditor_Content_Line[]) => void;
|
||||
uploadFile: (file: File, handleFunc: (fileId: string, path: string) => void) => void;
|
||||
popMenuClick: (type: any, handleFunc: (item: IEditor_Content_Line_Config) => void) => void;
|
||||
customAnchorClick: (type: any, value: string, link: string, label: string) => void;
|
||||
quoteList: (keyword: string, handleFunc: (list: {
|
||||
value: string;
|
||||
label: string;
|
||||
photo: string;
|
||||
}[]) => void) => void;
|
||||
metaEnter: () => void;
|
||||
linkClick: (type: any, value: string, x: number, y: number) => void;
|
||||
setLineConfigType: (linkElement: HTMLElement, objConfig: IEditor_Content_Line_Config) => void;
|
||||
getLineConfigType: (config: IEditor_Content_Line_Config, linkElement: HTMLElement) => void;
|
||||
}, string, PublicProps, Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{
|
||||
readonly?: boolean;
|
||||
modelValue: IEditor_Content_Line[];
|
||||
border?: boolean;
|
||||
popMenuList?: {
|
||||
type: any;
|
||||
title: string;
|
||||
}[];
|
||||
placeholder?: string;
|
||||
quoteType?: any;
|
||||
}>>> & {
|
||||
onQuoteList?: (keyword: string, handleFunc: (list: {
|
||||
value: string;
|
||||
label: string;
|
||||
photo: string;
|
||||
}[]) => void) => any;
|
||||
"onUpdate:modelValue"?: (value: IEditor_Content_Line[]) => any;
|
||||
onUploadFile?: (file: File, handleFunc: (fileId: string, path: string) => void) => any;
|
||||
onPopMenuClick?: (type: any, handleFunc: (item: IEditor_Content_Line_Config) => void) => any;
|
||||
onCustomAnchorClick?: (type: any, value: string, link: string, label: string) => any;
|
||||
onMetaEnter?: () => any;
|
||||
onLinkClick?: (type: any, value: string, x: number, y: number) => any;
|
||||
onSetLineConfigType?: (linkElement: HTMLElement, objConfig: IEditor_Content_Line_Config) => any;
|
||||
onGetLineConfigType?: (config: IEditor_Content_Line_Config, linkElement: HTMLElement) => any;
|
||||
}, {}, {}>;
|
||||
|
||||
export { }
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "tleditor",
|
||||
"version": "0.0.3",
|
||||
"description": "A simple block-style editor based on vue3 and typescript",
|
||||
"main": "TLEditor.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Teamlinker/TLEditor.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": ["vue","teamlinker","editor","text editor","block editor"],
|
||||
"author": "teamlinker",
|
||||
"license": "ISC",
|
||||
"types": "./TLEditor.d.ts"
|
||||
}
|
@ -0,0 +1 @@
|
||||
.item[data-v-f6e1fb69]:hover,.hover[data-v-dd631648]:hover{background-color:#d3d3d3}div[data-v-7636bc68]{word-break:break-all}[contenteditable][data-v-7636bc68]{outline:0px solid transparent}[contenteditable=true][data-v-7636bc68]:first-child:empty:not(:focus):before{content:attr(placeholder);color:gray;font-style:italic;pointer-events:none;display:block}
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "editor",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build && cp -f ./dist/* ./npm",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.13.0"
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="root" @mouseover="onMouseOver" @keydown="onKeyDown" style="padding: 10px" @keyup="onKeyUp" :style="{border:border?'border: 1px solid lightgray;':'0px'}" @copy="onCopy" v-bind="$attrs">
|
||||
<div v-for="(item,index) in lineList" :key="index" contenteditable="true" @blur="onBlur(item,$event)" ref="elementList" v-html="RichEditorHandle.handle(item,objEditor.onSetLineConfigType)" @keydown.enter="onEnter(item,index,$event)" @keydown.delete="onDelete(index,item,$event)" style="line-height: 1.5" @focus="onFocus(item,$event)" @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onMouseMove" @dblclick="onDbClick" @paste="onPaste" @click="onClick" :placeholder="placeholder" v-if="!readonly">
|
||||
<div v-for="(item,index) in lineList" :key="index" contenteditable="true" @blur="onBlur(item,$event)" ref="elementList" v-html="RichEditorHandle.handle(item,objEditor.onSetLineConfigType)" @keydown.enter="onEnter(item,index,$event)" @keydown.delete="onDelete(index,item,$event)" style="line-height: 1.5" @focus="onFocus(item,$event)" @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onMouseMove" @dblclick="onDbClick" @paste="onPaste" @click="onClick" :placeholder="placeholder??'type something'" v-if="!readonly">
|
||||
</div>
|
||||
<div v-for="(item,index) in lineList" @click="onClick" :key="index+1" v-html="RichEditorHandle.handle(item,objEditor.onSetLineConfigType)" style="line-height: 1.5;min-height: 21px" v-else>
|
||||
</div>
|
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div style="width: 300px;height: 500px;">
|
||||
<TLEditor v-model="content" style="width: 100%;box-sizing: border-box"></TLEditor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
|
||||
import {IEditor_Content_Line, TLEditor} from "../npm/TLEditor";
|
||||
import "../npm/style.css"
|
||||
|
||||
const content=ref<IEditor_Content_Line[]>([])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,7 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
//import "../npm/style.css"
|
||||
|
||||
const app=createApp(App)
|
||||
//app.use(Editor)
|
||||
app.mount('#app')
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noImplicitUseStrict":true,
|
||||
"sourceMap": true,
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitThis": true,
|
||||
"ignoreDeprecations": "5.0"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","index.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
// @ts-ignore
|
||||
import {defineConfig} from "vite"
|
||||
// @ts-ignore
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import path from "path";
|
||||
// @ts-ignore
|
||||
import dts from "vite-plugin-dts"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(),dts({
|
||||
rollupTypes:true
|
||||
})],
|
||||
build: {
|
||||
outDir: "dist", //输出文件名称
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "./index.ts"), //指定组件编译入口文件
|
||||
name: "TLEditor",
|
||||
fileName: "TLEditor",
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["vue"],
|
||||
output: {
|
||||
globals: {
|
||||
vue: "Vue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
@ -48,7 +48,7 @@
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import {MeetingClient} from "../../../common/component/meeting/client";
|
||||
import {MeetingClient} from "../../../common/component/meeting/src/client";
|
||||
import RichEditor from "../../../common/component/richEditor/richEditor.vue";
|
||||
import {getCurrentInstance, onBeforeMount, ref} from "vue";
|
||||
import {apiFile} from "../../../common/request/request";
|
||||
|
@ -92,7 +92,7 @@ import {apiOrganization} from "../../../common/request/request";
|
||||
import {Dialog} from "../../../common/component/dialog/dialog";
|
||||
import {Message} from "@arco-design/web-vue";
|
||||
import {getCurrentNavigator, getRootNavigatorRef} from "../../../../teamOS/common/component/navigator/navigator";
|
||||
import {MeetingClient} from "../../../common/component/meeting/client";
|
||||
import {MeetingClient} from "../../../common/component/meeting/src/client";
|
||||
import {getCurrentInstance, onBeforeMount, onBeforeUnmount, reactive, ref, watch} from "vue";
|
||||
import {SocketIOClient} from "../../../common/socket/socket";
|
||||
import {ECommon_Socket_Type} from "../../../../../../common/socket/types";
|
||||
|
@ -51,7 +51,7 @@ import {SessionStorage} from "../../../common/storage/session";
|
||||
import {Dialog} from "../../../common/component/dialog/dialog";
|
||||
import {getCurrentNavigator, getRootNavigatorRef} from "../../../../teamOS/common/component/navigator/navigator";
|
||||
import {getCurrentInstance} from "vue";
|
||||
import {MeetingClient} from "../../../common/component/meeting/client";
|
||||
import {MeetingClient} from "../../../common/component/meeting/src/client";
|
||||
import {SocketIOClient} from "../../../common/socket/socket";
|
||||
import {ECommon_Socket_Type} from "../../../../../../common/socket/types";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
@ -34,7 +34,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onBeforeMount, onBeforeUnmount, reactive, ref, watch} from "vue";
|
||||
import {MeetingClient} from "../../../common/component/meeting/client";
|
||||
import {MeetingClient} from "../../../common/component/meeting/src/client";
|
||||
import {onDialogOk} from "../../../common/component/dialog/dialog";
|
||||
import {dialogFuncGenerator} from "../../../common/util/helper";
|
||||
import {apiUser} from "@/business/common/request/request";
|
||||
|
@ -67,7 +67,7 @@
|
||||
import {SocketIOClient} from "../../../common/socket/socket";
|
||||
import {ECommon_Socket_Type} from "../../../../../../common/socket/types";
|
||||
import {getCurrentInstance, markRaw, onBeforeMount, onBeforeUnmount, ref, watch} from "vue";
|
||||
import {MeetingClient} from "../../../common/component/meeting/client";
|
||||
import {MeetingClient} from "../../../common/component/meeting/src/client";
|
||||
import {ECommon_Meeting_Room_Permission, ICommon_Model_Meeting_Room} from "../../../../../../common/model/meeting_room";
|
||||
import {SessionStorage} from "../../../common/storage/session";
|
||||
import {userTeamInfoPick} from "../../../common/component/userInfoPick";
|
||||
|
@ -30,7 +30,7 @@ export default defineConfig({
|
||||
port: 3000,
|
||||
hmr: true,
|
||||
open: false, //自动打开
|
||||
base: "./ ", //生产环境路径
|
||||
//base: "./ ", //生产环境路径
|
||||
proxy: { // 本地开发环境通过代理实现跨域,生产环境使用 nginx 转发
|
||||
// 正则表达式写法
|
||||
'^/api': {
|
||||
|
217
code/server/common/meeting/README.md
Normal file
217
code/server/common/meeting/README.md
Normal file
@ -0,0 +1,217 @@
|
||||
<h1 align="center">
|
||||
TLMeetingServer
|
||||
</h1>
|
||||
<p align="center">
|
||||
A simple video meeting library based on <b>node.js</b> and <b>typescript</b>
|
||||
</p>
|
||||
<p align="center">
|
||||
This is <b>server</b> package,you can retrieve client package from <a href="https://github.com/Teamlinker/TLMeetingClient">here</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/tlmeetingserver">
|
||||
<img src="https://flat.badgen.net/npm/v/tlmeetingserver?icon=npm" alt="npm"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tlmeetingserver">
|
||||
<img src="https://flat.badgen.net/bundlephobia/minzip/tlmeetingserver?color=green" alt="Minzipped size"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## About
|
||||
It is an open-source video meeting package of [Teamlinker](https://team-linker.com). It provides a variety of features to help users to build their own meeting rooms like these below:
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
1. Very easy to complete video meeting functionality
|
||||
2. Support screen share,presenter management
|
||||
3. mute & unmute
|
||||
4. meeting chat
|
||||
5. Free and open-source based on mediasoup
|
||||
|
||||
|
||||
## Demo
|
||||
[Teamlinker](https://team-linker.com)
|
||||
|
||||
Teamlinker provides a full experience of this package.Have a try!
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
npm i tlmeetingserver
|
||||
```
|
||||
## Usage
|
||||
|
||||
TLMeetingServer is based on socket.io,you should build a socket.io connection from client and pass the io instance and meeting config to the TLMeetingServer construct function.
|
||||
|
||||
```typescript
|
||||
let objMeeting=new MeetingServer(io,meetingConfig as any)
|
||||
objMeeting.onJoinRoom=async (roomId,extraData, socketData, socketId) => {
|
||||
try {
|
||||
let objRoom=await MeetingRoomService.getItemById(roomId)
|
||||
if(!objRoom) {
|
||||
return {
|
||||
businessId:null,
|
||||
roomName:null,
|
||||
error:"room not found"
|
||||
}
|
||||
} else if(objRoom.getItem().password!==extraData){
|
||||
return {
|
||||
businessId:null,
|
||||
roomName:null,
|
||||
error:"password wrong"
|
||||
}
|
||||
} else if(objRoom.getItem().organization_id!==socketData.organizationId) {
|
||||
return {
|
||||
businessId:null,
|
||||
roomName:null,
|
||||
error:"access forbidden"
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
businessId:socketData.organizationUserId,
|
||||
roomName:objRoom.getItem().name
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
objMeeting.onJoinedRoom=async (roomId, businessId, socketId) => {
|
||||
try {
|
||||
let objRoom=await MeetingRoomService.getItemById(roomId)
|
||||
if(objRoom) {
|
||||
await objRoom.addMember(businessId)
|
||||
emit.in(businessId).socketsJoin(roomId)
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
objMeeting.onLeavedRoom=async (type, roomId, businessId) => {
|
||||
try {
|
||||
let objRoom=await MeetingRoomService.getItemById(roomId)
|
||||
if(objRoom) {
|
||||
await objRoom.removeMember(businessId)
|
||||
emit.in(businessId).socketsLeave(roomId)
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
objMeeting.onHandleOperation=async (type, roomId, fromBusinessId, toBusinessId, kind) => {
|
||||
try {
|
||||
let objRoom=await MeetingRoomService.getItemById(roomId)
|
||||
if(objRoom) {
|
||||
let ret=await objRoom.getPermission(fromBusinessId)
|
||||
if(ret===ECommon_Meeting_Room_Permission.PRESENTER) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
objMeeting.onDeleteRoom=async roomId => {
|
||||
try {
|
||||
await rpcContentApi.clearByRefId(roomId)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
}
|
||||
objMeeting.onMessageSend=async (roomId, fromBusinessId, message) => {
|
||||
try {
|
||||
await rpcContentApi.add(roomId,ECommon_Model_Content_Type.MEETING_CHAT, fromBusinessId,message as string)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
}
|
||||
await objMeeting.start()
|
||||
```
|
||||
|
||||
### Meeting Config
|
||||
```typescript
|
||||
export default {
|
||||
"worker": {
|
||||
"logLevel": "warn",
|
||||
"logTags": [
|
||||
"info",
|
||||
"ice",
|
||||
"dtls",
|
||||
"rtp",
|
||||
"srtp",
|
||||
"rtcp"
|
||||
],
|
||||
"rtcMinPort": 40000,
|
||||
"rtcMaxPort": 49999
|
||||
},
|
||||
"codecs": [
|
||||
{
|
||||
"kind": "audio",
|
||||
"mimeType": "audio/opus",
|
||||
"clockRate": 48000,
|
||||
"channels": 2
|
||||
},
|
||||
{
|
||||
"kind": "video",
|
||||
"mimeType": "video/VP8",
|
||||
"clockRate": 90000,
|
||||
"parameters": {
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "video",
|
||||
"mimeType": "video/VP9",
|
||||
"clockRate": 90000,
|
||||
"parameters": {
|
||||
"profile-id": 2,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "video",
|
||||
"mimeType": "video/h264",
|
||||
"clockRate": 90000,
|
||||
"parameters": {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "4d0032",
|
||||
"level-asymmetry-allowed": 1,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "video",
|
||||
"mimeType": "video/h264",
|
||||
"clockRate": 90000,
|
||||
"parameters": {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "42e01f",
|
||||
"level-asymmetry-allowed": 1,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
}
|
||||
],
|
||||
"webRtcTransport": {
|
||||
"listenIps": [
|
||||
{
|
||||
"ip": "0.0.0.0",
|
||||
"announcedIp": "192.168.110.6" //this ip should be the public ip
|
||||
}
|
||||
],
|
||||
"enableUdp": true,
|
||||
"enableTcp": true,
|
||||
"preferUdp": true,
|
||||
"enableSctp": false,
|
||||
"initialAvailableOutgoingBitrate": 1000000,
|
||||
"maxSctpMessageSize": 262144
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## About Teamlinker
|
||||
Teamlinker is a cooperation platform that integrates different kind of modules.You can contact your teammates,assign your tasks,start a meeting,schedule your events,manage your files and so on with Teamlinker.
|
1
code/server/common/meeting/index.ts
Normal file
1
code/server/common/meeting/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {MeetingServer} from "./src/server"
|
217
code/server/common/meeting/npm/README.md
Normal file
217
code/server/common/meeting/npm/README.md
Normal file
@ -0,0 +1,217 @@
|
||||
<h1 align="center">
|
||||
TLMeetingServer
|
||||
</h1>
|
||||
<p align="center">
|
||||
A simple video meeting library based on <b>node.js</b> and <b>typescript</b>
|
||||
</p>
|
||||
<p align="center">
|
||||
This is <b>server</b> package,you can retrieve client package from <a href="https://github.com/Teamlinker/TLMeetingClient">here</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/tlmeetingserver">
|
||||
<img src="https://flat.badgen.net/npm/v/tlmeetingserver?icon=npm" alt="npm"/>
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tlmeetingserver">
|
||||
<img src="https://flat.badgen.net/bundlephobia/minzip/tlmeetingserver?color=green" alt="Minzipped size"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## About
|
||||
It is an open-source video meeting package of [Teamlinker](https://team-linker.com). It provides a variety of features to help users to build their own meeting rooms like these below:
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
1. Very easy to complete video meeting functionality
|
||||
2. Support screen share,presenter management
|
||||
3. mute & unmute
|
||||
4. meeting chat
|
||||
5. Free and open-source based on mediasoup
|
||||
|
||||
|
||||
## Demo
|
||||
[Teamlinker](https://team-linker.com)
|
||||
|
||||
Teamlinker provides a full experience of this package.Have a try!
|
||||
|
||||
## Installation
|
||||
```shell
|
||||
npm i tlmeetingserver
|
||||
```
|
||||
## Usage
|
||||
|
||||
TLMeetingServer is based on socket.io,you should build a socket.io connection from client and pass the io instance and meeting config to the TLMeetingServer construct function.
|
||||
|
||||
```typescript
|
||||
let objMeeting=new MeetingServer(io,meetingConfig as any)
|
||||
objMeeting.onJoinRoom=async (roomId,extraData, socketData, socketId) => {
|
||||
try {
|
||||
let objRoom=await MeetingRoomService.getItemById(roomId)
|
||||
if(!objRoom) {
|
||||
return {
|
||||
businessId:null,
|
||||
roomName:null,
|
||||
error:"room not found"
|
||||
}
|
||||
} else if(objRoom.getItem().password!==extraData){
|
||||
return {
|
||||
businessId:null,
|
||||
roomName:null,
|
||||
error:"password wrong"
|
||||
}
|
||||
} else if(objRoom.getItem().organization_id!==socketData.organizationId) {
|
||||
return {
|
||||
businessId:null,
|
||||
roomName:null,
|
||||
error:"access forbidden"
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
businessId:socketData.organizationUserId,
|
||||
roomName:objRoom.getItem().name
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
objMeeting.onJoinedRoom=async (roomId, businessId, socketId) => {
|
||||
try {
|
||||
let objRoom=await MeetingRoomService.getItemById(roomId)
|
||||
if(objRoom) {
|
||||
await objRoom.addMember(businessId)
|
||||
emit.in(businessId).socketsJoin(roomId)
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
objMeeting.onLeavedRoom=async (type, roomId, businessId) => {
|
||||
try {
|
||||
let objRoom=await MeetingRoomService.getItemById(roomId)
|
||||
if(objRoom) {
|
||||
await objRoom.removeMember(businessId)
|
||||
emit.in(businessId).socketsLeave(roomId)
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
objMeeting.onHandleOperation=async (type, roomId, fromBusinessId, toBusinessId, kind) => {
|
||||
try {
|
||||
let objRoom=await MeetingRoomService.getItemById(roomId)
|
||||
if(objRoom) {
|
||||
let ret=await objRoom.getPermission(fromBusinessId)
|
||||
if(ret===ECommon_Meeting_Room_Permission.PRESENTER) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
objMeeting.onDeleteRoom=async roomId => {
|
||||
try {
|
||||
await rpcContentApi.clearByRefId(roomId)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
}
|
||||
objMeeting.onMessageSend=async (roomId, fromBusinessId, message) => {
|
||||
try {
|
||||
await rpcContentApi.add(roomId,ECommon_Model_Content_Type.MEETING_CHAT, fromBusinessId,message as string)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
}
|
||||
await objMeeting.start()
|
||||
```
|
||||
|
||||
### Meeting Config
|
||||
```typescript
|
||||
export default {
|
||||
"worker": {
|
||||
"logLevel": "warn",
|
||||
"logTags": [
|
||||
"info",
|
||||
"ice",
|
||||
"dtls",
|
||||
"rtp",
|
||||
"srtp",
|
||||
"rtcp"
|
||||
],
|
||||
"rtcMinPort": 40000,
|
||||
"rtcMaxPort": 49999
|
||||
},
|
||||
"codecs": [
|
||||
{
|
||||
"kind": "audio",
|
||||
"mimeType": "audio/opus",
|
||||
"clockRate": 48000,
|
||||
"channels": 2
|
||||
},
|
||||
{
|
||||
"kind": "video",
|
||||
"mimeType": "video/VP8",
|
||||
"clockRate": 90000,
|
||||
"parameters": {
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "video",
|
||||
"mimeType": "video/VP9",
|
||||
"clockRate": 90000,
|
||||
"parameters": {
|
||||
"profile-id": 2,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "video",
|
||||
"mimeType": "video/h264",
|
||||
"clockRate": 90000,
|
||||
"parameters": {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "4d0032",
|
||||
"level-asymmetry-allowed": 1,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "video",
|
||||
"mimeType": "video/h264",
|
||||
"clockRate": 90000,
|
||||
"parameters": {
|
||||
"packetization-mode": 1,
|
||||
"profile-level-id": "42e01f",
|
||||
"level-asymmetry-allowed": 1,
|
||||
"x-google-start-bitrate": 1000
|
||||
}
|
||||
}
|
||||
],
|
||||
"webRtcTransport": {
|
||||
"listenIps": [
|
||||
{
|
||||
"ip": "0.0.0.0",
|
||||
"announcedIp": "192.168.110.6" //this ip should be the public ip
|
||||
}
|
||||
],
|
||||
"enableUdp": true,
|
||||
"enableTcp": true,
|
||||
"preferUdp": true,
|
||||
"enableSctp": false,
|
||||
"initialAvailableOutgoingBitrate": 1000000,
|
||||
"maxSctpMessageSize": 262144
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## About Teamlinker
|
||||
Teamlinker is a cooperation platform that integrates different kind of modules.You can contact your teammates,assign your tasks,start a meeting,schedule your events,manage your files and so on with Teamlinker.
|
71
code/server/common/meeting/npm/TLMeetingServer.d.ts
vendored
Normal file
71
code/server/common/meeting/npm/TLMeetingServer.d.ts
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
import { MediaKind } from 'mediasoup/node/lib/RtpParameters';
|
||||
import { RtpCodecCapability } from 'mediasoup/node/lib/RtpParameters';
|
||||
import { WorkerLogLevel } from 'mediasoup/node/lib/Worker';
|
||||
import { WorkerLogTag } from 'mediasoup/node/lib/Worker';
|
||||
|
||||
declare type MeetingConfig = {
|
||||
worker: {
|
||||
logLevel: WorkerLogLevel;
|
||||
logTags: WorkerLogTag[];
|
||||
rtcMinPort: number;
|
||||
rtcMaxPort: number;
|
||||
};
|
||||
codecs: RtpCodecCapability[];
|
||||
webRtcTransport: {
|
||||
listenIps: {
|
||||
ip: string;
|
||||
announcedIp: string;
|
||||
}[];
|
||||
enableUdp: boolean;
|
||||
enableTcp: boolean;
|
||||
preferUdp: boolean;
|
||||
enableSctp: boolean;
|
||||
initialAvailableOutgoingBitrate: number;
|
||||
maxSctpMessageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export declare class MeetingServer {
|
||||
private io;
|
||||
private config;
|
||||
private roomMap;
|
||||
private workList;
|
||||
private peerInfoMap;
|
||||
private workerIndex;
|
||||
onJoinRoom: (roomId: string, extraData: any, socketData: any, socketId: string) => Promise<{
|
||||
roomName: string;
|
||||
businessId: string;
|
||||
error?: string;
|
||||
}>;
|
||||
onJoinedRoom: (roomId: string, businessId: string, socketId: string) => void;
|
||||
onLeaveRoom: (type: "self" | "kick" | "end", roomId: string, businessId: string, socketId: string) => Promise<void>;
|
||||
onLeavedRoom: (type: "self" | "kick" | "end", roomId: string, businessId: string, socketId: string) => Promise<void>;
|
||||
onHandleOperation: (type: "pause" | "resume" | "kick" | "end", roomId: string, fromBusinessId: string, toBusinessId?: string, kind?: MediaKind) => Promise<boolean>;
|
||||
onMessageSend: (roomId: string, fromBusinessId: string, message: string | Buffer) => Promise<void>;
|
||||
onDeleteRoom: (roomId: string) => void;
|
||||
constructor(io: any, config: MeetingConfig);
|
||||
start(): Promise<void>;
|
||||
private getPeerFromProducerId;
|
||||
private getPeerFromProducerDataId;
|
||||
private getPeerFromProducerIdAndRoomId;
|
||||
private getPeerFromBusinessId;
|
||||
private getPeerFromBusinessIdAndRoomId;
|
||||
private getWorker;
|
||||
private createRoom;
|
||||
private createRoomTransport;
|
||||
private addConsumer;
|
||||
private addConsumerData;
|
||||
private addProducer;
|
||||
private addProducerData;
|
||||
private addTransport;
|
||||
private addTransportData;
|
||||
private getProducer;
|
||||
private getTransport;
|
||||
private getTransportData;
|
||||
private createConsumer;
|
||||
private createConsumerData;
|
||||
private leaveRoom;
|
||||
private getScreenProducers;
|
||||
}
|
||||
|
||||
export { }
|
1
code/server/common/meeting/npm/index.js
Normal file
1
code/server/common/meeting/npm/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { MeetingServer } from "./src/server";
|
21
code/server/common/meeting/npm/package.json
Normal file
21
code/server/common/meeting/npm/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "tlmeetingserver",
|
||||
"version": "0.0.2",
|
||||
"description": "A simple video meeting library based on node.js and typescript",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Teamlinker/TLMeetingServer.git"
|
||||
},
|
||||
"keywords": ["teamlinker","meeting","video","nodejs","typescript","meeting server"],
|
||||
"author": "teamlinker",
|
||||
"license": "ISC",
|
||||
"types": "./TLMeetingServer.d.ts",
|
||||
"dependencies": {
|
||||
"mediasoup": "^3.14.1",
|
||||
"socket.io": "^4.7.2"
|
||||
}
|
||||
}
|
941
code/server/common/meeting/npm/src/server.js
Normal file
941
code/server/common/meeting/npm/src/server.js
Normal file
@ -0,0 +1,941 @@
|
||||
import * as mediaSoup from "mediasoup";
|
||||
import * as os from "os";
|
||||
import { Producer } from "mediasoup/node/lib/Producer";
|
||||
export class MeetingServer {
|
||||
io;
|
||||
config;
|
||||
roomMap = new Map();
|
||||
workList = new Array(os.cpus().length);
|
||||
peerInfoMap = new Map();
|
||||
workerIndex = 0;
|
||||
onJoinRoom;
|
||||
onJoinedRoom;
|
||||
onLeaveRoom;
|
||||
onLeavedRoom;
|
||||
onHandleOperation;
|
||||
onMessageSend;
|
||||
onDeleteRoom;
|
||||
constructor(io, config) {
|
||||
this.io = io;
|
||||
this.config = config;
|
||||
this.io.addListener("connection", (socket) => {
|
||||
socket.on("joinRoom", async (roomId, extraData, callback) => {
|
||||
try {
|
||||
let roomName = "", businessId = "";
|
||||
if (this.onJoinRoom) {
|
||||
let ret = await this.onJoinRoom(roomId, extraData, socket.data, socket.id);
|
||||
if (ret.error) {
|
||||
callback(null, ret.error);
|
||||
return;
|
||||
}
|
||||
else if (!ret.businessId) {
|
||||
callback(null, "businessId can't be empty");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
roomName = ret.roomName;
|
||||
businessId = ret.businessId;
|
||||
}
|
||||
}
|
||||
let obj = this.getPeerFromBusinessIdAndRoomId(roomId, businessId);
|
||||
if (obj) {
|
||||
if (this.onLeaveRoom) {
|
||||
await this.onLeaveRoom("kick", obj.roomId, obj.businessId, obj.socketId);
|
||||
}
|
||||
this.leaveRoom(obj.socketId);
|
||||
if (this.onLeavedRoom) {
|
||||
await this.onLeavedRoom("kick", obj.roomId, obj.businessId, obj.socketId);
|
||||
}
|
||||
this.io.in(obj.socketId).emit("kick");
|
||||
}
|
||||
await this.createRoom(roomId, roomName, socket.id, businessId);
|
||||
callback({
|
||||
roomId,
|
||||
roomName
|
||||
});
|
||||
if (this.onJoinedRoom) {
|
||||
this.onJoinedRoom(roomId, businessId, socket.id);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("leaveRoom", async (callback) => {
|
||||
try {
|
||||
let obj = this.peerInfoMap.get(socket.id);
|
||||
if (!obj) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (this.onLeaveRoom) {
|
||||
await this.onLeaveRoom("self", obj.roomId, obj.businessId, socket.id);
|
||||
}
|
||||
this.leaveRoom(socket.id);
|
||||
callback();
|
||||
if (this.onLeavedRoom) {
|
||||
this.onLeavedRoom("self", obj.roomId, obj.businessId, socket.id);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("getRouterRtpCapabilities", (callback) => {
|
||||
try {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
if (!objPeer) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
let obj = this.roomMap.get(objPeer.roomId);
|
||||
callback(obj.router.rtpCapabilities);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("createProducerTransport", async (callback) => {
|
||||
try {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
if (!objPeer) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
const { transport, params } = await this.createRoomTransport(objPeer.roomId);
|
||||
this.addTransport(transport, objPeer.roomId, socket.id, false);
|
||||
callback(params);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("createDataProducerTransport", async (callback) => {
|
||||
try {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
if (!objPeer) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
const { transport, params } = await this.createRoomTransport(objPeer.roomId, true);
|
||||
this.addTransportData(transport, objPeer.roomId, socket.id, false);
|
||||
callback(params);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("createConsumerTransport", async (callback) => {
|
||||
try {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
if (!objPeer) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
const { transport, params } = await this.createRoomTransport(objPeer.roomId);
|
||||
this.addTransport(transport, objPeer.roomId, socket.id, true);
|
||||
callback(params);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("createDataConsumerTransport", async (callback) => {
|
||||
try {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
if (!objPeer) {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
const { transport, params } = await this.createRoomTransport(objPeer.roomId, true);
|
||||
this.addTransportData(transport, objPeer.roomId, socket.id, true);
|
||||
callback(params);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on('connectProducerTransport', async (data, callback) => {
|
||||
try {
|
||||
console.log("Connecting Producer Transport");
|
||||
let obj = await this.getTransport(socket.id, false);
|
||||
if (!obj) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
await obj.connect({ dtlsParameters: data.dtlsParameters });
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
socket.on('connectDataProducerTransport', async (data, callback) => {
|
||||
try {
|
||||
console.log("Connecting Data Producer Transport");
|
||||
let obj = await this.getTransportData(socket.id, false);
|
||||
if (!obj) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
await obj.connect({ dtlsParameters: data.dtlsParameters });
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
socket.on('connectConsumerTransport', async (data, callback) => {
|
||||
try {
|
||||
console.log("Connecting Consumer Transport");
|
||||
const consumerTransport = await this.getTransport(socket.id, true);
|
||||
if (!consumerTransport) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
await consumerTransport.connect({ dtlsParameters: data.dtlsParameters });
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
socket.on('connectDataConsumerTransport', async (data, callback) => {
|
||||
try {
|
||||
console.log("Connecting Consumer Transport");
|
||||
const consumerTransport = await this.getTransportData(socket.id, true);
|
||||
if (!consumerTransport) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
await consumerTransport.connect({ dtlsParameters: data.dtlsParameters });
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
socket.on('produce', async (data, callback) => {
|
||||
try {
|
||||
const { kind, rtpParameters, appData } = data;
|
||||
console.log("Starting the producer");
|
||||
let producer = await this.getTransport(socket.id, false).produce({
|
||||
kind, rtpParameters, appData,
|
||||
...(appData.paused !== null && {
|
||||
paused: appData.paused
|
||||
})
|
||||
});
|
||||
producer.on('transportclose', () => {
|
||||
console.log('transport for this producer closed ');
|
||||
producer.close();
|
||||
});
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
let peerList = this.roomMap.get(objPeer.roomId).peerList;
|
||||
callback({ id: producer.id, producersExist: peerList.size > 1 ? true : false });
|
||||
this.addProducer(producer, socket.id);
|
||||
if (kind === "audio") {
|
||||
let audioLevelObserver = this.roomMap.get(objPeer.roomId).audioLevelObserver;
|
||||
await audioLevelObserver.addProducer({
|
||||
producerId: producer.id
|
||||
});
|
||||
}
|
||||
io.to(objPeer.roomId).except(socket.id).emit('newProducer', producer.id, kind, objPeer.businessId, appData?.screen ? "screen" : "camera");
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on('produceData', async (callback) => {
|
||||
try {
|
||||
console.log("Starting the producer");
|
||||
let producer = await this.getTransportData(socket.id, false).produceData({ sctpStreamParameters: {
|
||||
streamId: 0,
|
||||
ordered: true
|
||||
}
|
||||
});
|
||||
producer.on('transportclose', () => {
|
||||
console.log('transport for this producer closed ');
|
||||
producer.close();
|
||||
});
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
let peerList = this.roomMap.get(objPeer.roomId).peerList;
|
||||
callback({ id: producer.id, producersExist: peerList.size > 1 ? true : false });
|
||||
this.addProducerData(producer, socket.id);
|
||||
io.to(objPeer.roomId).except(socket.id).emit('newProducer', producer.id, null, objPeer.businessId, "data");
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on('consume', async (data, callback) => {
|
||||
try {
|
||||
console.log("Consume call on the server side, data is below");
|
||||
let obj = await this.createConsumer(data.rtpCapabilities, data.remoteProducerId, data.transportId, socket.id);
|
||||
let objPeer = this.getPeerFromProducerId(socket.id, data.remoteProducerId);
|
||||
callback(Object.assign({}, obj, {
|
||||
businessId: objPeer.businessId
|
||||
}));
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on('consumeData', async (data, callback) => {
|
||||
try {
|
||||
console.log("Consume call on the server side, data is below");
|
||||
let obj = await this.createConsumerData(data.remoteProducerId, data.transportId, socket.id);
|
||||
let objPeer = this.getPeerFromProducerDataId(socket.id, data.remoteProducerId);
|
||||
callback(Object.assign({}, obj, {
|
||||
businessId: objPeer.businessId
|
||||
}));
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on('resume', async (consumerId, callback) => {
|
||||
try {
|
||||
let obj = this.peerInfoMap.get(socket.id);
|
||||
for (let consumer of obj.receive.consumer) {
|
||||
if (consumer.id === consumerId) {
|
||||
await consumer.resume();
|
||||
break;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on('getProducers', callback => {
|
||||
try {
|
||||
let producerList = [];
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
if (!objPeer) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
let objRoom = this.roomMap.get(objPeer.roomId);
|
||||
for (let socketId of objRoom.peerList) {
|
||||
if (socketId != socket.id) {
|
||||
let objPeer = this.peerInfoMap.get(socketId);
|
||||
producerList = [...producerList, ...objPeer.send.producer.map(item => ({
|
||||
id: item.id,
|
||||
type: item.appData?.screen ? "screen" : "camera"
|
||||
}))];
|
||||
}
|
||||
}
|
||||
callback(producerList);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("pauseSelf", async (kind, callback) => {
|
||||
try {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
for (let producer of objPeer.send.producer) {
|
||||
if (producer.kind === kind) {
|
||||
await producer.pause();
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("resumeSelf", async (kind, callback) => {
|
||||
try {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
for (let producer of objPeer.send.producer) {
|
||||
if (producer.kind === kind) {
|
||||
await producer.resume();
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("pauseOther", async (kind, businessId, callback) => {
|
||||
try {
|
||||
if (this.onHandleOperation) {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
let ret = await this.onHandleOperation("pause", objPeer.roomId, objPeer.businessId, businessId, kind);
|
||||
if (!ret) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let obj = this.getPeerFromBusinessId(socket.id, businessId);
|
||||
if (obj) {
|
||||
for (let producer of obj.send.producer) {
|
||||
if (producer.kind === kind) {
|
||||
await producer.pause();
|
||||
}
|
||||
}
|
||||
callback(true);
|
||||
}
|
||||
else {
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("resumeOther", async (kind, businessId, callback) => {
|
||||
try {
|
||||
if (this.onHandleOperation) {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
let ret = await this.onHandleOperation("resume", objPeer.roomId, objPeer.businessId, businessId, kind);
|
||||
if (!ret) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let obj = this.getPeerFromBusinessId(socket.id, businessId);
|
||||
if (obj) {
|
||||
for (let producer of obj.send.producer) {
|
||||
if (producer.kind === kind) {
|
||||
await producer.resume();
|
||||
}
|
||||
}
|
||||
callback(true);
|
||||
}
|
||||
else {
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("kick", async (businessId, callback) => {
|
||||
try {
|
||||
if (this.onHandleOperation) {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
let ret = await this.onHandleOperation("kick", objPeer.roomId, objPeer.businessId, businessId, null);
|
||||
if (!ret) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let obj = this.getPeerFromBusinessId(socket.id, businessId);
|
||||
if (obj) {
|
||||
if (this.onLeaveRoom) {
|
||||
await this.onLeaveRoom("kick", obj.roomId, obj.businessId, obj.socketId);
|
||||
}
|
||||
this.leaveRoom(obj.socketId);
|
||||
callback(true);
|
||||
if (this.onLeavedRoom) {
|
||||
this.onLeavedRoom("kick", obj.roomId, obj.businessId, obj.socketId);
|
||||
}
|
||||
this.io.in(obj.socketId).emit("kick");
|
||||
}
|
||||
else {
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("end", async (callback) => {
|
||||
try {
|
||||
if (this.onHandleOperation) {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
let ret = await this.onHandleOperation("end", objPeer.roomId, objPeer.businessId, null, null);
|
||||
if (!ret) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
if (objPeer) {
|
||||
let objRoom = this.roomMap.get(objPeer.roomId);
|
||||
for (let id of objRoom.peerList) {
|
||||
let obj = this.peerInfoMap.get(id);
|
||||
if (obj) {
|
||||
if (this.onLeaveRoom) {
|
||||
await this.onLeaveRoom("end", obj.roomId, obj.businessId, obj.socketId);
|
||||
}
|
||||
this.leaveRoom(obj.socketId);
|
||||
if (this.onLeavedRoom) {
|
||||
this.onLeavedRoom("end", obj.roomId, obj.businessId, obj.socketId);
|
||||
}
|
||||
this.io.in(obj.socketId).emit("kick");
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(true);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("states", callback => {
|
||||
try {
|
||||
let objPeer = this.peerInfoMap.get(socket.id);
|
||||
if (objPeer) {
|
||||
let objRoom = this.roomMap.get(objPeer.roomId);
|
||||
if (objRoom) {
|
||||
let arr = [];
|
||||
for (let socketId of objRoom.peerList) {
|
||||
let obj = this.peerInfoMap.get(socketId);
|
||||
if (obj) {
|
||||
let kinds = {};
|
||||
obj.send.producer.forEach(item => {
|
||||
if (item instanceof Producer && !item.appData.screen)
|
||||
kinds[item.kind] = !item.paused;
|
||||
});
|
||||
arr = [...arr, {
|
||||
businessId: obj.businessId,
|
||||
kinds: kinds
|
||||
}];
|
||||
}
|
||||
}
|
||||
callback(arr);
|
||||
}
|
||||
else {
|
||||
callback([]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
callback([]);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("messageSend", async (message, callback) => {
|
||||
try {
|
||||
let obj = this.peerInfoMap.get(socket.id);
|
||||
await this.onMessageSend?.(obj.roomId, obj.businessId, message);
|
||||
callback(true);
|
||||
this.io.in(obj.roomId).emit("messageReceive", message, obj.businessId);
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
socket.on("getScreenProducers", callback => {
|
||||
try {
|
||||
let obj = this.peerInfoMap.get(socket.id);
|
||||
let ret = this.getScreenProducers(obj.roomId);
|
||||
callback(ret ? {
|
||||
video: ret.video?.id,
|
||||
audio: ret.audio?.id
|
||||
} : null);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
socket.on("stopScreen", async () => {
|
||||
let obj = this.peerInfoMap.get(socket.id);
|
||||
for (let i = 0; i < obj?.send.producer.length; i++) {
|
||||
let producer = obj.send.producer[i];
|
||||
if (producer.appData?.screen) {
|
||||
producer.close();
|
||||
obj.send.producer.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
});
|
||||
socket.addListener("disconnect", async (reason) => {
|
||||
let obj = this.peerInfoMap.get(socket.id);
|
||||
if (obj && this.onLeaveRoom) {
|
||||
await this.onLeaveRoom("self", obj.roomId, obj.businessId, socket.id);
|
||||
}
|
||||
this.leaveRoom(socket.id);
|
||||
if (obj && this.onLeavedRoom) {
|
||||
this.onLeavedRoom("self", obj.roomId, obj.businessId, socket.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
async start() {
|
||||
for (let i = 0; i < this.workList.length; i++) {
|
||||
let worker = await mediaSoup.createWorker(this.config.worker);
|
||||
worker.on("died", args => {
|
||||
console.log(args);
|
||||
});
|
||||
this.workList[i] = worker;
|
||||
}
|
||||
}
|
||||
getPeerFromProducerId(socketId, producerId, isAll = false) {
|
||||
let objPeer = this.peerInfoMap.get(socketId);
|
||||
let objRoom = this.roomMap.get(objPeer.roomId);
|
||||
for (let id of objRoom.peerList) {
|
||||
if (isAll || id !== socketId) {
|
||||
let obj = this.peerInfoMap.get(id);
|
||||
for (let produce of obj.send.producer) {
|
||||
if (produce.id === producerId) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getPeerFromProducerDataId(socketId, producerId, isAll = false) {
|
||||
let objPeer = this.peerInfoMap.get(socketId);
|
||||
let objRoom = this.roomMap.get(objPeer.roomId);
|
||||
for (let id of objRoom.peerList) {
|
||||
if (isAll || id !== socketId) {
|
||||
let obj = this.peerInfoMap.get(id);
|
||||
if (obj.send.producerData.id === producerId) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getPeerFromProducerIdAndRoomId(roomId, producerId) {
|
||||
let objRoom = this.roomMap.get(roomId);
|
||||
for (let id of objRoom.peerList) {
|
||||
let obj = this.peerInfoMap.get(id);
|
||||
for (let produce of obj.send.producer) {
|
||||
if (produce.id === producerId) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getPeerFromBusinessId(socketId, businessId, isAll = false) {
|
||||
let objPeer = this.peerInfoMap.get(socketId);
|
||||
let objRoom = this.roomMap.get(objPeer.roomId);
|
||||
for (let id of objRoom.peerList) {
|
||||
if (isAll || id !== socketId) {
|
||||
let obj = this.peerInfoMap.get(id);
|
||||
if (obj.businessId === businessId) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getPeerFromBusinessIdAndRoomId(roomId, businessId) {
|
||||
let objRoom = this.roomMap.get(roomId);
|
||||
if (objRoom) {
|
||||
for (let id of objRoom.peerList) {
|
||||
let obj = this.peerInfoMap.get(id);
|
||||
if (obj.businessId === businessId) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getWorker() {
|
||||
let obj = this.workList[this.workerIndex];
|
||||
this.workerIndex++;
|
||||
if (this.workerIndex >= this.workList.length) {
|
||||
this.workerIndex = 0;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
async createRoom(roomId, roomName, socketId, businessId) {
|
||||
let objRoom = this.roomMap.get(roomId);
|
||||
if (!objRoom) {
|
||||
let worker = this.getWorker();
|
||||
let mediaSoupRouter = await worker.createRouter({
|
||||
mediaCodecs: this.config.codecs
|
||||
});
|
||||
let audioLevelObserver = await mediaSoupRouter.createAudioLevelObserver({
|
||||
threshold: -55
|
||||
});
|
||||
audioLevelObserver.on("volumes", args => {
|
||||
let produce = args[0]?.producer;
|
||||
if (produce) {
|
||||
let objPeer = this.getPeerFromProducerIdAndRoomId(roomId, produce.id);
|
||||
if (objPeer) {
|
||||
let objRoom = this.roomMap.get(roomId);
|
||||
for (let socketId of objRoom.peerList) {
|
||||
this.io.in(socketId).emit("speaker", objPeer.businessId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.roomMap.set(roomId, {
|
||||
roomId,
|
||||
peerList: new Set([socketId]),
|
||||
roomName,
|
||||
router: mediaSoupRouter,
|
||||
audioLevelObserver: audioLevelObserver
|
||||
});
|
||||
}
|
||||
else {
|
||||
objRoom.peerList.add(socketId);
|
||||
}
|
||||
this.peerInfoMap.set(socketId, {
|
||||
businessId,
|
||||
roomId: roomId,
|
||||
socketId: socketId,
|
||||
send: {
|
||||
transport: null,
|
||||
producer: [],
|
||||
transportData: null,
|
||||
producerData: null
|
||||
},
|
||||
receive: {
|
||||
transport: null,
|
||||
consumer: [],
|
||||
transportData: null,
|
||||
consumerData: []
|
||||
}
|
||||
});
|
||||
}
|
||||
async createRoomTransport(roomId, enableSctp = false) {
|
||||
let obj = this.roomMap.get(roomId);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
const transport = await obj.router.createWebRtcTransport({
|
||||
...this.config.webRtcTransport,
|
||||
enableSctp
|
||||
});
|
||||
transport.on("dtlsstatechange", state => {
|
||||
if (state == "closed") {
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
return {
|
||||
transport,
|
||||
params: {
|
||||
id: transport.id,
|
||||
iceParameters: transport.iceParameters,
|
||||
iceCandidates: transport.iceCandidates,
|
||||
dtlsParameters: transport.dtlsParameters,
|
||||
sctpParameters: transport.sctpParameters,
|
||||
},
|
||||
};
|
||||
}
|
||||
addConsumer(consumer, socketId) {
|
||||
let obj = this.peerInfoMap.get(socketId);
|
||||
if (obj) {
|
||||
obj.receive.consumer.push(consumer);
|
||||
}
|
||||
}
|
||||
addConsumerData(consumer, socketId) {
|
||||
let obj = this.peerInfoMap.get(socketId);
|
||||
if (obj) {
|
||||
obj.receive.consumerData.push(consumer);
|
||||
}
|
||||
}
|
||||
addProducer(producer, socketId) {
|
||||
let obj = this.peerInfoMap.get(socketId);
|
||||
if (obj) {
|
||||
obj.send.producer.push(producer);
|
||||
}
|
||||
}
|
||||
addProducerData(producer, socketId) {
|
||||
let obj = this.peerInfoMap.get(socketId);
|
||||
if (obj) {
|
||||
if (obj.send.producerData) {
|
||||
obj.send.producerData.close();
|
||||
}
|
||||
obj.send.producerData = producer;
|
||||
}
|
||||
}
|
||||
addTransport(transport, roomId, socketId, isConsumer) {
|
||||
let obj = this.peerInfoMap.get(socketId);
|
||||
if (obj) {
|
||||
if (isConsumer) {
|
||||
obj.receive.transport = transport;
|
||||
}
|
||||
else {
|
||||
obj.send.transport = transport;
|
||||
}
|
||||
}
|
||||
}
|
||||
addTransportData(transport, roomId, socketId, isConsumer) {
|
||||
let obj = this.peerInfoMap.get(socketId);
|
||||
if (obj) {
|
||||
if (isConsumer) {
|
||||
obj.receive.transportData = transport;
|
||||
}
|
||||
else {
|
||||
obj.send.transportData = transport;
|
||||
}
|
||||
}
|
||||
}
|
||||
getProducer(socketId, type) {
|
||||
const [producerTransport] = this.peerInfoMap.get(socketId).send.producer.filter(item => {
|
||||
if (item instanceof Producer) {
|
||||
return item.kind === type;
|
||||
}
|
||||
});
|
||||
return producerTransport;
|
||||
}
|
||||
getTransport(socketId, isConsumer) {
|
||||
const producerTransport = this.peerInfoMap.get(socketId);
|
||||
return isConsumer ? producerTransport.receive.transport : producerTransport.send.transport;
|
||||
}
|
||||
getTransportData(socketId, isConsumer) {
|
||||
const producerTransport = this.peerInfoMap.get(socketId);
|
||||
return isConsumer ? producerTransport.receive.transportData : producerTransport.send.transportData;
|
||||
}
|
||||
async createConsumer(rtpCapabilities, remoteProducerId, serverConsumerTransportId, socketId) {
|
||||
const objTransport = this.peerInfoMap.get(socketId);
|
||||
const roomId = objTransport.roomId;
|
||||
const router = this.roomMap.get(roomId).router;
|
||||
console.log("Creating consumer for remote producerId = " + remoteProducerId);
|
||||
const consumerTransport = objTransport.receive.transport;
|
||||
if (!router.canConsume({
|
||||
producerId: remoteProducerId,
|
||||
rtpCapabilities,
|
||||
})) {
|
||||
console.error('can not consume');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let objPeer = this.getPeerFromProducerId(socketId, remoteProducerId);
|
||||
let producer = objPeer.send.producer.find(item => item.id === remoteProducerId);
|
||||
let consumer = await consumerTransport.consume({
|
||||
producerId: remoteProducerId,
|
||||
rtpCapabilities,
|
||||
paused: true,
|
||||
...(producer.appData && {
|
||||
appData: Object.assign({}, producer.appData)
|
||||
})
|
||||
});
|
||||
consumer.on('transportclose', () => {
|
||||
console.log('transport close from consumer');
|
||||
});
|
||||
consumer.on('producerclose', async () => {
|
||||
console.log('producer of consumer closed');
|
||||
this.io.in(roomId).emit('producerClosed', remoteProducerId, consumer.kind, objPeer.businessId, consumer.appData?.screen ? "screen" : "camera");
|
||||
objTransport.receive.consumer = objTransport.receive.consumer.filter(consumer => consumer.id !== consumer.id);
|
||||
consumer.close();
|
||||
if (consumer.kind === "audio") {
|
||||
let objRoom = this.roomMap.get(objPeer.roomId);
|
||||
try {
|
||||
await objRoom.audioLevelObserver.removeProducer({
|
||||
producerId: consumer.producerId
|
||||
});
|
||||
}
|
||||
catch {
|
||||
console.log(`${consumer.producerId} not found`);
|
||||
}
|
||||
}
|
||||
});
|
||||
consumer.on("producerpause", () => {
|
||||
this.io.in(roomId).emit('producerPause', remoteProducerId, consumer.kind, objPeer.businessId);
|
||||
});
|
||||
consumer.on("producerresume", () => {
|
||||
this.io.in(roomId).emit('producerResume', remoteProducerId, consumer.kind, objPeer.businessId);
|
||||
});
|
||||
this.addConsumer(consumer, socketId);
|
||||
if (consumer.type === 'simulcast') {
|
||||
await consumer.setPreferredLayers({ spatialLayer: 2, temporalLayer: 2 });
|
||||
}
|
||||
return {
|
||||
producerId: remoteProducerId,
|
||||
id: consumer.id,
|
||||
kind: consumer.kind,
|
||||
rtpParameters: consumer.rtpParameters,
|
||||
type: consumer.appData?.screen ? "screen" : "camera",
|
||||
producerPaused: consumer.producerPaused
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
console.error('consume failed', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
async createConsumerData(remoteProducerId, serverConsumerTransportId, socketId) {
|
||||
const objTransport = this.peerInfoMap.get(socketId);
|
||||
const roomId = objTransport.roomId;
|
||||
const router = this.roomMap.get(roomId).router;
|
||||
console.log("Creating consumer for remote producerId = " + remoteProducerId);
|
||||
const consumerTransport = objTransport.receive.transportData;
|
||||
try {
|
||||
console.log(`producerId:${remoteProducerId}`);
|
||||
let consumer = await consumerTransport.consumeData({
|
||||
dataProducerId: remoteProducerId,
|
||||
ordered: true
|
||||
});
|
||||
let objPeer = this.getPeerFromProducerDataId(socketId, remoteProducerId);
|
||||
consumer.on('transportclose', () => {
|
||||
console.log('transport close from consumer');
|
||||
});
|
||||
consumer.on('dataproducerclose', async () => {
|
||||
console.log('producer of consumer closed');
|
||||
this.io.in(roomId).emit('producerClosed', remoteProducerId, null, objPeer.businessId, "data");
|
||||
objTransport.receive.consumerData = objTransport.receive.consumerData.filter(consumer => consumer.id !== consumer.id);
|
||||
consumer.close();
|
||||
});
|
||||
this.addConsumerData(consumer, socketId);
|
||||
return {
|
||||
producerId: remoteProducerId,
|
||||
id: consumer.id,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
console.error('consume failed', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
leaveRoom(socketId) {
|
||||
let obj = this.peerInfoMap.get(socketId);
|
||||
if (obj) {
|
||||
if (obj.send.transport) {
|
||||
obj.send.transport.close();
|
||||
}
|
||||
if (obj.send.transportData) {
|
||||
obj.send.transportData.close();
|
||||
}
|
||||
obj.send.producer.forEach(item => item.close());
|
||||
obj.send.producerData?.close();
|
||||
if (obj.receive.transport) {
|
||||
obj.receive.transport.close();
|
||||
}
|
||||
if (obj.receive.transportData) {
|
||||
obj.receive.transportData.close();
|
||||
}
|
||||
obj.receive.consumer.forEach(item => item.close());
|
||||
obj.receive.consumerData.forEach(item => item.close());
|
||||
this.peerInfoMap.delete(socketId);
|
||||
let objRoom = this.roomMap.get(obj.roomId);
|
||||
if (objRoom) {
|
||||
objRoom.peerList.delete(socketId);
|
||||
if (objRoom.peerList.size == 0) {
|
||||
objRoom.router.close();
|
||||
objRoom.audioLevelObserver.close();
|
||||
this.roomMap.delete(obj.roomId);
|
||||
this.onDeleteRoom?.(obj.roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getScreenProducers(roomId) {
|
||||
let objRoom = this.roomMap.get(roomId);
|
||||
if (objRoom) {
|
||||
let video, audio;
|
||||
for (let id of objRoom.peerList) {
|
||||
let objPeer = this.peerInfoMap.get(id);
|
||||
if (objPeer) {
|
||||
for (let producer of objPeer.send.producer) {
|
||||
if (producer.appData?.screen) {
|
||||
if (producer.kind === "video") {
|
||||
video = producer;
|
||||
}
|
||||
else if (producer.kind === "audio") {
|
||||
audio = producer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (video || audio) {
|
||||
return {
|
||||
video,
|
||||
audio
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
code/server/common/meeting/npm/src/type.js
Normal file
1
code/server/common/meeting/npm/src/type.js
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
9
code/server/common/meeting/package.json
Normal file
9
code/server/common/meeting/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "meeting-server",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vue-tsc && vite build && rm -f ./dist/*js && tsc --noEmit false && cp -rf ./dist/* ./npm"
|
||||
}
|
||||
}
|
26
code/server/common/meeting/tsconfig.json
Normal file
26
code/server/common/meeting/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noImplicitUseStrict":true,
|
||||
"sourceMap": false,
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitThis": true,
|
||||
//"ignoreDeprecations": "5.0",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/*.ts","index.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
code/server/common/meeting/tsconfig.node.json
Normal file
10
code/server/common/meeting/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
19
code/server/common/meeting/vite.config.ts
Normal file
19
code/server/common/meeting/vite.config.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// @ts-ignore
|
||||
import {defineConfig} from 'vite'
|
||||
import path from "path";
|
||||
// @ts-ignore
|
||||
import dts from "vite-plugin-dts"
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [dts({
|
||||
rollupTypes:true
|
||||
})],
|
||||
build: {
|
||||
outDir: "dist", //输出文件名称
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "./index.ts"), //指定组件编译入口文件
|
||||
name: "TLMeetingServer",
|
||||
fileName: "TLMeetingServer",
|
||||
}
|
||||
},
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
import {getSocketEmitterInstance, getSocketIOInstance, SocketIO} from "../../common/socket/socket";
|
||||
import {ECommon_Socket_Type} from "../../../common/socket/types";
|
||||
import {MeetingServer} from "../../common/meeting/server";
|
||||
import {MeetingServer} from "../../common/meeting/src/server";
|
||||
import {MeetingRoomService} from "../service/room";
|
||||
import {ECommon_Meeting_Room_Permission} from "../../../common/model/meeting_room";
|
||||
import rpcUserApi from "../../user/rpc/user";
|
||||
@ -11,7 +11,6 @@ import {ECommon_Model_Content_Type} from "../../../common/model/content";
|
||||
import {handleImageFields} from "../../gateway/util/util";
|
||||
import {ECommon_Model_Organization_Member_Type} from "../../../common/model/organization";
|
||||
import {MeetingMissCallService} from "../service/missCall";
|
||||
import {REDIS_ORGANIZATION} from "../../common/cache/keys/organization";
|
||||
|
||||
|
||||
export async function handleMeetingConnection() {
|
||||
|
@ -69,6 +69,9 @@
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-node": "^10.9.1",
|
||||
"ttypescript": "^1.5.15",
|
||||
"typescript": "4.6.4"
|
||||
"typescript": "4.6.4",
|
||||
"vue-tsc": "^2.0.13",
|
||||
"vite-plugin-dts": "^3.9.1",
|
||||
"vite": "^5.2.9"
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "code",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": ["code/*","code/client/src/business/component/calendar"]
|
||||
"packages": ["code/*"]
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
|
@ -279,6 +279,15 @@ importers:
|
||||
typescript:
|
||||
specifier: 4.6.4
|
||||
version: 4.6.4
|
||||
vite:
|
||||
specifier: ^5.2.9
|
||||
version: 5.2.9(@types/node@18.19.31)
|
||||
vite-plugin-dts:
|
||||
specifier: ^3.9.1
|
||||
version: 3.9.1(@types/node@18.19.31)(rollup@4.14.3)(typescript@4.6.4)(vite@5.2.9(@types/node@18.19.31))
|
||||
vue-tsc:
|
||||
specifier: ^2.0.13
|
||||
version: 2.0.13(typescript@4.6.4)
|
||||
|
||||
packages:
|
||||
|
||||
@ -3757,6 +3766,20 @@ snapshots:
|
||||
|
||||
'@vue/devtools-api@6.6.1': {}
|
||||
|
||||
'@vue/language-core@1.8.27(typescript@4.6.4)':
|
||||
dependencies:
|
||||
'@volar/language-core': 1.11.1
|
||||
'@volar/source-map': 1.11.1
|
||||
'@vue/compiler-dom': 3.4.23
|
||||
'@vue/shared': 3.4.23
|
||||
computeds: 0.0.1
|
||||
minimatch: 9.0.4
|
||||
muggle-string: 0.3.1
|
||||
path-browserify: 1.0.1
|
||||
vue-template-compiler: 2.7.16
|
||||
optionalDependencies:
|
||||
typescript: 4.6.4
|
||||
|
||||
'@vue/language-core@1.8.27(typescript@4.9.5)':
|
||||
dependencies:
|
||||
'@volar/language-core': 1.11.1
|
||||
@ -3771,6 +3794,18 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 4.9.5
|
||||
|
||||
'@vue/language-core@2.0.13(typescript@4.6.4)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.2.0-alpha.8
|
||||
'@vue/compiler-dom': 3.4.23
|
||||
'@vue/shared': 3.4.23
|
||||
computeds: 0.0.1
|
||||
minimatch: 9.0.4
|
||||
path-browserify: 1.0.1
|
||||
vue-template-compiler: 2.7.16
|
||||
optionalDependencies:
|
||||
typescript: 4.6.4
|
||||
|
||||
'@vue/language-core@2.0.13(typescript@4.9.5)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.2.0-alpha.8
|
||||
@ -6004,6 +6039,23 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-dts@3.9.1(@types/node@18.19.31)(rollup@4.14.3)(typescript@4.6.4)(vite@5.2.9(@types/node@18.19.31)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.43.0(@types/node@18.19.31)
|
||||
'@rollup/pluginutils': 5.1.0(rollup@4.14.3)
|
||||
'@vue/language-core': 1.8.27(typescript@4.6.4)
|
||||
debug: 4.3.4(supports-color@9.4.0)
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.10
|
||||
typescript: 4.6.4
|
||||
vue-tsc: 1.8.27(typescript@4.6.4)
|
||||
optionalDependencies:
|
||||
vite: 5.2.9(@types/node@18.19.31)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-plugin-dts@3.9.1(@types/node@18.19.31)(rollup@4.14.3)(typescript@4.9.5)(vite@5.2.9(@types/node@18.19.31)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.43.0(@types/node@18.19.31)
|
||||
@ -6185,6 +6237,13 @@ snapshots:
|
||||
de-indent: 1.0.2
|
||||
he: 1.2.0
|
||||
|
||||
vue-tsc@1.8.27(typescript@4.6.4):
|
||||
dependencies:
|
||||
'@volar/typescript': 1.11.1
|
||||
'@vue/language-core': 1.8.27(typescript@4.6.4)
|
||||
semver: 7.6.0
|
||||
typescript: 4.6.4
|
||||
|
||||
vue-tsc@1.8.27(typescript@4.9.5):
|
||||
dependencies:
|
||||
'@volar/typescript': 1.11.1
|
||||
@ -6192,6 +6251,13 @@ snapshots:
|
||||
semver: 7.6.0
|
||||
typescript: 4.9.5
|
||||
|
||||
vue-tsc@2.0.13(typescript@4.6.4):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.2.0-alpha.8
|
||||
'@vue/language-core': 2.0.13(typescript@4.6.4)
|
||||
semver: 7.6.0
|
||||
typescript: 4.6.4
|
||||
|
||||
vue-tsc@2.0.13(typescript@4.9.5):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.2.0-alpha.8
|
||||
|
@ -1,3 +1,2 @@
|
||||
packages:
|
||||
- code/*
|
||||
- code/client/src/business/component/calendar
|
||||
- code/*
|
Loading…
Reference in New Issue
Block a user