Exemplo n.º 1
0
    def _send_command_impl(self, message, args, quiet=False):
        """
        Must be run from the pomp loop
        """

        argv = message._encode_args(*args.values())

        # Check if we are already sending a command.
        # if it the case, wait for the lock to be released
        # Define an Arsdk structure
        cmd = od.struct_arsdk_cmd()

        # Find the description of the command in libarsdk.so
        command_desc = od.struct_arsdk_cmd_desc.in_dll(
            od._libraries["libarsdk.so"], message.g_arsdk_cmd_desc)  # pylint: disable=E1101

        # argv is an array of struct_arsdk_value
        argc = argv._length_
        # Encode the command
        res = od.arsdk_cmd_enc_argv(ctypes.pointer(cmd),
                                    ctypes.pointer(command_desc), argc, argv)

        if res != 0:
            self.logger.error(
                f"Error while encoding command {message.fullName}: {res}")
        else:
            self.logger.debug(f"Command {message.fullName} has been encoded")

        # cmd_itf must exist to send command
        if self._cmd_itf is None:
            raise RuntimeError(
                "[sendcmd] Error cmd interface seems to be destroyed")

        # Send the command
        send_command_future = Future(self._thread_loop)
        send_status_userdata = ctypes.pointer(
            ctypes.py_object((send_command_future, message, args)))
        self._send_status_userdata[id(
            send_command_future)] = send_status_userdata
        res = od.arsdk_cmd_itf_send(self._cmd_itf, ctypes.pointer(cmd),
                                    self._send_status, send_status_userdata)

        if res != 0:
            self.logger.error(f"Error while sending command: {res}")
            send_command_future.set_result(False)
            return send_command_future

        event = ArsdkMessageEvent(message, args)
        # Update the currently monitored expectations
        self._scheduler.process_event(event)
        log_msg = f"{event} has been sent to the device"
        if quiet:
            self.logger.debug(log_msg)
        else:
            self.logger.info(log_msg)

        return send_command_future
Exemplo n.º 2
0
    def async_connect(self, *, timeout=None, later=False, retry=1):
        if timeout is None:
            timeout = 6 * retry

        # If not already connected to a device
        if self.connected:
            f = Future(self._thread_loop)
            f.set_result(True)
            return f

        if self._connected_future is None:
            if not later:
                self._connected_future = self._thread_loop.run_async(
                    self._do_connect, timeout, retry)
            else:
                self._connected_future = self._thread_loop.run_later(
                    self._do_connect, timeout, retry)

        def _on_connect_done(f):
            self._connected_future = None
        self._connected_future.add_done_callback(_on_connect_done)

        return self._connected_future
Exemplo n.º 3
0
 def close(self):
     """
     Close a playing or paused video stream session
     """
     if self.state is PdrawState.Closed:
         f = Future(self.pdraw_thread_loop)
         f.set_result(True)
     if self.state in (PdrawState.Opened, PdrawState.Paused,
                       PdrawState.Playing, PdrawState.Error):
         self.logger.debug(f"pdraw closing from the {self.state} state")
         if self._close_resp_future.done():
             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
Exemplo n.º 4
0
    def async_disconnect(self, *, timeout=5):
        """
        Disconnects current device (if any)
        Blocks until it is done or abandoned

        :rtype: bool
        """
        if not self.connected:
            f = Future(self._thread_loop)
            f.set_result(True)
            self.logger.debug("Already disconnected")
            return f

        if self._connected_future is not None:
            f = Future(self._thread_loop)
            f.set_result(False)
            self.logger.warning(
                "Cannot disconnect while a connection is in progress")
            return f

        self.logger.debug("We are not disconnected yet")
        disconnected = self._thread_loop.run_async(self._disconnection_impl)
        return disconnected
Exemplo n.º 5
0
    def _disconnection_impl(self):
        f = Future(self._thread_loop)
        if not self.connected:
            return f.set_result(True)

        self._disconnect_future = f
        res = od.arsdk_device_disconnect(self._device.arsdk_device)
        if res != 0:
            self.logger.error(
                f"Error while disconnecting from device: {self._ip_addr} ({res})")
            self._disconnect_future.set_result(False)
        else:
            self.logger.info(
                f"disconnected from device: {self._ip_addr}")
        return self._disconnect_future
Exemplo n.º 6
0
class ControllerBase(CommandInterfaceBase):
    def __init__(self,
                 ip_addr,
                 *,
                 name=None,
                 drone_type=0,
                 proto_v_min=1,
                 proto_v_max=3,
                 is_skyctrl=None,
                 video_buffer_queue_size=8,
                 media_autoconnect=True):
        self._logger_scope = "controller"
        self._ip_addr_str = str(ip_addr)
        self._ip_addr = ip_addr.encode('utf-8')
        self._is_skyctrl = is_skyctrl
        self._piloting = False
        self._piloting_command = PilotingCommand()
        super().__init__(
            name=name,
            drone_type=drone_type,
            proto_v_min=1,
            proto_v_max=3)
        self._connected_future = None
        self._last_disconnection_time = None
        # Setup piloting commands timer
        self._piloting_timer = self._thread_loop.create_timer(
            self._piloting_timer_cb)

    def _recv_message_type(self):
        return messages.ArsdkMessageType.CMD

    def _create_backend(self, name, proto_v_min, proto_v_max):
        self._backend = CtrlBackend(
            name=name, proto_v_min=proto_v_min, proto_v_max=proto_v_max
        )

    def _declare_callbacks(self):
        """
        Define all callbacks
        """
        super()._declare_callbacks()
        self._device_cbs_cfg = od.struct_arsdk_device_conn_cbs.bind({
            "connecting": self._connecting_cb,
            "connected": self._connected_cb,
            "disconnected": self._disconnected_cb,
            "canceled": self._canceled_cb,
            "link_status": self._link_status_cb,
        })

    @callback_decorator()
    def _create_command_interface(self):
        """
        Create a command interface to send command to the device
        """

        cmd_itf = od.POINTER_T(od.struct_arsdk_cmd_itf)()

        res = od.arsdk_device_create_cmd_itf(
            self._device.arsdk_device,
            self._cmd_itf_cbs,
            ctypes.pointer(cmd_itf))

        if res != 0:
            self.logger.error(f"Error while creating command interface: {res}")
            cmd_itf = None
        else:
            self.logger.info("Command interface has been created")

        return cmd_itf

    @callback_decorator()
    def _connecting_cb(self, _arsdk_device, arsdk_device_info, _user_data):
        """
        Notify connection initiation.
        """
        device_name = od.string_cast(arsdk_device_info.contents.name)
        self.logger.info(f"Connecting to device: {device_name}")

    @callback_decorator()
    def _connected_cb(self, _arsdk_device, arsdk_device_info, _user_data):
        """
        Notify connection completion.
        """
        device_name = od.string_cast(arsdk_device_info.contents.name)
        self.set_device_name(device_name)
        self.logger.info(f"Connected to device: {device_name}")
        json_info = od.string_cast(arsdk_device_info.contents.json)
        try:
            json_info = json.loads(json_info)
            self.logger.info(pprint.pformat(json_info))
        except ValueError:
            self.logger.error(f'json contents cannot be parsed: {json_info}')

        self.connected = True

        if self._connect_future is not None and not self._connect_future.done():
            self._connect_future.set_result(True)

    @callback_decorator()
    def _disconnected_cb(self, _arsdk_device, arsdk_device_info, _user_data):
        """
         Notify disconnection.
        """
        device_name = od.string_cast(arsdk_device_info.contents.name)
        self.logger.info(f"Disconnected from device: {device_name}")
        self.connected = False
        if self._disconnect_future is not None and not self._disconnect_future.done():
            self._disconnect_future.set_result(True)
        self._thread_loop.run_later(self._on_device_removed)

    @callback_decorator()
    def _canceled_cb(self, _arsdk_device, arsdk_device_info, reason, _user_data):
        """
        Notify connection cancellation. Either because 'disconnect' was
        called before 'connected' callback or remote aborted/rejected the
        request.
        """
        device_name = od.string_cast(arsdk_device_info.contents.name)
        reason = od.string_cast(
            od.arsdk_conn_cancel_reason_str(reason))
        self.logger.info(
            f"Connection to device: {device_name} has been canceled for reason: {reason}")
        if self._connect_future is not None and not self._connect_future.done():
            self._connect_future.set_result(False)
        self._thread_loop.run_later(self._on_device_removed)

    @callback_decorator()
    def _link_status_cb(self, _arsdk_device, _arsdk_device_info, status, _user_data):
        """
         Notify link status. At connection completion, it is assumed to be
         initially OK. If called with KO, user is responsible to take action.
         It can either wait for link to become OK again or disconnect
         immediately. In this case, call arsdk_device_disconnect and the
         'disconnected' callback will be called.
        """
        self.logger.info(f"Link status: {status}")
        # If link has been lost, we must start disconnection procedure
        if status == od.ARSDK_LINK_STATUS_KO:
            # the device has been disconnected
            self.connected = False
            self._thread_loop.run_later(self._on_device_removed)

    @callback_decorator()
    def _disconnection_impl(self):
        f = Future(self._thread_loop)
        if not self.connected:
            return f.set_result(True)

        self._disconnect_future = f
        res = od.arsdk_device_disconnect(self._device.arsdk_device)
        if res != 0:
            self.logger.error(
                f"Error while disconnecting from device: {self._ip_addr} ({res})")
            self._disconnect_future.set_result(False)
        else:
            self.logger.info(
                f"disconnected from device: {self._ip_addr}")
        return self._disconnect_future

    def _synchronize_clock(self):

        date_time = datetime.datetime.now(
            get_localzone()).strftime("%Y%m%dT%H%M%S%z")
        if not self._is_skyctrl:
            current_date_time = common.Common.CurrentDateTime
        else:
            current_date_time = skyctrl.Common.CurrentDateTime
        res = self(current_date_time(datetime=date_time, _timeout=0.5))

        def _on_sync_done(res):
            if not res.success():
                msg = "Time synchronization failed for {}".format(
                    self._ip_addr)
                self.logger.warning(msg)
            else:
                self.logger.info("Synchronization of {} at {}".format(
                    self._ip_addr, date_time))

        res.add_done_callback(_on_sync_done)

    @callback_decorator()
    def _start_piloting_impl(self):
        delay = 100
        period = 25

        ok = self._thread_loop.set_timer(self._piloting_timer, delay, period)

        if ok:
            self._piloting = True
            self.logger.info(
                "Piloting interface has been correctly launched")
        else:
            self.logger.error("Unable to launch piloting interface")
        return self._piloting

    @callback_decorator()
    def _stop_piloting_impl(self):

        # Stop the drone movements
        self._piloting_command.set_default_piloting_command()
        time.sleep(0.1)

        ok = self._thread_loop.clear_timer(self._piloting_timer)
        if ok:
            # Reset piloting state value to False
            self._piloting = False
            self.logger.info("Piloting interface stopped")
        else:
            self.logger.error("Unable to stop piloting interface")

    @callback_decorator()
    def _piloting_timer_cb(self, timer, _user_data):
        self.logger.debug(f"piloting timer callback: {timer}")
        if self.connected:
            self._send_piloting_command()

    def _send_piloting_command(self):
        # When piloting time is 0 => send default piloting commands
        if self._piloting_command.piloting_time:
            # Check if piloting time since last pcmd order has been reached
            diff_time = (
                datetime.datetime.now() -
                self._piloting_command.initial_time
            )
            if diff_time.total_seconds() >= self._piloting_command.piloting_time:
                self._piloting_command.set_default_piloting_command()

        # Flag to activate movement on roll and pitch. 1 activate, 0 deactivate
        if self._piloting_command.roll or (
            self._piloting_command.pitch
        ):
            activate_movement = 1
        else:
            activate_movement = 0

        self._send_command_impl(
            ardrone3.Piloting.PCMD,
            dict(
                flag=activate_movement,
                roll=self._piloting_command.roll,
                pitch=self._piloting_command.pitch,
                yaw=self._piloting_command.yaw,
                gaz=self._piloting_command.gaz,
                timestampAndSeqNum=0,
            ),
            quiet=True,
        )

    def start_piloting(self):
        """
        Start interface to send piloting commands

        :rtype: bool
        """
        if self._piloting:
            self.logger.debug("Piloting interface already started")
            return True

        f = self._thread_loop.run_async(self._start_piloting_impl)

        try:
            ok = f.result_or_cancel(timeout=2)
        except (FutureTimeoutError, CancelledError):
            self.logger.error("Unable to launch piloting interface")
            return False
        if not ok:
            self.logger.error("Unable to launch piloting interface")
            return False

        self.logger.info("Piloting started")
        return True

    def stop_piloting(self):
        """
        Stop interface to send piloting commands

        :rtype: bool
        """
        # Check piloting interface is running
        if not self._piloting:
            self.logger.debug("Piloting interface already stopped")
            return True

        f = self._thread_loop.run_async(self._stop_piloting_impl)

        try:
            ok = f.result_or_cancel(timeout=2)
        except (FutureTimeoutError, CancelledError):
            self.logger.error("Unable to stop piloting interface")
            return False
        if not ok:
            self.logger.error("Unable to stop piloting interface")
            return False

        self.logger.info("Piloting stopped")
        return True

    def piloting(self, roll, pitch, yaw, gaz, piloting_time):
        """
        Send manual piloting commands to the drone.
        This function is a non-blocking.

        :type roll: int
        :param roll: roll consign for the drone (must be in [-100:100])
        :type pitch: int
        :param pitch: pitch consign for the drone  (must be in [-100:100])
        :type yaw: int
        :param yaw: yaw consign for the drone  (must be in [-100:100])
        :type gaz: int
        :param gaz: gaz consign for the drone  (must be in [-100:100])
        :type piloting_time: float
        :param piloting_time: The time of the piloting command
        :rtype: bool

        """
        if not self.start_piloting():
            return False
        self._piloting_command.update_piloting_command(roll, pitch, yaw, gaz, piloting_time)
        return True

    def piloting_pcmd(self, roll, pitch, yaw, gaz, piloting_time):
        warn(
            "ControllerBase.piloting_pcmd is deprecated, "
            "please use ControllerBase.piloting instead",
            DeprecationWarning
        )
        return self.piloting(roll, pitch, yaw, gaz, piloting_time)

    async def _async_discover_device(self):
        # Try to identify the device type we are attempting to connect to...
        discovery = DiscoveryNet(self._backend, ip_addr=self._ip_addr)
        device = await discovery.async_get_device()
        if device is None:
            self.logger.info(f"Net discovery failed for {self._ip_addr}")
            self.logger.info(f"Trying 'NetRaw' discovery for {self._ip_addr} ...")
            assert discovery.stop()
            discovery = DiscoveryNetRaw(self._backend, ip_addr=self._ip_addr)
            device = await discovery.async_get_device()
            if device is None:
                discovery.stop()
        return device, discovery

    async def _async_get_device(self):
        if self._device is not None:
            return True
        device, discovery = await self._async_discover_device()

        if device is None:
            self.logger.info(f"Unable to discover the device: {self._ip_addr}")
            return False

        # Save device related info
        self._device = device
        self._discovery = discovery
        self._device_type = self._device.type
        if self._is_skyctrl is None:
            if self._device_type in SKYCTRL_DEVICE_TYPE_LIST:
                self._is_skyctrl = True
            else:
                self._is_skyctrl = False
        return True

    @callback_decorator()
    def _connect_impl(self):

        # Use default values for connection json. If we want to changes values
        # (or add new info), we just need to add them in req (using json format)
        # For instance:
        req = bytes('{ "%s": "%s", "%s": "%s", "%s": "%s"}' % (
            "arstream2_client_stream_port", PDRAW_LOCAL_STREAM_PORT,
            "arstream2_client_control_port", PDRAW_LOCAL_CONTROL_PORT,
            "arstream2_supported_metadata_version", "1"), 'utf-8')
        device_id = b""

        device_conn_cfg = od.struct_arsdk_device_conn_cfg(
            ctypes.create_string_buffer(b"arsdk-ng"), ctypes.create_string_buffer(b"desktop"),
            ctypes.create_string_buffer(bytes(device_id)), ctypes.create_string_buffer(req))

        # Send connection command
        self._connect_future = Future(self._thread_loop)
        res = od.arsdk_device_connect(
            self._device.arsdk_device,
            device_conn_cfg,
            self._device_cbs_cfg,
            self._thread_loop.pomp_loop
        )
        if res != 0:
            self.logger.error(f"Error while connecting: {res}")
            self._connect_future.set_result(False)
        else:
            self.logger.info("Connection in progress...")
        return self._connect_future

    async def _do_connect(self, timeout, retry):
        grace_period = 5.
        if self._last_disconnection_time is not None:
            last_disconnection = (time.time() - self._last_disconnection_time)
            if last_disconnection < grace_period:
                await self._thread_loop.asleep(grace_period - last_disconnection)
        # the deadline does not include the grace period
        deadline = time.time() + timeout
        backoff = 2.
        for i in range(retry):
            if self.connected:
                # previous connection attempt timedout but we're connected..
                break
            if deadline < time.time():
                self.logger.error(f"'{self._ip_addr_str} connection timed out")
                return False
            self.logger.debug(f"Discovering device {self._ip_addr_str} ...")
            if not await self._async_get_device():
                self.logger.debug(f"Discovering device {self._ip_addr_str} failed")
                await self._thread_loop.asleep(backoff)
                backoff *= 2.
                continue

            self.logger.debug(f"Connecting device {self._ip_addr_str} ...")
            connected = await self._connect_impl()
            if not connected:
                self.logger.debug(f"Connecting device {self._ip_addr_str} failed")
                await self._thread_loop.asleep(backoff)
                backoff *= 2.
                continue

            # We're connected
            break
        else:
            self.logger.error(f"'{self._ip_addr_str} connection retries failed")
            return False

        # Create the arsdk command interface
        if self._cmd_itf is None:
            self.logger.debug(f"Creating cmd_itf for {self._ip_addr_str} ...")
            self._cmd_itf = self._create_command_interface()
            if self._cmd_itf is None:
                self.logger.error(f"Creating cmd_itf for {self._ip_addr_str} failed")
                self.disconnect()
                return False

        if self.connected:
            return await self._on_connected(deadline)
        else:
            return False

    async def _send_states_settings_cmd(self, command):
        res = await self(command)
        if not res.success():
            self.logger.error(
                f"Unable get device state/settings: {command} "
                f"for {self._ip_addr}")
            self.disconnect()
            return False
        return True

    async def _on_connected(self, deadline):
        if not self._ip_addr_str.startswith("10.202") and (
                not self._ip_addr_str.startswith("127.0")):
            self._synchronize_clock()
        # We're connected to the device, get all device states and settings if necessary
        if not self._is_skyctrl:
            all_states_settings_commands = [
                common.Common.AllStates(), common.Settings.AllSettings()]
        else:
            all_states_settings_commands = [
                skyctrl.Common.AllStates(), skyctrl.Settings.AllSettings()]

        # Get device specific states and settings
        timeout = deadline - time.time()
        for states_settings_command in all_states_settings_commands:
            res = await self._thread_loop.await_for(
                timeout,
                self._send_states_settings_cmd, states_settings_command
            )
            if not res:
                return False

        if self._is_skyctrl:
            # If the skyctrl is connected to a drone get the drone states and settings too
            if self(drone_manager.connection_state(
                    state="connected", _policy="check")):
                all_states = await self(
                    common.CommonState.AllStatesChanged() &
                    common.SettingsState.AllSettingsChanged(),
                )
                if not all_states.success():
                    self.logger.error("Unable get connected drone states and/or settings")
                    return False
                # Enable airsdk mission support from the drone
                if not await self(mission.custom_msg_enable()):
                    self.logger.error("Failed to send mission.custom_msg_enable")
                    return False
        else:
            # Enable airsdk mission support from the drone
            if not await self(mission.custom_msg_enable()):
                self.logger.error("Failed to send mission.custom_msg_enable")
                return False

        # Process the ConnectedEvent
        event = ConnectedEvent()
        self.logger.info(str(event))
        self._scheduler.process_event(event)
        return True

    def async_connect(self, *, timeout=None, later=False, retry=1):
        if timeout is None:
            timeout = 6 * retry

        # If not already connected to a device
        if self.connected:
            f = Future(self._thread_loop)
            f.set_result(True)
            return f

        if self._connected_future is None:
            if not later:
                self._connected_future = self._thread_loop.run_async(
                    self._do_connect, timeout, retry)
            else:
                self._connected_future = self._thread_loop.run_later(
                    self._do_connect, timeout, retry)

        def _on_connect_done(f):
            self._connected_future = None
        self._connected_future.add_done_callback(_on_connect_done)

        return self._connected_future

    def connect(self, *, timeout=None, retry=1):
        """
        Make all step to make the connection between the device and the controller

        :param timeout: the global connection timeout in seconds  (including the
         retried connection attempt duration when `retry` > 1)
        :param retry: the number of connection attempts (default to `1`)

        :rtype: bool
        """

        connected_future = self.async_connect(timeout=timeout, retry=retry)
        try:
            connected_future.result_or_cancel(timeout=timeout)
        except (FutureTimeoutError, CancelledError):
            self.logger.error(f"'{self._ip_addr_str} connection timed out")
            # If the connection timedout we must disconnect
            self.disconnect()

        return self.connected

    def _on_device_removed(self):
        if self._discovery:
            self._discovery.stop()
        if self._piloting:
            self._stop_piloting_impl()
        self._disconnection_impl()
        self._last_disconnection_time = time.time()
        self._piloting_command = PilotingCommand()
        super()._on_device_removed()

    def _reset_instance(self):
        self._piloting = False
        self._piloting_command = PilotingCommand()
        self._device = None
        self._device_name = None
        self._discovery = None
        self._connected_future = None
        super()._reset_instance()
Exemplo n.º 7
0
class Pdraw(LogMixin):
    def __init__(
        self,
        name=None,
        device_name=None,
        server_addr=None,
        buffer_queue_size=8,
        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
        """

        super().__init__(name, device_name, "pdraw")

        if pdraw_thread_loop is None:
            self.own_pdraw_thread_loop = True
            self.pdraw_thread_loop = PompLoopThread(self.logger)
            self.pdraw_thread_loop.start()
        else:
            self.own_pdraw_thread_loop = False
            self.pdraw_thread_loop = pdraw_thread_loop

        self.callbacks_thread_loop = PompLoopThread(
            self.logger, parent=self.pdraw_thread_loop)
        self.callbacks_thread_loop.start()
        self.buffer_queue_size = buffer_queue_size
        self.pomp_loop = self.pdraw_thread_loop.pomp_loop

        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._stop_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.pdraw_demuxer = od.POINTER_T(od.struct_pdraw_demuxer)()
        self.streams = defaultdict(StreamFactory)
        self.session_metadata = {}

        self.outfiles = {
            od.VDEF_FRAME_TYPE_CODED: {
                'data': None,
                'meta': None,
                'info': None,
            },
            od.VDEF_FRAME_TYPE_RAW: {
                'data': None,
                'meta': None,
                'info': None,
            },
        }

        self.frame_callbacks = {
            (od.VDEF_FRAME_TYPE_CODED, od.VDEF_CODED_DATA_FORMAT_AVCC): None,
            (od.VDEF_FRAME_TYPE_CODED, od.VDEF_CODED_DATA_FORMAT_BYTE_STREAM):
            None,
            (od.VDEF_FRAME_TYPE_RAW, None): None,
        }
        self.start_callback = None
        self.end_callback = None
        self.flush_callbacks = {
            od.VDEF_FRAME_TYPE_CODED: None,
            od.VDEF_FRAME_TYPE_RAW: None,
        }

        self.url = None
        if server_addr is None:
            server_addr = "192.168.42.1"
        self.server_addr = server_addr
        self.resource_name = "live"
        self.media_name = None

        self.demuxer_cbs = od.struct_pdraw_demuxer_cbs.bind({
            "open_resp":
            self._open_resp,
            "close_resp":
            self._close_resp,
            "unrecoverable_error":
            self._unrecoverable_error,
            "ready_to_play":
            self._ready_to_play,
            "play_resp":
            self._play_resp,
            "pause_resp":
            self._pause_resp,
            "seek_resp":
            self._seek_resp,
            "select_media":
            self._select_media,
            "end_of_range":
            self._end_of_range,
        })
        self.pdraw_cbs = od.struct_pdraw_cbs.bind({
            "socket_created": self._socket_created,
            "media_added": self._media_added,
            "media_removed": self._media_removed,
            "stop_resp": self.stop_resp,
        })

        self.video_sink_vt = {
            od.VDEF_FRAME_TYPE_CODED: _CodedVideoSink,
            od.VDEF_FRAME_TYPE_RAW: _RawVideoSink,
        }

        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)

    def dispose(self):
        return self.pdraw_thread_loop.run_later(self._dispose)

    def destroy(self):
        self.callbacks_thread_loop.stop()
        try:
            self.dispose().result_or_cancel(timeout=2.)
        except FutureTimeoutError:
            self.logger.error("Pdraw.destroy() timedout")
        self.pdraw_thread_loop.stop()

    @callback_decorator()
    def _dispose(self):
        self.pdraw_thread_loop.unregister_cleanup(self._dispose,
                                                  ignore_error=True)
        return self.pdraw_thread_loop.run_async(self._dispose_impl)

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

    @callback_decorator()
    def _stop(self):
        if self._stop_resp_future.done():
            self._stop_resp_future = Future(self.pdraw_thread_loop)
        if not self.pdraw:
            self._stop_resp_future.set_result(True)
            return self._stop_resp_future
        res = od.pdraw_stop(self.pdraw)
        if res != 0:
            self.logger.error(f"cannot stop pdraw session {res}")
            self._stop_resp_future.set_result(False)
        if self.callbacks_thread_loop.stop():
            self.logger.info("pdraw callbacks thread loop stopped")
        # 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.logger.warning(
                    "cleanup leftover pdraw callbacks eventfds")
                self.callbacks_thread_loop.remove_event_from_loop(
                    stream['video_queue_event'])
                stream['video_queue_event'] = None
        return self._stop_resp_future

    @callback_decorator()
    def stop_resp(self, pdraw, status, userdata):
        if status != 0:
            self.logger.error(f"_stop_resp called {status}")
            self._stop_resp_future.set_result(False)
            self.state = PdrawState.Error
        else:
            self.logger.info(f"_stop_resp called {status}")
            self._stop_resp_future.set_result(True)
            self.state = PdrawState.Stopped
        return self._stop_resp_future

    def _destroy_pdraw(self):
        ret = True
        if self.pdraw_demuxer:
            if not self.pdraw:
                self.logger.error(
                    "Cannot destroy pdraw demuxer: a NULL pdraw session")
                return False
            self.logger.info("destroying pdraw demuxer...")
            res = od.pdraw_demuxer_destroy(self.pdraw, self.pdraw_demuxer)
            if res != 0:
                self.logger.error(f"cannot destroy pdraw demuxer {res}")
                ret = False
            else:
                self.logger.info("pdraw demuxer destroyed")
            self.pdraw_demuxer = od.POINTER_T(od.struct_pdraw_demuxer)()
        if self.pdraw:
            self.logger.info("destroying pdraw...")
            res = od.pdraw_destroy(self.pdraw)
            if res != 0:
                self.logger.error(f"cannot destroy pdraw {res}")
                ret = False
            else:
                self.logger.info("pdraw destroyed")
            self.pdraw = od.POINTER_T(od.struct_pdraw)()
        return ret

    def _open_url(self):
        """
        Opening rtsp streaming url
        """
        if self.resource_name.startswith("replay/"):
            if self.media_name is None:
                self.logger.error(
                    "Error media_name should be provided in video stream "
                    "replay mode")
                return False
        res = od.pdraw_demuxer_new_from_url(
            self.pdraw, self.url, self.demuxer_cbs,
            ctypes.cast(ctypes.pointer(ctypes.py_object(self)),
                        ctypes.c_void_p), ctypes.byref(self.pdraw_demuxer))

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

    @callback_decorator()
    def _open_stream(self):
        """
        Opening pdraw stream using an rtsp url
        """
        self._open_resp_future = Future(self.pdraw_thread_loop)
        if self.state not in (PdrawState.Error, PdrawState.Stopped,
                              PdrawState.Closed, PdrawState.Created):
            self.logger.warning(f"Cannot open stream from {self.state}")
            self._open_resp_future.set_result(False)
            return self._open_resp_future

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

        ret = self._open_url()

        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 is PdrawState.Closed:
            f = Future(self.pdraw_thread_loop)
            f.set_result(True)
        if self.state in (PdrawState.Opened, PdrawState.Paused,
                          PdrawState.Playing, PdrawState.Error):
            self.logger.debug(f"pdraw closing from the {self.state} state")
            if self._close_resp_future.done():
                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.logger.info("pdraw is already closed")
            self._close_resp_future.set_result(True)
            return self._close_resp_future

        if not self.pdraw:
            self.logger.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_demuxer_close(self.pdraw, self.pdraw_demuxer)

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

        return True

    @callback_decorator()
    def _on_close_resp_done(self, close_resp_future):
        if not close_resp_future.cancelled():
            return

        close_resp_future.cancel()
        self.logger.error("Closing pdraw demuxer timedout")

        if self.state not in (PdrawState.Closed, PdrawState.Stopped,
                              PdrawState.Error):
            return

        # FIXME: workaround pdraw closing timeout
        # This random issue is quiet hard to reproduce
        self.logger.error("destroying pdraw demuxer...")
        if self.pdraw and self.pdraw_demuxer:
            self.pdraw_thread_loop.run_later(
                od.pdraw_demuxer_destroy, self.pdraw,
                self.pdraw_demuxer).then(lambda _: self._stop())
        self.pdraw = od.POINTER_T(od.struct_pdraw)()
        self.state = PdrawState.Closed

    def _open_resp(self, pdraw, demuxer, status, userdata):
        self.logger.debug("_open_resp called")
        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, demuxer, status, userdata):
        self._close_output_files()
        if status != 0:
            self.logger.error(f"_close_resp called {status}")
            self._close_resp_future.set_result(False)
            self.state = PdrawState.Error
        else:
            self.logger.debug(f"_close_resp called {status}")
            self.state = PdrawState.Closed
            self._close_resp_future.set_result(True)
        if not self._open_resp_future.done():
            self._open_resp_future.set_result(False)
        if demuxer == self.pdraw_demuxer:
            self.pdraw_demuxer = od.POINTER_T(od.struct_pdraw_demuxer)()
        res = od.pdraw_demuxer_destroy(pdraw, demuxer)
        if res != 0:
            self.logger.error(f"pdraw_demuxer_destroy: {res}")
        else:
            self.logger.debug(f"pdraw_demuxer_destroy: {res}")

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

    def _unrecoverable_error(self, pdraw, demuxer, userdata):
        self.logger.error("_unrecoverable_error() -> pdraw teardown...")
        # remove every video sinks
        for id_ in self.streams:
            self._video_sink_flush_impl(id_)
            self._media_removed_impl(id_)

        # demuxer.close -> demuxer.destroy
        if self.pdraw and self.pdraw_demuxer:
            od.pdraw_demuxer_close(self.pdraw, self.pdraw_demuxer)
            # the demuxer will be destroyed in close_resp
            self.pdraw_demuxer = od.POINTER_T(od.struct_pdraw_demuxer)()
        self.logger.error("_unrecoverable_error() -> pdraw teardown done")

        # we should be good to go again with a Pdraw.play()
        self._state = PdrawState.Created

    def _ready_to_play(self, pdraw, demuxer, ready, userdata):
        self.logger.info(f"_ready_to_play({ready}) called")
        self._is_ready_to_play = bool(ready)
        if self._is_ready_to_play:
            if self._play_resp_future.done():
                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, demuxer, status, timestamp, speed, userdata):
        if status == 0:
            self.logger.debug(f"_play_resp called {status}")
            self.state = PdrawState.Playing
            if not self._play_resp_future.done():
                self._play_resp_future.set_result(True)
        else:
            self.logger.error(f"_play_resp called {status}")
            self.state = PdrawState.Error
            if not self._play_resp_future.done():
                self._play_resp_future.set_result(False)

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

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

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

    def _select_media(self, pdraw, demuxer, medias, count, userdata):
        # by default select the default media (media_id=0)
        selected_media_id = 0
        selected_media_idx = 0
        default_media_id = 0
        default_media_idx = 0
        for idx in range(count):
            self.logger.info(f"_select_media: "
                             f"idx={idx} media_id={medias[idx].media_id} "
                             f"name={od.string_cast(medias[idx].name)} "
                             f"default={str(bool(medias[idx].is_default))}")
            if bool(medias[idx].is_default):
                default_media_id = medias[idx].media_id
                default_media_idx = idx
            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):
            default_media_name = od.string_cast(medias[default_media_idx].name)
            self.logger.warning(
                f"media_name {self.media_name} is unavailable. "
                f"Selecting the default media instead: {default_media_name}")
            self.session_metadata = od.struct_vmeta_session.as_dict(
                medias[default_media_idx].session_meta)
        else:
            self.session_metadata = od.struct_vmeta_session.as_dict(
                medias[selected_media_idx].session_meta)
        if selected_media_id:
            return 1 << selected_media_id
        elif default_media_id:
            return 1 << default_media_id
        else:
            return 0

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

        video_info = media_info.contents.pdraw_media_info_0.video
        frame_type = video_info.format

        if frame_type == od.VDEF_FRAME_TYPE_CODED:
            video_info = video_info.pdraw_video_info_0.coded
            vdef_format = video_info.format
            data_format = vdef_format.data_format
        else:
            video_info = video_info.pdraw_video_info_0.raw
            vdef_format = video_info.format
            data_format = None
        media_type = (frame_type, data_format)

        # store the information if it is supported and requested media
        # otherwise exit
        if (frame_type != od.VDEF_FRAME_TYPE_RAW
                and frame_type != od.VDEF_FRAME_TYPE_CODED):
            self.logger.warning(f"Ignoring unsupported media id {id_} "
                                f"(type {video_info.format})")
            return

        requested_media = False
        if self.frame_callbacks[media_type] is not None:
            requested_media = True
        elif any(map(lambda f: f is not None, self.outfiles[frame_type])):
            requested_media = True

        if not requested_media:
            self.logger.info(f"Skipping non-requested media id {id_} "
                             f"(type {video_info.format})")
            return

        self.streams[id_]["media_type"] = media_type
        self.streams[id_]["frame_type"] = frame_type
        self.streams[id_]["vdef_format"] = vdef_format

        if frame_type == od.VDEF_FRAME_TYPE_CODED and (
                od.VDEF_CODED_DATA_FORMAT_BYTE_STREAM):
            outfile = self.outfiles[frame_type]["data"]
            if outfile:
                header = video_info.pdraw_coded_video_info_0.h264
                header = H264Header(
                    bytearray(header.sps),
                    int(header.spslen),
                    bytearray(header.pps),
                    int(header.ppslen),
                )
                self.streams[id_]['h264_header'] = header
                self.streams[id_]['track_id'] = outfile.add_track(
                    type=od.MP4_TRACK_TYPE_VIDEO,
                    name="video",
                    enabled=1,
                    in_movie=1,
                    in_preview=1,
                )
                self.streams[id_]['metadata_track_id'] = outfile.add_track(
                    type=od.MP4_TRACK_TYPE_METADATA,
                    name="metadata",
                    enabled=0,
                    in_movie=0,
                    in_preview=0,
                )

                outfile.ref_to_track(self.streams[id_]['metadata_track_id'],
                                     self.streams[id_]['track_id'])

        # start a video sink attached to the new media
        video_sink_params = od.struct_pdraw_video_sink_params.bind(
            dict(
                # drop buffers when the queue is full (buffer_queue_size > 0)
                queue_max_count=self.buffer_queue_size,  # buffer queue size
            ))
        self.streams[id_]['id_userdata'] = ctypes.cast(
            ctypes.pointer(ctypes.py_object(id_)), ctypes.c_void_p)
        self.streams[id_]['id'] = id_
        self.streams[id_]['video_sink_cbs'] = self.video_sink_vt[
            frame_type].cbs.bind({"flush": self._video_sink_flush})
        self.streams[id_]["frame_type"] = frame_type
        self.streams[id_]['video_sink'] = od.POINTER_T(
            self.video_sink_vt[frame_type].video_sink_type)()

        res = self.video_sink_vt[frame_type].new(
            pdraw, id_, video_sink_params, self.streams[id_]['video_sink_cbs'],
            self.streams[id_]['id_userdata'],
            ctypes.byref(self.streams[id_]['video_sink']))
        if res != 0 or not self.streams[id_]['video_sink']:
            self.logger.error("Unable to start video sink")
            return

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

        # Retrieve event object and related file descriptor
        res = self.video_sink_vt[frame_type].queue_get_event(
            queue, ctypes.byref(self.streams[id_]['video_queue_event']))
        if res < 0 or not self.streams[id_]['video_queue_event']:
            self.logger.error(f"Unable to get video sink queue event: {-res}")
            return

        # 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.logger.error(f"Received removed event from unknown ID {id_}")
            return

        self.logger.info(f"_media_removed called id : {id_}")

        # FIXME: Workaround media_removed called with destroyed media
        if not self.pdraw:
            self.logger.error(
                f"_media_removed called with a destroyed pdraw id : {id_}")
            return
        self._media_removed_impl(id_)

    def _media_removed_impl(self, id_):
        frame_type = self.streams[id_]['frame_type']
        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']).result_or_cancel(
                        timeout=5.)
            self.streams[id_]['video_queue_event'] = None

            if not self.streams[id_]['video_sink']:
                self.logger.error(f"pdraw_video_sink for media_id {id_} "
                                  f"has already been stopped")
                return
            res = self.video_sink_vt[frame_type].destroy(
                self.pdraw, self.streams[id_]['video_sink'])
            if res < 0:
                self.logger.error(f"pdraw_stop_video_sink() returned {res}")
            else:
                self.logger.debug(
                    f"_media_removed video sink destroyed id : {id_}")
            self.streams[id_]['video_queue'] = None
            self.streams[id_]['video_sink'] = None
            self.streams[id_]['video_sink_cbs'] = None

    def _end_of_range(self, pdraw, demuxer, timestamp, userdata):
        self.logger.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.logger.error(f"Received flush event from unknown ID {id_}")
            return -errno.ENOENT

        return self._video_sink_flush_impl(id_)

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

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

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

            flush_callback = self.flush_callbacks[self.streams[id_]
                                                  ['frame_type']]
            if flush_callback is not None:
                flushed = self.callbacks_thread_loop.run_async(
                    flush_callback, self.streams[id_])
                try:
                    if not flushed.result_or_cancel(timeout=5.):
                        self.logger.error(f"video sink flush id {id_} error")
                except FutureTimeoutError:
                    self.logger.error(f"video sink flush id {id_} timeout")
                # 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...
            frame_type = self.streams[id_]['frame_type']
            res = self.video_sink_vt[frame_type].queue_flush(
                self.streams[id_]['video_queue'])
            if res < 0:
                self.logger.error(
                    f"mbuf_coded/raw_video_frame_queue_flush() returned {res}")
            else:
                self.logger.info(
                    f"mbuf_coded/raw_video_frame_queue_flush() returned {res}")

            res = self.video_sink_vt[frame_type].queue_flushed(
                self.pdraw, self.streams[id_]['video_sink'])
            if res < 0:
                self.logger.error(
                    f"pdraw_coded/raw_video_sink_queue_flushed() "
                    f"returned {res}")
            else:
                self.logger.debug(
                    f"pdraw_coded/raw_video_sink_queue_flushed() "
                    f"returned {res}")
            return 0

    @callback_decorator()
    def _video_sink_queue_event(self, pomp_evt, userdata):
        id_ = py_object_cast(userdata)
        self.logger.debug(f"media id = {id_}")

        if id_ not in self.streams:
            self.logger.error(f"Received queue event from unknown ID {id_}")
            return

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

        if not self._is_ready_to_play:
            self.logger.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_):
        frame_type = self.streams[id_]['frame_type']
        mbuf_video_frame = od.POINTER_T(
            self.video_sink_vt[frame_type].mbuf_video_frame_type)()
        res = self.video_sink_vt[frame_type].queue_pop(
            self.streams[id_]['video_queue'], ctypes.byref(mbuf_video_frame))
        if res < 0:
            if res not in (-errno.EAGAIN, -errno.ENOENT):
                self.logger.error(
                    f"mbuf_coded_video_frame_queue_pop returned error {res}")
            mbuf_video_frame = od.POINTER_T(
                self.video_sink_vt[frame_type].mbuf_video_frame_type)()
        elif not mbuf_video_frame:
            self.logger.error('mbuf_coded_video_frame_queue_pop returned NULL')
        return mbuf_video_frame

    def _process_stream(self, id_):
        self.logger.debug(f"media id = {id_}")
        mbuf_video_frame = self._pop_stream_buffer(id_)
        if not mbuf_video_frame:
            return False
        video_frame = VideoFrame(self.logger, mbuf_video_frame,
                                 id_, self.streams[id_],
                                 self.get_session_metadata())
        try:
            self._process_stream_buffer(id_, video_frame)
            return True
        except Exception:
            self.logger.exception("_process_stream_buffer exception")
            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_]
        frame_type = stream['frame_type']
        media_type = stream["media_type"]

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

        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 frame_type == od.VDEF_FRAME_TYPE_CODED:
                if stream["track_id"] is not None:
                    track_id = stream["track_id"]
                    metadata_track_id = stream["metadata_track_id"]
                    h264_header = stream["h264_header"]
                    if f.tell(track_id) == 0:
                        now = time.time()
                        f.set_decoder_config(track_id, h264_header,
                                             video_frame.width,
                                             video_frame.height)
                        f.set_metadata_mime_type(
                            metadata_track_id,
                            od.VMETA_FRAME_PROTO_CONTENT_ENCODING,
                            od.VMETA_FRAME_PROTO_MIME_TYPE)
                        f.add_track_metadata(
                            track_id, "com.parrot.olympe.first_timestamp",
                            str(now * PDRAW_TIMESCALE))
                        f.add_track_metadata(
                            track_id,
                            "com.parrot.olympe.resolution",
                            f"{video_frame.width}x{video_frame.height}",
                        )
                    f.add_coded_frame(track_id, metadata_track_id, video_frame)
            else:
                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[media_type]
        if cb is not None:
            cb(video_frame)

    def set_output_files(self, video=None, metadata=None, info=None):
        """
        Records the video stream session to the disk

        - video: path to the video stream mp4 recording file
        - metadata: path to the video stream metadata json output file
        - info: path to video stream frames info json output file

        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 frame_type, data_type, filepath, attrib in (
            (od.VDEF_FRAME_TYPE_CODED, 'data', video,
             'wb'), (od.VDEF_FRAME_TYPE_CODED, 'meta', metadata,
                     'w'), (od.VDEF_FRAME_TYPE_CODED, 'info', info, 'w')):
            if self.outfiles[frame_type][data_type]:
                self.outfiles[frame_type][data_type].close()
                self.outfiles[frame_type][data_type] = None

            if filepath is None:
                continue

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

    def set_callbacks(self,
                      h264_cb=None,
                      h264_avcc_cb=None,
                      h264_bytestream_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 (AVCC) stream
        - `h264_avcc_cb` is associated to the H264 encoded video (AVCC) stream
        - `h264_bytestream_cb` is associated to the H264 encoded video
            (ByteStream) 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.
        """

        if h264_cb and (h264_avcc_cb or h264_bytestream_cb):
            raise ValueError(
                "Invalid parameters combination: "
                "h264_cb and one of h264_avcc_cb or h264_bytestream_cb have "
                "been set")

        h264_avcc_cb = h264_avcc_cb or h264_cb

        for media_type, cb in (((od.VDEF_FRAME_TYPE_CODED,
                                 od.VDEF_CODED_DATA_FORMAT_AVCC),
                                h264_avcc_cb),
                               ((od.VDEF_FRAME_TYPE_CODED,
                                 od.VDEF_CODED_DATA_FORMAT_BYTE_STREAM),
                                h264_bytestream_cb), ((od.VDEF_FRAME_TYPE_RAW,
                                                       None), raw_cb)):
            self.frame_callbacks[media_type] = callback_decorator(
                logger=self.logger)(cb)
        for frame_type, cb in ((od.VDEF_FRAME_TYPE_CODED, flush_h264_cb),
                               (od.VDEF_FRAME_TYPE_RAW, flush_raw_cb)):
            self.flush_callbacks[frame_type] = callback_decorator(
                logger=self.logger)(cb)
        self.start_callback = callback_decorator(logger=self.logger)(start_cb)
        self.end_callback = callback_decorator(logger=self.logger)(end_cb)

    def _open_output_files(self):
        self.logger.debug('opening video output files')
        for frame_type, data in self.outfiles.items():
            for data_type, f in data.items():
                if f and f.closed:
                    if data_type == "data" and (frame_type
                                                == od.VDEF_FRAME_TYPE_CODED):
                        self.outfiles[frame_type][data_type] = Mp4Mux(f.name)
                    else:
                        self.outfiles[frame_type][data_type] = open(
                            f.name, f.mode)

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

    def start(self, *args, **kwds):
        """
        See :py:func:`~olympe.video.Pdraw.play`
        """
        return self.play(*args, **kwds)

    def stop(self, timeout=5):
        """
        Stops the video stream
        """
        f = self.pause().then(lambda f: self.close() if f else None)
        if timeout > 0:
            return f.result_or_cancel(timeout=timeout)
        else:
            return f

    def play(self,
             url=None,
             media_name="DefaultVideo",
             resource_name="live",
             timeout=5):
        """
        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.logger.error("Error Pdraw interface seems to be destroyed")
            self._play_resp_future.set_result(False)
            return self._play_resp_future

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

        self.resource_name = resource_name
        self.media_name = media_name

        if url is None:
            self.url = b"rtsp://%s/%s" % (self.server_addr.encode(),
                                          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

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

        self._open_output_files()
        if self._play_resp_future.done():
            self._play_resp_future = Future(self.pdraw_thread_loop)
        if self.state in (PdrawState.Created, PdrawState.Closed):
            open_resp_future = self.pdraw_thread_loop.run_async(
                self._open_stream)
            f = self.pdraw_thread_loop.complete_futures(
                open_resp_future, self._play_resp_future)
        else:
            f = self._play_resp_future
            self.pdraw_thread_loop.run_async(self._play_impl)
        if timeout > 0:
            return f.result_or_cancel(timeout=timeout)
        else:
            return f

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

        res = od.pdraw_demuxer_play(self.pdraw, self.pdraw_demuxer)
        if res != 0:
            self.logger.error(f"Unable to start streaming ({res})")
            if not self._play_resp_future.done():
                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.logger.error("Error Pdraw interface seems to be destroyed")
            self._pause_resp_future.set_result(False)
            return self._pause_resp_future

        if self._pause_resp_future.done():
            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 is PdrawState.Closed:
            # Pause an closed stream is OK
            self._pause_resp_future.set_result(True)
        else:
            self.logger.error(
                f"Cannot pause stream from the {self.state} state")
            self._pause_resp_future.set_result(False)
        return self._pause_resp_future

    @callback_decorator()
    def _pause_impl(self):
        res = od.pdraw_demuxer_pause(self.pdraw, self.pdraw_demuxer)
        if res != 0:
            self.logger.error(f"Unable to stop streaming ({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.logger.error("Error Pdraw interface seems to be destroyed")
            return None

        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:
            self.logger.error("Unable to get sessions metata")
            return None
        self.session_metadata = od.struct_vmeta_session.as_dict(vmeta_session)
        return self.session_metadata
Exemplo n.º 8
0
    def play(self,
             url=None,
             media_name="DefaultVideo",
             resource_name="live",
             timeout=5):
        """
        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.logger.error("Error Pdraw interface seems to be destroyed")
            self._play_resp_future.set_result(False)
            return self._play_resp_future

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

        self.resource_name = resource_name
        self.media_name = media_name

        if url is None:
            self.url = b"rtsp://%s/%s" % (self.server_addr.encode(),
                                          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

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

        self._open_output_files()
        if self._play_resp_future.done():
            self._play_resp_future = Future(self.pdraw_thread_loop)
        if self.state in (PdrawState.Created, PdrawState.Closed):
            open_resp_future = self.pdraw_thread_loop.run_async(
                self._open_stream)
            f = self.pdraw_thread_loop.complete_futures(
                open_resp_future, self._play_resp_future)
        else:
            f = self._play_resp_future
            self.pdraw_thread_loop.run_async(self._play_impl)
        if timeout > 0:
            return f.result_or_cancel(timeout=timeout)
        else:
            return f