This commit is contained in:
parent
a01f8857ab
commit
8e19f55dc0
@ -30,9 +30,14 @@ export const useMediasoup = createGlobalState(() => {
|
||||
let recvTransport: mediasoupClient.types.Transport
|
||||
|
||||
socket.on('producers', async (producers) => {
|
||||
for (const producer of producers) {
|
||||
await consume(producer.producerId)
|
||||
}
|
||||
watch(connected, async () => {
|
||||
if (!connected.value)
|
||||
return
|
||||
|
||||
for (const producer of producers) {
|
||||
await consume(producer.producerId)
|
||||
}
|
||||
}, { immediate: true })
|
||||
})
|
||||
|
||||
socket.on('newProducer', async ({ producerId }) => {
|
||||
@ -81,7 +86,7 @@ export const useMediasoup = createGlobalState(() => {
|
||||
dtlsParameters,
|
||||
})
|
||||
|
||||
callback()
|
||||
// callback()
|
||||
}
|
||||
catch (err) {
|
||||
errback(err)
|
||||
@ -90,12 +95,13 @@ export const useMediasoup = createGlobalState(() => {
|
||||
|
||||
sendTransport.on('produce', async ({ kind, rtpParameters }, callback, errback) => {
|
||||
try {
|
||||
const { id } = await socket.emitWithAck('produce', {
|
||||
const { producerId } = await socket.emitWithAck('produce', {
|
||||
transportId: sendTransport.id,
|
||||
kind,
|
||||
rtpParameters,
|
||||
})
|
||||
callback({ id })
|
||||
|
||||
// callback({ producerId })
|
||||
}
|
||||
catch (err) {
|
||||
errback(err)
|
||||
@ -104,7 +110,18 @@ export const useMediasoup = createGlobalState(() => {
|
||||
}
|
||||
|
||||
async function publishMic() {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||
console.log(devices)
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
// audio: true,
|
||||
audio: {
|
||||
autoGainControl: false,
|
||||
noiseSuppression: true,
|
||||
echoCancellation: false,
|
||||
latency: 0,
|
||||
},
|
||||
})
|
||||
const track = stream.getAudioTracks()[0]
|
||||
|
||||
await sendTransport.produce({ track })
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
"mediasoup-client": "^3.16.7",
|
||||
"nuxt": "^4.1.2",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
@ -23,6 +22,7 @@
|
||||
"@antfu/eslint-config": "^5.4.1",
|
||||
"@tauri-apps/cli": "^2.8.4",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-format": "^1.0.2"
|
||||
"eslint-plugin-format": "^1.0.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ RUN yarn install --frozen-lockfile
|
||||
COPY . .
|
||||
|
||||
ENV PORT=80
|
||||
ENV DEBUG=mediasoup*
|
||||
ENV CORS_ORIGIN=chad.koptilnya.xyz
|
||||
ENV ANNOUNCED_ADDRESS=91.144.171.182
|
||||
|
||||
EXPOSE 80
|
||||
RUN ls -la
|
||||
CMD ["yarn", "node", "index.mjs"]
|
||||
|
||||
CMD ["yarn", "node", "index.ts"]
|
||||
|
||||
10
server/eslint.config.mjs
Normal file
10
server/eslint.config.mjs
Normal file
@ -0,0 +1,10 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default antfu({
|
||||
typescript: {
|
||||
overrides: {
|
||||
'no-console': 'off',
|
||||
'n/prefer-global/process': 'off',
|
||||
},
|
||||
},
|
||||
})
|
||||
164
server/index.mjs
164
server/index.mjs
@ -1,164 +0,0 @@
|
||||
import express from "express";
|
||||
import http from "http";
|
||||
import cors from "cors";
|
||||
import { Server } from "socket.io";
|
||||
import * as mediasoup from "mediasoup";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server, {
|
||||
path: '/chad/ws',
|
||||
cors: {
|
||||
origin: '*'
|
||||
}
|
||||
});
|
||||
|
||||
let worker;
|
||||
let router;
|
||||
const transports = new Map(); // socketId -> [transports]
|
||||
const producers = new Map(); // socketId -> [producers]
|
||||
const consumers = new Map(); // socketId -> [consumers]
|
||||
|
||||
async function createWorker() {
|
||||
worker = await mediasoup.createWorker();
|
||||
worker.on("died", () => {
|
||||
console.error("mediasoup worker died, exiting...");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
router = await worker.createRouter({
|
||||
mediaCodecs: [
|
||||
{
|
||||
kind: "audio",
|
||||
mimeType: "audio/opus",
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
console.log("Mediasoup worker & router created");
|
||||
}
|
||||
|
||||
createWorker();
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log("Client connected:", socket.id);
|
||||
|
||||
socket.emit('producers', Array.from(producers.values()).flatMap(producers => {
|
||||
return producers.map(producer => {
|
||||
return {
|
||||
producerId: producer.id,
|
||||
kind: producer.kind,
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
socket.on("getRtpCapabilities", (cb) => {
|
||||
cb(router.rtpCapabilities);
|
||||
});
|
||||
|
||||
socket.on("createTransport", async (cb) => {
|
||||
try {
|
||||
const transport = await router.createWebRtcTransport({
|
||||
listenInfos: [
|
||||
{
|
||||
protocol: 'udp',
|
||||
ip: "0.0.0.0",
|
||||
announcedIp: "91.144.171.182",
|
||||
portRange: {
|
||||
min: 40000,
|
||||
max: 40100
|
||||
}
|
||||
}
|
||||
],
|
||||
enableUdp: true,
|
||||
preferUdp: true,
|
||||
});
|
||||
|
||||
transports.set(socket.id, [...(transports.get(socket.id) || []), transport]);
|
||||
|
||||
cb({
|
||||
id: transport.id,
|
||||
iceParameters: transport.iceParameters,
|
||||
iceCandidates: transport.iceCandidates,
|
||||
dtlsParameters: transport.dtlsParameters,
|
||||
});
|
||||
|
||||
transport.observer.on("close", () => {
|
||||
console.log("transport closed", transport.id);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("createTransport error:", err);
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("connectTransport", async ({ transportId, dtlsParameters }, cb) => {
|
||||
const transport = transports.get(socket.id)?.find((t) => t.id === transportId);
|
||||
if (!transport) return cb({ error: "transport not found" });
|
||||
await transport.connect({ dtlsParameters });
|
||||
cb({ connected: true });
|
||||
});
|
||||
|
||||
socket.on("produce", async ({ transportId, kind, rtpParameters }, cb) => {
|
||||
const transport = transports.get(socket.id)?.find((t) => t.id === transportId);
|
||||
if (!transport) return cb({ error: "transport not found" });
|
||||
|
||||
const producer = await transport.produce({ kind, rtpParameters });
|
||||
producers.set(socket.id, [...(producers.get(socket.id) || []), producer]);
|
||||
|
||||
cb({ id: producer.id });
|
||||
socket.broadcast.emit("newProducer", { producerId: producer.id, kind: producer.kind });
|
||||
|
||||
producer.observer.on("close", () => {
|
||||
console.log("producer closed", producer.id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("consume", async ({ producerId, transportId, rtpCapabilities }, cb) => {
|
||||
try {
|
||||
if (!router.canConsume({ producerId, rtpCapabilities })) {
|
||||
return cb({ error: "cannot consume" });
|
||||
}
|
||||
|
||||
const transport = transports.get(socket.id)?.find((t) => t.id === transportId);
|
||||
if (!transport) return cb({ error: "transport not found" });
|
||||
|
||||
const consumer = await transport.consume({
|
||||
producerId,
|
||||
rtpCapabilities,
|
||||
paused: false,
|
||||
});
|
||||
|
||||
consumers.set(socket.id, [...(consumers.get(socket.id) || []), consumer]);
|
||||
|
||||
cb({
|
||||
id: consumer.id,
|
||||
producerId,
|
||||
kind: consumer.kind,
|
||||
rtpParameters: consumer.rtpParameters,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("consume error:", err);
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("Client disconnected:", socket.id);
|
||||
|
||||
transports.get(socket.id)?.forEach((t) => t.close());
|
||||
producers.get(socket.id)?.forEach((p) => p.close());
|
||||
consumers.get(socket.id)?.forEach((c) => c.close());
|
||||
|
||||
transports.delete(socket.id);
|
||||
producers.delete(socket.id);
|
||||
consumers.delete(socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(process.env.PORT || 3000);
|
||||
46
server/index.ts
Normal file
46
server/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { createServer as createHttpServer } from 'node:http'
|
||||
import { consola } from 'consola'
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import * as mediasoup from 'mediasoup'
|
||||
import { Server as SocketServer } from 'socket.io'
|
||||
import { webrtcSocket } from './sockets'
|
||||
|
||||
(async () => {
|
||||
const app = express()
|
||||
|
||||
app.use(cors())
|
||||
|
||||
const server = createHttpServer(app)
|
||||
|
||||
const worker = await mediasoup.createWorker()
|
||||
worker.on('died', () => {
|
||||
consola.error('[Mediasoup]', 'Worker died, exiting...')
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
const router = await worker.createRouter({
|
||||
mediaCodecs: [
|
||||
{
|
||||
kind: 'audio',
|
||||
mimeType: 'audio/opus',
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const io = new SocketServer(server, {
|
||||
path: '/chad/ws',
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || '*',
|
||||
},
|
||||
})
|
||||
|
||||
webrtcSocket(io, router)
|
||||
|
||||
server.listen(process.env.PORT || 3000, () => {
|
||||
consola.success('[Server]', 'Server started!')
|
||||
})
|
||||
})()
|
||||
@ -1,13 +1,24 @@
|
||||
{
|
||||
"name": "server",
|
||||
"scripts": {
|
||||
"start": "node index.mjs"
|
||||
"start": "ts-node --transpile-only index.ts"
|
||||
},
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"dependencies": {
|
||||
"consola": "^3.4.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"mediasoup": "^3.19.3",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^5.4.1",
|
||||
"@types/express": "^5.0.3",
|
||||
"eslint": "^9.36.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
5
server/sockets/index.ts
Normal file
5
server/sockets/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import webrtcSocket from './webrtc'
|
||||
|
||||
export {
|
||||
webrtcSocket,
|
||||
}
|
||||
235
server/sockets/webrtc.ts
Normal file
235
server/sockets/webrtc.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import type { types } from 'mediasoup'
|
||||
import type { Namespace, Server as SocketServer } from 'socket.io'
|
||||
import { consola } from 'consola'
|
||||
|
||||
interface ProducerShort {
|
||||
producerId: types.Producer['id']
|
||||
kind: types.MediaKind
|
||||
}
|
||||
|
||||
interface ErrorCallbackResult {
|
||||
error: string
|
||||
}
|
||||
|
||||
interface SuccessCallbackResult {
|
||||
ok: true
|
||||
}
|
||||
|
||||
type EventCallback<T = SuccessCallbackResult> = (result: T | ErrorCallbackResult) => void
|
||||
|
||||
interface ClientToServerEvents extends Record<string, any> {
|
||||
getRtpCapabilities: (
|
||||
cb: EventCallback<types.RtpCapabilities>
|
||||
) => void
|
||||
createTransport: (
|
||||
cb: EventCallback<Pick<types.WebRtcTransport, 'id' | 'iceParameters' | 'iceCandidates' | 'dtlsParameters'>>
|
||||
) => void
|
||||
connectTransport: (
|
||||
options: {
|
||||
transportId: types.WebRtcTransport['id']
|
||||
dtlsParameters: types.WebRtcTransport['dtlsParameters']
|
||||
},
|
||||
cb: EventCallback
|
||||
) => void
|
||||
produce: (
|
||||
options: {
|
||||
transportId: types.WebRtcTransport['id']
|
||||
kind: types.MediaKind
|
||||
rtpParameters: types.RtpParameters
|
||||
},
|
||||
cb: EventCallback<{ producerId: types.Producer['id'] }>
|
||||
) => void
|
||||
consume: (
|
||||
options: {
|
||||
producerId: types.Producer['id']
|
||||
transportId: types.WebRtcTransport['id']
|
||||
rtpCapabilities: types.RtpCapabilities
|
||||
},
|
||||
cb: EventCallback<{
|
||||
consumerId: types.Consumer['id']
|
||||
producerId: types.Producer['id']
|
||||
kind: types.MediaKind
|
||||
rtpParameters: types.RtpParameters
|
||||
}>
|
||||
) => void
|
||||
}
|
||||
|
||||
interface ServerToClientEvents {
|
||||
producers: (arg: ProducerShort[]) => void
|
||||
newProducer: (arg: ProducerShort) => void
|
||||
}
|
||||
|
||||
const transports = new Map<string, types.WebRtcTransport[]>()
|
||||
const producers = new Map<string, types.Producer[]>()
|
||||
const consumers = new Map<string, types.Consumer[]>()
|
||||
|
||||
export default function (io: SocketServer, router: types.Router) {
|
||||
const namespace: Namespace<ClientToServerEvents, ServerToClientEvents> = io.of('/webrtc')
|
||||
|
||||
namespace.on('connection', (socket) => {
|
||||
consola.info('[WebRtc]', 'Client connected', socket.id)
|
||||
|
||||
transports.set(socket.id, [])
|
||||
producers.set(socket.id, [])
|
||||
consumers.set(socket.id, [])
|
||||
|
||||
socket.emit('producers', Array.from(producers.values()).flatMap((producers) => {
|
||||
return producers.map((producer) => {
|
||||
return {
|
||||
producerId: producer.id,
|
||||
kind: producer.kind,
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
socket.on('getRtpCapabilities', (cb) => {
|
||||
cb(router.rtpCapabilities)
|
||||
})
|
||||
|
||||
socket.on('createTransport', async (cb) => {
|
||||
try {
|
||||
const transport = await router.createWebRtcTransport({
|
||||
listenInfos: [
|
||||
{
|
||||
protocol: 'udp',
|
||||
ip: '0.0.0.0',
|
||||
announcedAddress: process.env.ANNOUNCED_ADDRESS || '127.0.0.1',
|
||||
portRange: {
|
||||
min: 40000,
|
||||
max: 40100,
|
||||
},
|
||||
},
|
||||
],
|
||||
enableUdp: true,
|
||||
preferUdp: true,
|
||||
})
|
||||
|
||||
transports.get(socket.id)!.push(transport)
|
||||
|
||||
cb({
|
||||
id: transport.id,
|
||||
iceParameters: transport.iceParameters,
|
||||
iceCandidates: transport.iceCandidates,
|
||||
dtlsParameters: transport.dtlsParameters,
|
||||
})
|
||||
|
||||
transport.observer.on('close', () => {
|
||||
transports.set(socket.id, transports.get(socket.id)!.filter(t => t.id === transport.id))
|
||||
|
||||
consola.info('[WebRtc]', 'Transport closed', transport.id)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
consola.error('[WebRtc]', '[createTransport]', error.message)
|
||||
cb({ error: error.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('connectTransport', async ({ transportId, dtlsParameters }, cb) => {
|
||||
const transport = transports.get(socket.id)!.find(t => t.id === transportId)
|
||||
|
||||
if (!transport) {
|
||||
consola.error('[WebRtc]', '[connectTransport]', 'Transport not found')
|
||||
cb({ error: 'Transport not found' })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await transport.connect({ dtlsParameters })
|
||||
|
||||
cb({ ok: true })
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
consola.error('[WebRtc]', '[connectTransport]', error.message)
|
||||
cb({ error: error.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('produce', async ({ transportId, kind, rtpParameters }, cb) => {
|
||||
const transport = transports.get(socket.id)!.find(t => t.id === transportId)
|
||||
|
||||
if (!transport) {
|
||||
consola.error('[WebRtc]', '[produce]', 'Transport not found')
|
||||
cb({ error: 'Transport not found' })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const producer = await transport.produce({ kind, rtpParameters })
|
||||
producers.get(socket.id)!.push(producer)
|
||||
|
||||
cb({ producerId: producer.id })
|
||||
socket.broadcast.emit('newProducer', { producerId: producer.id, kind: producer.kind })
|
||||
|
||||
producer.observer.on('close', () => {
|
||||
consola.log('[WebRtc]', 'Producer closed', producer.id)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
consola.error('[WebRtc]', '[produce]', error.message)
|
||||
cb({ error: error.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('consume', async ({ producerId, transportId, rtpCapabilities }, cb) => {
|
||||
if (!router.canConsume({ producerId, rtpCapabilities })) {
|
||||
consola.error('[WebRtc]', '[consume]', 'Cannot consume')
|
||||
cb({ error: 'Cannot consume' })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const transport = transports.get(socket.id)?.find(t => t.id === transportId)
|
||||
|
||||
if (!transport) {
|
||||
consola.error('[WebRtc]', '[consume]', 'Transport not found')
|
||||
cb({ error: 'Transport not found' })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const consumer = await transport.consume({
|
||||
producerId,
|
||||
rtpCapabilities,
|
||||
paused: false,
|
||||
})
|
||||
|
||||
consumers.get(socket.id)!.push(consumer)
|
||||
|
||||
cb({
|
||||
consumerId: consumer.id,
|
||||
producerId,
|
||||
kind: consumer.kind,
|
||||
rtpParameters: consumer.rtpParameters,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof Error) {
|
||||
consola.error('[WebRtc]', '[consume]', error.message)
|
||||
cb({ error: error.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
consola.info('Client disconnected:', socket.id)
|
||||
|
||||
transports.get(socket.id)?.forEach(t => t.close())
|
||||
producers.get(socket.id)?.forEach(p => p.close())
|
||||
consumers.get(socket.id)?.forEach(c => c.close())
|
||||
|
||||
transports.delete(socket.id)
|
||||
producers.delete(socket.id)
|
||||
consumers.delete(socket.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
10
server/tsconfig.json
Normal file
10
server/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
3285
server/yarn.lock
3285
server/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user