def _ready_to_play(self, pdraw, ready, userdata): self.logging.logI("_ready_to_play({}) called".format(ready)) if ready: self._play_resp_future = Future(self.pdraw_thread_loop) self._play_impl() if self._state in (State.Playing, State.Closing, State.Closed): if self.end_callback is not None: self.callbacks_thread_loop.run_async(self.end_callback)
def __init__(self, buffer_queue_size=2, loglevel=TraceLogger.level.info, logfile=sys.stdout, legacy=False, pdraw_thread_loop=None, logging=None): """ :param buffer_queue_size: video buffer queue size (defaults to 2) :type buffer_queue_size: int :param loglevel: pdraw logger log level (defaults to :py:attr:`olympe.tools.logger.level.info`) :type loglevel: int :param logfile: pdraw logger file (defaults to sys.stdout) :type logfile: FileObjectLike :param legacy: Defaults to False, set this parameter to True for legacy drones (Bebop, Disco, ...) streaming support :type legacy: bool """ if logging is None: self.logging = TraceLogger(loglevel, logfile) else: self.logging = logging if pdraw_thread_loop is None: self.pdraw_thread_loop = PompLoopThread(self.logging) self.pdraw_thread_loop.start() else: self.pdraw_thread_loop = pdraw_thread_loop self.callbacks_thread_loop = PompLoopThread(self.logging) self.callbacks_thread_loop.start() self.buffer_queue_size = buffer_queue_size self.pomp_loop = self.pdraw_thread_loop.pomp_loop self._legacy = legacy self._open_resp_future = Future(self.pdraw_thread_loop) self._close_resp_future = Future(self.pdraw_thread_loop) self._close_resp_future.add_done_callback(self._on_close_resp_done) self._play_resp_future = Future(self.pdraw_thread_loop) self._pause_resp_future = Future(self.pdraw_thread_loop) self._state = State.Created self.pdraw = od.POINTER_T(od.struct_pdraw)() self.streams = defaultdict(lambda: { 'id': None, 'type': od.PDRAW_VIDEO_MEDIA_FORMAT_UNKNOWN, 'h264_header': None, 'video_sink': od.POINTER_T(od.struct_pdraw_video_sink)(), 'video_sink_flushed': False, 'video_sink_lock': threading.Lock(), 'video_queue': None, 'video_queue_event': None, }) self.session_metadata = {} self.outfiles = { od.PDRAW_VIDEO_MEDIA_FORMAT_H264: { 'data': None, 'meta': None, }, od.PDRAW_VIDEO_MEDIA_FORMAT_YUV: { 'data': None, 'meta': None, }, } self.frame_callbacks = { od.PDRAW_VIDEO_MEDIA_FORMAT_H264: None, od.PDRAW_VIDEO_MEDIA_FORMAT_YUV: None, } self.end_callback = None self.flush_callback = None self.url = None self.server_addr = None self.resource_name = "live" self.media_name = None self.local_stream_port = PDRAW_LOCAL_STREAM_PORT self.local_control_port = PDRAW_LOCAL_CONTROL_PORT self.cbs = od.struct_pdraw_cbs.bind({ "open_resp": self._open_resp, "close_resp": self._close_resp, "ready_to_play": self._ready_to_play, "play_resp": self._play_resp, "pause_resp": self._pause_resp, "seek_resp": self._seek_resp, "socket_created": self._socket_created, "select_demuxer_media": self._select_demuxer_media, "media_added": self._media_added, "media_removed": self._media_removed, "end_of_range": self._end_of_range, }) self.video_sink_cb = od.struct_pdraw_video_sink_cbs.bind({ "flush": self._video_sink_flush }) self.vbuf_cbs = od.struct_vbuf_cbs() res = od.vbuf_generic_get_cbs(ctypes.pointer(self.vbuf_cbs)) if res != 0: msg = "Error while creating vbuf generic callbacks {}".format(res) self.logging.logE(msg) raise RuntimeError("ERROR: {}".format(msg)) self.yuv_packed_buffer_pool = od.POINTER_T(od.struct_vbuf_pool)() res = od.vbuf_pool_new( self.buffer_queue_size, 0, 0, self.vbuf_cbs, ctypes.byref(self.yuv_packed_buffer_pool) ) if res != 0: msg = "Error while creating yuv packged buffer pool {}".format(res) self.logging.logE(msg) raise RuntimeError("ERROR: {}".format(msg)) self.pdraw_thread_loop.register_cleanup(self.dispose)
class Pdraw(object): def __init__(self, buffer_queue_size=2, loglevel=TraceLogger.level.info, logfile=sys.stdout, legacy=False, pdraw_thread_loop=None, logging=None): """ :param buffer_queue_size: video buffer queue size (defaults to 2) :type buffer_queue_size: int :param loglevel: pdraw logger log level (defaults to :py:attr:`olympe.tools.logger.level.info`) :type loglevel: int :param logfile: pdraw logger file (defaults to sys.stdout) :type logfile: FileObjectLike :param legacy: Defaults to False, set this parameter to True for legacy drones (Bebop, Disco, ...) streaming support :type legacy: bool """ if logging is None: self.logging = TraceLogger(loglevel, logfile) else: self.logging = logging if pdraw_thread_loop is None: self.pdraw_thread_loop = PompLoopThread(self.logging) self.pdraw_thread_loop.start() else: self.pdraw_thread_loop = pdraw_thread_loop self.callbacks_thread_loop = PompLoopThread(self.logging) self.callbacks_thread_loop.start() self.buffer_queue_size = buffer_queue_size self.pomp_loop = self.pdraw_thread_loop.pomp_loop self._legacy = legacy self._open_resp_future = Future(self.pdraw_thread_loop) self._close_resp_future = Future(self.pdraw_thread_loop) self._close_resp_future.add_done_callback(self._on_close_resp_done) self._play_resp_future = Future(self.pdraw_thread_loop) self._pause_resp_future = Future(self.pdraw_thread_loop) self._state = State.Created self.pdraw = od.POINTER_T(od.struct_pdraw)() self.streams = defaultdict(lambda: { 'id': None, 'type': od.PDRAW_VIDEO_MEDIA_FORMAT_UNKNOWN, 'h264_header': None, 'video_sink': od.POINTER_T(od.struct_pdraw_video_sink)(), 'video_sink_flushed': False, 'video_sink_lock': threading.Lock(), 'video_queue': None, 'video_queue_event': None, }) self.session_metadata = {} self.outfiles = { od.PDRAW_VIDEO_MEDIA_FORMAT_H264: { 'data': None, 'meta': None, }, od.PDRAW_VIDEO_MEDIA_FORMAT_YUV: { 'data': None, 'meta': None, }, } self.frame_callbacks = { od.PDRAW_VIDEO_MEDIA_FORMAT_H264: None, od.PDRAW_VIDEO_MEDIA_FORMAT_YUV: None, } self.end_callback = None self.flush_callback = None self.url = None self.server_addr = None self.resource_name = "live" self.media_name = None self.local_stream_port = PDRAW_LOCAL_STREAM_PORT self.local_control_port = PDRAW_LOCAL_CONTROL_PORT self.cbs = od.struct_pdraw_cbs.bind({ "open_resp": self._open_resp, "close_resp": self._close_resp, "ready_to_play": self._ready_to_play, "play_resp": self._play_resp, "pause_resp": self._pause_resp, "seek_resp": self._seek_resp, "socket_created": self._socket_created, "select_demuxer_media": self._select_demuxer_media, "media_added": self._media_added, "media_removed": self._media_removed, "end_of_range": self._end_of_range, }) self.video_sink_cb = od.struct_pdraw_video_sink_cbs.bind({ "flush": self._video_sink_flush }) self.vbuf_cbs = od.struct_vbuf_cbs() res = od.vbuf_generic_get_cbs(ctypes.pointer(self.vbuf_cbs)) if res != 0: msg = "Error while creating vbuf generic callbacks {}".format(res) self.logging.logE(msg) raise RuntimeError("ERROR: {}".format(msg)) self.yuv_packed_buffer_pool = od.POINTER_T(od.struct_vbuf_pool)() res = od.vbuf_pool_new( self.buffer_queue_size, 0, 0, self.vbuf_cbs, ctypes.byref(self.yuv_packed_buffer_pool) ) if res != 0: msg = "Error while creating yuv packged buffer pool {}".format(res) self.logging.logE(msg) raise RuntimeError("ERROR: {}".format(msg)) self.pdraw_thread_loop.register_cleanup(self.dispose) def dispose(self): self.callbacks_thread_loop.stop() return self.pdraw_thread_loop.run_async( self._dispose_impl) def _dispose_impl(self): if not self.pdraw: return f = self.close().then( lambda _: self._destroy(), deferred=True) return f def _destroy(self): res = od.vbuf_pool_destroy(self.yuv_packed_buffer_pool) if res != 0: self.logging.logE("Cannot destroy yuv packed buffer pool") self.yuv_packed_buffer_pool = od.POINTER_T(od.struct_vbuf_pool)() if self.pdraw: res = od.pdraw_destroy(self.pdraw) if res != 0: self.logging.logE("Cannot destroy pdraw object") self.pdraw = od.POINTER_T(od.struct_pdraw)() self.logging.logI("pdraw destroyed") return True def _open_single_stream(self): """ Opening pdraw single stream (legacy API) """ res = od.pdraw_open_single_stream( self.pdraw, PDRAW_LOCAL_ADDR, self.local_stream_port, self.local_control_port, self.server_addr, PDRAW_REMOTE_STREAM_PORT, PDRAW_REMOTE_CONTROL_PORT, PDRAW_IFACE_ADRR ) if res != 0: self.logging.logE( "Error while opening pdraw single stream: {}".format(res)) return False else: self.logging.logI("Opening pdraw single stream OK") return True def _open_url(self): """ Opening rtsp streaming url """ if self.resource_name.startswith("replay/"): if self.media_name is None: self.logging.logE( "Error media_name should be provided in video stream replay mode") return False res = od.pdraw_open_url(self.pdraw, self.url) if res != 0: self.logging.logE( "Error while opening pdraw url: {} ({})".format(self.url, res)) return False else: self.logging.logI("Opening pdraw url OK: {}".format(self.url)) return True def _open_stream(self): """ Opening pdraw stream using the appropriate method (legacy or rtsp) according to the device type """ self._open_resp_future = Future(self.pdraw_thread_loop) if self._state not in (State.Error, State.Closed, State.Created): self.logging.logW("Cannot open stream from {}".format(self._state)) self._open_resp_future.set_result(False) return self._open_resp_future self._state = State.Opening if not self._pdraw_new(): self._open_resp_future.set_result(False) return self._open_resp_future if not self._legacy: ret = self._open_url() else: ret = self._open_single_stream() if not ret: self._open_resp_future.set_result(False) return self._open_resp_future def close(self): """ Close a playing or paused video stream session """ if self._state in (State.Opened, State.Paused, State.Playing, State.Error): self.logging.logD("pdraw closing from the {} state".format(self._state)) self._close_resp_future = Future(self.pdraw_thread_loop) self._close_resp_future.add_done_callback(self._on_close_resp_done) f = self._close_resp_future self._state = State.Closing self.pdraw_thread_loop.run_async(self._close_stream) elif self._state is not State.Closing: f = Future(self.pdraw_thread_loop) f.set_result(False) else: f = self._close_resp_future return f def _close_stream(self): """ Close pdraw stream """ if self._state is State.Closed: self.logging.logI("pdraw is already closed".format(self._state)) self._close_resp_future.set_result(True) return self._close_resp_future if not self.pdraw: self.logging.logE("Error Pdraw interface seems to be destroyed") self._state = State.Error self._close_resp_future.set_result(False) return self._close_resp_future if not self._close_stream_impl(): self._state = State.Error self._close_resp_future.set_result(False) return self._close_resp_future def _close_stream_impl(self): res = od.pdraw_close(self.pdraw) if res != 0: self.logging.logE( "Error while closing pdraw stream: {}".format(res)) self._state = State.Error return False else: self.logging.logI("Closing pdraw stream OK") return True def _on_close_resp_done(self, close_resp_future): if close_resp_future.cancelled(): # FIXME: workaround pdraw closing timeout # This random issue is quiet hard to reproduce self.logging.logE("Closing Pdraw timedout") if self.pdraw: res = od.pdraw_destroy(self.pdraw) if res != 0: self.logging.logE("Cannot destroy pdraw object") self.pdraw = od.POINTER_T(od.struct_pdraw)() self._state = State.Closed self.logging.logE("Pdraw has been closed") def _open_resp(self, pdraw, status, userdata): self.logging.logD("_open_resp called") self.local_stream_port = od.pdraw_get_single_stream_local_stream_port(self.pdraw) self.local_control_port = od.pdraw_get_single_stream_local_control_port(self.pdraw) if status != 0: self._state = State.Error else: self._state = State.Opened self._open_resp_future.set_result(status == 0) def _close_resp(self, pdraw, status, userdata): self._close_output_files() if status != 0: self.logging.logE("_close_resp called {}".format(status)) self._close_resp_future.set_result(False) self._state = State.Error else: self.logging.logI("_close_resp called {}".format(status)) self._state = State.Closed self._close_resp_future.set_result(True) if self.pdraw: res = od.pdraw_destroy(self.pdraw) if res != 0: self.logging.logE("Cannot destroy pdraw object") self.pdraw = od.POINTER_T(od.struct_pdraw)() self._close_resp_future.set_result(True) def _pdraw_new(self): res = od.pdraw_new( self.pomp_loop, self.cbs, ctypes.cast(ctypes.pointer(ctypes.py_object(self)), ctypes.c_void_p), ctypes.byref(self.pdraw) ) if res != 0: msg = "Error while creating pdraw interface: {}".format(res) self.logging.logE(msg) self.pdraw = od.POINTER_T(od.struct_pdraw)() return False else: self.logging.logI("Pdraw interface has been created") return True def _ready_to_play(self, pdraw, ready, userdata): self.logging.logI("_ready_to_play({}) called".format(ready)) if ready: self._play_resp_future = Future(self.pdraw_thread_loop) self._play_impl() if self._state in (State.Playing, State.Closing, State.Closed): if self.end_callback is not None: self.callbacks_thread_loop.run_async(self.end_callback) def _play_resp(self, pdraw, status, timestamp, speed, userdata): if status == 0: self.logging.logD("_play_resp called {}".format(status)) self._state = State.Playing self._play_resp_future.set_result(True) else: self.logging.logE("_play_resp called {}".format(status)) self._state = State.Error self._play_resp_future.set_result(False) def _pause_resp(self, pdraw, status, timestamp, userdata): if status == 0: self.logging.logD("_pause_resp called {}".format(status)) self._state = State.Paused self._pause_resp_future.set_result(True) else: self.logging.logE("_pause_resp called {}".format(status)) self._state = State.Error self._pause_resp_future.set_result(False) def _seek_resp(self, pdraw, status, timestamp, userdata): if status == 0: self.logging.logD("_seek_resp called {}".format(status)) else: self.logging.logE("_seek_resp called {}".format(status)) self._state = State.Error def _socket_created(self, pdraw, fd, userdata): self.logging.logD("_socket_created called") def _select_demuxer_media(self, pdraw, medias, count, userdata): # by default select the default media (media_id=0) selected_media_id = 0 selected_media_idx = 0 for idx in range(count): self.logging.logI( "_select_demuxer_media: " "idx={} media_id={} name={} default={}".format( idx, medias[idx].media_id, od.string_cast(medias[idx].name), str(bool(medias[idx].is_default))) ) if (self.media_name is not None and self.media_name == od.string_cast(medias[idx].name)): selected_media_id = medias[idx].media_id selected_media_idx = idx if ( self.media_name is not None and od.string_cast(medias[selected_media_idx].name) != self.media_name ): self.logging.logW( "media_name {} is unavailable. " "Selecting the default media instead".format(self.media_name) ) return selected_media_id def _media_added(self, pdraw, media_info, userdata): id_ = media_info.contents.id self.logging.logI("_media_added id : {}".format(id_)) # store the information if supported media type, otherwise exit if (media_info.contents._2.video.format != od.PDRAW_VIDEO_MEDIA_FORMAT_YUV and media_info.contents._2.video.format != od.PDRAW_VIDEO_MEDIA_FORMAT_H264): self.logging.logW( 'Ignoring media id {} (type {})'.format( id_, media_info.contents._2.video.format)) return self.streams[id_]['type'] = int(media_info.contents._2.video.format) if (media_info.contents._2.video.format == od.PDRAW_VIDEO_MEDIA_FORMAT_H264): header = media_info.contents._2.video._2.h264 header = H264Header( bytearray(header.sps), int(header.spslen), bytearray(header.pps), int(header.ppslen), ) self.streams[id_]['h264_header'] = header # start a video sink attached to the new media video_sink_params = od.struct_pdraw_video_sink_params( self.buffer_queue_size, # buffer queue size 1, # drop buffers when the queue is full ) self.streams[id_]['id'] = ctypes.cast( ctypes.pointer(ctypes.py_object(id_)), ctypes.c_void_p) res = od.pdraw_start_video_sink( pdraw, id_, video_sink_params, self.video_sink_cb, self.streams[id_]['id'], ctypes.byref(self.streams[id_]['video_sink']) ) if res != 0: self.logging.logE("Unable to start video sink") return # Retrieve the queue belonging to the sink queue = od.pdraw_get_video_sink_queue( pdraw, self.streams[id_]['video_sink'], ) self.streams[id_]['video_queue'] = queue # Retrieve event object and related file descriptor self.streams[id_]['video_queue_event'] = \ od.vbuf_queue_get_evt(self.streams[id_]['video_queue']) # add the file description to our pomp loop self.callbacks_thread_loop.add_event_to_loop( self.streams[id_]['video_queue_event'], lambda *args: self._video_sink_queue_event(*args), id_ ) def _media_removed(self, pdraw, media_info, userdata): id_ = media_info.contents.id if id_ not in self.streams: self.logging.logE( 'Received removed event from unknown ID {}'.format(id_)) return self.logging.logI("_media_removed called id : {}".format(id_)) if self.streams[id_]['video_queue_event']: self.callbacks_thread_loop.remove_event_from_loop( self.streams[id_]['video_queue_event']) res = od.pdraw_stop_video_sink(pdraw, self.streams[id_]['video_sink']) if res < 0: self.logging.logE('pdraw_stop_video_sink() returned %s' % res) self.streams.pop(id_) def _end_of_range(self, pdraw, timestamp, userdata): self.logging.logI("_end_for_range") self.close() def _video_sink_flush(self, pdraw, videosink, userdata): id_ = py_object_cast(userdata) if id_ not in self.streams: self.logging.logE( 'Received flush event from unknown ID {}'.format(id_)) return with self.streams[id_]['video_sink_lock']: self.logging.logD("flush_callback {}".format(id_)) if self.flush_callback is not None: res = self.flush_callback(id_) if res != 0: self.logging.logE( 'video sink flush id {} error {}'.format(id_, res)) res = od.vbuf_queue_flush(self.streams[id_]['video_queue']) if res < 0: self.logging.logE('vbuf_queue_flush() returned %s' % res) else: self.logging.logI('vbuf_queue_flush() returned %s' % res) res = od.pdraw_video_sink_queue_flushed(pdraw, videosink) self.streams[id_]['video_sink_flushed'] = True if res < 0: self.logging.logE( 'pdraw_video_sink_queue_flushed() returned %s' % res) else: self.logging.logD( 'pdraw_video_sink_queue_flushed() returned %s' % res) def _video_sink_queue_event(self, pomp_evt, userdata): id_ = py_object_cast(userdata) self.logging.logD('media id = {}'.format(id_)) if id_ not in self.streams: self.logging.logE( 'Received queue event from unknown ID {}'.format(id_)) return # acknowledge event res = od.pomp_evt_clear(self.streams[id_]['video_queue_event']) if res != 0: self.logging.logE( "Unable to clear frame received event ({})".format(res)) # process all available buffers in the queue with self.streams[id_]['video_sink_lock']: while self._process_stream(id_): pass def _pop_stream_buffer(self, id_): buf = od.POINTER_T(od.struct_vbuf_buffer)() ret = od.vbuf_queue_pop( self.streams[id_]['video_queue'], 0, ctypes.byref(buf) ) if ret < 0: if ret != -errno.EAGAIN: self.logging.logE('vbuf_queue_pop returned error %d' % ret) buf = od.POINTER_T(od.struct_vbuf_buffer)() elif not buf: self.logging.logE('vbuf_queue_pop returned NULL') return buf def _process_stream(self, id_): self.logging.logD('media id = {}'.format(id_)) if self.streams[id_]['video_sink_flushed']: self.logging.logI( 'Video sink has already been flushed ID {}'.format(id_)) return False if od.vbuf_queue_get_count(self.streams[id_]['video_queue']) == 0: return False buf = self._pop_stream_buffer(id_) if not buf: return False video_frame = VideoFrame( self.logging, buf, id_, self.streams[id_], self.yuv_packed_buffer_pool, self.get_session_metadata() ) try: if not self._process_stream_buffer(id_, video_frame): return False return True finally: # Once we're done with this frame, dispose the associated frame buffer video_frame.unref() def _process_stream_buffer(self, id_, video_frame): stream = self.streams[id_] mediatype = stream['type'] # write and/or send data over the requested channels # handle output files files = self.outfiles[mediatype] f = files['meta'] if f and not f.closed: vmeta_type, vmeta = video_frame.vmeta() files['meta'].write(json.dumps((str(vmeta_type), vmeta)) + '\n') f = files['data'] if f and not f.closed: if mediatype == od.PDRAW_VIDEO_MEDIA_FORMAT_H264: if f.tell() == 0: # h264 files need a header to be readable stream['h264_header'].tofile(f) frame_array = video_frame.as_ndarray() if frame_array is not None: f.write(ctypes.string_at( frame_array.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)), frame_array.size, )) # call callbacks when existing cb = self.frame_callbacks[mediatype] if cb is not None: cb(video_frame) def set_output_files(self, h264_data_file, h264_meta_file, raw_data_file, raw_meta_file): """ Records the video stream session to the disk - xxx_meta_file: video stream metadata output files - xxx_data_file: video stream frames output files - h264_***_file: files associated to the H264 encoded video stream - raw_***_file: files associated to the decoded video stream This function MUST NOT be called when a video streaming session is active. Setting a file parameter to `None` disables the recording for the related stream part. """ if self._state is State.Playing: raise RuntimeError( 'Cannot set video streaming files while streaming is on.') for mediatype, datatype, filepath, attrib in ( (od.PDRAW_VIDEO_MEDIA_FORMAT_H264, 'data', h264_data_file, 'wb'), (od.PDRAW_VIDEO_MEDIA_FORMAT_H264, 'meta', h264_meta_file, 'w'), (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, 'data', raw_data_file, 'wb'), (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, 'meta', raw_meta_file, 'w')): if self.outfiles[mediatype][datatype]: self.outfiles[mediatype][datatype].close() self.outfiles[mediatype][datatype] = None if filepath is None: continue # open and close file to store its filename and attribute self.outfiles[mediatype][datatype] = open(filepath, attrib) self.outfiles[mediatype][datatype].close() def set_callbacks(self, h264_cb=None, raw_cb=None, end_cb=None, flush_cb=None): """ Set the callback functions that will be called when a new video stream frame is available or when the video stream has ended. Video frame callbacks: - `h264_cb` is associated to the H264 encoded video stream - `raw_cb` is associated to the decoded video stream Each video frame callback function takes an :py:func:`~olympe.VideoFrame` parameter The `end_cb` callback function is called when the (replayed) video stream ends and takes no parameter. The return value of all these callback functions are ignored. If a callback is not desired, just set it to `None`. """ for mediatype, cb in ((od.PDRAW_VIDEO_MEDIA_FORMAT_H264, h264_cb), (od.PDRAW_VIDEO_MEDIA_FORMAT_YUV, raw_cb)): self.frame_callbacks[mediatype] = cb self.end_callback = end_cb self.flush_callback = flush_cb def _open_output_files(self): self.logging.logD('opening video output files') for mediatype, data in self.outfiles.items(): for datatype, f in data.items(): if f and f.closed: self.outfiles[mediatype][datatype] = open(f.name, f.mode) def _close_output_files(self): self.logging.logD('closing video output files') for files in self.outfiles.values(): for f in files.values(): if f: f.close() def play(self, url=None, media_name="DefaultVideo", server_addr=None, resource_name="live"): """ Play a video By default, open and play a live video streaming session available from rtsp://192.168.42.1/live where "192.168.42.1" is the default IP address of a physical (Anafi) drone. The default is equivalent to `Pdraw.play(url="rtsp://192.168.42.1/live")` For a the live video streaming from a **simulated drone**, you have to specify the default simulated drone IP address (10.202.0.1) instead: `Pdraw.play(url="rtsp://10.202.0.1/live")`. The `url` parameter can also point to a local file example: `Pdraw.play(url="file://~/Videos/100000010001.MP4")`. :param url: rtsp or local file video URL :type url: str :param media_name: name of the media/track (defaults to "DefaultVideo"). If the provided media name is not available from the requested video stream, the default media is selected instead. :type media_name: str """ if self.pdraw is None: self.logging.logE("Error Pdraw interface seems to be destroyed") self._play_resp_future.set_result(False) return self._pause_resp_future if self._state in (State.Opening, State.Closing): self.logging.logW("Cannot play stream from the {} state".format( self._state)) f = Future(self.pdraw_thread_loop) f.set_result(False) return f self.resource_name = resource_name self.media_name = media_name if server_addr is None: self.server_addr = "192.168.42.1" else: self.server_addr = server_addr if url is None: self.url = b"rtsp://%s/%s" % ( self.server_addr, self.resource_name.encode()) else: if isinstance(url, bytes): url = url.decode('utf-8') if url.startswith('file://'): url = url[7:] if url.startswith('~/'): url = os.path.expanduser(url) url = os.path.expandvars(url) url = url.encode('utf-8') self.url = url if self.is_legacy(): self.logging.logW("Cannot open streaming url for legacy drones") # reset session metadata from any previous session self.session_metadata = {} self.streams = defaultdict(lambda: { 'id': None, 'type': od.PDRAW_VIDEO_MEDIA_FORMAT_UNKNOWN, 'h264_header': None, 'video_sink': od.POINTER_T(od.struct_pdraw_video_sink)(), 'video_sink_flushed': False, 'video_sink_lock': threading.Lock(), 'video_queue': None, 'video_queue_event': None, }) self._open_output_files() if self._state in (State.Created, State.Closed): f = self.pdraw_thread_loop.run_async(self._open_stream) else: f = self._play_resp_future = Future(self.pdraw_thread_loop) self.pdraw_thread_loop.run_async(self._play_impl) return f def _play_impl(self): self.logging.logD("play_impl") if self._state is State.Playing: self._play_resp_future.set_result(True) return self._play_resp_future res = od.pdraw_play(self.pdraw) if res != 0: msg = "Unable to start streaming ({})".format(res) self.logging.logE(msg) self._play_resp_future.set_result(False) return self._play_resp_future def pause(self): """ Pause the currently playing video """ if self.pdraw is None: self.logging.logE("Error Pdraw interface seems to be destroyed") self._pause_resp_future.set_result(False) return self._pause_resp_future self._pause_resp_future = Future(self.pdraw_thread_loop) if self._state is State.Playing: self.pdraw_thread_loop.run_async(self._pause_impl) elif self._state in (State.Closed, State.Opened): # Pause an opened/closed stream is OK self._pause_resp_future.set_result(True) else: self.logging.logW("Cannot pause stream from the {} state".format( self._state)) self._pause_resp_future.set_result(False) return self._pause_resp_future def _pause_impl(self): res = od.pdraw_pause(self.pdraw) if res != 0: self.logging.logE("Unable to stop streaming ({})".format(res)) self._pause_resp_future.set_result(False) return self._pause_resp_future def get_session_metadata(self): """ Returns a dictionary of video stream session metadata """ if self.pdraw is None: self.logging.logE("Error Pdraw interface seems to be destroyed") return None if self.session_metadata: return self.session_metadata vmeta_session = od.struct_vmeta_session() res = od.pdraw_get_peer_session_metadata( self.pdraw, ctypes.pointer(vmeta_session)) if res != 0: msg = "Unable to get sessions metata" self.logging.logE(msg) return None self.session_metadata = od.struct_vmeta_session.as_dict( vmeta_session) return self.session_metadata def is_legacy(self): return self._legacy