133 lines
3.1 KiB
TypeScript
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`)
|
|
})
|
|
}
|
|
}
|