This commit is contained in:
sx1989827 2024-06-04 16:21:52 +08:00
parent 58e27b0717
commit f7858d1c77
75 changed files with 10176 additions and 51 deletions

1
.gitignore vendored
View File

@ -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

View 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:
![](https://team-linker.com/assets/exampleCalendar2-92024062.png)
![](https://team-linker.com/assets/exampleCalendar1-c0bf9528.png)
## 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.

View 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:
![](https://team-linker.com/assets/exampleCalendar2-92024062.png)
![](https://team-linker.com/assets/exampleCalendar1-c0bf9528.png)
## 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.

View 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 { }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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"
}
}

View File

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

View File

@ -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({

View 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:
![](https://team-linker.com/assets/exampleMeeting1-0dc2e795.png)
## 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.

View File

@ -0,0 +1 @@
export {MeetingClient} from "./src/client";

View 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:
![](https://team-linker.com/assets/exampleMeeting1-0dc2e795.png)
## 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.

View 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 { }

View File

@ -0,0 +1 @@
export { MeetingClient } from "./src/client";

View File

@ -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"
}
}

View File

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

View File

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

View File

@ -0,0 +1 @@
export {};

View File

@ -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"
}
}

View File

@ -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:{

View File

@ -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>

View File

@ -0,0 +1,4 @@
import {createApp} from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View 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" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -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",
},
},
},
},
})

View File

@ -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";

View File

@ -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:
![](https://team-linker.com/assets/exampleWiki1-fade1060.png)
![](https://team-linker.com/assets/exampleIM2-1c2642fb.png)
## 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.

View File

@ -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>

View File

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

View File

@ -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:
![](https://team-linker.com/assets/exampleWiki1-fade1060.png)
![](https://team-linker.com/assets/exampleIM2-1c2642fb.png)
## 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.

View 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

View File

@ -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"
}

View File

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

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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')

View File

@ -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" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -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",
},
},
},
},
})

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -30,7 +30,7 @@ export default defineConfig({
port: 3000,
hmr: true,
open: false, //自动打开
base: "./ ", //生产环境路径
//base: "./ ", //生产环境路径
proxy: { // 本地开发环境通过代理实现跨域,生产环境使用 nginx 转发
// 正则表达式写法
'^/api': {

View 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:
![](https://team-linker.com/assets/exampleMeeting1-0dc2e795.png)
## 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.

View File

@ -0,0 +1 @@
export {MeetingServer} from "./src/server"

View 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:
![](https://team-linker.com/assets/exampleMeeting1-0dc2e795.png)
## 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.

View 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 { }

View File

@ -0,0 +1 @@
export { MeetingServer } from "./src/server";

View 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"
}
}

View 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
};
}
}
}
}

View File

@ -0,0 +1 @@
export {};

View 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"
}
}

View 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" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View 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",
}
},
})

View File

@ -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() {

View File

@ -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"
}
}

View File

@ -2,7 +2,7 @@
"name": "code",
"private": true,
"workspaces": {
"packages": ["code/*","code/client/src/business/component/calendar"]
"packages": ["code/*"]
},
"version": "1.0.0",
"description": "",

View File

@ -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

View File

@ -1,3 +1,2 @@
packages:
- code/*
- code/client/src/business/component/calendar
- code/*