import { Collection } from "@discordjs/collection" import { gateway, voiceStateManager } from "../api.js" import { AudioReceiveStream, DiscordGatewayAdapterLibraryMethods, EndBehaviorType, joinVoiceChannel, VoiceConnection, } from "@discordjs/voice" import { Snowflake } from "discord-api-types/v10" import * as prism from "prism-media" import { pipeline } from "stream" import { createWriteStream, existsSync, mkdirSync } from "fs" import { getAppIdFromToken } from "./appid.js" import { config } from "../config.js" import path from "path" export class RecordingSessionManager { sessionMap = new Collection() adapters = new Collection() hasSession(guild: Snowflake): boolean { return this.sessionMap.has(guild) } addSession(guild: Snowflake, channel: Snowflake, shardId: number) { const session = new RecordingSession( joinVoiceChannel({ guildId: guild, channelId: channel, adapterCreator: (methods) => { this.adapters.set(guild, methods) return { sendPayload: (payload) => { gateway.send(shardId, payload) return true }, destroy: () => { this.adapters.delete(guild) }, } }, selfDeaf: false, selfMute: true, debug: true, }) ) this.sessionMap.set(guild, session) } } export class RecordingSession { id: string streams = new Collection() startedAt = Date.now() constructor(public vc: VoiceConnection) { this.id = Date.now().toString() vc.receiver.speaking.on("start", (userId) => this.addReceiver(userId)) vc.receiver.speaking.on("end", (userId) => this.removeReceiver(userId)) } get receiver() { return this.vc.receiver } addReceiver(userId: string) { if (userId === getAppIdFromToken(config.token)) return if (this.streams.has(userId)) return console.log("Starting record:", userId) const receiver = this.receiver.subscribe(userId) this.streams.set(userId, new UserStream(this, receiver, userId)) } removeReceiver(userId: string) { const stream = this.streams.get(userId) if (stream) { console.log("Stopping record:", userId) stream.subscription.push(null) this.streams.delete(userId) } } } class UserStream { oggStream: prism.opus.OggLogicalBitstream constructor( public session: RecordingSession, public subscription: AudioReceiveStream, public userId: string ) { this.oggStream = new prism.opus.OggLogicalBitstream({ opusHead: new prism.opus.OpusHead({ channelCount: 2, sampleRate: 48000, }), pageSizeControl: { maxPackets: 10, }, }) const dir = path.resolve("recordings", session.id, userId) if (!existsSync(dir)) { mkdirSync(dir, { recursive: true, }) } const filename = path.join(dir, `${Date.now() - session.startedAt}.ogg`) const outputStream = createWriteStream(filename) pipeline(subscription, this.oggStream, outputStream, (err) => { outputStream.end() if (err) { console.log(`Recording ${userId} ended with error: ${err}`) subscription.destroy(err) return } console.log(`Recording ${userId} ended`) }) } }