feat: merge audio
parent
6dece81a93
commit
a7dd7869f2
|
@ -132,4 +132,5 @@ dist
|
||||||
|
|
||||||
config.toml
|
config.toml
|
||||||
recordings
|
recordings
|
||||||
.idea
|
.idea
|
||||||
|
prisma/data.db*
|
|
@ -4,7 +4,8 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx src/index.ts"
|
"dev": "tsx src/index.ts",
|
||||||
|
"merge": "tsx src/merge.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -23,10 +24,12 @@
|
||||||
"@discordjs/rest": "^1.7.1",
|
"@discordjs/rest": "^1.7.1",
|
||||||
"@discordjs/voice": "^0.16.0",
|
"@discordjs/voice": "^0.16.0",
|
||||||
"@discordjs/ws": "^0.8.3",
|
"@discordjs/ws": "^0.8.3",
|
||||||
|
"@prisma/client": "4.16.1",
|
||||||
"discord-api-types": "^0.37.46",
|
"discord-api-types": "^0.37.46",
|
||||||
"libsodium-wrappers": "^0.7.11",
|
"libsodium-wrappers": "^0.7.11",
|
||||||
"node-crc": "1.3.2",
|
"node-crc": "1.3.2",
|
||||||
"prism-media": "2.0.0-alpha.0",
|
"prism-media": "2.0.0-alpha.0",
|
||||||
|
"prisma": "^4.16.1",
|
||||||
"toml": "^3.0.0"
|
"toml": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ dependencies:
|
||||||
'@discordjs/ws':
|
'@discordjs/ws':
|
||||||
specifier: ^0.8.3
|
specifier: ^0.8.3
|
||||||
version: 0.8.3
|
version: 0.8.3
|
||||||
|
'@prisma/client':
|
||||||
|
specifier: 4.16.1
|
||||||
|
version: 4.16.1(prisma@4.16.1)
|
||||||
discord-api-types:
|
discord-api-types:
|
||||||
specifier: ^0.37.46
|
specifier: ^0.37.46
|
||||||
version: 0.37.46
|
version: 0.37.46
|
||||||
|
@ -35,6 +38,9 @@ dependencies:
|
||||||
prism-media:
|
prism-media:
|
||||||
specifier: 2.0.0-alpha.0
|
specifier: 2.0.0-alpha.0
|
||||||
version: 2.0.0-alpha.0
|
version: 2.0.0-alpha.0
|
||||||
|
prisma:
|
||||||
|
specifier: ^4.16.1
|
||||||
|
version: 4.16.1
|
||||||
toml:
|
toml:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
@ -379,6 +385,29 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@prisma/client@4.16.1(prisma@4.16.1):
|
||||||
|
resolution: {integrity: sha512-CoDHu7Bt+NuDo40ijoeHP79EHtECsPBTy3yte5Yo3op8TqXt/kV0OT5OrsWewKvQGKFMHhYQ+ePed3zzjYdGAw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
requiresBuild: true
|
||||||
|
peerDependencies:
|
||||||
|
prisma: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
prisma:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@prisma/engines-version': 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c
|
||||||
|
prisma: 4.16.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@prisma/engines-version@4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c:
|
||||||
|
resolution: {integrity: sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@prisma/engines@4.16.1:
|
||||||
|
resolution: {integrity: sha512-gpZG0kGGxfemgvK/LghHdBIz+crHkZjzszja94xp4oytpsXrgt/Ice82MvPsWMleVIniKuARrowtsIsim0PFJQ==}
|
||||||
|
requiresBuild: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@sapphire/async-queue@1.5.0:
|
/@sapphire/async-queue@1.5.0:
|
||||||
resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==}
|
resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==}
|
||||||
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
|
||||||
|
@ -803,6 +832,15 @@ packages:
|
||||||
duplex-child-process: 1.0.1
|
duplex-child-process: 1.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/prisma@4.16.1:
|
||||||
|
resolution: {integrity: sha512-C2Xm7yxHxjFjjscBEW4tmoraPHH/Vyu/A0XABdbaFtoiOZARsxvOM7rwc2iZ0qVxbh0bGBGBWZUSXO/52/nHBQ==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
requiresBuild: true
|
||||||
|
dependencies:
|
||||||
|
'@prisma/engines': 4.16.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/readable-stream@3.6.2:
|
/readable-stream@3.6.2:
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"channel" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "File" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"filename" TEXT NOT NULL,
|
||||||
|
"timestamp" BIGINT NOT NULL,
|
||||||
|
"sessionId" TEXT,
|
||||||
|
CONSTRAINT "File_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Made the column `sessionId` on table `File` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_File" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"filename" TEXT NOT NULL,
|
||||||
|
"timestamp" BIGINT NOT NULL,
|
||||||
|
"sessionId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "File_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_File" ("filename", "id", "sessionId", "timestamp") SELECT "filename", "id", "sessionId", "timestamp" FROM "File";
|
||||||
|
DROP TABLE "File";
|
||||||
|
ALTER TABLE "new_File" RENAME TO "File";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "sqlite"
|
|
@ -0,0 +1,27 @@
|
||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = "file:data.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
channel String
|
||||||
|
files File[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model File {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
filename String
|
||||||
|
timestamp BigInt
|
||||||
|
|
||||||
|
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||||
|
sessionId String
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { Client, GatewayIntentBits } from "@discordjs/core"
|
||||||
import { config } from "./config.js"
|
import { config } from "./config.js"
|
||||||
import { VoiceStateManager } from "./utils/wrapper/VoiceState.js"
|
import { VoiceStateManager } from "./utils/wrapper/VoiceState.js"
|
||||||
import { RecordingSessionManager } from "./utils/sessions.js"
|
import { RecordingSessionManager } from "./utils/sessions.js"
|
||||||
|
import { PrismaClient } from "@prisma/client"
|
||||||
|
|
||||||
export const rest = new REST({ version: "10" }).setToken(config.token)
|
export const rest = new REST({ version: "10" }).setToken(config.token)
|
||||||
|
|
||||||
|
@ -20,3 +21,5 @@ export const client = new Client({ rest, gateway })
|
||||||
export const voiceStateManager = new VoiceStateManager()
|
export const voiceStateManager = new VoiceStateManager()
|
||||||
|
|
||||||
export const sessionManager = new RecordingSessionManager()
|
export const sessionManager = new RecordingSessionManager()
|
||||||
|
|
||||||
|
export const db = new PrismaClient()
|
||||||
|
|
|
@ -34,7 +34,7 @@ export const startCommand = async ({
|
||||||
content: "먼저 음성 채널에 들어가주세요!",
|
content: "먼저 음성 채널에 들어가주세요!",
|
||||||
})
|
})
|
||||||
|
|
||||||
sessionManager.addSession(data.guild_id, vc, shardId)
|
await sessionManager.addSession(data.guild_id, vc, data.channel_id, shardId)
|
||||||
|
|
||||||
await api.interactions.reply(data.id, data.token, {
|
await api.interactions.reply(data.id, data.token, {
|
||||||
content: "냥냥",
|
content: "냥냥",
|
||||||
|
|
44
src/index.ts
44
src/index.ts
|
@ -13,6 +13,7 @@ import { startCommand } from "./commands/start.js"
|
||||||
import { config } from "./config.js"
|
import { config } from "./config.js"
|
||||||
import { syncCommands } from "./utils/commands.js"
|
import { syncCommands } from "./utils/commands.js"
|
||||||
import { VoiceState } from "./utils/wrapper/VoiceState.js"
|
import { VoiceState } from "./utils/wrapper/VoiceState.js"
|
||||||
|
import { isMe } from "./utils/appid.js"
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => console.error(error))
|
process.on("uncaughtException", (error) => console.error(error))
|
||||||
process.on("unhandledRejection", (error) => console.error(error))
|
process.on("unhandledRejection", (error) => console.error(error))
|
||||||
|
@ -82,20 +83,41 @@ client.on(GatewayDispatchEvents.InteractionCreate, async (args) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
client.on(GatewayDispatchEvents.VoiceStateUpdate, async (args) => {
|
client.on(GatewayDispatchEvents.VoiceStateUpdate, async ({ data, api }) => {
|
||||||
if (args.data.guild_id) {
|
if (data.guild_id) {
|
||||||
const adapter = sessionManager.adapters.get(args.data.guild_id)
|
const adapter = sessionManager.adapters.get(data.guild_id)
|
||||||
if (adapter) adapter.onVoiceStateUpdate(args.data)
|
if (adapter) adapter.onVoiceStateUpdate(data)
|
||||||
const guild = voiceStateManager.guilds.get(args.data.guild_id)
|
const guild = voiceStateManager.guilds.get(data.guild_id)
|
||||||
if (guild) {
|
if (guild) {
|
||||||
const state = guild.get(args.data.user_id)
|
const state = guild.get(data.user_id)
|
||||||
if (state) {
|
if (state) {
|
||||||
state.update(args.data)
|
state.update(data)
|
||||||
} else {
|
} else {
|
||||||
guild.set(
|
guild.set(data.user_id, new VoiceState(data, data.guild_id))
|
||||||
args.data.user_id,
|
}
|
||||||
new VoiceState(args.data, args.data.guild_id)
|
|
||||||
)
|
const session = sessionManager.sessionMap.get(data.guild_id)
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
let counter = 0
|
||||||
|
let hasMe = false
|
||||||
|
|
||||||
|
for (const state of guild.values()) {
|
||||||
|
if (state.channelId !== session.voiceChannelId) continue
|
||||||
|
if (isMe(state.id)) {
|
||||||
|
hasMe = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counter === 0 || !hasMe) {
|
||||||
|
session.vc.destroy()
|
||||||
|
sessionManager.removeSession(data.guild_id)
|
||||||
|
await api.channels.createMessage(session.textChannelId, {
|
||||||
|
content: `${session.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { db } from "./api.js"
|
||||||
|
import { mergeAudio } from "./utils/merge.js"
|
||||||
|
|
||||||
|
const id = process.argv[2]
|
||||||
|
|
||||||
|
const session = await db.session.findUnique({ where: { id } })
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new Error("session not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
await mergeAudio(id)
|
|
@ -1,3 +1,8 @@
|
||||||
|
import { config } from "../config.js"
|
||||||
|
|
||||||
export const getAppIdFromToken = (token: string) => {
|
export const getAppIdFromToken = (token: string) => {
|
||||||
return atob(token.split(".")[0])
|
return atob(token.split(".")[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isMe = (userId: string) =>
|
||||||
|
userId === getAppIdFromToken(config.token)
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import path from "path"
|
||||||
|
import { db } from "../api.js"
|
||||||
|
import { spawn } from "child_process"
|
||||||
|
import { existsSync, mkdirSync } from "fs"
|
||||||
|
|
||||||
|
export const mergeAudio = async (id: string) => {
|
||||||
|
const finalDir = "recordings/final"
|
||||||
|
const sourceDir = path.resolve("recordings", id)
|
||||||
|
|
||||||
|
if (!existsSync(finalDir)) {
|
||||||
|
mkdirSync(finalDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await db.file.findMany({
|
||||||
|
where: {
|
||||||
|
session: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let inputs: string[] = []
|
||||||
|
|
||||||
|
let filter: string[] = []
|
||||||
|
let trackNames: string[] = []
|
||||||
|
|
||||||
|
let counter = 0
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(sourceDir, file.filename)
|
||||||
|
const index = counter++
|
||||||
|
const trackName = `[c${index}]`
|
||||||
|
|
||||||
|
inputs.push("-i", filePath)
|
||||||
|
|
||||||
|
filter.push(
|
||||||
|
`[${index}:a] adelay=${file.timestamp}|${file.timestamp} ${trackName}`
|
||||||
|
)
|
||||||
|
|
||||||
|
trackNames.push(trackName)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.push(
|
||||||
|
`${trackNames.join(" ")} amix=inputs=${
|
||||||
|
trackNames.length
|
||||||
|
}:normalize=0 [out]`
|
||||||
|
)
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
...inputs,
|
||||||
|
"-filter_complex",
|
||||||
|
filter.join(";"),
|
||||||
|
"-map",
|
||||||
|
"[out]",
|
||||||
|
"-async",
|
||||||
|
"1",
|
||||||
|
"-y",
|
||||||
|
path.join(finalDir, `${id}.ogg`),
|
||||||
|
]
|
||||||
|
|
||||||
|
const p = spawn("ffmpeg", args).on("exit", (code) => {
|
||||||
|
console.log("ffmpeg exited with code", code)
|
||||||
|
})
|
||||||
|
p.stderr.pipe(process.stderr)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { Collection } from "@discordjs/collection"
|
import { Collection } from "@discordjs/collection"
|
||||||
import { gateway, voiceStateManager } from "../api.js"
|
import { db, gateway, voiceStateManager } from "../api.js"
|
||||||
import {
|
import {
|
||||||
AudioReceiveStream,
|
AudioReceiveStream,
|
||||||
DiscordGatewayAdapterLibraryMethods,
|
DiscordGatewayAdapterLibraryMethods,
|
||||||
|
@ -11,9 +11,9 @@ import { Snowflake } from "discord-api-types/v10"
|
||||||
import * as prism from "prism-media"
|
import * as prism from "prism-media"
|
||||||
import { pipeline } from "stream"
|
import { pipeline } from "stream"
|
||||||
import { createWriteStream, existsSync, mkdirSync } from "fs"
|
import { createWriteStream, existsSync, mkdirSync } from "fs"
|
||||||
import { getAppIdFromToken } from "./appid.js"
|
|
||||||
import { config } from "../config.js"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { isMe } from "./appid.js"
|
||||||
|
import { exec, spawn } from "child_process"
|
||||||
|
|
||||||
export class RecordingSessionManager {
|
export class RecordingSessionManager {
|
||||||
sessionMap = new Collection<Snowflake, RecordingSession>()
|
sessionMap = new Collection<Snowflake, RecordingSession>()
|
||||||
|
@ -23,11 +23,16 @@ export class RecordingSessionManager {
|
||||||
return this.sessionMap.has(guild)
|
return this.sessionMap.has(guild)
|
||||||
}
|
}
|
||||||
|
|
||||||
addSession(guild: Snowflake, channel: Snowflake, shardId: number) {
|
async addSession(
|
||||||
|
guild: Snowflake,
|
||||||
|
voiceChannel: Snowflake,
|
||||||
|
textChannel: Snowflake,
|
||||||
|
shardId: number
|
||||||
|
) {
|
||||||
const session = new RecordingSession(
|
const session = new RecordingSession(
|
||||||
joinVoiceChannel({
|
joinVoiceChannel({
|
||||||
guildId: guild,
|
guildId: guild,
|
||||||
channelId: channel,
|
channelId: voiceChannel,
|
||||||
adapterCreator: (methods) => {
|
adapterCreator: (methods) => {
|
||||||
this.adapters.set(guild, methods)
|
this.adapters.set(guild, methods)
|
||||||
return {
|
return {
|
||||||
|
@ -43,22 +48,48 @@ export class RecordingSessionManager {
|
||||||
selfDeaf: false,
|
selfDeaf: false,
|
||||||
selfMute: true,
|
selfMute: true,
|
||||||
debug: true,
|
debug: true,
|
||||||
})
|
}),
|
||||||
|
voiceChannel,
|
||||||
|
textChannel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await session.init()
|
||||||
|
|
||||||
this.sessionMap.set(guild, session)
|
this.sessionMap.set(guild, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeSession(guild: Snowflake) {
|
||||||
|
const session = this.sessionMap.get(guild)
|
||||||
|
if (session) session.shutdown()
|
||||||
|
this.sessionMap.delete(guild)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecordingSession {
|
export class RecordingSession {
|
||||||
id: string
|
|
||||||
streams = new Collection<Snowflake, UserStream>()
|
streams = new Collection<Snowflake, UserStream>()
|
||||||
startedAt = Date.now()
|
startedAt = Date.now()
|
||||||
|
id!: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public vc: VoiceConnection,
|
||||||
|
public voiceChannelId: Snowflake,
|
||||||
|
public textChannelId: Snowflake
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const result = await db.session.create({
|
||||||
|
data: {
|
||||||
|
id: this.id,
|
||||||
|
channel: this.voiceChannelId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.id = result.id
|
||||||
|
|
||||||
constructor(public vc: VoiceConnection) {
|
|
||||||
this.id = Date.now().toString()
|
this.id = Date.now().toString()
|
||||||
vc.receiver.speaking.on("start", (userId) => this.addReceiver(userId))
|
this.receiver.speaking.on("start", (userId) => this.addReceiver(userId))
|
||||||
vc.receiver.speaking.on("end", (userId) => this.removeReceiver(userId))
|
this.receiver.speaking.on("end", (userId) =>
|
||||||
|
this.removeReceiver(userId)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get receiver() {
|
get receiver() {
|
||||||
|
@ -66,11 +97,9 @@ export class RecordingSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
addReceiver(userId: string) {
|
addReceiver(userId: string) {
|
||||||
if (userId === getAppIdFromToken(config.token)) return
|
if (isMe(userId)) return
|
||||||
if (this.streams.has(userId)) return
|
if (this.streams.has(userId)) return
|
||||||
|
|
||||||
console.log("Starting record:", userId)
|
|
||||||
|
|
||||||
const receiver = this.receiver.subscribe(userId)
|
const receiver = this.receiver.subscribe(userId)
|
||||||
|
|
||||||
this.streams.set(userId, new UserStream(this, receiver, userId))
|
this.streams.set(userId, new UserStream(this, receiver, userId))
|
||||||
|
@ -80,11 +109,12 @@ export class RecordingSession {
|
||||||
const stream = this.streams.get(userId)
|
const stream = this.streams.get(userId)
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
console.log("Stopping record:", userId)
|
|
||||||
stream.subscription.push(null)
|
stream.subscription.push(null)
|
||||||
this.streams.delete(userId)
|
this.streams.delete(userId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async shutdown() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserStream {
|
class UserStream {
|
||||||
|
@ -105,7 +135,9 @@ class UserStream {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const dir = path.resolve("recordings", session.id, userId)
|
const dir = path.resolve("recordings", session.id)
|
||||||
|
const timestamp = Date.now() - session.startedAt
|
||||||
|
const file = `${userId}-${timestamp}.ogg`
|
||||||
|
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
mkdirSync(dir, {
|
mkdirSync(dir, {
|
||||||
|
@ -113,20 +145,36 @@ class UserStream {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = path.join(dir, `${Date.now() - session.startedAt}.ogg`)
|
const filename = path.join(dir, file)
|
||||||
|
|
||||||
|
console.log(`Recording ${filename} started`)
|
||||||
|
|
||||||
const outputStream = createWriteStream(filename)
|
const outputStream = createWriteStream(filename)
|
||||||
|
|
||||||
pipeline(subscription, this.oggStream, outputStream, (err) => {
|
pipeline(subscription, this.oggStream, outputStream, async (err) => {
|
||||||
outputStream.end()
|
outputStream.end()
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(`Recording ${userId} ended with error: ${err}`)
|
console.log(`Recording ${filename} ended with error: ${err}`)
|
||||||
subscription.destroy(err)
|
subscription.destroy(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Recording ${userId} ended`)
|
await db.file
|
||||||
|
.create({
|
||||||
|
data: {
|
||||||
|
session: {
|
||||||
|
connect: {
|
||||||
|
id: session.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filename: file,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
|
||||||
|
console.log(`Recording ${filename} ended`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue