Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/labthings_fastapi/outputs/mjpeg_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@
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 timestamp is
the time that the frame was added to the ringbuffer."""

index: int
"""The index of the frame within the stream."""

Expand Down Expand Up @@ -191,15 +194,15 @@
:raise ValueError: if the frame is not available.
"""
if i < 0:
raise ValueError("i must be >= 0")

Check warning on line 197 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

197 line is not covered with tests
if i < self.last_frame_i - len(self._ringbuffer) + 2:
raise ValueError("the ith frame has been overwritten")

Check warning on line 199 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

199 line is not covered with tests
if i > self.last_frame_i:
# TODO: await the ith frame
raise ValueError("the ith frame has not yet been acquired")

Check warning on line 202 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

202 line is not covered with tests
entry = self._ringbuffer[i % len(self._ringbuffer)]
if entry.index != i:
raise ValueError("the ith frame has been overwritten")

Check warning on line 205 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

205 line is not covered with tests
return entry

async def next_frame(self) -> int:
Expand Down Expand Up @@ -227,9 +230,9 @@

:return: The next JPEG frame, as a `bytes` object.
"""
i = await self.next_frame()
entry = await self.ringbuffer_entry(i)
return entry.frame

Check warning on line 235 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

233-235 lines are not covered with tests

async def next_frame_size(self) -> int:
"""Wait for the next frame and return its size.
Expand All @@ -238,9 +241,9 @@

:return: The size of the next JPEG frame, in bytes.
"""
i = await self.next_frame()
entry = await self.ringbuffer_entry(i)
return len(entry.frame)

Check warning on line 246 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

244-246 lines are not covered with tests

async def frame_async_generator(self) -> AsyncGenerator[bytes, None]:
"""Yield frames as bytes objects.
Expand All @@ -260,12 +263,12 @@
yield entry.frame
except StopAsyncIteration:
break
except Exception as e: # noqa: BLE001

Check warning on line 266 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

266 line is not covered with tests
# It's important that errors in the stream don't crash the server.
# This may be something we can remove in the future, now streams stop
# more elegantly. However, it will require careful testing.f
logging.exception(f"Error in stream: {e}, stream stopped")
return

Check warning on line 271 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

270-271 lines are not covered with tests

async def mjpeg_stream_response(self) -> MJPEGStreamResponse:
"""Return a StreamingResponse that streams an MJPEG stream.
Expand All @@ -280,7 +283,7 @@
"""
return MJPEGStreamResponse(self.frame_async_generator())

def add_frame(self, frame: bytes) -> None:
def add_frame(self, frame: bytes, timestamp: Optional[datetime] = None) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I have favoured datetime | None but let's not worry about it until there's a linter rule to enforce consistency.

"""Add a JPEG to the MJPEG stream.

This function adds a frame to the stream. It may be called from
Expand All @@ -289,6 +292,9 @@
are handled.

:param frame: The frame to add
: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.
Expand All @@ -299,10 +305,10 @@
and frame[-2] == 0xFF
and frame[-1] == 0xD9
):
raise ValueError("Invalid JPEG")

Check warning on line 308 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

308 line is not covered with tests
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(
Expand All @@ -327,7 +333,7 @@
:raises RuntimeError: if the stream is still streaming.
"""
if self._streaming is True:
raise RuntimeError(

Check warning on line 336 in src/labthings_fastapi/outputs/mjpeg_stream.py

View workflow job for this annotation

GitHub Actions / coverage

336 line is not covered with tests
"This function should only be called when the stream is stopped."
)
async with self.condition:
Expand Down
Loading