№4
All checks were successful
Deploy / deploy (push) Successful in 3m59s

This commit is contained in:
Никита Круглицкий 2025-10-02 23:39:59 +06:00
parent a01f8857ab
commit 8e19f55dc0
11 changed files with 3633 additions and 178 deletions

View File

@ -30,9 +30,14 @@ export const useMediasoup = createGlobalState(() => {
let recvTransport: mediasoupClient.types.Transport
socket.on('producers', async (producers) => {
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 })

View File

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

View File

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

@ -0,0 +1,10 @@
import antfu from '@antfu/eslint-config'
export default antfu({
typescript: {
overrides: {
'no-console': 'off',
'n/prefer-global/process': 'off',
},
},
})

View File

@ -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
View 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!')
})
})()

View File

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

@ -0,0 +1,5 @@
import webrtcSocket from './webrtc'
export {
webrtcSocket,
}

235
server/sockets/webrtc.ts Normal file
View 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
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

File diff suppressed because it is too large Load Diff