-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathav-capture.spec.ts
More file actions
122 lines (107 loc) · 4.49 KB
/
Copy pathav-capture.spec.ts
File metadata and controls
122 lines (107 loc) · 4.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import assert from 'node:assert/strict';
import {mkdtemp, rm} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {type TestContext, after, before, describe, it} from 'node:test';
import type {DisplayService, MediaStreamAnswer} from '../../../../src/services/ios/display/index.js';
import {recordScreenAndAudioToFiles} from '../../../../src/services/ios/display/recording/av-capture.js';
import {mockImport} from '../../../helpers/mock-module.js';
const AV_CAPTURE_MODULE = '../../../../src/services/ios/display/recording/av-capture.js';
const M4A_WRITER_MODULE = '../../../../src/services/ios/display/audio/m4a-writer.js';
const SCREEN_CAPTURE_MODULE = '../../../../src/services/ios/display/video/screen-stream-capture.js';
/** A stub service that negotiates without a device and counts teardowns. */
function makeStubService(): {service: DisplayService; stopCalls: () => number} {
let stopCalls = 0;
// An empty streamConfig means no RTCP identity, so no keepalive timer is
// started — the negotiation is all this test needs.
const answer = {
clientSessionId: undefined,
streamConfig: {},
connection: {},
raw: {},
} as unknown as MediaStreamAnswer;
const service = {
getTunnelLocalAddress: async (): Promise<string> => '::1',
getDeviceAddress: async (): Promise<string> => '::1',
startVideoStream: async (): Promise<MediaStreamAnswer> => answer,
startAudioStream: async (): Promise<MediaStreamAnswer> => answer,
stopAllMediaStreams: async (): Promise<number[]> => {
stopCalls += 1;
return [];
},
} as unknown as DisplayService;
return {service, stopCalls: () => stopCalls};
}
describe('recordScreenAndAudioToFiles', function () {
let directory: string;
before(async function () {
directory = await mkdtemp(join(tmpdir(), 'record-av-'));
});
after(async function () {
await rm(directory, {force: true, recursive: true});
});
it('stops both captures when the audio file cannot be created', async function () {
// Both captures are streaming before either writer is opened, so a failing
// audio writer used to strand two live device streams plus the video file
// descriptor. None of them escape the function, so no caller could release
// them.
const {service, stopCalls} = makeStubService();
await assert.rejects(
() =>
recordScreenAndAudioToFiles(service, {
videoPath: join(directory, 'screen.h265'),
audioPath: join(directory, 'no', 'such', 'dir', 'audio.m4a'),
}),
/ENOENT/,
);
assert.strictEqual(stopCalls(), 2, 'both captures must be stopped before the error propagates');
});
it('closes the video writer even when closing the audio writer throws', async function (t: TestContext) {
// M4aFileWriter finalizes by reopening the finished file to patch mdat's
// length, so it can fail on a full disk after every frame is already on
// disk. Sequencing the closes would skip the video one, leaking its file
// descriptor and leaving the error its stream holds unread.
let videoCloseCalls = 0;
const closeFailure = new Error('failed to patch the mdat header');
class StubM4aFileWriter {
static async create(): Promise<StubM4aFileWriter> {
return new StubM4aFileWriter();
}
async write(): Promise<void> {
return undefined;
}
async close(): Promise<never> {
throw closeFailure;
}
}
class StubAnnexBFileWriter {
constructor(path: string) {
void path;
}
async write(): Promise<void> {
return undefined;
}
async close(): Promise<void> {
videoCloseCalls += 1;
}
}
const {recordScreenAndAudioToFiles: record} = await mockImport<{
recordScreenAndAudioToFiles: typeof recordScreenAndAudioToFiles;
}>(t, AV_CAPTURE_MODULE, import.meta.url, {
[M4A_WRITER_MODULE]: {M4aFileWriter: StubM4aFileWriter},
// Merged over the real exports, so ScreenStreamCapture stays intact.
[SCREEN_CAPTURE_MODULE]: {AnnexBFileWriter: StubAnnexBFileWriter},
});
const {service} = makeStubService();
await assert.rejects(
() =>
record(service, {
videoPath: join(directory, 'screen-close.h265'),
audioPath: join(directory, 'audio-close.m4a'),
durationMs: 50,
}),
/failed to patch the mdat header/,
);
assert.strictEqual(videoCloseCalls, 1, 'the video writer must be closed even though the audio close threw');
});
});