def __init__(self, role: Optional[str] = EMITTER, max_packet_size: Optional[int] = 60000, socket_ip: Optional[str] = "127.0.0.1", socket_port: Optional[int] = 50000, encryption_in_transit: Optional[bool] = False, max_queue_size: Optional[int] = 100, buffer_size: Optional[int] = 65543, key: Optional[Union[None, bytes]] = None, enable_multicast: Optional[bool] = False, multicast_ttl: Optional[int] = 2, use_rcv_img_buffer: Optional[bool] = False, from_source: Optional[Union[int, str]] = None, run_new_process: Optional[bool] = True, async_msg_generation: Optional[bool] = False, encoding: Optional[int] = 0, encoding_param: Optional[Union[dict, None]] = None): """Create a new VideoStream object with given parameter. :param role: Tell if the VideoStream is emitter or consumer. :param max_packet_size: The max size of a packet (in byte). :param socket_ip: The ip used to bind the socket. :param socket_port: The port used to bind the socket. :param encryption_in_transit: Define if the messages must be encrypted. :param max_queue_size: The max size of message queue. :param buffer_size: The max size of the received message buffer. :param key: The encryption key used to encrypt message. If no value is provided it will generate a new one. :param enable_multicast: Specify if the socket can use multicast. :param multicast_ttl: A list of tuples containing ip address and port of subscribers. :param use_rcv_img_buffer: A bool that tell if received image are stored in a buffer or in a single variable. :param from_source: Make the VideoStream stream from a source. :param run_new_process: Specify if the Eye object must be run in a new process. :param async_msg_generation: Specify if the messages representing the image must be generated asynchronously. :param encoding: Define the encoding used to send images. :param encoding_param: Parameters used to encode image. See cv2.imencode for more details. """ self.internal_pipe, self.external_pipe = mp.Pipe() if role != VideoStream.EMITTER and role != VideoStream.CONSUMER: raise ValueError self.role = role self.im: ImageManager = ImageManager(max_packet_size=max_packet_size, async_msg_generation=async_msg_generation, encoding=encoding, encoding_param=encoding_param) self.opened_topics: List[VideoTopic] = [] self.udp_socket: Union[UDPSocket, None] = None self.socket_ip = socket_ip self.socket_port = socket_port self.encryption_in_transit: bool = encryption_in_transit self.max_queue_size: int = max_queue_size self.buffer_size: int = buffer_size self.is_running: bool = False self.enable_multicast: bool = enable_multicast self.multicast_ttl: int = multicast_ttl if self.enable_multicast: raise NotImplementedError self.key: bytes = key self.subs_list: List[Tuple[str, int]] = [] self.tm = TopicManager() self.use_rcv_img_buffer = use_rcv_img_buffer self.rcv_img_buffer: List[np.array] = [] if use_rcv_img_buffer is False: self.rcv_img_buffer.append(None) self.from_source = from_source self.eye: Union[None, Eye] = None self.run_new_process = run_new_process self.async_msg_generation = async_msg_generation self.encoding = encoding self.encoding_param = encoding_param if encoding_param is not None else {}
def test_add_msg_correctly_create_a_topic_if_a_new_topic_need_to_be_created(get_msg_sample): # Given messages = get_msg_sample tm = TopicManager() # When tm.add_message(UDPMessage.from_bytes(messages[0])) # Then assert type(tm.open_topic[int.from_bytes(UDPMessage.from_bytes(messages[0]).topic, 'little')]) is VideoTopic
def test_add_msg_add_data_messages_to_dead_letter_queue_when_its_topic_does_not_exist(get_msg_sample): # Given messages = get_msg_sample tm = TopicManager() # When tm.add_message(UDPMessage.from_bytes(messages[1])) # Then assert tm.dead_letter_queue[0].payload == UDPMessage.from_bytes(messages[1]).payload assert tm.dead_letter_queue[0].message_nb == UDPMessage.from_bytes(messages[1]).message_nb
def test_add_msg_correctly_add_msg_to_an_existing_topic(get_msg_sample): # Given messages = get_msg_sample tm = TopicManager() # When tm.add_message(UDPMessage.from_bytes(messages[0])) tm.add_message(UDPMessage.from_bytes(messages[1])) # Then assert tm.open_topic[1].rcv_messages[0].payload == UDPMessage.from_bytes(messages[1]).payload assert tm.open_topic[1].rcv_messages[0].message_nb == UDPMessage.from_bytes(messages[1]).message_nb
def test_in_waiting_return_true_if_an_image_is_waiting_in_img_queue_else_false(): # Given tm = TopicManager() # When rslt1 = tm.in_waiting() tm.img_queue.append(np.array([])) rslt2 = tm.in_waiting() # Then assert rslt1 is False assert rslt2 is True
def test_add_msg_add_message_from_dlq_to_its_topic_when_it_is_created_and_remove_msg_from_dlq(get_msg_sample): # Given messages = get_msg_sample tm = TopicManager() # When tm.add_message(UDPMessage.from_bytes(messages[1])) tm.add_message(UDPMessage.from_bytes(messages[0])) # Then assert tm.open_topic[1].rcv_messages[0].payload == UDPMessage.from_bytes(messages[1]).payload assert tm.open_topic[1].rcv_messages[0].message_nb == UDPMessage.from_bytes(messages[1]).message_nb assert len(tm.dead_letter_queue) == 0
def test_topic_manager_is_created_with_empty_open_topic_and_img_queue(): # Given expected_open_topic = {} expected_img_queue = [] # When tm = TopicManager() # Then assert tm.open_topic == expected_open_topic assert tm.img_queue == expected_img_queue
def test_check_topic_remove_old_opened_topic_when_a_topic_is_completed(get_msg_sample): # Given messages = get_msg_sample tm = TopicManager() tm.add_message(UDPMessage.from_bytes(messages[0])) time.sleep(.01) new_image = np.array(4 * [4 * 4 * [[0, 0, 0]]]) im = ImageManager(max_packet_size=64) im.refresh_image(new_image) new_messages = list(im.get_messages(2)) tm.add_message(UDPMessage.from_bytes(new_messages[0])) # When for i in new_messages[1:]: tm.add_message(UDPMessage.from_bytes(i)) # Then assert len(tm.open_topic.keys()) == 0
def test_add_msg_remove_completed_topics_from_open_topic(get_msg_sample): # Given messages = get_msg_sample tm = TopicManager() tm.add_message(UDPMessage.from_bytes(messages[0])) # When for i in messages[1:]: tm.add_message(UDPMessage.from_bytes(i)) # Then assert len(tm.open_topic.keys()) == 0
def test_check_topic_add_topic_image_to_img_queue_when_topic_is_complete(get_msg_sample): # Given expected_img = np.array(4 * [4 * 4 * [[0, 0, 0]]]) messages = get_msg_sample tm = TopicManager() tm.add_message(UDPMessage.from_bytes(messages[0])) # When for i in messages[1:]: tm.add_message(UDPMessage.from_bytes(i)) # Then assert np.array_equiv(expected_img, tm.img_queue[0])
def test_process_dlq_correctly_remove_outdated_messages_from_dlq(get_msg_sample): # Given messages = get_msg_sample tm = TopicManager() tm.add_message(UDPMessage.from_bytes(messages[1])) time.sleep(.01) new_image = np.array(4 * [4 * 4 * [[0, 0, 0]]]) im = ImageManager(max_packet_size=64) im.refresh_image(new_image) new_messages = list(im.get_messages(2)) tm.add_message(UDPMessage.from_bytes(new_messages[1])) # When tm.add_message(UDPMessage.from_bytes(new_messages[0])) # Then assert len(tm.dead_letter_queue) == 0
def test_pull_return_the_first_image_of_the_list_if_an_image_is_waiting_else_none(get_msg_sample): # Given expected_img = np.array(4 * [4 * 4 * [[0, 0, 0]]]) messages = get_msg_sample tm = TopicManager() tm.add_message(UDPMessage.from_bytes(messages[0])) # When for i in messages[1:]: tm.add_message(UDPMessage.from_bytes(i)) pull1 = tm.pull() pull2 = tm.pull() # Then assert np.array_equiv(expected_img, pull1) assert pull2 is None
def test_topic_exist_return_true_if_asked_topic_exist_else_return_false(get_msg_sample): # Given messages = get_msg_sample tm = TopicManager() existing_topic = 1 non_existing_topic = 2 # When tm.add_message(UDPMessage.from_bytes(messages[0])) result1 = tm.topic_exist(existing_topic) result2 = tm.topic_exist(non_existing_topic) # Then assert result1 is True assert result2 is False
class VideoStream: """A class to manage video stream. This class inherit from Process to run the VideoStream on a different CPU core than parent process. Constants : EMITTER : Value that tell the VideoStream will send video stream. CONSUMER : Value that tell the VideoStream will receive video stream. Attributes : internal_pipe : Internal side of the pipe used for communication with the process. external_pipe : External side of the pipe used for communication with the process. im : The ImageManager used for video stream. role : Tell if the VideoStream is emitter or consumer. opened_topics : A list of VideoTopic waiting for completion. udp_socket : The UDPSocket used for sending or receiving data. socket_ip : The ip used to bind the socket. socket_port : The port used to bind the socket. encryption_in_transit : Define if the messages must be encrypted. max_queue_size : The max size of message queue. buffer_size : The max size of the received message buffer. is_running : Tell if the process is running. key : The encryption key used to encrypt message. If no value is provided it will generate a new one. enable_multicast : Specify if the socket can use multicast. multicast_ttl : The TTL used for multicast. subs_list : A list of tuples containing ip address and port of subscribers. use_rcv_img_buffer : A bool that tell if received image are stored in a buffer or in a single variable. rcv_img_buffer : A buffer to store incoming image. from_source : Specify the source to use if needed. eye : The Eye object used to stream if from_source is not None. run_new_process : Specify if the Eye object must be run in a new process. async_msg_generation: Specify if the messages representing the image must be generated asynchronously. encoding: Define the encoding used to send images. encoding_param : Parameters used to encode image. See cv2.imencode for more details. """ EMITTER = "emitter" CONSUMER = "consumer" def __init__(self, role: Optional[str] = EMITTER, max_packet_size: Optional[int] = 60000, socket_ip: Optional[str] = "127.0.0.1", socket_port: Optional[int] = 50000, encryption_in_transit: Optional[bool] = False, max_queue_size: Optional[int] = 100, buffer_size: Optional[int] = 65543, key: Optional[Union[None, bytes]] = None, enable_multicast: Optional[bool] = False, multicast_ttl: Optional[int] = 2, use_rcv_img_buffer: Optional[bool] = False, from_source: Optional[Union[int, str]] = None, run_new_process: Optional[bool] = True, async_msg_generation: Optional[bool] = False, encoding: Optional[int] = 0, encoding_param: Optional[Union[dict, None]] = None): """Create a new VideoStream object with given parameter. :param role: Tell if the VideoStream is emitter or consumer. :param max_packet_size: The max size of a packet (in byte). :param socket_ip: The ip used to bind the socket. :param socket_port: The port used to bind the socket. :param encryption_in_transit: Define if the messages must be encrypted. :param max_queue_size: The max size of message queue. :param buffer_size: The max size of the received message buffer. :param key: The encryption key used to encrypt message. If no value is provided it will generate a new one. :param enable_multicast: Specify if the socket can use multicast. :param multicast_ttl: A list of tuples containing ip address and port of subscribers. :param use_rcv_img_buffer: A bool that tell if received image are stored in a buffer or in a single variable. :param from_source: Make the VideoStream stream from a source. :param run_new_process: Specify if the Eye object must be run in a new process. :param async_msg_generation: Specify if the messages representing the image must be generated asynchronously. :param encoding: Define the encoding used to send images. :param encoding_param: Parameters used to encode image. See cv2.imencode for more details. """ self.internal_pipe, self.external_pipe = mp.Pipe() if role != VideoStream.EMITTER and role != VideoStream.CONSUMER: raise ValueError self.role = role self.im: ImageManager = ImageManager(max_packet_size=max_packet_size, async_msg_generation=async_msg_generation, encoding=encoding, encoding_param=encoding_param) self.opened_topics: List[VideoTopic] = [] self.udp_socket: Union[UDPSocket, None] = None self.socket_ip = socket_ip self.socket_port = socket_port self.encryption_in_transit: bool = encryption_in_transit self.max_queue_size: int = max_queue_size self.buffer_size: int = buffer_size self.is_running: bool = False self.enable_multicast: bool = enable_multicast self.multicast_ttl: int = multicast_ttl if self.enable_multicast: raise NotImplementedError self.key: bytes = key self.subs_list: List[Tuple[str, int]] = [] self.tm = TopicManager() self.use_rcv_img_buffer = use_rcv_img_buffer self.rcv_img_buffer: List[np.array] = [] if use_rcv_img_buffer is False: self.rcv_img_buffer.append(None) self.from_source = from_source self.eye: Union[None, Eye] = None self.run_new_process = run_new_process self.async_msg_generation = async_msg_generation self.encoding = encoding self.encoding_param = encoding_param if encoding_param is not None else {} def start(self) -> NoReturn: """Start a new thread or a new process for asynchronous camera reading. :return eye: The current instance of the class. """ if self.run_new_process is False: self._start() else: mp.Process(target=self._start).start() return self def _start(self) -> NoReturn: """Start the thread of the class.""" Thread(target=self._work, args=()).start() def _refresh_image(self, new_image: np.array) -> NoReturn: """Change the value of current image by the value of new_image. :param new_image: The new image to send. """ self.im.refresh_image(new_image) def refresh_image(self, new_image: np.array) -> NoReturn: """External call to _refresh_image. :param new_image: The new image to send. """ if self.run_new_process is False: return self._refresh_image(new_image) self.external_pipe.send((VideoStream._refresh_image, {"new_image": new_image})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() def _get_current_image(self) -> np.array: """Return the current value of current image. :return current_image: The current value of current image. """ return self.im.current_image def get_current_image(self) -> np.array: """External call to _get_current_image :return current_image: The current value of current image. """ if self.run_new_process is False: return self._get_current_image() self.external_pipe.send((VideoStream._get_current_image, {})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() def _work(self) -> NoReturn: """The main process of the VideoStream.""" self._setup() self._loop() def _setup(self) -> NoReturn: """Initialization of the process.""" must_listen = self.role == VideoStream.CONSUMER self.udp_socket = UDPSocket(socket_ip=self.socket_ip, socket_port=self.socket_port, encryption_in_transit=self.encryption_in_transit, max_queue_size=self.max_queue_size, buffer_size=self.buffer_size, key=self.key, enable_multicast=self.enable_multicast, multicast_ttl=self.multicast_ttl, must_listen=must_listen) self.udp_socket.start() self.eye = None if self.from_source is None else Eye(src=self.from_source, run_new_process=False).start() self.im = self.im.start() self.is_running = True def _loop(self) -> NoReturn: """The main loop of the process.""" max_topic = 2 ** (8 * UDPMessage.TOPIC_LENGTH) img_topic = 0 while self.is_running: # Manage external call of class method when using Process class. if self.run_new_process and self.internal_pipe.poll(): command = self.internal_pipe.recv() if type(command) is tuple: self.internal_pipe.send(command[0](self, **command[1])) # Send image packets if the VideoStream object is emitter. if self.role == VideoStream.EMITTER: if self.eye is not None: self.im.refresh_image(self.eye.read()) self.cast(img_topic) img_topic = (img_topic + 1) % max_topic if self.run_new_process: VideoStream.delay(1) # Receive packets if the VideoStream object is consumer. if self.role == VideoStream.CONSUMER: while self.udp_socket.in_waiting(): msg = UDPMessage.from_bytes(self.udp_socket.pull()[0]) if type(msg) is not UDPMessage: continue self.tm.add_message(msg) if self.tm.in_waiting(): if self.use_rcv_img_buffer: self.rcv_img_buffer.append(self.tm.pull()) else: self.rcv_img_buffer[0] = self.tm.pull() def _stop(self) -> NoReturn: """Stop the process and its UDPSocket.""" self.is_running = False self.udp_socket.stop() if self.im.async_msg_generation is True: self.im.stop() if self.eye is not None: self.eye.stop() def stop(self) -> NoReturn: """External call to _stop""" if self.run_new_process is False: return self._stop() self.external_pipe.send((VideoStream._stop, {})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() def _get_is_running(self) -> bool: """Return True if the process is currently running. :return is_running: A bool that tell if the process is currently running. """ return self.is_running def get_is_running(self): """External call to _get_is_running. :return is_running: A bool that tell if the process is currently running. """ if self.run_new_process is False: return self._get_is_running() self.external_pipe.send((VideoStream._get_is_running, {})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() def _add_subscriber(self, address_port) -> NoReturn: """Add a subscriber in the list of subscriber. :param address_port: A tuple containing the ip address and the port of the new subscriber. """ self.subs_list.append(address_port) def add_subscriber(self, address_port) -> NoReturn: """External call to _add_subscriber. :param address_port: A tuple containing the ip address and the port of the new subscriber. """ if self.run_new_process is False: return self._add_subscriber(address_port) self.external_pipe.send((VideoStream._add_subscriber, {"address_port": address_port})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() def _get_subs_list(self) -> List[Tuple]: """Return the list of subscribers. :return subs_list: The list of subscribers. """ return self.subs_list def get_subs_list(self) -> List[Tuple]: """External call to _get_subs_list. :return subs_list: The list of subscribers. """ if self.run_new_process is False: return self._get_subs_list() self.external_pipe.send((VideoStream._get_subs_list, {})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() def _remove_subscriber(self, index: int) -> NoReturn: """Remove a subscriber from the list of subscriber. :param index: The index of the subscriber to remove. """ self.subs_list.pop(index) def remove_subscriber(self, index: int) -> NoReturn: """External call to _remove_subscriber. :param index: The index of the subscriber to remove. """ if self.run_new_process is False: return self._remove_subscriber(index) self.external_pipe.send((VideoStream._remove_subscriber, {"index": index})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() def cast(self, topic: int) -> NoReturn: """Send the current image using given topic number. :param topic: The number of the topic used to send the image. """ if np.array_equiv(self.im.current_image, np.array([])) or len(self.subs_list) == 0: return for msg_to_send in self.im.get_messages(topic): for sub in self.subs_list: self.udp_socket.sendto(msg_to_send, sub) if self.run_new_process: VideoStream.delay(1) def _get_rcv_img(self) -> np.array: """Return the received image. :return rcv_img: The received image. """ if len(self.rcv_img_buffer) == 0: return None if self.use_rcv_img_buffer is False: return self.rcv_img_buffer[0] return self.rcv_img_buffer.pop(0) def get_rcv_img(self): """External call to _get_rcv_img. :return rcv_img: The received image. """ if self.run_new_process is False: return self._get_rcv_img() self.external_pipe.send((VideoStream._get_rcv_img, {})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() def _get_key(self) -> bytes: """Return the key used by the socket for encryption. :return: The encryption key of the server. """ return self.udp_socket.get_key() def get_key(self) -> bytes: """External call to _get_key. :return: The encryption key of the server. """ if self.run_new_process is False: return self._get_key() self.external_pipe.send((VideoStream._get_key, {})) while self.external_pipe.poll() is False: pass return self.external_pipe.recv() @staticmethod def delay(delay_ms: int) -> NoReturn: """Wait for delay_ms microseconds. :param delay_ms: The delay duration in ms """ t_stop = np.int64(delay_ms * 10) + np.int64(np.float64(time.time()) * np.float64(10000000)) while np.int64(np.float64(time.time()) * np.float64(10000000)) <= t_stop: pass