Skip to content

fix: camera remains active after closing video recording modal#38491

Open
abhinavkrin wants to merge 4 commits intodevelopfrom
fix/camera-remains-active-after-closing-video-modal
Open

fix: camera remains active after closing video recording modal#38491
abhinavkrin wants to merge 4 commits intodevelopfrom
fix/camera-remains-active-after-closing-video-modal

Conversation

@abhinavkrin
Copy link
Member

@abhinavkrin abhinavkrin commented Feb 4, 2026

Proposed changes (including videos or screenshots)

When users close the video recording modal quickly before the camera has fully initialized, the camera light remains on until the page is refreshed. This occurs due to a race condition where stop() is called before the asynchronous getUserMedia() callback completes, causing the stream to initialize after cleanup has been attempted

  • Implemented a session ID pattern to track and invalidate stale async callbacks:
  • Added a sessionId counter that increments on each start() and stop() call
  • The getUserMedia success callback validates its session ID before initializing the stream
  • If the session ID has changed (indicating stop() was called), the stream is immediately cleaned up
Why Session ID Mechanism is Needed

Without a way to invalidate pending async callbacks, the original code had no mechanism to prevent stale getUserMedia success handlers from executing after stop() was called. Even though stop() cleared the stream reference, the pending callback would still execute and reinitialize the camera, leaving it active indefinitely.

Screen.Recording.2026-02-04.at.6.51.09.PM.mov

Issue(s)

Steps to test or reproduce

Further comments

CORE-1301

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where the camera could remain active after closing the video recording modal.
  • Tests
    • Added comprehensive unit tests for the video recorder covering start/stop cycles, permission errors, and stream handling.
  • Refactor
    • Improved recorder start/stop logic to prevent lingering streams and race conditions.
  • Chores
    • Expanded test discovery to include client-side spec files.

Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Feb 4, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is targeting the wrong base branch. It should target 8.2.0, but it targets 8.1.0

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link

changeset-bot bot commented Feb 4, 2026

🦋 Changeset detected

Latest commit: 05f756f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 40 packages
Name Type
@rocket.chat/meteor Patch
@rocket.chat/core-typings Patch
@rocket.chat/rest-typings Patch
@rocket.chat/uikit-playground Patch
@rocket.chat/api-client Patch
@rocket.chat/apps Patch
@rocket.chat/core-services Patch
@rocket.chat/cron Patch
@rocket.chat/ddp-client Patch
@rocket.chat/fuselage-ui-kit Patch
@rocket.chat/gazzodown Patch
@rocket.chat/http-router Patch
@rocket.chat/livechat Patch
@rocket.chat/model-typings Patch
@rocket.chat/ui-avatar Patch
@rocket.chat/ui-client Patch
@rocket.chat/ui-contexts Patch
@rocket.chat/ui-voip Patch
@rocket.chat/web-ui-registration Patch
@rocket.chat/account-service Patch
@rocket.chat/authorization-service Patch
@rocket.chat/ddp-streamer Patch
@rocket.chat/omnichannel-transcript Patch
@rocket.chat/presence-service Patch
@rocket.chat/queue-worker Patch
@rocket.chat/abac Patch
@rocket.chat/federation-matrix Patch
@rocket.chat/license Patch
@rocket.chat/media-calls Patch
@rocket.chat/omnichannel-services Patch
@rocket.chat/pdf-worker Patch
@rocket.chat/presence Patch
rocketchat-services Patch
@rocket.chat/models Patch
@rocket.chat/network-broker Patch
@rocket.chat/omni-core-ee Patch
@rocket.chat/mock-providers Patch
@rocket.chat/ui-video-conf Patch
@rocket.chat/instance-status Patch
@rocket.chat/omni-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Walkthrough

Adds a changeset, unit tests, and modifies VideoRecorder to use a sessionId guard and a stopStreamTracks utility so pending getUserMedia results are stopped if stop() is called before getUserMedia resolves. Also updates Jest to discover client-side spec files.

Changes

