feat: implement full session recording with muxing support

- Add session recording metadata and mux filter builder in src/recorder/sessionRecording.ts.
- Update SegmentMetadata to include recordingSessionId in src/types.ts and src/recorder/metadata.ts.
- Modify recorder lifecycle to track sessions, register segments, and finalize recordings on stop.
- Create tests for session recording functionality in tests/recorder/sessionRecording.test.ts and tests/recorder/metadata.test.ts.
- Document session recording design and implementation plan in docs/superpowers/specs/2026-05-16-session-full-recording-design.md and docs/superpowers/plans/2026-05-16-session-full-recording.md.
This commit is contained in:
MythEclipse
2026-05-16 17:59:17 +07:00
parent 8b33af8286
commit 2744e7035b
9 changed files with 1338 additions and 40 deletions
+48 -5
View File
@@ -20,6 +20,11 @@ import {
createSegmentMetadata,
} from "./recorder/metadata";
import { SegmentManager } from "./recorder/segment";
import {
createRecordingSession,
finalizeRecordingSession,
type RecordingSession,
} from "./recorder/sessionRecording";
import { retryWithBackoff } from "./retry";
import type { PcmBroadcaster } from "./types";
@@ -32,6 +37,21 @@ if (!fs.existsSync(recordingsDir)) {
fs.mkdirSync(recordingsDir, { recursive: true });
}
const activeSessions = new Map<string, RecordingSession>();
export function resetActiveSessions(): void {
activeSessions.clear();
}
function finalizeActiveRecordingSession(guildId: string): void {
const session = activeSessions.get(guildId);
if (!session) return;
activeSessions.delete(guildId);
finalizeRecordingSession(session).catch((error) => {
logger.error({ error }, "Failed to finalize recording session");
});
}
/**
* Join ke voice channel dan mulai merekam semua user yang bicara.
*/
@@ -78,6 +98,17 @@ export async function startRecording(
},
);
logger.info("Connected to voice channel. Recording started");
// Create recording session after connection is ready
const sessionStartTime = Date.now();
const session = createRecordingSession({
guildId: channel.guild.id,
channelId: channel.id,
channelName: channel.name,
startTime: sessionStartTime,
recordingsDir,
});
activeSessions.set(channel.guild.id, session);
} catch (err) {
logger.error({ error: err }, "Failed to connect to voice channel");
connection.destroy();
@@ -109,9 +140,6 @@ export async function startRecording(
// Jangan record kalau sudah ada stream aktif untuk user ini
if (receiver.subscriptions.has(userId)) return;
const timestamp = Date.now();
const sessionStartTime = timestamp;
const sessionId = `${userId}-${sessionStartTime}`;
const userDir = path.join(recordingsDir, userId);
if (!fs.existsSync(userDir)) {
fs.mkdirSync(userDir, { recursive: true });
@@ -149,16 +177,28 @@ export async function startRecording(
},
});
const activeSession = activeSessions.get(channel.guild.id);
let currentSegment = segmentManager.open(oggPacketStream);
currentSegment.out.on("finish", () => {
if (config.VERBOSE) {
logger.info({ filename: currentSegment.filename }, "Segment saved");
}
const endTime = currentSegment.endTime ?? Date.now();
if (activeSession) {
activeSession.registerSegment({
user: userMetadata,
oggPath: currentSegment.filename,
jsonPath: currentSegment.jsonFilename,
startTime: currentSegment.startTime,
endTime,
});
}
const metadata = createSegmentMetadata(
userMetadata,
currentSegment,
sessionId,
sessionStartTime,
activeSession?.sessionId ?? `${userId}-0`,
activeSession?.sessionId ?? `${channel.guild.id}-${channel.id}-0`,
activeSession?.startTime ?? 0,
config.RECORDING_SEGMENT_MS,
);
fs.writeFileSync(
@@ -240,6 +280,7 @@ export async function startRecording(
});
connection.on(VoiceConnectionStatus.Destroyed, () => {
finalizeActiveRecordingSession(channel.guild.id);
if (config.VERBOSE) {
logger.info("Voice connection destroyed");
}
@@ -261,4 +302,6 @@ export function stopRecording(guildId: string): void {
} else {
logger.warn("No active connection to stop");
}
finalizeActiveRecordingSession(guildId);
}