koibot/src/utils/sessions.ts

133 lines
3.1 KiB
TypeScript

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<Snowflake, RecordingSession>()
adapters = new Collection<Snowflake, DiscordGatewayAdapterLibraryMethods>()
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<Snowflake, UserStream>()
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`)
})
}
}