Cohort / File(s) Summary
Release Documentation
.changeset/great-kings-cry.md, package.json
New changeset entry documenting a patch release that notes a bug fix where the camera could remain active after closing the recording modal; package manifest updated.
VideoRecorder Implementation
apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.ts
Adds private sessionId: number and private stopStreamTracks(stream: MediaStream); increments sessionId on start/stop to invalidate pending async getUserMedia callbacks; ensures late-resolving streams are stopped and stop() stops active tracks and clears video element state; only invokes final blob callback when a start occurred and chunks exist.
VideoRecorder Tests
apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.spec.ts
Adds comprehensive unit tests covering race conditions where stop precedes getUserMedia resolution, early-stop prevention, multiple start/stop cycles, invalidation of pending callbacks, normal start/stop behavior, MIME type support, and permission error handling.
Test Configuration
apps/meteor/jest.config.ts
Adds a testMatch glob to include client-side spec files under app/ui/client so the new tests are discovered.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant VideoRecorder
    participant BrowserAPI as "navigator.mediaDevices"
    User->>VideoRecorder: start()
    Note over VideoRecorder: capture currentSessionId = 1
    VideoRecorder->>BrowserAPI: getUserMedia()
    User->>VideoRecorder: stop()
    Note over VideoRecorder: increment sessionId = 2\nstop active stream if any
    BrowserAPI-->>VideoRecorder: getUserMedia resolves (stream)
    Note over VideoRecorder: captured 1 != sessionId 2\ncall stopStreamTracks(stream)
    Note over BrowserAPI: Media tracks stopped (camera released)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hopped in swift to calm the light,
Session IDs chased the lingering sight,
When start ran slow and stop ran past,
I stilled the stream — the camera's cast,
Now quiet eyes at last sleep tight. 📹✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main bug fix: preventing the camera from remaining active after closing the video recording modal.
Linked Issues check ✅ Passed The changes directly address CORE-1301 by implementing a session ID mechanism to invalidate stale async callbacks and properly clean up camera resources when the modal is closed.
Out of Scope Changes check ✅ Passed All code changes are directly related to fixing the camera resource leak: implementation of session tracking, comprehensive unit tests, and jest configuration updates for test discovery.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/camera-remains-active-after-closing-video-modal

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
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 4 files

Copy link
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: 1

🤖 Fix all issues with AI agents
In `@apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.ts`:
- Line 127: The code currently references the video pause method without
invoking it (this.videoel.pause), so update the call in videoRecorder.ts to
invoke the method (this.videoel.pause()); also guard the call by checking the
element and the method exist (e.g., if (this.videoel && typeof
this.videoel.pause === 'function') this.videoel.pause()) to avoid runtime errors
when videoel is null/undefined.
🧹 Nitpick comments (1)
apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.spec.ts (1)

52-54: Consider resetting VideoRecorder state between tests.

Since VideoRecorder is a singleton and tests directly mutate internal state (e.g., lines 167-169), there's potential for state leakage between tests. Consider adding a reset mechanism or calling VideoRecorder.stop() in afterEach to ensure clean state.

♻️ Suggested improvement
 	afterEach(() => {
+		// Reset VideoRecorder state to ensure test isolation
+		VideoRecorder.stop();
 		jest.clearAllMocks();
 	});

@codecov
Copy link

codecov bot commented Feb 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.44%. Comparing base (b0a2011) to head (05f756f).
⚠️ Report is 2 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #38491    +/-   ##
=========================================
  Coverage    70.44%   70.44%            
=========================================
  Files         3162     3161     -1     
  Lines       110340   110740   +400     
  Branches     19841    19911    +70     
=========================================
+ Hits         77726    78008   +282     
- Misses       30581    30705   +124     
+ Partials      2033     2027     -6     
Flag Coverage Δ
e2e 60.36% <6.25%> (-0.03%) ⬇️
e2e-api 48.83% <ø> (+1.01%) ⬆️
unit 71.37% <100.00%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
@abhinavkrin abhinavkrin closed this Feb 4, 2026
@abhinavkrin abhinavkrin deleted the fix/camera-remains-active-after-closing-video-modal branch February 4, 2026 13:36
@abhinavkrin abhinavkrin reopened this Feb 4, 2026
Copy link
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: 1

🤖 Fix all issues with AI agents
In `@apps/meteor/app/ui/client/lib/recorderjs/videoRecorder.ts`:
- Around line 20-22: Remove inline implementation comments that describe how the
code works; specifically delete the implementation comments around the
session-guard logic for the private field sessionId and the other noted inline
comments (near the block containing sessionId, the block at lines referenced as
36-42, and the block at 117-118). Keep the code and any necessary JSDoc/type
comments that describe public intent or API, but remove explanatory
implementation comments that repeat what the code already expresses (e.g.,
comments immediately above or beside the sessionId field and the internal guard
logic in start/stop or initialization functions).

@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

📦 Docker Image Size Report

➡️ Changes

Service Current Baseline Change Percent
sum of all images 0B 0B 0B
account-service 0B 0B 0B
authorization-service 0B 0B 0B
ddp-streamer-service 0B 0B 0B
omnichannel-transcript-service 0B 0B 0B
presence-service 0B 0B 0B
queue-worker-service 0B 0B 0B
rocketchat 0B 0B 0B

📊 Historical Trend

