import fs from "node:fs"; import path from "node:path"; import * as prism from "prism-media"; import type { SegmentState } from "../types"; export function buildSegmentPaths( userDir: string, startTime: number, ): { filename: string; jsonFilename: string } { return { filename: path.join(userDir, `${startTime}.ogg`), jsonFilename: path.join(userDir, `${startTime}.json`), }; } export function shouldRotateSegment( startTime: number, now: number, recordingSegmentMs: number, ): boolean { return recordingSegmentMs > 0 && now - startTime >= recordingSegmentMs; } export class SegmentManager { private currentSegment: SegmentState | null = null; private segmentIndex = 0; constructor( private readonly userDir: string, private readonly recordingSegmentMs: number, ) {} open(oggPacketStream: NodeJS.ReadableStream): SegmentState { const index = this.segmentIndex++; const startTime = Date.now(); const { filename, jsonFilename } = buildSegmentPaths( this.userDir, startTime, ); const oggStream = new prism.opus.OggLogicalBitstream({ opusHead: new prism.opus.OpusHead({ channelCount: 2, sampleRate: 48000 }), pageSizeControl: { maxPackets: 10 }, crc: true, }); const out = fs.createWriteStream(filename); oggPacketStream.pipe(oggStream).pipe(out); this.currentSegment = { index, startTime, endTime: null, filename, jsonFilename, oggStream, out, }; return this.currentSegment; } close(oggPacketStream: NodeJS.ReadableStream): SegmentState | null { if (!this.currentSegment) return null; const segment = this.currentSegment; segment.endTime = Date.now(); oggPacketStream.unpipe(segment.oggStream); segment.oggStream.end(); this.currentSegment = null; return segment; } rotateIfNeeded(oggPacketStream: NodeJS.ReadableStream): SegmentState | null { if (!this.currentSegment) return null; if ( !shouldRotateSegment( this.currentSegment.startTime, Date.now(), this.recordingSegmentMs, ) ) return null; this.close(oggPacketStream); return this.open(oggPacketStream); } getCurrent(): SegmentState | null { return this.currentSegment; } }