Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 12, 2026

Summary

The CameraMJPEGPlayerViewModel class is marked @MainActor but was accessing MainActor-isolated properties from background thread callbacks without proper synchronization. This creates race conditions and potential crashes.

Changes:

  • Wrapped startStream() method call in DispatchQueue.main.async when invoked from background .pipe callback
  • Wrapped errorMessage property assignment in DispatchQueue.main.async in the rejected case
  • Removed redundant hasStarted = false assignment that could race with the cleanup block

All MainActor-isolated members are now accessed exclusively on the main thread.

Screenshots

N/A - Internal thread safety fix with no UI changes

Link to pull request in Documentation repository

N/A - Internal implementation change

Any other notes

This PR merges into #4213 and addresses feedback from #4213 (comment)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] WIP Address feedback on simplifying CameraView MPEG implementation Fix thread safety for MainActor property access in MJPEG stream callbacks Jan 12, 2026
Copilot AI requested a review from bgoncal January 12, 2026 20:03
@bgoncal bgoncal marked this pull request as ready for review January 12, 2026 20:05
Copilot AI review requested due to automatic review settings January 12, 2026 20:05
@bgoncal bgoncal merged commit ee8e4e9 into Simplify-MJPEG-player Jan 12, 2026
1 check passed
@bgoncal bgoncal deleted the copilot/sub-pr-4213-another-one branch January 12, 2026 20:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses thread safety issues in the CameraMJPEGPlayerViewModel class by ensuring all MainActor-isolated property accesses from background thread callbacks are wrapped in DispatchQueue.main.async. The changes prevent race conditions and potential crashes when accessing @MainActor properties from non-main threads.

Changes:

  • Wrapped startStream() call in DispatchQueue.main.async to ensure MainActor-isolated method is called on main thread
  • Wrapped errorMessage property assignment in DispatchQueue.main.async in the error rejection case
  • Removed redundant hasStarted = false assignment from the success path
Comments suppressed due to low confidence (1)

Sources/App/Cameras/CameraPlayer/MJPEG/CameraMJPEGPlayerViewModel.swift:55

  • The cleanup block at lines 52-55 sets hasStarted = false immediately after the .pipe callback completes, but the startStream() call on line 40 is now asynchronous. This creates a race condition where hasStarted could be reset to false before startStream() even executes on the main thread. This means a subsequent call to start() could pass the guard check on line 25 and create duplicate streams.

Consider moving the cleanup logic into the completion handlers for both success and error cases, or use a different synchronization mechanism to ensure hasStarted is only reset after the stream has actually started or definitively failed.

            DispatchQueue.main.async { [weak self] in
                self?.isLoading = false
                self?.hasStarted = false
            }

self?.startStream(url, api: api)
}
} else {
Current.Log.error("Failed to get active URL for server \(api.server.info.name)")
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

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

In the error path where the active URL cannot be obtained, the code logs an error but doesn't update the UI state. The isLoading flag remains true and hasStarted remains true (until the cleanup block executes), leaving the UI in an inconsistent loading state. Consider setting appropriate error state here similar to the rejected case.

Suggested change
Current.Log.error("Failed to get active URL for server \(api.server.info.name)")
Current.Log.error("Failed to get active URL for server \(api.server.info.name)")
DispatchQueue.main.async { [weak self] in
self?.errorMessage = StreamError.unableToConnect.localizedDescription
}

Copilot uses AI. Check for mistakes.
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.

2 participants