---
config:
  theme: "dark"
  xyChart:
    width: 900
    height: 400
---
xychart
  title "Image Size Evolution by Service (Last 30 Days + This PR)"
  x-axis ["11/18 22:53", "11/19 23:02", "11/21 16:49", "11/24 17:34", "11/27 22:32", "11/28 19:05", "12/01 23:01", "12/02 21:57", "12/03 21:00", "12/04 18:17", "12/05 21:56", "12/08 20:15", "12/09 22:17", "12/10 23:26", "12/11 21:56", "12/12 22:45", "12/13 01:34", "12/15 22:31", "12/16 22:18", "12/17 21:04", "12/18 23:12", "12/19 23:27", "12/20 21:03", "12/22 18:54", "12/23 16:16", "12/24 19:38", "12/25 17:51", "12/26 13:18", "12/29 19:01", "12/30 20:52", "02/05 09:59 (PR)"]
  y-axis "Size (GB)" 0 --> 0.5
  line "account-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.00]
  line "authorization-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.00]
  line "ddp-streamer-service" [0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.00]
  line "omnichannel-transcript-service" [0.14, 0.14, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.00]
  line "presence-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.00]
  line "queue-worker-service" [0.14, 0.14, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.00]
  line "rocketchat" [0.35, 0.35, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.00]
Loading

Statistics (last 30 days):

  • 📊 Average: 1.5GiB
  • ⬇️ Minimum: 1.4GiB
  • ⬆️ Maximum: 1.6GiB
  • 🎯 Current PR: 0B
ℹ️ About this report

This report compares Docker image sizes from this build against the develop baseline.

  • Tag: pr-38491
  • Baseline: develop
  • Timestamp: 2026-02-05 09:59:55 UTC
  • Historical data points: 30

Updated: Thu, 05 Feb 2026 09:59:56 GMT

jest.clearAllMocks();
});

describe('Race condition fix', () => {
Copy link
Contributor

@aleksandernsilva aleksandernsilva Feb 4, 2026

Choose a reason for hiding this comment

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

Although this is a test suite aimed at the race condition fix, describing it like that seems very specific imo. I recommend changing to something along the lines of start and stop edge case handling or Asynchronous start and stop handling, etc.

Something that describes what we are testing instead of why the tests were added.

Comment on lines 58 to 60
const streamPromise = new Promise<MediaStream>((resolve) => {
setTimeout(() => resolve(mockStream), 100);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

You could reduce the use of setTimeout in this file by using the util createDeferredPromise. In this this instance the implementation could change to something like:

const streamDeferred = createDeferredPromise<MediaStream>();

getUserMediaMock.mockReturnValue(streamDeferred.promise);

// [...]

streamDeferred.resolve(mockStream);

// [...]

VideoRecorder.stop();

await streamPromise;
await new Promise((resolve) => setTimeout(resolve, 150));
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we take advantage of jest fake timers and jest.runAllTicks to eliminate some of these arbitrary timeouts?

Comment on lines 93 to 101
const stream1 = {
getVideoTracks: jest.fn(() => [{ stop: jest.fn() } as unknown as MediaStreamTrack]),
getAudioTracks: jest.fn(() => [{ stop: jest.fn() } as unknown as MediaStreamTrack]),
} as unknown as MediaStream;

const stream2 = {
getVideoTracks: jest.fn(() => [mockVideoTrack]),
getAudioTracks: jest.fn(() => [mockAudioTrack]),
} as unknown as MediaStream;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: You could create a small utility function to facilitate creating these mock streams. Would improve code readability as well to some extent,

Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
@dougfabris dougfabris added this to the 8.2.0 milestone Feb 5, 2026
stop: jest.fn(),
} as unknown as MediaStreamTrack;

mockStream = {
Copy link
Contributor

Choose a reason for hiding this comment

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

mockStream = createMockStream(...)

Comment on lines +78 to +80
await Promise.resolve();
jest.runAllTimers();
await Promise.resolve();
Copy link
Contributor

Choose a reason for hiding this comment

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

await Promise.resolve() is doing all the heavy lifting here, rendering the fake timers unnecessary. This applies to all other tests in this file.

Suggested change
await Promise.resolve();
jest.runAllTimers();
await Promise.resolve();
await jest.runAllTimersAsync();

getAudioTracks: jest.fn(() => [mockAudioTrack]),
} as unknown as MediaStream;

mockVideoElement = document.createElement('video');
Copy link
Contributor

Choose a reason for hiding this comment

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

jsdom doesn't seem to support any loading or playback media operations. When running these tests it ends up generating a lot of console.error's which makes it difficult to read the logs. Could you mock the necessary functions as well?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants