Skip to content

fix(recording): prefer playable browser capture codecs#275

Closed
meiiie wants to merge 2 commits intowebadderallorg:mainfrom
meiiie:fix/browser-recording-codec-selection
Closed

fix(recording): prefer playable browser capture codecs#275
meiiie wants to merge 2 commits intowebadderallorg:mainfrom
meiiie:fix/browser-recording-codec-selection

Conversation

@meiiie
Copy link
Copy Markdown
Collaborator

@meiiie meiiie commented Apr 19, 2026

Description

Fix browser-based recording sessions that save successfully but fail to open in the editor with "Failed to load video".

Motivation

The browser capture path could prefer MIME types that MediaRecorder accepts but the in-app player cannot reliably decode, and it could still pass an explicitly unsupported fallback MIME type into new MediaRecorder(...) when probing found no supported preference.

Type of Change

  • New Feature
  • Bug Fix
  • Refactor / Code Cleanup
  • Documentation Update
  • Other (please specify)

Related Issue(s)

No tracked issue was linked for this bug.

Screenshots / Video

N/A for this bugfix-only change.

Testing Guide

  1. Start a browser-based recording session.
  2. Stop the recording.
  3. Verify the editor opens the saved recording instead of showing "Failed to load video".
  4. Run npx vitest --run src/hooks/recordingMimeType.test.ts src/hooks/useScreenRecorder.test.ts.
  5. Run npx tsc --noEmit.

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

New MIME-type selection logic was extracted into selectRecordingMimeType() with injectable probes for isTypeSupported and canPlayType. The screen recorder hook now delegates MIME selection to this utility and introduces a shared LINUX_PORTAL_SOURCE constant. A Vitest suite validates selection and fallback behaviors.

Changes

Cohort / File(s) Summary
MIME Type Selection Utility
src/hooks/recordingMimeType.ts
New module exporting selectRecordingMimeType() and MimeTypeSelectorOptions; implements ordered WebM/H.264 preference, probes isTypeSupported and canPlayType, and returns first playable or first supported MIME, or undefined.
Hook Refactor
src/hooks/useScreenRecorder.ts
Replaced inline MIME selection with selectRecordingMimeType() usage; added LINUX_PORTAL_SOURCE constant; only pass mimeType to MediaRecorder when truthy; adjust blob type handling and log messaging for browser-default case.
Test Suite
src/hooks/recordingMimeType.test.ts
Added Vitest tests covering: preferred playable MIME, skipping recorder-only codecs, fallback to isTypeSupported when probing empty, and undefined when no codec supported; stubs isTypeSupported and canPlayType.

Sequence Diagram(s)

sequenceDiagram
    participant Hook as "useScreenRecorder\n(hook)"
    participant Selector as "selectRecordingMimeType\n(utility)"
    participant RecorderAPI as "MediaRecorder.isTypeSupported\n(recorder probe)"
    participant Player as "HTMLVideoElement.canPlayType\n(playback probe)"
    Hook->>Selector: request mimeType
    Selector->>RecorderAPI: probe supported types
    RecorderAPI-->>Selector: supported list
    Selector->>Player: probe playback for candidates
    Player-->>Selector: canPlayType results
    Selector-->>Hook: selected mimeType | undefined
    Hook->>MediaRecorder: new MediaRecorder(stream, { ...mimeType? })
    note right of MediaRecorder: recording starts with chosen mimeType
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • webadderall/Recordly#269: Modifies src/hooks/useScreenRecorder.ts with Linux portal handling and capture/recording flow changes affecting the same hook refactor.

Poem

🐰 I hopped from hook into a MIME-filled glade,
Probed codecs, played samples, decisions were made.
Extracted the logic, so tidy and spry,
Now recordings pick formats that actually fly.
A nibble, a hop—tests cheer on high!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing recording by preferring codecs that are both recordable and playable.
Description check ✅ Passed The pull request description is comprehensive and follows the template structure, including Description, Motivation, Type of Change, Testing Guide, and a completed Checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hooks/recordingMimeType.test.ts`:
- Around line 24-36: The test currently makes vp9 both supported and playable so
it doesn't verify skipping an unplayable higher-preference codec; change the
mocked probes passed to selectRecordingMimeType so the first supported
preference (e.g., "video/webm;codecs=av1") is supported by isTypeSupported but
returns an empty string from canPlayType (unplayable), while a lower-preference
codec like "video/webm;codecs=vp9" returns "probably"; update the
isTypeSupported and canPlayType lambdas accordingly and keep the assertion
expect(mimeType).toBe("video/webm;codecs=vp9").

In `@src/hooks/recordingMimeType.ts`:
- Around line 25-30: The hook currently falls back to
FALLBACK_RECORDING_MIME_TYPE even when RECORDING_MIME_TYPE_PREFERENCES are all
rejected by isTypeSupported(), which can pass a rejected mimeType to new
MediaRecorder; change the hook so it returns undefined when no preference is
actually supported (i.e., return playableType ?? supportedTypes[0] ?? undefined)
instead of FALLBACK_RECORDING_MIME_TYPE, and update callers in
useScreenRecorder.ts where new MediaRecorder(..., { mimeType }) is constructed
to only include mimeType in MediaRecorderOptions when the returned value is
defined (or throw a controlled error before constructing the recorder if you
prefer explicit failure).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 22fd8bea-0fb1-45d6-9ab3-458edecfd765

📥 Commits

Reviewing files that changed from the base of the PR and between e4a780a and 84d7d7d.

📒 Files selected for processing (3)
  • src/hooks/recordingMimeType.test.ts
  • src/hooks/recordingMimeType.ts
  • src/hooks/useScreenRecorder.ts

Comment on lines +24 to +36
it("skips recorder-only codecs when playback support is missing", () => {
const mimeType = selectRecordingMimeType({
isTypeSupported: (type) =>
[
"video/webm;codecs=av1",
"video/webm;codecs=h264",
"video/webm;codecs=vp9",
].includes(type),
canPlayType: (type) => (type === "video/webm;codecs=vp9" ? "probably" : ""),
});

expect(mimeType).toBe("video/webm;codecs=vp9");
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make this test actually exercise skipping an unplayable higher-preference codec.

As written, vp9 is both supported and playable, so a broken implementation that ignores playback probing would still pass. Make the first supported preference unplayable and assert that selection falls through.

🧪 Proposed test adjustment
 	it("skips recorder-only codecs when playback support is missing", () => {
 		const mimeType = selectRecordingMimeType({
 			isTypeSupported: (type) =>
 				[
-					"video/webm;codecs=av1",
-					"video/webm;codecs=h264",
 					"video/webm;codecs=vp9",
+					"video/webm;codecs=vp8",
 				].includes(type),
-			canPlayType: (type) => (type === "video/webm;codecs=vp9" ? "probably" : ""),
+			canPlayType: (type) => (type === "video/webm;codecs=vp8" ? "probably" : ""),
 		});
 
-		expect(mimeType).toBe("video/webm;codecs=vp9");
+		expect(mimeType).toBe("video/webm;codecs=vp8");
 	});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("skips recorder-only codecs when playback support is missing", () => {
const mimeType = selectRecordingMimeType({
isTypeSupported: (type) =>
[
"video/webm;codecs=av1",
"video/webm;codecs=h264",
"video/webm;codecs=vp9",
].includes(type),
canPlayType: (type) => (type === "video/webm;codecs=vp9" ? "probably" : ""),
});
expect(mimeType).toBe("video/webm;codecs=vp9");
});
it("skips recorder-only codecs when playback support is missing", () => {
const mimeType = selectRecordingMimeType({
isTypeSupported: (type) =>
[
"video/webm;codecs=vp9",
"video/webm;codecs=vp8",
].includes(type),
canPlayType: (type) => (type === "video/webm;codecs=vp8" ? "probably" : ""),
});
expect(mimeType).toBe("video/webm;codecs=vp8");
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/recordingMimeType.test.ts` around lines 24 - 36, The test currently
makes vp9 both supported and playable so it doesn't verify skipping an
unplayable higher-preference codec; change the mocked probes passed to
selectRecordingMimeType so the first supported preference (e.g.,
"video/webm;codecs=av1") is supported by isTypeSupported but returns an empty
string from canPlayType (unplayable), while a lower-preference codec like
"video/webm;codecs=vp9" returns "probably"; update the isTypeSupported and
canPlayType lambdas accordingly and keep the assertion
expect(mimeType).toBe("video/webm;codecs=vp9").

Comment thread src/hooks/recordingMimeType.ts Outdated
webadderall added a commit that referenced this pull request Apr 19, 2026
Verifies the selected recording MIME type can also be decoded by the
in-app <video> element via canPlayType(), so that codecs like av1 —
which MediaRecorder.isTypeSupported() reports as supported for encoding
but are not decodable on fresh Windows installs without the AV1 Video
Extension — are not chosen.

Also passes undefined (not an unsupported fallback string) into
new MediaRecorder() when no preferred type is supported, letting the
browser pick a default instead of throwing NotSupportedError.

Replaces the previous 'as ProcessedDesktopSource' cast with a properly
typed LINUX_PORTAL_SOURCE constant.

Co-authored-by: meiiie <meiiie@users.noreply.github.com>
@webadderall
Copy link
Copy Markdown
Collaborator

Merged as daf99c0 via direct cherry-pick for v1.1.24 release. Thanks @meiiie! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants