Beispiel #1
0
 def close(self):
     """
     Close a playing or paused video stream session
     """
     if self.state in (PdrawState.Opened, PdrawState.Paused, PdrawState.Playing, PdrawState.Error):
         self.logger.debug("pdraw closing from the {} state".format(self.state))
         self._close_resp_future = Future(self.pdraw_thread_loop)
         self._close_resp_future.add_done_callback(self._on_close_resp_done)
         f = self._close_resp_future
         self.state = PdrawState.Closing
         self.pdraw_thread_loop.run_async(self._close_stream)
     elif self.state is not PdrawState.Closing:
         f = Future(self.pdraw_thread_loop)
         f.set_result(False)
     else:
         f = self._close_resp_future
     return f
Beispiel #2
0
class Pdraw(object):
    def __init__(
        self,
        name=None,
        device_name=None,
        buffer_queue_size=8,
        legacy=False,
        pdraw_thread_loop=None,
    ):
        """
        :param name: (optional) pdraw client name (used by Olympe logs)
        :type name: str
        :param device_name: (optional) the drone device name (used by Olympe logs)
        :type device_name: str
        :param buffer_queue_size: (optional) video buffer queue size (defaults to 8)
        :type buffer_queue_size: int
        :param legacy: Defaults to False, set this parameter to True for legacy
            drones (Bebop, Disco, ...) streaming support
        :type legacy: bool
        """

        self.name = name
        self.device_name = device_name
        if self.name is not None:
            self.logging = getLogger("olympe.{}.pdraw".format(self.name))
        elif self.device_name is not None:
            self.logging = getLogger("olympe.pdraw.{}".format(
                self.device_name))
        else:
            self.logging = getLogger("olympe.pdraw")

        if pdraw_thread_loop is None:
            self.pdraw_thread_loop = PompLoopThread(self.logging)
            self.pdraw_thread_loop.start()
        else:
            self.pdraw_thread_loop = pdraw_thread_loop

        self.callbacks_thread_loop = PompLoopThread(self.logging)
        self.callbacks_thread_loop.start()
        self.buffer_queue_size = buffer_queue_size
        self.pomp_loop = self.pdraw_thread_loop.pomp_loop
        self._legacy = legacy

        self._open_resp_future = Future(self.pdraw_thread_loop)
        self._close_resp_future = Future(self.pdraw_thread_loop)
        self._close_resp_future.add_done_callback(self._on_close_resp_done)
        self._play_resp_future = Future(self.pdraw_thread_loop)
        self._pause_resp_future = Future(self.pdraw_thread_loop)
        self._state = PdrawState.Created
        self._state_lock = threading.Lock()
        self._state_wait_events = {k: list() for k in PdrawState}

        self.pdraw = od.POINTER_T(od.struct_pdraw)()
        self.streams = defaultdict(StreamFactory)
        self.session_metadata = {}

        self.outfiles = {
            od.PDRAW_VIDEO_MEDIA_FORMAT_H264: {
                'data': None,
                'meta': None,
                'info': None,
            },
            od.PDRAW_VIDEO_MEDIA_FORMAT_YUV: {
                'data': None,
                'meta': None,
                'info': None,
            },
        }

        self.frame_callbacks = {
            od.PDRAW_VIDEO_MEDIA_FORMAT_H264: None,
            od.PDRAW_VIDEO_MEDIA_FORMAT_YUV: None,
        }
        self.start_callback = None
        self.end_callback = None
        self.flush_callbacks = {
            od.PDRAW_VIDEO_MEDIA_FORMAT_H264: None,
            od.PDRAW_VIDEO_MEDIA_FORMAT_YUV: None,
        }

        self.url = None
        self.server_addr = None
        self.resource_name = "live"
        self.media_name = None

        self.local_stream_port = PDRAW_LOCAL_STREAM_PORT
        self.local_control_port = PDRAW_LOCAL_CONTROL_PORT

        self.cbs = od.struct_pdraw_cbs.bind({
            "open_resp": self._open_resp,
            "close_resp": self._close_resp,
            "ready_to_play": self._ready_to_play,
            "play_resp": self._play_resp,
            "pause_resp": self._pause_resp,
            "seek_resp": self._seek_resp,
            "socket_created": self._socket_created,
            "select_demuxer_media": self._select_demuxer_media,
            "media_added": self._media_added,
            "media_removed": self._media_removed,
            "end_of_range": self._end_of_range,
        })

        self.video_sink_cb = od.struct_pdraw_video_sink_cbs.bind(
            {"flush": self._video_sink_flush})

        self.vbuf_cbs = od.struct_vbuf_cbs()
        res = od.vbuf_generic_get_cbs(ctypes.pointer(self.vbuf_cbs))
        if res != 0:
            msg = "Error while creating vbuf generic callbacks {}".format(res)
            self.logging.error(msg)
            raise RuntimeError("ERROR: {}".format(msg))

        self.yuv_packed_buffer_pool = od.POINTER_T(od.struct_vbuf_pool)()
        res = od.vbuf_pool_new(self.buffer_queue_size, 0, 0, self.vbuf_cbs,
                               ctypes.byref(self.yuv_packed_buffer_pool))
        if res != 0:
            msg = "Error while creating yuv packged buffer pool {}".format(res)
            self.logging.error(msg)
            raise RuntimeError("ERROR: {}".format(msg))

        self.pdraw_thread_loop.register_cleanup(self.dispose)

    @property
    def state(self):
        """
        Return the current Pdraw state

        :rtype: PdrawState
        """
        return self._state

    @state.setter
    def state(self, value):
        with self._state_lock:
            self._state = value
            for event in self._state_wait_events[self._state]:
                event.set()
            self._state_wait_events[self._state] = []

    def wait(self, state, timeout=None):
        """
        Wait for the provided Pdraw state

        This function returns True when the requested state is reached or False
        if the timeout duration is reached.

        If the requested state is already reached, this function returns True
        immediately.

        This function may block indefinitely when called without a timeout
        value.

        :type state: PdrawState
        :param timeout: the timeout duration in seconds or None (the default)
        :type timeout: float
        :rtype: bool
        """
        with self._state_lock:
            if self._state == state:
                return True
            event = threading.Event()
            self._state_wait_events[state].append(event)
        return event.wait(timeout=timeout)

    @callback_decorator()
    def dispose(self):
        # cleanup some FDs from the callbacks thread loop that might have been lost
        for stream in self.streams.values():
            if stream['video_queue_event'] is not None:
                self.logging.warning(
                    "cleanup leftover pdraw callbacks eventfds")
                self.callbacks_thread_loop.remove_event_from_loop(
                    stream['video_queue_event'])
                stream['video_queue_event'] = None
        if self.callbacks_thread_loop.stop():
            self.logging.info("pdraw callbacks thread loop stopped")
        return self.pdraw_thread_loop.run_async(self._dispose_impl)

    @callback_decorator()
    def _dispose_impl(self):
        f = self.close().then(lambda _: self._destroy(), deferred=True)
        return f

    @callback_decorator()
    def _destroy(self):
        if self.pdraw:
            self.logging.info("destroying pdraw...")
            res = od.pdraw_destroy(self.pdraw)
            if res != 0:
                self.logging.error("cannot destroy pdraw {}".format(res))
            else:
                self.logging.info("pdraw destroyed")
        if self.yuv_packed_buffer_pool:
            self.logging.info("destroying yuv buffer pool...")
            res = od.vbuf_pool_destroy(self.yuv_packed_buffer_pool)
            if res != 0:
                self.logging.error(
                    "cannot destroy yuv packed buffer pool: {}".format(res))
            else:
                self.logging.info("yuv buffer pool destroyed")
        self.yuv_packed_buffer_pool = od.POINTER_T(od.struct_vbuf_pool)()
        self.pdraw = od.POINTER_T(od.struct_pdraw)()
        if self.pdraw_thread_loop.stop():
            self.logging.info("pdraw thread loop stopped")
        return True

    def _open_single_stream(self):
        """
        Opening pdraw single stream (legacy API)
        """
        res = od.pdraw_open_single_stream(
            self.pdraw, PDRAW_LOCAL_ADDR, self.local_stream_port,
            self.local_control_port, self.server_addr,
            PDRAW_REMOTE_STREAM_PORT, PDRAW_REMOTE_CONTROL_PORT,
            PDRAW_IFACE_ADRR)

        if res != 0:
            self.logging.error(
                "Error while opening pdraw single stream: {}".format(res))
            return False
        else:
            self.logging.info("Opening pdraw single stream OK")
        return True

    def _open_url(self):
        """
        Opening rtsp streaming url
        """
        if self.resource_name.startswith("replay/"):
            if self.media_name is None:
                self.logging.error(
                    "Error media_name should be provided in video stream replay mode"
                )
                return False
        res = od.pdraw_open_url(self.pdraw, self.url)

        if res != 0:
            self.logging.error("Error while opening pdraw url: {} ({})".format(
                self.url, res))
            return False
        else:
            self.logging.info("Opening pdraw url OK: {}".format(self.url))
        return True

    @callback_decorator()
    def _open_stream(self):
        """
        Opening pdraw stream using the appropriate method (legacy or rtsp)
        according to the device type
        """
        self._open_resp_future = Future(self.pdraw_thread_loop)
        if self.state not in (PdrawState.Error, PdrawState.Closed,
                              PdrawState.Created):
            self.logging.warning("Cannot open stream from {}".format(
                self.state))
            self._open_resp_future.set_result(False)
            return self._open_resp_future

        self.state = PdrawState.Opening
        if not self._pdraw_new():
            self._open_resp_future.set_result(False)
            return self._open_resp_future

        if not self._legacy:
            ret = self._open_url()
        else:
            ret = self._open_single_stream()

        if not ret:
            self._open_resp_future.set_result(False)

        return self._open_resp_future

    def close(self):
        """
        Close a playing or paused video stream session
        """
        if self.state in (PdrawState.Opened, PdrawState.Paused,
                          PdrawState.Playing, PdrawState.Error):
            self.logging.debug("pdraw closing from the {} state".format(
                self.state))
            self._close_resp_future = Future(self.pdraw_thread_loop)
            self._close_resp_future.add_done_callback(self._on_close_resp_done)
            f = self._close_resp_future
            self.state = PdrawState.Closing
            self.pdraw_thread_loop.run_async(self._close_stream)
        elif self.state is not PdrawState.Closing:
            f = Future(self.pdraw_thread_loop)
            f.set_result(False)
        else:
            f = self._close_resp_future
        return f

    @callback_decorator()
    def _close_stream(self):
        """
        Close pdraw stream
        """
        if self.state is PdrawState.Closed:
            self.logging.info("pdraw is already closed".format(self.state))
            self._close_resp_future.set_result(True)
            return self._close_resp_future

        if not self.pdraw:
            self.logging.error("Error Pdraw interface seems to be destroyed")
            self.state = PdrawState.Error
            self._close_resp_future.set_result(False)
            return self._close_resp_future

        if not self._close_stream_impl():
            self.state = PdrawState.Error
            self._close_resp_future.set_result(False)

        return self._close_resp_future

    def _close_stream_impl(self):
        res = od.pdraw_close(self.pdraw)

        if res != 0:
            self.logging.error(
                "Error while closing pdraw stream: {}".format(res))
            self.state = PdrawState.Error
            return False
        else:
            self.logging.info("Closing pdraw stream OK")

        return True

    @callback_decorator()
    def _on_close_resp_done(self, close_resp_future):
        if close_resp_future.cancelled():
            # FIXME: workaround pdraw closing timeout
            # This random issue is quiet hard to reproduce
            self.logging.error("Closing Pdraw timedout")
            if self.pdraw:
                self.pdraw_thread_loop.run_later(od.pdraw_destroy, self.pdraw)
            self.pdraw = od.POINTER_T(od.struct_pdraw)()
            self.state = PdrawState.Error
            self.logging.error("Pdraw has been closed")

    def _open_resp(self, pdraw, status, userdata):
        self.logging.debug("_open_resp called")
        self.local_stream_port = od.pdraw_get_single_stream_local_stream_port(
            self.pdraw)

        self.local_control_port = od.pdraw_get_single_stream_local_control_port(
            self.pdraw)

        if status != 0:
            self.state = PdrawState.Error
        else:
            self.state = PdrawState.Opened

        self._open_resp_future.set_result(status == 0)

    def _close_resp(self, pdraw, status, userdata):
        self._close_output_files()
        if status != 0:
            self.logging.error("_close_resp called {}".format(status))
            self._close_resp_future.set_result(False)
            self.state = PdrawState.Error
        else:
            self.logging.info("_close_resp called {}".format(status))
            self.state = PdrawState.Closed
            self._close_resp_future.set_result(True)

        if self.pdraw:
            res = od.pdraw_destroy(self.pdraw)
            if res != 0:
                self.logging.error("Cannot destroy pdraw object")
        self.pdraw = od.POINTER_T(od.struct_pdraw)()
        self._close_resp_future.set_result(True)

    def _pdraw_new(self):
        res = od.pdraw_new(
            self.pomp_loop, self.cbs,
            ctypes.cast(ctypes.pointer(ctypes.py_object(self)),
                        ctypes.c_void_p), ctypes.byref(self.pdraw))
        if res != 0:
            msg = "Error while creating pdraw interface: {}".format(res)
            self.logging.error(msg)
            self.pdraw = od.POINTER_T(od.struct_pdraw)()
            return False
        else:
            self.logging.info("Pdraw interface has been created")
            return True

    def _ready_to_play(self, pdraw, ready, userdata):
        self.logging.info("_ready_to_play({}) called".format(ready))
        self._is_ready_to_play = bool(ready)
        if self._is_ready_to_play:
            self._play_resp_future = Future(self.pdraw_thread_loop)
            self._play_impl()
            if self.start_callback is not None:
                self.callbacks_thread_loop.run_async(self.start_callback)
        else:
            if self.end_callback is not None:
                self.callbacks_thread_loop.run_async(self.end_callback)

    def _play_resp(self, pdraw, status, timestamp, speed, userdata):
        if status == 0:
            self.logging.debug("_play_resp called {}".format(status))
            self.state = PdrawState.Playing
            self._play_resp_future.set_result(True)
        else:
            self.logging.error("_play_resp called {}".format(status))
            self.state = PdrawState.Error
            self._play_resp_future.set_result(False)

    def _pause_resp(self, pdraw, status, timestamp, userdata):
        if status == 0:
            self.logging.debug("_pause_resp called {}".format(status))
            self.state = PdrawState.Paused
            self._pause_resp_future.set_result(True)
        else:
            self.logging.error("_pause_resp called {}".format(status))
            self.state = PdrawState.Error
            self._pause_resp_future.set_result(False)

    def _seek_resp(self, pdraw, status, timestamp, userdata):
        if status == 0:
            self.logging.debug("_seek_resp called {}".format(status))
        else:
            self.logging.error("_seek_resp called {}".format(status))
            self.state = PdrawState.Error

    def _socket_created(self, pdraw, fd, userdata):
        self.logging.debug("_socket_created called")

    def _select_demuxer_media(self, pdraw, media, count, userdata):
        # by default select the default media (media_id=0)
        selected_media_id = 0
        selected_media_idx = 0
        for idx in range(count):
            self.logging.info("_select_demuxer_media: "
                              "idx={} media_id={} name={} default={}".format(
                                  idx, media[idx].media_id,
                                  od.string_cast(media[idx].name),
                                  str(bool(media[idx].is_default))))
            if (self.media_name is not None
                    and self.media_name == od.string_cast(media[idx].name)):
                selected_media_id = media[idx].media_id
                selected_media_idx = idx
        if (self.media_name is not None and od.string_cast(
                media[selected_media_idx].name) != self.media_name):
            self.logging.warning("media_name {} is unavailable. "
                                 "Selecting the default media instead".format(
                                     self.media_name))
        return selected_media_id

    def _media_added(self, pdraw, media_info, userdata):
        id_ = int(media_info.contents.id)
        self.logging.info("_media_added id : {}".format(id_))

        # store the information if supported media type, otherwise exit
        if (media_info.contents._2.video.format !=
                od.PDRAW_VIDEO_MEDIA_FORMAT_YUV
                and media_info.contents._2.video.format !=
                od.PDRAW_VIDEO_MEDIA_FORMAT_H264):
            self.logging.warning('Ignoring media id {} (type {})'.format(
                id_, media_info.contents._2.video.format))
            return
        self.streams[id_]['type'] = int(media_info.contents._2.video.format)
        if (media_info.contents._2.video.format ==
                od.PDRAW_VIDEO_MEDIA_FORMAT_H264):
            header = media_info.contents._2.video._2.h264
            header = H264Header(
                bytearray(header.sps),
                int(header.spslen),
                bytearray(header.pps),
                int(header.ppslen),
            )
            self.streams[id_]['h264_header'] = header

        # start a video sink attached to the new media
        video_sink_params = od.struct_pdraw_video_sink_params(
            self.buffer_queue_size,  # buffer queue size
            1,  # drop buffers when the queue is full
        )
        self.streams[id_]['id_userdata'] = ctypes.cast(
            ctypes.pointer(ctypes.py_object(id_)), ctypes.c_void_p)
        self.streams[id_]['id'] = id_

        res = od.pdraw_start_video_sink(
            pdraw, id_, video_sink_params, self.video_sink_cb,
            self.streams[id_]['id_userdata'],
            ctypes.byref(self.streams[id_]['video_sink']))
        if res != 0 or not self.streams[id_]['video_sink']:
            self.logging.error("Unable to start video sink")
            return

        # Retrieve the queue belonging to the sink
        queue = od.pdraw_get_video_sink_queue(
            pdraw,
            self.streams[id_]['video_sink'],
        )
        self.streams[id_]['video_queue'] = queue

        # Retrieve event object and related file descriptor
        self.streams[id_]['video_queue_event'] = \
            od.vbuf_queue_get_evt(self.streams[id_]['video_queue'])

        # add the file description to our pomp loop
        self.callbacks_thread_loop.add_event_to_loop(
            self.streams[id_]['video_queue_event'],
            lambda *args: self._video_sink_queue_event(*args), id_)

    def _media_removed(self, pdraw, media_info, userdata):
        id_ = media_info.contents.id
        if id_ not in self.streams:
            self.logging.error(
                'Received removed event from unknown ID {}'.format(id_))
            return

        self.logging.info("_media_removed called id : {}".format(id_))

        # FIXME: Workaround media_removed called with destroyed media
        if not self.pdraw:
            self.logging.error(
                "_media_removed called with a destroyed pdraw id : {}".format(
                    id_))
            return
        with self.streams[id_]['video_sink_lock']:
            if self.streams[id_]['video_queue_event']:
                self.callbacks_thread_loop.remove_event_from_loop(
                    self.streams[id_]['video_queue_event'])
                self.streams[id_]['video_queue_event'] = None

            if not self.streams[id_]['video_sink']:
                self.logging.error(
                    'pdraw_video_sink for media_id {} has already been stopped'
                    .format(id_))
                return
            res = od.pdraw_stop_video_sink(pdraw,
                                           self.streams[id_]['video_sink'])
            if res < 0:
                self.logging.error('pdraw_stop_video_sink() returned %s' % res)
            self.streams[id_]['video_queue'] = None
            self.streams[id_]['video_sink'] = od.POINTER_T(
                od.struct_pdraw_video_sink)()

    def _end_of_range(self, pdraw, timestamp, userdata):
        self.logging.info("_end_of_range")
        self.close()

    def _video_sink_flush(self, pdraw, videosink, userdata):
        id_ = py_object_cast(userdata)
        if id_ not in self.streams:
            self.logging.error(
                'Received flush event from unknown ID {}'.format(id_))
            return -errno.ENOENT

        # FIXME: Workaround video_sink_flush called with destroyed media
        if not self.pdraw:
            self.logging.error(
                "_video_sink_flush called with a destroyed pdraw id : {}".
                format(id_))
            return -errno.EINVAL

        # FIXME: Workaround video_sink_flush called with destroyed video queue
        if not self.streams[id_]['video_queue']:
            self.logging.error(
                "_video_sink_flush called with a destroyed queue id : {}".
                format(id_))
            return -errno.EINVAL

        with self.streams[id_]['video_sink_lock']:
            self.logging.debug("flush_callback {}".format(id_))

            flush_callback = self.flush_callbacks[self.streams[id_]['type']]
            if flush_callback is not None:
                flushed = self.callbacks_thread_loop.run_async(flush_callback)
                try:
                    if not flushed.result_or_cancel(timeout=5.):
                        self.logging.error(
                            'video sink flush id {} error'.format(id_))
                except FutureTimeoutError:
                    self.logging.error(
                        'video sink flush id {} timeout'.format(id_))
                # NOTE: If the user failed to flush its buffer at this point,
                # bad things WILL happen we're acknowledging the buffer flush
                # in all cases...
            res = od.vbuf_queue_flush(self.streams[id_]['video_queue'])
            if res < 0:
                self.logging.error('vbuf_queue_flush() returned %s' % res)
            else:
                self.logging.info('vbuf_queue_flush() returned %s' % res)

            res = od.pdraw_video_sink_queue_flushed(pdraw, videosink)
            if res < 0:
                self.logging.error(
                    'pdraw_video_sink_queue_flushed() returned %s' % res)
            else:
                self.logging.debug(
                    'pdraw_video_sink_queue_flushed() returned %s' % res)
            return 0

    @callback_decorator()
    def _video_sink_queue_event(self, pomp_evt, userdata):
        id_ = py_object_cast(userdata)
        self.logging.debug('media id = {}'.format(id_))

        if id_ not in self.streams:
            self.logging.error(
                'Received queue event from unknown ID {}'.format(id_))
            return

        # acknowledge event
        res = od.pomp_evt_clear(self.streams[id_]['video_queue_event'])
        if res != 0:
            self.logging.error(
                "Unable to clear frame received event ({})".format(res))

        if not self._is_ready_to_play:
            self.logging.debug("The stream is no longer ready: drop one frame")
            return

        # process all available buffers in the queue
        with self.streams[id_]['video_sink_lock']:
            while self._process_stream(id_):
                pass

    def _pop_stream_buffer(self, id_):
        buf = od.POINTER_T(od.struct_vbuf_buffer)()
        ret = od.vbuf_queue_pop(self.streams[id_]['video_queue'], 0,
                                ctypes.byref(buf))
        if ret < 0:
            if ret != -errno.EAGAIN:
                self.logging.error('vbuf_queue_pop returned error %d' % ret)
            buf = od.POINTER_T(od.struct_vbuf_buffer)()
        elif not buf:
            self.logging.error('vbuf_queue_pop returned NULL')
        return buf

    def _process_stream(self, id_):
        self.logging.debug('media id = {}'.format(id_))
        if od.vbuf_queue_get_count(self.streams[id_]['video_queue']) == 0:
            return False
        buf = self._pop_stream_buffer(id_)
        if not buf:
            return False
        video_frame = VideoFrame(self.logging, buf, id_, self.streams[id_],
                                 self.yuv_packed_buffer_pool,
                                 self.get_session_metadata())
        try:
            self._process_stream_buffer(id_, video_frame)
            return True
        except Exception:
            self.logging.error('_process_stream_buffer exception:\n{}'.format(
                traceback.format_exc()))
            return False
        finally:
            # Once we're done with this frame, dispose the associated frame buffer
            video_frame.unref()

    def _process_stream_buffer(self, id_, video_frame):
        stream = self.streams[id_]
        mediatype = stream['type']

        # write and/or send data over the requested channels
        # handle output files
        files = self.outfiles[mediatype]

        f = files['meta']
        if f and not f.closed:
            vmeta_type, vmeta = video_frame.vmeta()
            files['meta'].write(json.dumps((str(vmeta_type), vmeta)) + '\n')

        f = files['info']
        if f and not f.closed:
            info = video_frame.info()
            files['info'].write(json.dumps(info) + '\n')

        f = files['data']
        if f and not f.closed:
            if mediatype == od.PDRAW_VIDEO_MEDIA_FORMAT_H264:
                if f.tell() == 0:
                    # h264 files need a header to be readable
                    stream['h264_header'].tofile(f)
            frame_array = video_frame.as_ndarray()
            if frame_array is not None:
                f.write(
                    ctypes.string_at(
                        frame_array.ctypes.data_as(
                            ctypes.POINTER(ctypes.c_ubyte)),
                        frame_array.size,
                    ))

        # call callbacks when existing
        cb = self.frame_callbacks[mediatype]
        if cb is not None:
            cb(video_frame)

    def set_output_files(self, h264_data_file, h264_meta_file, h264_info_file,
                         raw_data_file, raw_meta_file, raw_info_file):
        """
        Records the video stream session to the disk

        - xxx_meta_file: video stream metadata output files
        - xxx_data_file: video stream frames output files
        - xxx_info_file: video stream frames info files
        - h264_***_file: files associated to the H264 encoded video stream
        - raw_***_file: files associated to the decoded video stream

        This function MUST NOT be called when a video streaming session is
        active.
        Setting a file parameter to `None` disables the recording for the
        related stream part.
        """
        if self.state is PdrawState.Playing:
            raise RuntimeError(
                'Cannot set video streaming files while streaming is on.')

        for mediatype, datatype, filepath, attrib in (
            (od.PDRAW_VIDEO_MEDIA_FORMAT_H264, 'data', h264_data_file,
             'wb'), (od.PDRAW_VIDEO_MEDIA_FORMAT_H264, 'meta', h264_meta_file,
                     'w'), (od.PDRAW_VIDEO_MEDIA_FORMAT_H264, 'info',
                            h264_info_file, 'w'),
            (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, 'data', raw_data_file,
             'wb'), (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, 'meta', raw_meta_file,
                     'w'), (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, 'info',
                            raw_info_file, 'w')):
            if self.outfiles[mediatype][datatype]:
                self.outfiles[mediatype][datatype].close()
                self.outfiles[mediatype][datatype] = None

            if filepath is None:
                continue

            # open and close file to store its filename and attribute
            self.outfiles[mediatype][datatype] = open(filepath, attrib)
            self.outfiles[mediatype][datatype].close()

    def set_callbacks(self,
                      h264_cb=None,
                      raw_cb=None,
                      start_cb=None,
                      end_cb=None,
                      flush_h264_cb=None,
                      flush_raw_cb=None):
        """
        Set the callback functions that will be called when a new video stream frame is available,
        when the video stream starts/ends or when the video buffer needs to get flushed.

        **Video frame callbacks**

        - `h264_cb` is associated to the H264 encoded video stream
        - `raw_cb` is associated to the decoded video stream

        Each video frame callback function takes an :py:func:`~olympe.VideoFrame` parameter whose
        lifetime ends after the callback execution. If this video frame is passed to another thread,
        its internal reference count need to be incremented first by calling
        :py:func:`~olympe.VideoFrame.ref`. In this case, once the frame is no longer needed, its
        reference count needs to be decremented so that this video frame can be returned to
        memory pool.

        **Video flush callbacks**

        - `flush_h264_cb` is associated to the H264 encoded video stream
        - `flush_raw_cb` is associated to the decoded video stream

        Video flush callback functions are called when a video stream reclaim all its associated
        video buffer. Every frame that has been referenced

        **Start/End callbacks**

        The `start_cb`/`end_cb` callback functions are called when the video stream start/ends.
        They don't accept any parameter.

        The return value of all these callback functions are ignored.
        If a callback is not desired, leave the parameter to its default value or set it to `None`
        explicitly.
        """

        for mediatype, cb in ((od.PDRAW_VIDEO_MEDIA_FORMAT_H264, h264_cb),
                              (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, raw_cb)):
            self.frame_callbacks[mediatype] = callback_decorator(
                self.logging)(cb)
        for mediatype, cb in ((od.PDRAW_VIDEO_MEDIA_FORMAT_H264,
                               flush_h264_cb),
                              (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, flush_raw_cb)):
            self.flush_callbacks[mediatype] = callback_decorator(
                self.logging)(cb)
        self.start_callback = callback_decorator(self.logging)(start_cb)
        self.end_callback = callback_decorator(self.logging)(end_cb)

    def _open_output_files(self):
        self.logging.debug('opening video output files')
        for mediatype, data in self.outfiles.items():
            for datatype, f in data.items():
                if f and f.closed:
                    self.outfiles[mediatype][datatype] = open(f.name, f.mode)

    def _close_output_files(self):
        self.logging.debug('closing video output files')
        for files in self.outfiles.values():
            for f in files.values():
                if f:
                    f.close()

    def play(self,
             url=None,
             media_name="DefaultVideo",
             server_addr=None,
             resource_name="live"):
        """
        Play a video

        By default, open and play a live video streaming session available
        from rtsp://192.168.42.1/live where "192.168.42.1" is the default IP
        address of a physical (Anafi) drone. The default is equivalent to
        `Pdraw.play(url="rtsp://192.168.42.1/live")`

        For a the live video streaming from a **simulated drone**, you have to
        specify the default simulated drone IP address (10.202.0.1) instead:
        `Pdraw.play(url="rtsp://10.202.0.1/live")`.

        The `url` parameter can also point to a local file example:
        `Pdraw.play(url="file://~/Videos/100000010001.MP4")`.

        :param url: rtsp or local file video URL
        :type url: str
        :param media_name: name of the media/track (defaults to "DefaultVideo").
            If the provided media name is not available from the requested video
            stream, the default media is selected instead.
        :type media_name: str

        """
        if self.pdraw is None:
            self.logging.error("Error Pdraw interface seems to be destroyed")
            self._play_resp_future.set_result(False)
            return self._pause_resp_future

        if self.state in (PdrawState.Opening, PdrawState.Closing):
            self.logging.warning("Cannot play stream from the {} state".format(
                self.state))
            f = Future(self.pdraw_thread_loop)
            f.set_result(False)
            return f

        self.resource_name = resource_name
        self.media_name = media_name

        if server_addr is None:
            self.server_addr = "192.168.42.1"
        else:
            self.server_addr = server_addr

        if url is None:
            self.url = b"rtsp://%s/%s" % (self.server_addr,
                                          self.resource_name.encode())
        else:
            if isinstance(url, bytes):
                url = url.decode('utf-8')
            if url.startswith('file://'):
                url = url[7:]
            if url.startswith('~/'):
                url = os.path.expanduser(url)
            url = os.path.expandvars(url)
            url = url.encode('utf-8')
            self.url = url
            if self.is_legacy():
                self.logging.warning(
                    "Cannot open streaming url for legacy drones")

        # reset session metadata from any previous session
        self.session_metadata = {}
        self.streams = defaultdict(StreamFactory)

        self._open_output_files()
        if self.state in (PdrawState.Created, PdrawState.Closed):
            f = self.pdraw_thread_loop.run_async(self._open_stream)
        else:
            f = self._play_resp_future = Future(self.pdraw_thread_loop)
            self.pdraw_thread_loop.run_async(self._play_impl)
        return f

    @callback_decorator()
    def _play_impl(self):
        self.logging.debug("play_impl")
        if self.state is PdrawState.Playing:
            self._play_resp_future.set_result(True)
            return self._play_resp_future

        res = od.pdraw_play(self.pdraw)
        if res != 0:
            msg = "Unable to start streaming ({})".format(res)
            self.logging.error(msg)
            self._play_resp_future.set_result(False)

        return self._play_resp_future

    def pause(self):
        """
        Pause the currently playing video
        """
        if self.pdraw is None:
            self.logging.error("Error Pdraw interface seems to be destroyed")
            self._pause_resp_future.set_result(False)
            return self._pause_resp_future

        self._pause_resp_future = Future(self.pdraw_thread_loop)
        if self.state is PdrawState.Playing:
            self.pdraw_thread_loop.run_async(self._pause_impl)
        elif self.state in (PdrawState.Closed, PdrawState.Opened):
            # Pause an opened/closed stream is OK
            self._pause_resp_future.set_result(True)
        else:
            self.logging.warning(
                "Cannot pause stream from the {} state".format(self.state))
            self._pause_resp_future.set_result(False)
        return self._pause_resp_future

    @callback_decorator()
    def _pause_impl(self):
        res = od.pdraw_pause(self.pdraw)
        if res != 0:
            self.logging.error("Unable to stop streaming ({})".format(res))
            self._pause_resp_future.set_result(False)
        return self._pause_resp_future

    def get_session_metadata(self):
        """
        Returns a dictionary of video stream session metadata
        """
        if self.pdraw is None:
            self.logging.error("Error Pdraw interface seems to be destroyed")
            return None

        if self.session_metadata:
            return self.session_metadata

        vmeta_session = od.struct_vmeta_session()
        res = od.pdraw_get_peer_session_metadata(self.pdraw,
                                                 ctypes.pointer(vmeta_session))
        if res != 0:
            msg = "Unable to get sessions metata"
            self.logging.error(msg)
            return None
        self.session_metadata = od.struct_vmeta_session.as_dict(vmeta_session)
        return self.session_metadata

    def is_legacy(self):
        return self._legacy
Beispiel #3
0
    def play(self,
             url=None,
             media_name="DefaultVideo",
             server_addr=None,
             resource_name="live"):
        """
        Play a video

        By default, open and play a live video streaming session available
        from rtsp://192.168.42.1/live where "192.168.42.1" is the default IP
        address of a physical (Anafi) drone. The default is equivalent to
        `Pdraw.play(url="rtsp://192.168.42.1/live")`

        For a the live video streaming from a **simulated drone**, you have to
        specify the default simulated drone IP address (10.202.0.1) instead:
        `Pdraw.play(url="rtsp://10.202.0.1/live")`.

        The `url` parameter can also point to a local file example:
        `Pdraw.play(url="file://~/Videos/100000010001.MP4")`.

        :param url: rtsp or local file video URL
        :type url: str
        :param media_name: name of the media/track (defaults to "DefaultVideo").
            If the provided media name is not available from the requested video
            stream, the default media is selected instead.
        :type media_name: str

        """
        if self.pdraw is None:
            self.logging.error("Error Pdraw interface seems to be destroyed")
            self._play_resp_future.set_result(False)
            return self._pause_resp_future

        if self.state in (PdrawState.Opening, PdrawState.Closing):
            self.logging.warning("Cannot play stream from the {} state".format(
                self.state))
            f = Future(self.pdraw_thread_loop)
            f.set_result(False)
            return f

        self.resource_name = resource_name
        self.media_name = media_name

        if server_addr is None:
            self.server_addr = "192.168.42.1"
        else:
            self.server_addr = server_addr

        if url is None:
            self.url = b"rtsp://%s/%s" % (self.server_addr,
                                          self.resource_name.encode())
        else:
            if isinstance(url, bytes):
                url = url.decode('utf-8')
            if url.startswith('file://'):
                url = url[7:]
            if url.startswith('~/'):
                url = os.path.expanduser(url)
            url = os.path.expandvars(url)
            url = url.encode('utf-8')
            self.url = url
            if self.is_legacy():
                self.logging.warning(
                    "Cannot open streaming url for legacy drones")

        # reset session metadata from any previous session
        self.session_metadata = {}
        self.streams = defaultdict(StreamFactory)

        self._open_output_files()
        if self.state in (PdrawState.Created, PdrawState.Closed):
            f = self.pdraw_thread_loop.run_async(self._open_stream)
        else:
            f = self._play_resp_future = Future(self.pdraw_thread_loop)
            self.pdraw_thread_loop.run_async(self._play_impl)
        return f
Beispiel #4
0
class Pdraw(object):

    def __init__(self,
                 buffer_queue_size=2,
                 loglevel=TraceLogger.level.info,
                 logfile=sys.stdout,
                 legacy=False,
                 pdraw_thread_loop=None,
                 logging=None):
        """
        :param buffer_queue_size: video buffer queue size (defaults to 2)
        :type buffer_queue_size: int
        :param loglevel: pdraw logger log level (defaults to :py:attr:`olympe.tools.logger.level.info`)
        :type loglevel: int
        :param logfile: pdraw logger file (defaults to sys.stdout)
        :type logfile: FileObjectLike
        :param legacy: Defaults to False, set this parameter to True for legacy
            drones (Bebop, Disco, ...) streaming support
        :type legacy: bool
        """

        if logging is None:
            self.logging = TraceLogger(loglevel, logfile)
        else:
            self.logging = logging

        if pdraw_thread_loop is None:
            self.pdraw_thread_loop = PompLoopThread(self.logging)
            self.pdraw_thread_loop.start()
        else:
            self.pdraw_thread_loop = pdraw_thread_loop

        self.callbacks_thread_loop = PompLoopThread(self.logging)
        self.callbacks_thread_loop.start()
        self.buffer_queue_size = buffer_queue_size
        self.pomp_loop = self.pdraw_thread_loop.pomp_loop
        self._legacy = legacy

        self._open_resp_future = Future(self.pdraw_thread_loop)
        self._close_resp_future = Future(self.pdraw_thread_loop)
        self._close_resp_future.add_done_callback(self._on_close_resp_done)
        self._play_resp_future = Future(self.pdraw_thread_loop)
        self._pause_resp_future = Future(self.pdraw_thread_loop)
        self._state = State.Created

        self.pdraw = od.POINTER_T(od.struct_pdraw)()
        self.streams = defaultdict(lambda: {
            'id': None,
            'type': od.PDRAW_VIDEO_MEDIA_FORMAT_UNKNOWN,
            'h264_header': None,
            'video_sink': od.POINTER_T(od.struct_pdraw_video_sink)(),
            'video_sink_flushed': False,
            'video_sink_lock': threading.Lock(),
            'video_queue': None,
            'video_queue_event': None,
        })
        self.session_metadata = {}

        self.outfiles = {
            od.PDRAW_VIDEO_MEDIA_FORMAT_H264:
            {
                'data': None,
                'meta': None,
            },
            od.PDRAW_VIDEO_MEDIA_FORMAT_YUV:
            {
                'data': None,
                'meta': None,
            },
        }

        self.frame_callbacks = {
            od.PDRAW_VIDEO_MEDIA_FORMAT_H264: None,
            od.PDRAW_VIDEO_MEDIA_FORMAT_YUV: None,
        }
        self.end_callback = None
        self.flush_callback = None

        self.url = None
        self.server_addr = None
        self.resource_name = "live"
        self.media_name = None

        self.local_stream_port = PDRAW_LOCAL_STREAM_PORT
        self.local_control_port = PDRAW_LOCAL_CONTROL_PORT

        self.cbs = od.struct_pdraw_cbs.bind({
            "open_resp": self._open_resp,
            "close_resp": self._close_resp,
            "ready_to_play": self._ready_to_play,
            "play_resp": self._play_resp,
            "pause_resp": self._pause_resp,
            "seek_resp": self._seek_resp,
            "socket_created": self._socket_created,
            "select_demuxer_media": self._select_demuxer_media,
            "media_added": self._media_added,
            "media_removed": self._media_removed,
            "end_of_range": self._end_of_range,
        })

        self.video_sink_cb = od.struct_pdraw_video_sink_cbs.bind({
            "flush": self._video_sink_flush
        })

        self.vbuf_cbs = od.struct_vbuf_cbs()
        res = od.vbuf_generic_get_cbs(ctypes.pointer(self.vbuf_cbs))
        if res != 0:
            msg = "Error while creating vbuf generic callbacks {}".format(res)
            self.logging.logE(msg)
            raise RuntimeError("ERROR: {}".format(msg))

        self.yuv_packed_buffer_pool = od.POINTER_T(od.struct_vbuf_pool)()
        res = od.vbuf_pool_new(
            self.buffer_queue_size,
            0,
            0,
            self.vbuf_cbs,
            ctypes.byref(self.yuv_packed_buffer_pool)
        )
        if res != 0:
            msg = "Error while creating yuv packged buffer pool {}".format(res)
            self.logging.logE(msg)
            raise RuntimeError("ERROR: {}".format(msg))

        self.pdraw_thread_loop.register_cleanup(self.dispose)

    def dispose(self):
        self.callbacks_thread_loop.stop()
        return self.pdraw_thread_loop.run_async(
            self._dispose_impl)

    def _dispose_impl(self):
        if not self.pdraw:
            return

        f = self.close().then(
            lambda _: self._destroy(), deferred=True)
        return f

    def _destroy(self):
        res = od.vbuf_pool_destroy(self.yuv_packed_buffer_pool)
        if res != 0:
            self.logging.logE("Cannot destroy yuv packed buffer pool")
        self.yuv_packed_buffer_pool = od.POINTER_T(od.struct_vbuf_pool)()
        if self.pdraw:
            res = od.pdraw_destroy(self.pdraw)
            if res != 0:
                self.logging.logE("Cannot destroy pdraw object")
        self.pdraw = od.POINTER_T(od.struct_pdraw)()
        self.logging.logI("pdraw destroyed")
        return True

    def _open_single_stream(self):
        """
        Opening pdraw single stream (legacy API)
        """
        res = od.pdraw_open_single_stream(
            self.pdraw,
            PDRAW_LOCAL_ADDR,
            self.local_stream_port,
            self.local_control_port,
            self.server_addr,
            PDRAW_REMOTE_STREAM_PORT,
            PDRAW_REMOTE_CONTROL_PORT,
            PDRAW_IFACE_ADRR
        )

        if res != 0:
            self.logging.logE(
                "Error while opening pdraw single stream: {}".format(res))
            return False
        else:
            self.logging.logI("Opening pdraw single stream OK")
        return True

    def _open_url(self):
        """
        Opening rtsp streaming url
        """
        if self.resource_name.startswith("replay/"):
            if self.media_name is None:
                self.logging.logE(
                    "Error media_name should be provided in video stream replay mode")
                return False
        res = od.pdraw_open_url(self.pdraw, self.url)

        if res != 0:
            self.logging.logE(
                "Error while opening pdraw url: {} ({})".format(self.url, res))
            return False
        else:
            self.logging.logI("Opening pdraw url OK: {}".format(self.url))
        return True

    def _open_stream(self):
        """
        Opening pdraw stream using the appropriate method (legacy or rtsp)
        according to the device type
        """
        self._open_resp_future = Future(self.pdraw_thread_loop)
        if self._state not in (State.Error, State.Closed, State.Created):
            self.logging.logW("Cannot open stream from {}".format(self._state))
            self._open_resp_future.set_result(False)
            return self._open_resp_future

        self._state = State.Opening
        if not self._pdraw_new():
            self._open_resp_future.set_result(False)
            return self._open_resp_future

        if not self._legacy:
            ret = self._open_url()
        else:
            ret = self._open_single_stream()

        if not ret:
            self._open_resp_future.set_result(False)

        return self._open_resp_future

    def close(self):
        """
        Close a playing or paused video stream session
        """
        if self._state in (State.Opened, State.Paused, State.Playing, State.Error):
            self.logging.logD("pdraw closing from the {} state".format(self._state))
            self._close_resp_future = Future(self.pdraw_thread_loop)
            self._close_resp_future.add_done_callback(self._on_close_resp_done)
            f = self._close_resp_future
            self._state = State.Closing
            self.pdraw_thread_loop.run_async(self._close_stream)
        elif self._state is not State.Closing:
            f = Future(self.pdraw_thread_loop)
            f.set_result(False)
        else:
            f = self._close_resp_future
        return f

    def _close_stream(self):
        """
        Close pdraw stream
        """
        if self._state is State.Closed:
            self.logging.logI("pdraw is already closed".format(self._state))
            self._close_resp_future.set_result(True)
            return self._close_resp_future

        if not self.pdraw:
            self.logging.logE("Error Pdraw interface seems to be destroyed")
            self._state = State.Error
            self._close_resp_future.set_result(False)
            return self._close_resp_future

        if not self._close_stream_impl():
            self._state = State.Error
            self._close_resp_future.set_result(False)

        return self._close_resp_future

    def _close_stream_impl(self):
        res = od.pdraw_close(self.pdraw)

        if res != 0:
            self.logging.logE(
                "Error while closing pdraw stream: {}".format(res))
            self._state = State.Error
            return False
        else:
            self.logging.logI("Closing pdraw stream OK")

        return True

    def _on_close_resp_done(self, close_resp_future):
        if close_resp_future.cancelled():
            # FIXME: workaround pdraw closing timeout
            # This random issue is quiet hard to reproduce
            self.logging.logE("Closing Pdraw timedout")
            if self.pdraw:
                res = od.pdraw_destroy(self.pdraw)
                if res != 0:
                    self.logging.logE("Cannot destroy pdraw object")
            self.pdraw = od.POINTER_T(od.struct_pdraw)()
            self._state = State.Closed
            self.logging.logE("Pdraw has been closed")

    def _open_resp(self, pdraw, status, userdata):
        self.logging.logD("_open_resp called")
        self.local_stream_port = od.pdraw_get_single_stream_local_stream_port(self.pdraw)

        self.local_control_port = od.pdraw_get_single_stream_local_control_port(self.pdraw)

        if status != 0:
            self._state = State.Error
        else:
            self._state = State.Opened

        self._open_resp_future.set_result(status == 0)

    def _close_resp(self, pdraw, status, userdata):
        self._close_output_files()
        if status != 0:
            self.logging.logE("_close_resp called {}".format(status))
            self._close_resp_future.set_result(False)
            self._state = State.Error
        else:
            self.logging.logI("_close_resp called {}".format(status))
            self._state = State.Closed
            self._close_resp_future.set_result(True)

        if self.pdraw:
            res = od.pdraw_destroy(self.pdraw)
            if res != 0:
                self.logging.logE("Cannot destroy pdraw object")
        self.pdraw = od.POINTER_T(od.struct_pdraw)()
        self._close_resp_future.set_result(True)

    def _pdraw_new(self):
        res = od.pdraw_new(
            self.pomp_loop,
            self.cbs,
            ctypes.cast(ctypes.pointer(ctypes.py_object(self)), ctypes.c_void_p),
            ctypes.byref(self.pdraw)
        )
        if res != 0:
            msg = "Error while creating pdraw interface: {}".format(res)
            self.logging.logE(msg)
            self.pdraw = od.POINTER_T(od.struct_pdraw)()
            return False
        else:
            self.logging.logI("Pdraw interface has been created")
            return True

    def _ready_to_play(self, pdraw, ready, userdata):
        self.logging.logI("_ready_to_play({}) called".format(ready))
        if ready:
            self._play_resp_future = Future(self.pdraw_thread_loop)
            self._play_impl()
        if self._state in (State.Playing, State.Closing, State.Closed):
            if self.end_callback is not None:
                self.callbacks_thread_loop.run_async(self.end_callback)

    def _play_resp(self, pdraw, status, timestamp, speed, userdata):
        if status == 0:
            self.logging.logD("_play_resp called {}".format(status))
            self._state = State.Playing
            self._play_resp_future.set_result(True)
        else:
            self.logging.logE("_play_resp called {}".format(status))
            self._state = State.Error
            self._play_resp_future.set_result(False)

    def _pause_resp(self, pdraw, status, timestamp, userdata):
        if status == 0:
            self.logging.logD("_pause_resp called {}".format(status))
            self._state = State.Paused
            self._pause_resp_future.set_result(True)
        else:
            self.logging.logE("_pause_resp called {}".format(status))
            self._state = State.Error
            self._pause_resp_future.set_result(False)

    def _seek_resp(self, pdraw, status, timestamp, userdata):
        if status == 0:
            self.logging.logD("_seek_resp called {}".format(status))
        else:
            self.logging.logE("_seek_resp called {}".format(status))
            self._state = State.Error

    def _socket_created(self, pdraw, fd, userdata):
        self.logging.logD("_socket_created called")

    def _select_demuxer_media(self, pdraw, medias, count, userdata):
        # by default select the default media (media_id=0)
        selected_media_id = 0
        selected_media_idx = 0
        for idx in range(count):
            self.logging.logI(
                "_select_demuxer_media: "
                "idx={} media_id={} name={} default={}".format(
                    idx, medias[idx].media_id,
                    od.string_cast(medias[idx].name),
                    str(bool(medias[idx].is_default)))
            )
            if (self.media_name is not None and
                    self.media_name == od.string_cast(medias[idx].name)):
                selected_media_id = medias[idx].media_id
                selected_media_idx = idx
        if (
            self.media_name is not None and
            od.string_cast(medias[selected_media_idx].name) != self.media_name
        ):
            self.logging.logW(
                "media_name {} is unavailable. "
                "Selecting the default media instead".format(self.media_name)
            )
        return selected_media_id

    def _media_added(self, pdraw, media_info, userdata):
        id_ = media_info.contents.id
        self.logging.logI("_media_added id : {}".format(id_))

        # store the information if supported media type, otherwise exit
        if (media_info.contents._2.video.format !=
                od.PDRAW_VIDEO_MEDIA_FORMAT_YUV and
                media_info.contents._2.video.format !=
                od.PDRAW_VIDEO_MEDIA_FORMAT_H264):
            self.logging.logW(
                'Ignoring media id {} (type {})'.format(
                    id_, media_info.contents._2.video.format))
            return
        self.streams[id_]['type'] = int(media_info.contents._2.video.format)
        if (media_info.contents._2.video.format ==
                od.PDRAW_VIDEO_MEDIA_FORMAT_H264):
                header = media_info.contents._2.video._2.h264
                header = H264Header(
                    bytearray(header.sps),
                    int(header.spslen),
                    bytearray(header.pps),
                    int(header.ppslen),
                )
                self.streams[id_]['h264_header'] = header

        # start a video sink attached to the new media
        video_sink_params = od.struct_pdraw_video_sink_params(
            self.buffer_queue_size,  # buffer queue size
            1,  # drop buffers when the queue is full
        )
        self.streams[id_]['id'] = ctypes.cast(
            ctypes.pointer(ctypes.py_object(id_)), ctypes.c_void_p)

        res = od.pdraw_start_video_sink(
            pdraw,
            id_,
            video_sink_params,
            self.video_sink_cb,
            self.streams[id_]['id'],
            ctypes.byref(self.streams[id_]['video_sink'])
        )
        if res != 0:
            self.logging.logE("Unable to start video sink")
            return

        # Retrieve the queue belonging to the sink
        queue = od.pdraw_get_video_sink_queue(
            pdraw,
            self.streams[id_]['video_sink'],
        )
        self.streams[id_]['video_queue'] = queue

        # Retrieve event object and related file descriptor
        self.streams[id_]['video_queue_event'] = \
            od.vbuf_queue_get_evt(self.streams[id_]['video_queue'])

        # add the file description to our pomp loop
        self.callbacks_thread_loop.add_event_to_loop(
            self.streams[id_]['video_queue_event'],
            lambda *args: self._video_sink_queue_event(*args),
            id_
        )

    def _media_removed(self, pdraw, media_info, userdata):
        id_ = media_info.contents.id
        if id_ not in self.streams:
            self.logging.logE(
                'Received removed event from unknown ID {}'.format(id_))
            return

        self.logging.logI("_media_removed called id : {}".format(id_))

        if self.streams[id_]['video_queue_event']:
            self.callbacks_thread_loop.remove_event_from_loop(
                self.streams[id_]['video_queue_event'])

        res = od.pdraw_stop_video_sink(pdraw, self.streams[id_]['video_sink'])
        if res < 0:
            self.logging.logE('pdraw_stop_video_sink() returned %s' % res)

        self.streams.pop(id_)

    def _end_of_range(self, pdraw, timestamp, userdata):
        self.logging.logI("_end_for_range")
        self.close()

    def _video_sink_flush(self, pdraw, videosink, userdata):

        id_ = py_object_cast(userdata)
        if id_ not in self.streams:
            self.logging.logE(
                'Received flush event from unknown ID {}'.format(id_))
            return

        with self.streams[id_]['video_sink_lock']:
            self.logging.logD("flush_callback {}".format(id_))
            if self.flush_callback is not None:
                res = self.flush_callback(id_)
                if res != 0:
                    self.logging.logE(
                        'video sink flush id {} error {}'.format(id_, res))
            res = od.vbuf_queue_flush(self.streams[id_]['video_queue'])
            if res < 0:
                self.logging.logE('vbuf_queue_flush() returned %s' % res)
            else:
                self.logging.logI('vbuf_queue_flush() returned %s' % res)

            res = od.pdraw_video_sink_queue_flushed(pdraw, videosink)
            self.streams[id_]['video_sink_flushed'] = True
            if res < 0:
                self.logging.logE(
                    'pdraw_video_sink_queue_flushed() returned %s' % res)
            else:
                self.logging.logD(
                    'pdraw_video_sink_queue_flushed() returned %s' % res)

    def _video_sink_queue_event(self, pomp_evt, userdata):
        id_ = py_object_cast(userdata)
        self.logging.logD('media id = {}'.format(id_))

        if id_ not in self.streams:
            self.logging.logE(
                'Received queue event from unknown ID {}'.format(id_))
            return

        # acknowledge event
        res = od.pomp_evt_clear(self.streams[id_]['video_queue_event'])
        if res != 0:
            self.logging.logE(
                "Unable to clear frame received event ({})".format(res))

        # process all available buffers in the queue
        with self.streams[id_]['video_sink_lock']:
            while self._process_stream(id_):
                pass

    def _pop_stream_buffer(self, id_):
        buf = od.POINTER_T(od.struct_vbuf_buffer)()
        ret = od.vbuf_queue_pop(
            self.streams[id_]['video_queue'], 0, ctypes.byref(buf)
        )
        if ret < 0:
            if ret != -errno.EAGAIN:
                self.logging.logE('vbuf_queue_pop returned error %d' % ret)
            buf = od.POINTER_T(od.struct_vbuf_buffer)()
        elif not buf:
            self.logging.logE('vbuf_queue_pop returned NULL')
        return buf

    def _process_stream(self, id_):
        self.logging.logD('media id = {}'.format(id_))
        if self.streams[id_]['video_sink_flushed']:
            self.logging.logI(
                'Video sink has already been flushed ID {}'.format(id_))
            return False
        if od.vbuf_queue_get_count(self.streams[id_]['video_queue']) == 0:
            return False
        buf = self._pop_stream_buffer(id_)
        if not buf:
            return False
        video_frame = VideoFrame(
            self.logging,
            buf,
            id_,
            self.streams[id_],
            self.yuv_packed_buffer_pool,
            self.get_session_metadata()
        )
        try:
            if not self._process_stream_buffer(id_, video_frame):
                return False
            return True
        finally:
            # Once we're done with this frame, dispose the associated frame buffer
            video_frame.unref()

    def _process_stream_buffer(self, id_, video_frame):
        stream = self.streams[id_]
        mediatype = stream['type']

        # write and/or send data over the requested channels
        # handle output files
        files = self.outfiles[mediatype]

        f = files['meta']
        if f and not f.closed:
            vmeta_type, vmeta = video_frame.vmeta()
            files['meta'].write(json.dumps((str(vmeta_type), vmeta)) + '\n')

        f = files['data']
        if f and not f.closed:
            if mediatype == od.PDRAW_VIDEO_MEDIA_FORMAT_H264:
                if f.tell() == 0:
                    # h264 files need a header to be readable
                    stream['h264_header'].tofile(f)
            frame_array = video_frame.as_ndarray()
            if frame_array is not None:
                f.write(ctypes.string_at(
                    frame_array.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)),
                    frame_array.size,
                ))

        # call callbacks when existing
        cb = self.frame_callbacks[mediatype]
        if cb is not None:
            cb(video_frame)

    def set_output_files(self,
                         h264_data_file,
                         h264_meta_file,
                         raw_data_file,
                         raw_meta_file):
        """
        Records the video stream session to the disk

        - xxx_meta_file: video stream metadata output files
        - xxx_data_file: video stream frames output files
        - h264_***_file: files associated to the H264 encoded video stream
        - raw_***_file: files associated to the decoded video stream

        This function MUST NOT be called when a video streaming session is
        active.
        Setting a file parameter to `None` disables the recording for the
        related stream part.
        """
        if self._state is State.Playing:
            raise RuntimeError(
                'Cannot set video streaming files while streaming is on.')

        for mediatype, datatype, filepath, attrib in (
                (od.PDRAW_VIDEO_MEDIA_FORMAT_H264, 'data', h264_data_file, 'wb'),
                (od.PDRAW_VIDEO_MEDIA_FORMAT_H264, 'meta', h264_meta_file, 'w'),
                (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV,  'data', raw_data_file,  'wb'),
                (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV,  'meta', raw_meta_file,  'w')):
            if self.outfiles[mediatype][datatype]:
                self.outfiles[mediatype][datatype].close()
                self.outfiles[mediatype][datatype] = None

            if filepath is None:
                continue

            # open and close file to store its filename and attribute
            self.outfiles[mediatype][datatype] = open(filepath, attrib)
            self.outfiles[mediatype][datatype].close()

    def set_callbacks(self,
                      h264_cb=None,
                      raw_cb=None,
                      end_cb=None,
                      flush_cb=None):
        """
        Set the callback functions that will be called when a new video stream frame is available or
        when the video stream has ended.

        Video frame callbacks:
        - `h264_cb` is associated to the H264 encoded video stream
        - `raw_cb` is associated to the decoded video stream

        Each video frame callback function takes an :py:func:`~olympe.VideoFrame` parameter
        The `end_cb` callback function is called when the (replayed) video stream ends and takes
        no parameter.
        The return value of all these callback functions are ignored.
        If a callback is not desired, just set it to `None`.
        """

        for mediatype, cb in ((od.PDRAW_VIDEO_MEDIA_FORMAT_H264, h264_cb),
                              (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, raw_cb)):
            self.frame_callbacks[mediatype] = cb

        self.end_callback = end_cb
        self.flush_callback = flush_cb

    def _open_output_files(self):
        self.logging.logD('opening video output files')
        for mediatype, data in self.outfiles.items():
            for datatype, f in data.items():
                if f and f.closed:
                    self.outfiles[mediatype][datatype] = open(f.name, f.mode)

    def _close_output_files(self):
        self.logging.logD('closing video output files')
        for files in self.outfiles.values():
            for f in files.values():
                if f:
                    f.close()

    def play(self, url=None, media_name="DefaultVideo", server_addr=None, resource_name="live"):
        """
        Play a video

        By default, open and play a live video streaming session available
        from rtsp://192.168.42.1/live where "192.168.42.1" is the default IP
        address of a physical (Anafi) drone. The default is equivalent to
        `Pdraw.play(url="rtsp://192.168.42.1/live")`

        For a the live video streaming from a **simulated drone**, you have to
        specify the default simulated drone IP address (10.202.0.1) instead:
        `Pdraw.play(url="rtsp://10.202.0.1/live")`.

        The `url` parameter can also point to a local file example:
        `Pdraw.play(url="file://~/Videos/100000010001.MP4")`.

        :param url: rtsp or local file video URL
        :type url: str
        :param media_name: name of the media/track (defaults to "DefaultVideo").
            If the provided media name is not available from the requested video
            stream, the default media is selected instead.
        :type media_name: str

        """
        if self.pdraw is None:
            self.logging.logE("Error Pdraw interface seems to be destroyed")
            self._play_resp_future.set_result(False)
            return self._pause_resp_future

        if self._state in (State.Opening, State.Closing):
            self.logging.logW("Cannot play stream from the {} state".format(
                self._state))
            f = Future(self.pdraw_thread_loop)
            f.set_result(False)
            return f

        self.resource_name = resource_name
        self.media_name = media_name

        if server_addr is None:
            self.server_addr = "192.168.42.1"
        else:
            self.server_addr = server_addr

        if url is None:
            self.url = b"rtsp://%s/%s" % (
                self.server_addr, self.resource_name.encode())
        else:
            if isinstance(url, bytes):
                url = url.decode('utf-8')
            if url.startswith('file://'):
                url = url[7:]
            if url.startswith('~/'):
                url = os.path.expanduser(url)
            url = os.path.expandvars(url)
            url = url.encode('utf-8')
            self.url = url
            if self.is_legacy():
                self.logging.logW("Cannot open streaming url for legacy drones")

        # reset session metadata from any previous session
        self.session_metadata = {}
        self.streams = defaultdict(lambda: {
            'id': None,
            'type': od.PDRAW_VIDEO_MEDIA_FORMAT_UNKNOWN,
            'h264_header': None,
            'video_sink': od.POINTER_T(od.struct_pdraw_video_sink)(),
            'video_sink_flushed': False,
            'video_sink_lock': threading.Lock(),
            'video_queue': None,
            'video_queue_event': None,
        })

        self._open_output_files()
        if self._state in (State.Created, State.Closed):
            f = self.pdraw_thread_loop.run_async(self._open_stream)
        else:
            f = self._play_resp_future = Future(self.pdraw_thread_loop)
            self.pdraw_thread_loop.run_async(self._play_impl)
        return f

    def _play_impl(self):
        self.logging.logD("play_impl")
        if self._state is State.Playing:
            self._play_resp_future.set_result(True)
            return self._play_resp_future

        res = od.pdraw_play(self.pdraw)
        if res != 0:
            msg = "Unable to start streaming ({})".format(res)
            self.logging.logE(msg)
            self._play_resp_future.set_result(False)

        return self._play_resp_future

    def pause(self):
        """
        Pause the currently playing video
        """
        if self.pdraw is None:
            self.logging.logE("Error Pdraw interface seems to be destroyed")
            self._pause_resp_future.set_result(False)
            return self._pause_resp_future

        self._pause_resp_future = Future(self.pdraw_thread_loop)
        if self._state is State.Playing:
            self.pdraw_thread_loop.run_async(self._pause_impl)
        elif self._state in (State.Closed, State.Opened):
            # Pause an opened/closed stream is OK
            self._pause_resp_future.set_result(True)
        else:
            self.logging.logW("Cannot pause stream from the {} state".format(
                self._state))
            self._pause_resp_future.set_result(False)
        return self._pause_resp_future

    def _pause_impl(self):
        res = od.pdraw_pause(self.pdraw)
        if res != 0:
            self.logging.logE("Unable to stop streaming ({})".format(res))
            self._pause_resp_future.set_result(False)
        return self._pause_resp_future

    def get_session_metadata(self):
        """
        Returns a dictionary of video stream session metadata
        """
        if self.pdraw is None:
            self.logging.logE("Error Pdraw interface seems to be destroyed")
            return None

        if self.session_metadata:
            return self.session_metadata

        vmeta_session = od.struct_vmeta_session()
        res = od.pdraw_get_peer_session_metadata(
            self.pdraw, ctypes.pointer(vmeta_session))
        if res != 0:
            msg = "Unable to get sessions metata"
            self.logging.logE(msg)
            return None
        self.session_metadata = od.struct_vmeta_session.as_dict(
            vmeta_session)
        return self.session_metadata

    def is_legacy(self):
        return self._legacy