Skip to content

Mjpeg stream refactor - #409

Open
rwb27 wants to merge 11 commits into
mainfrom
mjpeg-stream-refactor
Open

rwb27 wants to merge 11 commits into
mainfrom
mjpeg-stream-refactor

Conversation

@rwb27

@rwb27 rwb27 commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

This PR strips out the custom pub/sub logic from MJPEGStream and replaces it with a couple of calls to MessageBroker. It goes further than #372 by actually using the MessageBroker object rather than just pinching a function from it. I think this results in a bigger improvement, from a smaller code change.

I hope this makes the code much more readable and much more testable.

The diff has more additions than deletions - but the deletions are (complicated) code while the additions are docstrings, comments, tests, and calls to much clearer code.

The pub/sub logic in MessageBroker is tested much more carefully than the old logic was, even though it was technically covered before.

I have written a fixture that lets me write async tests for the MJPEGStream independent of the server. It uses MessageBroker but not the server or FastAPI.

This PR passes both the old tests, and the relevant bits of the improved tests in #372, which are included in this PR.

OFM-Feature-Branch: v3-mjpeg-stream-refactor

This MR contains the following

  • Add some utility functions to MessageBroker
  • Add a new "stream" message type
  • Remove the ringbuffer and subscription mechanism from MJPEGStream
  • Publish JPEG frames via the MessageBroker

Before merge:

  • Test autofocus with real hardware
  • Add more unit tests
  • Don't dump JPEGS to the log whenever we drop a message.

Merge checklist:

  • All new/changed functions have up to date typehints and docstrings.
  • Any changes to the public API have been updated in docs/src/public_api.rst.
  • New or changed features have been added to (or updated in) the conceptual documentation.
  • Any new or updated dependencies have been added to the project configuration (e.g., pyproject.toml).
  • New functionality is fully tested.
  • Any decrease in test coverage has been justified.
  • Either the test-against-ofm-v3 job passes, or test-against-ofm-feature-branch passes.
  • New features have been used in a branch of the OpenFlexure Microscope, which is tested in test-against-ofm-feature-branch.
  • This code has been tested manually against simulated hardware (detail tests in "Before merge").
  • This code has been tested against real hardware (detail tests in "Before merge").

This should silence the pip audit warnings by updating to the most recent version.
@barecheck

barecheck Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Barecheck - Code coverage report

Total: 97.81%

Your code coverage diff: 0.38% ▴

Uncovered files and lines
FileLines
src/labthings_fastapi/actions.py601-602, 615, 651-652, 853, 994-995, 998, 1001, 1049
src/labthings_fastapi/message_broker.py202-203
src/labthings_fastapi/outputs/mjpeg_stream.py281, 339, 375

rwb27 added 9 commits October 13, 2026 21:28
`mypy` now flags my dynamic model creation as incorrectly typed.
I believe this is very close to the issue seen in:
pydantic/pydantic#3585

The solution there was to use a variable typed as `Any` rather than using the argument directly.
As that amounts to a stealthy type: ignore statement, I have opted to use
an explicit type ignore, with a justification.
I think that's clearer.
I've added "stream" as a message type, and provided "next message" and "close streams by affordance" methods.

This should prepare the way for using MessageBroker for MJPEG streams.
The thing server interface now exposes the message broker.
This strips out the pub/sub logic from MJPEGStream in favour of using
MessageBroker, as is already done for property notifications.

The resulting class should be much simpler.

I've also inherited from BaseDescriptor to save duplication in
MJPEGStreamDescriptor.
This uses an event to speed up tests by eliminating `time.sleep` and adds some more checks on the data received.
The stream no longer has the notion of being "active" so we can't check for errors there.

This might be a feature we want to revive - I'm not totally sure.
This is tested by a better-isolated test that mocks the server and uses a real MessageBroker.
The async generator actually works quite well with the new stream
based structure, so I've resinstated it. I've taken the opportunity to rename it, and return Frame objects, not just bytes. The old name remains for backwards compatibility.

This shrinks the code changes slightly, I think, and should also
make it neater for client code as there's less need to create/use/type
check streams directly.

I've added explicit tests for the new functions in message broker.
@rwb27
rwb27 force-pushed the mjpeg-stream-refactor branch from 2a7182c to 8d79b7f Compare October 13, 2026 20:35
@rwb27

rwb27 commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Testing this against the OFM simulator, I can see we need to do something in the log: currently it looks like it drops a message when a client disconnects from the stream, and that results in the bytes being dumped to the log/console, which is not desirable.

Also I managed to crash the simulator - I'm not sure if that's related to this change or not, but it bears investigating at least a little further.

The old MJPEG stream silently dropped frames.
We were logging every dropped frame at WARNING.
I've downgraded this to DEBUG for streams.
We may choose to turn it off entirely to replicate the old behaviour.

I have also altered the `repr` of `Frame` objects, to avoid dumping JPEG data to console/log accidentally.
@rwb27

rwb27 commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

I have fixed the log issue in two ways:

  • stream messages now only log at DEBUG when they drop frames. We may choose to downgrade this in the future to not logging at all.
  • Frame objects no longer print their bytes payload to the log or the console when dumped with repr or str.

I have not been able to reproduce the crash: what I can say is that my whole WSL2 environment crashed (including two different VSCode sessions) so it's possible that it got shut down for overloading my computer or something. The noise made by my fans when the simulator spins up suggests that is not unreasonable!

@rwb27

rwb27 commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

One point to note: currently, stand are being made with a max_buffer_size of 1 which means a slow consumer will lag by up to one frame. I think the old behaviour was equivalent to a zero buffer size. It may be possible to revert to zero buffer sizes, but might need some await anyio.sleep(0.001) in the relevant tests.

@rwb27 rwb27 mentioned this pull request Oct 15, 2026
1 task

This branch has not been deployed

No deployments
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.

1 participant