Merge branch 'better-bitrate-controls' into 'master'
Better stream bitrate controls See merge request openflexure/openflexure-microscope-server!106
This commit is contained in:
commit
e6c63e2eaf
3 changed files with 184 additions and 91 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue