@@ -191,6 +191,69 @@ export class ScreenStreamCapture {
191191 }
192192}
193193
194+ /**
195+ * The Annex-B output file, with the write stream's `'error'` event held instead
196+ * of left to reach the process.
197+ *
198+ * A recording runs unattended for minutes and the stream never escapes the
199+ * function that owns it, so a caller cannot attach a listener of its own.
200+ * Without one here, a mistyped path or a write that fails after the fact (a full
201+ * disk, an unplugged volume) arrives as an uncaught `'error'` event and takes
202+ * the process down — `end(callback)` does not help, since the callback receives
203+ * the error *and* the event is still emitted. Holding the first error and
204+ * re-throwing it from the next `write` or `close` turns that into a rejection
205+ * the caller can see, the guarantee {@link M4aFileWriter} already gives the
206+ * audio track.
207+ *
208+ * Opening is deliberately not awaited, unlike `M4aFileWriter.create`: the
209+ * capture is already streaming by the time the file is created, so a rejection
210+ * before the caller holds the writer would strand it. An open failure surfaces
211+ * from the first {@link write} instead.
212+ */
213+ export class AnnexBFileWriter {
214+ private readonly stream : ReturnType < typeof createWriteStream > ;
215+ /** First stream error seen, re-thrown from the next `write` or `close`. */
216+ private streamError : Error | undefined ;
217+
218+ /** @param path Destination file path; truncated if it exists. */
219+ constructor ( path : string ) {
220+ this . stream = createWriteStream ( path ) ;
221+ this . stream . on ( 'error' , ( error : Error ) => {
222+ this . streamError ??= error ;
223+ } ) ;
224+ }
225+
226+ /**
227+ * Appends one chunk, resolving once the stream has room for more so an
228+ * `await` per chunk applies backpressure.
229+ */
230+ async write ( chunk : Buffer ) : Promise < void > {
231+ if ( this . streamError ) {
232+ throw this . streamError ;
233+ }
234+ if ( ! this . stream . write ( chunk ) ) {
235+ await once ( this . stream , 'drain' ) ;
236+ }
237+ if ( this . streamError ) {
238+ throw this . streamError ;
239+ }
240+ }
241+
242+ /** Flushes and closes the file, re-throwing any error the stream saw. */
243+ async close ( ) : Promise < void > {
244+ // The held error is reported in preference to ending an already-failed
245+ // stream, which answers with a generic `ERR_STREAM_DESTROYED` and buries the
246+ // cause. That is the common path for a bad output path: the open fails
247+ // before the first keyframe arrives, so no `write` ever ran to surface it.
248+ if ( this . streamError ) {
249+ throw this . streamError ;
250+ }
251+ await new Promise < void > ( ( resolve , reject ) => {
252+ this . stream . end ( ( error ?: Error | null ) : void => ( error ? reject ( error ) : resolve ( ) ) ) ;
253+ } ) ;
254+ }
255+ }
256+
194257/** Options for {@link recordScreenToFile}. */
195258export interface RecordScreenOptions extends ScreenStreamCaptureOptions {
196259 /** How long to record, in milliseconds. Defaults to 5000. */
@@ -239,7 +302,7 @@ export async function recordScreenToFile(
239302 const { durationMs = 5000 , maxFrames = Number . POSITIVE_INFINITY , ...captureOptions } = options ;
240303
241304 const capture = await ScreenStreamCapture . start ( service , captureOptions ) ;
242- const output = createWriteStream ( outputPath ) ;
305+ const output = new AnnexBFileWriter ( outputPath ) ;
243306 let framesWritten = 0 ;
244307 let bytesWritten = 0 ;
245308 let sawKeyFrame = false ;
@@ -262,9 +325,7 @@ export async function recordScreenToFile(
262325 }
263326
264327 const chunk = toAnnexB ( unit . nals ) ;
265- if ( ! output . write ( chunk ) ) {
266- await once ( output , 'drain' ) ;
267- }
328+ await output . write ( chunk ) ;
268329 framesWritten += 1 ;
269330 bytesWritten += chunk . length ;
270331
@@ -277,15 +338,7 @@ export async function recordScreenToFile(
277338 await capture . stop ( ) . catch ( ( error : unknown ) => {
278339 log . debug ( `Failed to stop the media stream cleanly: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
279340 } ) ;
280- await new Promise < void > ( ( resolve , reject ) => {
281- output . end ( ( error ?: Error | null ) : void => {
282- if ( error ) {
283- reject ( error ) ;
284- return ;
285- }
286- resolve ( ) ;
287- } ) ;
288- } ) ;
341+ await output . close ( ) ;
289342 }
290343
291344 return {
0 commit comments