Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions packages/sdk-recorder/__tests__/recorder-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class Stream {
}

class Encoder {
static latest: Encoder;
constructor() {
Encoder.latest = this;
}
state = "inactive";
ondataavailable: ((event: { data: Blob }) => void) | null = null;
onstop: (() => void) | null = null;
Expand All @@ -33,7 +37,11 @@ class Encoder {
start() {
this.state = "recording";
}
pause() {
this.state = "paused";
}
stop() {
if (this.state === "inactive") return;
this.state = "inactive";
if (!this.onstop) return;
this.ondataavailable?.({ data: new Blob(["recording"]) });
Expand Down Expand Up @@ -163,3 +171,75 @@ test.each([
recorder.destroy();
}
});

test.each([
{ paused: false, pendingStop: false },
{ paused: true, pendingStop: false },
{ paused: false, pendingStop: true },
{ paused: true, pendingStop: true },
])(
"saves an ended source with paused=$paused and pendingStop=$pendingStop",
async ({ paused, pendingStop }) => {
const requests: string[] = [];
let uploaded = "";
const fetch = mock(async (input: string, init?: RequestInit) => {
requests.push(input);
if (input.endsWith("/videos/create"))
return Response.json({
videoId: "test-video",
s3Key: "test-key",
shareUrl: "https://cap.so/s/test-video",
embedUrl: "https://cap.so/embed/test-video",
});
if (input.endsWith("/initiate"))
return Response.json({ uploadId: "test-upload" });
if (input.endsWith("/presign-part"))
return Response.json({ presignedUrl: "https://upload.invalid/part" });
if (input === "https://upload.invalid/part") {
uploaded = await new Response(init?.body).text();
return new Response(null, { headers: { ETag: "test-etag" } });
}
if (input.endsWith("/complete")) return Response.json({ success: true });
throw new Error(`Unexpected request: ${input}`);
});
Object.defineProperty(globalThis, "fetch", {
configurable: true,
value: fetch,
});
const recorder = new CapRecorder({
publicKey: "fixture",
mode: "camera",
microphone: { enabled: false },
});
try {
await recorder.start();
if (paused) recorder.pause();
const encoder = Encoder.latest;
encoder.ondataavailable?.({ data: new Blob(["recording"]) });
encoder.state = "inactive";
if (!pendingStop) encoder.onstop?.();
let settled = false;
const stopping = recorder.stop().then((result) => {
settled = true;
return result;
});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(settled).toBe(!pendingStop);
if (pendingStop) {
expect(requests).toEqual([]);
encoder.ondataavailable?.({ data: new Blob(["final"]) });
encoder.onstop?.();
}
expect(await stopping).toEqual({
videoId: "test-video",
shareUrl: "https://cap.so/s/test-video",
embedUrl: "https://cap.so/embed/test-video",
});
expect(recorder.phase).toBe("complete");
expect(requests).toContain("https://upload.invalid/part");
expect(uploaded).toBe(pendingStop ? "recordingfinal" : "recording");
} finally {
recorder.destroy();
}
},
);
22 changes: 10 additions & 12 deletions packages/sdk-recorder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class CapRecorder {
private _videoId: string | null = null;
private _durationMs = 0;
private recorder: MediaRecorder | null = null;
private recordingStopped = Promise.resolve();
private streams: ManagedStreams | null = null;
private startup: AbortController | null = null;
private chunks: Blob[] = [];
Expand Down Expand Up @@ -144,9 +145,13 @@ export class CapRecorder {

const mimeType = getSupportedMimeType();
this.selectedMimeType = mimeType;
this.recorder = new MediaRecorder(composite, {
const recorder = new MediaRecorder(composite, {
mimeType: mimeType || undefined,
});
this.recorder = recorder;
this.recordingStopped = new Promise<void>((resolve) => {
recorder.onstop = () => resolve();
});

this.chunks = [];
this.recorder.ondataavailable = (e) => {
Expand Down Expand Up @@ -211,17 +216,10 @@ export class CapRecorder {
this.stopDurationTimer();

const recorder = this.recorder;
return new Promise((resolve, reject) => {
recorder.onstop = async () => {
try {
const result = await this.uploadRecording();
resolve(result);
} catch (err) {
reject(err);
}
};
recorder.stop();
});
const stopped = this.recordingStopped;
if (recorder.state !== "inactive") recorder.stop();
await stopped;
return this.uploadRecording();
}

destroy(): void {
Expand Down
Loading