Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import datetime
- import os
- import select
- import struct
- import kaiten.constants
- import kaiten.generators
- import kaiten.log
- import kaiten._bwcamera
- # Format numbers according to the kaiten API
- FMT_YUYV = 1
- FMT_JPEG = 2
- class CameraSettings(object):
- def __init__(self, width, height, format_nr, nr_buffers):
- self.width = width
- self.height = height
- self.format_nr = format_nr
- # Don't set < 2. More buffers == more latency, but
- # too few buffers hurts the framerate.
- self.nr_buffers = nr_buffers
- # Mapping of known camera types to standard resolution/formats
- SUPPORTED_CAMERAS = {
- 'Venus USB1.1 Camera': CameraSettings(320, 240, FMT_YUYV, 2),
- 'NMG HD WebCam': CameraSettings(640, 480, FMT_JPEG, 3),
- }
- class NoImageError(Exception):
- pass
- class GeneratorLock(object):
- """
- Like a thread lock, but for generators. Usage:
- lock = GeneratorLock()
- ...
- yield from lock.get_lock()
- with lock:
- ...
- """
- def __init__(self):
- self._locked = False
- def get_lock(self):
- while self._locked: yield
- def __enter__(self):
- if self._locked:
- raise RuntimeError("Tried to grab lock without calling get_lock()")
- self._locked = True
- def __exit__(self, e, v, tb):
- self._locked = False
- class CameraContract(object):
- """
- Wrapper around the camera IO generator. When the camera IO generator is
- running, it will constantly reschedule this generator so that this
- generator will never be iterated. When it is not running, this contract
- periodically tries to (re)start it.
- """
- def __init__(self, server, device_path=kaiten.constants.camera_dev_path):
- self._server = server
- self._dev_path = device_path
- self._log = kaiten.log.getlogger(self)
- self._camera = None
- self._init_fail_count = 0
- self._duration = datetime.timedelta(seconds=30)
- self._recurring_clients = []
- self._one_shot_clients = []
- self._one_shot_camera_frame_callbacks = []
- # Assume the device exists so that we only log when it does not
- self._dev_exists = True
- # Keep track of which clients we have not finished sending
- # a frame to. To limit server load we will not send another
- # frame to such clients.
- self._pending_clients = set()
- # On each iteration, we only grab a packed frame copy if a
- # client requests it, but we only grab one copy per iteration
- self._frame = None
- # Also limit the server load by capping the number of
- # clients we send a frame to on each iteration
- self._max_clients_per_frame = 5
- def request_frame(self, client):
- """
- Request that a single frame be sent to the given client.
- This will only succeed if a client is not already awaiting
- a frame that is still queued for sending. This will also
- unsubscribe the client from a stream.
- """
- if client in self._recurring_clients:
- self._recurring_clients.remove(client)
- return False
- elif client in self._one_shot_clients:
- return False
- elif client in self._pending_clients:
- return False
- else:
- self._one_shot_clients.append(client)
- return True
- def request_stream(self, client):
- """
- Subscribe the given client to a stream of frames.
- """
- if client not in self._recurring_clients:
- self._recurring_clients.append(client)
- def end_stream(self, client):
- """
- Subscribe the given client to a stream of frames.
- """
- if client in self._recurring_clients:
- self._recurring_clients.remove(client)
- def is_streaming(self):
- return bool(self._recurring_clients)
- def save_jpeg(self, path):
- if self._camera is None:
- raise NoImageError
- yield from self._camera.save_jpeg(path)
- def close(self):
- if self._camera:
- self._camera.close()
- self._server._iopoll.unregister(self._camera)
- self._camera = None
- def contract_duration(self):
- return self._duration
- def expected_run_time(self):
- return datetime.timedelta(seconds=kaiten.constants.normal_generator_time)
- def _ensure_frame(self):
- if self._frame is not None:
- return
- elif self._camera is None:
- self._frame = Camera.empty_frame
- else:
- self._frame = self._camera.get_latest_packed_frame()
- def set_one_shot_frame_callback(self, callback):
- self._one_shot_camera_frame_callbacks.append(callback)
- def _send_frame(self, client):
- def sent_callback():
- self._pending_clients -= {client}
- self._pending_clients |= {client}
- self._ensure_frame()
- method = 'camera_frame'
- params = {}
- self._server.notify_client(
- client, method, params, sent_callback, self._frame
- )
- def _check_client(self, client):
- """ Determine if this client should be sent a frame """
- if client in self._pending_clients:
- return False
- data_rate_limiter = getattr(client, 'data_rate_limiter', None)
- if data_rate_limiter:
- # We need to know the size of the frame to limit on data rate
- self._ensure_frame()
- return data_rate_limiter.try_data(len(self._frame))
- return True
- def _defer(self):
- self._server.reschedule_contract_generator(self)
- def _send_frames(self):
- self._defer()
- client_count = 0
- client_recur = []
- #giving the one shot callbacks priority over clients
- if self._one_shot_camera_frame_callbacks:
- client_count += 1
- self._ensure_frame()
- callback = self._one_shot_camera_frame_callbacks.pop(0)
- callback(self._frame)
- while client_count < self._max_clients_per_frame:
- if self._one_shot_clients:
- client = self._one_shot_clients.pop(0)
- elif self._recurring_clients:
- client = self._recurring_clients.pop(0)
- client_recur.append(client)
- else:
- break
- if not self._check_client(client):
- continue
- client_count += 1
- try:
- self._send_frame(client)
- except Exception:
- # Blame the client
- self._pending_clients -= {client}
- if client in client_recur:
- client_recur.remove(client)
- self._recurring_clients.extend(client_recur)
- self._frame = None
- def _hotplug_update(self):
- """
- We don't have actual hotplug notifications for the camera, we
- just periodically check if the device exists. This function
- performs that check and logs whenever the status changes.
- """
- dev_exists = os.path.exists(self._dev_path)
- if self._dev_exists and not dev_exists:
- self._log.warning('Camera device disappeared')
- if not self._dev_exists and dev_exists:
- self._log.info('Camera device found')
- self._dev_exists = dev_exists
- def __next__(self):
- self._hotplug_update()
- if not self._dev_exists:
- # Close the camera if it exists, but don't log anything
- # here since we already logged the device disappearance
- self._init_fail_count = 0
- self.close()
- return
- if self._camera:
- self._log.info('Dropping unresponsive camera object')
- self.close()
- try:
- self._camera = Camera(self._dev_path, frame_cb=self._send_frames)
- self._server._iopoll.register(self._camera)
- except Exception:
- self._init_fail_count += 1
- msg = 'Failed to start camera stream'
- if self._init_fail_count > 2:
- return
- elif self._init_fail_count == 2:
- msg += ' (further failures will not be logged)'
- self._log.error(msg, exc_info=True)
- else:
- self._init_fail_count = 0
- self._log.info('Started camera stream')
- class Camera(kaiten.generators.IOGenerator):
- """
- Camera object to interact with and store the state of the camera.
- """
- def __init__(self, device_path, frame_cb=None):
- self._log = kaiten.log.getlogger(self)
- self._dev = kaiten._bwcamera.Camera(device_path)
- self._frame_cb = frame_cb
- self._latest_buffer = None
- self._lock = GeneratorLock()
- self._initialize()
- super(Camera, self).__init__(self._stream(), self._dev, True, False)
- def _set_format(self):
- """
- _set_format will attempt to set the width, height, and image encoding
- according to the type of camera detected. It will also create buffers
- """
- camera_type = self._dev.card
- settings = SUPPORTED_CAMERAS[camera_type]
- # TODO allow custom format via config file or args
- self._dev.set_format(settings.width, settings.height,
- settings.format_nr)
- self._dev.create_buffers(settings.nr_buffers)
- # when you set the format, sometimes the device will reject it
- # and set different parameters. We check if this happens here.
- fmt = self._dev.format
- if fmt.width != settings.width or fmt.height != settings.height:
- self._log.warning(
- "Actual w/h (%d/%d) differs from requested (%d/%d)",
- fmt.width, fmt.height,
- settings.width, settings.height)
- empty_frame = struct.pack("!IIII", 16, 0, 0, 0)
- def get_latest_packed_frame(self):
- """
- get_latest_packed_frame will pack the cached image data
- into the following structure:
- uint32 total_blob_size
- uint32 image_width
- uint32 image_height
- uint32 pixelformat (0 = Invalid image, 1 = YUYV, 2 = JPEG)
- char[] latest_cached_image
- """
- # Because we do not yield and only make a copy of _latest_buffer,
- # we do not need to acquire self._lock here
- if self._latest_buffer is not None:
- fmt = self._latest_buffer.format
- data = memoryview(self._latest_buffer)
- total_size = 16 + len(data)
- header = struct.pack("!IIII", total_size, fmt.width,
- fmt.height, fmt.format_nr)
- return header + data
- else:
- return self.empty_frame
- def save_jpeg(self, path, rows_per_chunk=10):
- """
- save_jpeg will attempt to encode the current frame to a jpeg and save
- it to the supplied path on the filesystem. This is CPU intensive, so
- it is implemented as a generator that must be exhausted to complete
- the encoding.
- """
- yield from self._lock.get_lock()
- with self._lock:
- if self._latest_buffer is None:
- raise NoImageError()
- fmt = self._latest_buffer.format
- data = memoryview(self._latest_buffer)
- if fmt.format_nr == FMT_JPEG:
- with open(path, 'wb') as f:
- f.write(data)
- return
- writer = kaiten._bwcamera.JpegWriter(fmt.width, fmt.height, path)
- # Each pixel is two bytes
- chunk_size = 2 * fmt.width * rows_per_chunk
- for index in range(0, len(data), chunk_size):
- writer.send(data[index:index+chunk_size])
- yield
- def _initialize(self):
- """
- _initialize opens the camera device, sets the format and starts the
- camera stream.
- """
- self._dev.open_device()
- self._set_format()
- # Start the stream
- self._dev.start_stream()
- # Queue up all but one buffer
- for buff in self._dev.buffers[:-1]:
- self._dev.queue_buffer(buff)
- def _stream(self):
- """
- _stream will start pulling frames from the video device into
- self._latest_buffer, which will be a CameraBuffer Instance.
- """
- try:
- next_buff = self._dev.buffers[-1]
- while True:
- # Don't requeue the current buffer if we are using it
- yield from self._lock.get_lock()
- # Swap the buffers around
- self._dev.queue_buffer(next_buff)
- next_buff = self._dev.dequeue_buffer()
- self._latest_buffer = next_buff
- if self._frame_cb:
- self._frame_cb()
- yield
- finally:
- self._cleanup()
- def _cleanup(self):
- """
- _cleanup is a utility function that attempts to shut off
- the camera stream, clear all existing buffers, and close
- the camera file descriptor.
- Please note, that if a camera object exists, and is closed
- without calling cleanup, it is likely a reboot will be
- required to utilize the camera again.
- """
- try:
- self._dev.stop_stream()
- except Exception:
- self._log.warning('stop_stream failed', exc_info=True)
- self._latest_buffer = None
- self._dev.clear_buffers()
- try:
- self._dev.close_device()
- except Exception:
- self._log.warning('close_device failed', exc_info=True)
- def __del__(self):
- self._cleanup()
Add Comment
Please, Sign In to add comment