Merge branch 'better-bitrate-controls' into 'master'

Better stream bitrate controls

See merge request openflexure/openflexure-microscope-server!106
This commit is contained in:
Joel Collins 2020-12-09 11:55:38 +00:00
commit e6c63e2eaf
3 changed files with 184 additions and 91 deletions

View file

@ -5,93 +5,154 @@
<h3>Manual camera settings</h3> <h3>Manual camera settings</h3>
<form @submit.prevent="applySettingsRequest"> <form @submit.prevent="applySettingsRequest">
<div class="uk-margin-small-bottom"> <div class="uk-margin-small-bottom">
<h4>Raspberry Pi Camera</h4> <ul uk-accordion="multiple: true">
<!--PiCamera settings block--> <li class="uk-open">
<div v-if="picamera.shutter_speed !== undefined"> <a class="uk-accordion-title" href="#">Pi Camera Settings</a>
<label class="uk-form-label" for="form-stacked-text" <div class="uk-accordion-content">
>Exposure time</label <div v-if="picamera.shutter_speed !== undefined">
> <label class="uk-form-label" for="form-stacked-text"
<div class="uk-form-controls"> >Exposure time</label
<input >
v-model="picamera.shutter_speed" <div class="uk-form-controls">
class="uk-input uk-form-small" <input
type="number" v-model="picamera.shutter_speed"
/> class="uk-input uk-form-small"
</div> type="number"
</div> />
</div>
</div>
<div v-if="picamera.analog_gain !== undefined"> <div v-if="picamera.analog_gain !== undefined">
<label class="uk-form-label" for="form-stacked-text" <label class="uk-form-label" for="form-stacked-text"
>Analogue gain</label >Analogue gain</label
> >
<div class="uk-form-controls"> <div class="uk-form-controls">
<input <input
v-model="picamera.analog_gain" v-model="picamera.analog_gain"
class="uk-input uk-form-small" class="uk-input uk-form-small"
type="number" type="number"
step="0.000001" step="0.000001"
/> />
</div> </div>
</div> </div>
<div v-if="picamera.digital_gain !== undefined"> <div v-if="picamera.digital_gain !== undefined">
<label class="uk-form-label" for="form-stacked-text" <label class="uk-form-label" for="form-stacked-text"
>Digital gain</label >Digital gain</label
> >
<div class="uk-form-controls"> <div class="uk-form-controls">
<input <input
v-model="picamera.digital_gain" v-model="picamera.digital_gain"
class="uk-input uk-form-small" class="uk-input uk-form-small"
type="number" type="number"
step="0.000001" step="0.000001"
/> />
</div> </div>
</div> </div>
</div>
<div class="uk-margin-small-bottom">
<h4>Image quality</h4>
<div class="uk-child-width-1-2" uk-grid>
<div v-if="mjpeg_bitrate !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>Web stream bitrate</label
>
<select v-model="mjpeg_bitrate" class="uk-select uk-form-small">
<option
v-for="option in bitrateOptions"
:key="option.value"
:value="option.value"
>
{{ option.text }}
</option>
</select>
</div>
<div v-if="jpeg_quality !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>JPEG capture quality (%)</label
>
<div class="uk-form-controls">
<input
v-model="jpeg_quality"
class="uk-input uk-form-small"
type="number"
step="1"
/>
</div> </div>
</div> </li>
</div>
<span <li class="uk-open">
v-if=" <a class="uk-accordion-title" href="#">Image Quality</a>
mjpeg_bitrate !== undefined && <div class="uk-accordion-content">
mjpeg_bitrate < 17000000 && <div class="uk-child-width-1-2" uk-grid>
mjpeg_bitrate != -1 <div v-if="jpeg_quality !== undefined">
" <label class="uk-form-label" for="form-stacked-text"
class="uk-text-danger uk-margin-top-small" >JPEG capture quality (%)</label
>This stream bitrate may impact fast autofocus performance</span >
> <div class="uk-form-controls">
<input
v-model="jpeg_quality"
class="uk-input uk-form-small"
type="number"
step="1"
/>
</div>
</div>
<div v-if="stream_resolution !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>Stream resolution</label
>
<select
v-model="stream_resolution"
class="uk-select uk-form-small"
>
<option
v-for="option in resolutionOptions"
:key="option.value[0]"
:value="option.value"
>
{{ option.text }}
</option>
</select>
</div>
</div>
</div>
</li>
<li>
<a class="uk-accordion-title" href="#">Advanced</a>
<div class="uk-accordion-content">
<div v-if="mjpeg_bitrate !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>Camera bitrate</label
>
<select
v-model="mjpeg_bitrate"
class="uk-select uk-form-small"
>
<option
v-for="option in bitrateOptions"
:key="option.value"
:value="option.value"
>
{{ option.text }}
</option>
</select>
<p class="uk-margin-remove">
This option sets the target bitrate for camera
recordings.<br />
<b
>Limiting bitrate may impact fast-autofocus
reliability.</b
>
However is guaranteed to fix the live stream disappearing
for some highly detailed samples.
</p>
</div>
<div
v-if="picamera.framerate !== undefined"
class="uk-margin-top"
>
<label class="uk-form-label" for="form-stacked-text"
>Camera framerate</label
>
<select
v-model="picamera.framerate"
class="uk-select uk-form-small"
>
<option
v-for="option in framerateOptions"
:key="option.value"
:value="option.value"
>
{{ option.text }}
</option>
</select>
<p class="uk-margin-remove">
This option sets the framerate of the Pi Camera video
encoder. The actual stream framerate is determined by
network speed, and is limited by the encoder framerate.<br />
<b
>Lowering framerate may impact fast-autofocus
accuracy.</b
>
</p>
</div>
</div>
</li>
</ul>
</div> </div>
<button <button
@ -132,16 +193,27 @@ export default {
picamera: { picamera: {
shutter_speed: undefined, shutter_speed: undefined,
analog_gain: undefined, analog_gain: undefined,
digital_gain: undefined digital_gain: undefined,
framerate: undefined
}, },
mjpeg_bitrate: undefined, mjpeg_bitrate: undefined,
stream_resolution: undefined,
jpeg_quality: undefined, jpeg_quality: undefined,
bitrateOptions: [ bitrateOptions: [
{ text: "Maximum", value: -1 }, { text: "Maximum (unlimited)", value: -1 },
{ text: "High", value: 25000000 }, { text: "High (25Mbps)", value: 25000000 },
{ text: "Normal", value: 17000000 }, { text: "Normal (17Mbps)", value: 17000000 },
{ text: "Low", value: 5000000 }, { text: "Low (5Mbps)", value: 5000000 },
{ text: "Very low", value: 2500000 } { text: "Very low (2.5Mbps)", value: 2500000 }
],
resolutionOptions: [
{ text: "Higher (832, 624)", value: [832, 624] },
{ text: "Normal (640, 480)", value: [640, 480] }
],
framerateOptions: [
{ text: "Normal (30fps)", value: 30 },
{ text: "Low (15fps)", value: 15 },
{ text: "Very low (10fps)", value: 10 }
] ]
}; };
}, },
@ -165,11 +237,13 @@ export default {
// Get base camera settings // Get base camera settings
this.mjpeg_bitrate = cameraSettings.mjpeg_bitrate; this.mjpeg_bitrate = cameraSettings.mjpeg_bitrate;
this.jpeg_quality = cameraSettings.jpeg_quality; this.jpeg_quality = cameraSettings.jpeg_quality;
this.stream_resolution = cameraSettings.stream_resolution;
// Get Pi Camera settings if they exist // Get Pi Camera settings if they exist
if (cameraSettings.picamera) { if (cameraSettings.picamera) {
this.picamera.analog_gain = cameraSettings.picamera.analog_gain; this.picamera.analog_gain = cameraSettings.picamera.analog_gain;
this.picamera.digital_gain = cameraSettings.picamera.digital_gain; this.picamera.digital_gain = cameraSettings.picamera.digital_gain;
this.picamera.shutter_speed = cameraSettings.picamera.shutter_speed; this.picamera.shutter_speed = cameraSettings.picamera.shutter_speed;
this.picamera.framerate = cameraSettings.picamera.framerate;
} }
}) })
.catch(error => { .catch(error => {
@ -184,10 +258,12 @@ export default {
camera: { camera: {
mjpeg_bitrate: parseInt(this.mjpeg_bitrate), mjpeg_bitrate: parseInt(this.mjpeg_bitrate),
jpeg_quality: parseInt(this.jpeg_quality), jpeg_quality: parseInt(this.jpeg_quality),
stream_resolution: this.stream_resolution,
picamera: { picamera: {
shutter_speed: parseFloat(this.picamera.shutter_speed), shutter_speed: parseFloat(this.picamera.shutter_speed),
analog_gain: parseFloat(this.picamera.analog_gain), analog_gain: parseFloat(this.picamera.analog_gain),
digital_gain: parseFloat(this.picamera.digital_gain) digital_gain: parseFloat(this.picamera.digital_gain),
framerate: parseInt(this.picamera.framerate)
} }
} }
}; };

View file

@ -8,6 +8,7 @@ from typing import BinaryIO, List, NamedTuple, Optional, Tuple, Type, Union
from labthings import ClientEvent, StrictLock from labthings import ClientEvent, StrictLock
JPEG_END_BYTES: bytes = b"\xff\xd9"
# Class to store a frames metadata # Class to store a frames metadata
class TrackerFrame(NamedTuple): class TrackerFrame(NamedTuple):
@ -42,6 +43,8 @@ class FrameStream(io.BytesIO):
# We use a ClientEvent so that each thread can call getvalue() independantly # We use a ClientEvent so that each thread can call getvalue() independantly
self.new_frame: ClientEvent = ClientEvent() self.new_frame: ClientEvent = ClientEvent()
self._bad_frame: bool = False
def __enter__(self): def __enter__(self):
self.start_tracking() self.start_tracking()
return super().__enter__() return super().__enter__()
@ -79,6 +82,20 @@ class FrameStream(io.BytesIO):
3. Store the new frame image 3. Store the new frame image
4. Set the new_frame event 4. Set the new_frame event
""" """
# If we get a bad frame, and the last frame was good
if s[-2:] != JPEG_END_BYTES and not self._bad_frame:
# TODO: Handle this more cleverly. Automatically lower bitrate to compensate?
# Log error
logging.error(
"Incomplete frame data recieved. Camera bandwidth may have been exceeded. Consider lowing resolution, framerate, or target bitrate."
)
# Record that last frame was bad
self._bad_frame = True
# If the last frame was bad, but this frame was good
elif self._bad_frame and s[-2:] == JPEG_END_BYTES:
# Clear the bad frame record
self._bad_frame = False
# If we're tracking frame size
if self.tracking: if self.tracking:
frame = TrackerFrame(size=len(s), time=time.time()) frame = TrackerFrame(size=len(s), time=time.time())
self.frames.append(frame) self.frames.append(frame)

View file

@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry] [tool.poetry]
name = "openflexure-microscope-server" name = "openflexure-microscope-server"
version = "2.9.1" version = "2.9.2"
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope." description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
authors = [ authors = [