From 62e61fcda7d10a2d32646f91cbacf6a800db06ab Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Fri, 10 Jul 2026 11:30:40 +0100 Subject: [PATCH 1/2] Allow the timestamp for a frame to be supplied to the MJPEGStream. Cameras can report their capture timestamp. This allows add_frame to accept the timestamp (and fallback to datetime.now()) if no timestamp is supplied. This allows for more accurate timestamps for the frame which is useful if the frametime needs to be known relative to anothe event. --- src/labthings_fastapi/outputs/mjpeg_stream.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/labthings_fastapi/outputs/mjpeg_stream.py b/src/labthings_fastapi/outputs/mjpeg_stream.py index 91dca790..7673ad91 100644 --- a/src/labthings_fastapi/outputs/mjpeg_stream.py +++ b/src/labthings_fastapi/outputs/mjpeg_stream.py @@ -43,7 +43,10 @@ class RingbufferEntry: frame: bytes """The frame as a `bytes` object, which is a JPEG image for an MJPEG stream.""" timestamp: datetime - """The time the frame was added to the ringbuffer.""" + """The time the frame was captured. + + If the capture time is unknown then the it was added to the buffer.""" + index: int """The index of the frame within the stream.""" @@ -280,7 +283,7 @@ async def mjpeg_stream_response(self) -> MJPEGStreamResponse: """ return MJPEGStreamResponse(self.frame_async_generator()) - def add_frame(self, frame: bytes) -> None: + def add_frame(self, frame: bytes, timestamp: Optional[datetime] = None) -> None: """Add a JPEG to the MJPEG stream. This function adds a frame to the stream. It may be called from @@ -289,6 +292,7 @@ def add_frame(self, frame: bytes) -> None: are handled. :param frame: The frame to add + :param timestamp: The time the frame was captured. :raise ValueError: if the supplied frame does not start with the JPEG start bytes and end with the end bytes. @@ -302,7 +306,7 @@ def add_frame(self, frame: bytes) -> None: raise ValueError("Invalid JPEG") with self._lock: entry = self._ringbuffer[(self.last_frame_i + 1) % len(self._ringbuffer)] - entry.timestamp = datetime.now() + entry.timestamp = timestamp if timestamp is not None else datetime.now() entry.frame = frame entry.index = self.last_frame_i + 1 self._thing_server_interface.start_async_task_soon( From 5e070039150c667052a5ac2b4d1bfb75ef091988 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Sat, 11 Jul 2026 07:52:24 +0100 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Beth <167304066+bprobert97@users.noreply.github.com> Co-authored-by: Julian Stirling --- src/labthings_fastapi/outputs/mjpeg_stream.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/labthings_fastapi/outputs/mjpeg_stream.py b/src/labthings_fastapi/outputs/mjpeg_stream.py index 7673ad91..39f4d7e2 100644 --- a/src/labthings_fastapi/outputs/mjpeg_stream.py +++ b/src/labthings_fastapi/outputs/mjpeg_stream.py @@ -44,8 +44,8 @@ class RingbufferEntry: """The frame as a `bytes` object, which is a JPEG image for an MJPEG stream.""" timestamp: datetime """The time the frame was captured. - - If the capture time is unknown then the it was added to the buffer.""" + If the capture time is unknown then the timestamp is + the time that the frame was added to the ringbuffer.""" index: int """The index of the frame within the stream.""" @@ -292,7 +292,9 @@ def add_frame(self, frame: bytes, timestamp: Optional[datetime] = None) -> None: are handled. :param frame: The frame to add - :param timestamp: The time the frame was captured. + :param timestamp: The time the frame was captured. If not supplied, + the timestamp falls back to the time the frame was added to the + ringbuffer. :raise ValueError: if the supplied frame does not start with the JPEG start bytes and end with the end bytes.