diff --git a/scripts/examples/36-Web-Servers/rtsp_video_server.py b/scripts/examples/36-Web-Servers/rtsp_video_server.py new file mode 100644 index 000000000..8de8d8165 --- /dev/null +++ b/scripts/examples/36-Web-Servers/rtsp_video_server.py @@ -0,0 +1,81 @@ +# RTSP Video Server +# +# This example shows off how to stream video over RTSP with your OpenMV Cam. +# +# You can use a program like VLC to view the video stream by connecting to the +# OpenMV Cam's IP address. + +import network, omv, rtsp, sensor, time + +# RTP MJPEG streaming works using JPEG images produced by the OV2640/OV5640 camera modules. +# Not all programs (e.g. VLC) implement the full JPEG standard for decoding any JPEG image +# in RTP packets. Images JPEG compressed by the OpenMV Cam internally may not display. + +sensor.reset() + +sensor.set_pixformat(sensor.JPEG) # Only supported by the OV2640/OV5640. +sensor.set_framesize(sensor.UXGA) + +# Turn off the frame buffer connection to the IDE from the OpenMV Cam side. +# +# This needs to be done when manually compressing jpeg images at higher quality +# so that the OpenMV Cam does not try to stream them to the IDE using a fall back +# mechanism if the JPEG image is too large to fit in the IDE JPEG frame buffer on the OpenMV Cam. + +omv.disable_fb(True) + +# * ssid - WiFi network to connect to. +# * ssid_key - WiFi network password. +# * ssid_security - WiFi security. +# * port - Port to listen to (554). +# * mode - Regular or access-point mode. +# * static_ip - If not None then a tuple of the (IP Address, Subnet Mask, Gateway, DNS Address) + +server = rtsp.rtsp_server(ssid="", + ssid_key="", + ssid_security=network.WINC.WPA_PSK, + port=554, + mode=network.WINC.MODE_STA, + static_ip=None) + +# For the call back functions below: +# +# `pathname` is the name of the stream resource the client wants. You can ignore this if it's not +# needed. Otherwise, you can use it to determine what image object to return. By default the path +# name will be "/". +# +# `session` is random number that will change when a new connection is established. You can use +# session with a dictionary to differentiate different accesses to the same file name. + +def setup_callback(pathname, session): + print("Opening \"%s\" in session %d" % (pathname, session)) + +def play_callback(pathname, session): + print("Playing \"%s\" in session %d" % (pathname, session)) + +def pause_callback(pathname, session): # VLC only pauses locally. This is never called. + print("Pausing \"%s\" in session %d" % (pathname, session)) + +def teardown_callback(pathname, session): + print("Closing \"%s\" in session %d" % (pathname, session)) + +server.register_setup_cb(setup_callback) +server.register_play_cb(play_callback) +server.register_pause_cb(pause_callback) +server.register_teardown_cb(teardown_callback) + +# Track the current FPS. +clock = time.clock() + +# Called each time a new frame is needed. +def image_callback(pathname, session): + clock.tick() + img = sensor.snapshot() + # Markup image and/or do various things. + print(clock.fps()) + return img + +# Stream does not return. It will call `image_callback` when it needs to get an image object to send +# to the remote rtsp client connecting to the server. + +server.stream(image_callback) diff --git a/scripts/libraries/rtsp.py b/scripts/libraries/rtsp.py new file mode 100644 index 000000000..346dc5343 --- /dev/null +++ b/scripts/libraries/rtsp.py @@ -0,0 +1,219 @@ +# This file is part of the OpenMV project. +# +# Copyright (c) 2013-2020 Ibrahim Abdelkader +# Copyright (c) 2013-2020 Kwabena W. Agyeman +# +# This work is licensed under the MIT license, see the file LICENSE for details. + +import network, pyb, re, socket, struct + +class rtsp_server: + + def __valid_tcp_socket(self): # private + if self.__tcp__socket is None: + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(self.__myaddr) + s.listen(0) + s.settimeout(5) + self.__tcp__socket, self.__client_addr = s.accept() + s.close() + except OSError: self.__tcp__socket = None + return self.__tcp__socket is not None + + def __close_tcp_socket(self): # private + self.__tcp__socket.close() + self.__tcp__socket = None + + def __valid_udp_socket(self): # private + if self.__udp__socket is None: + try: + self.__udp__socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.__udp__socket.bind((self.__myip, self.__client_rtp_port)) + except OSError: self.__udp__socket = None + return self.__udp__socket is not None + + def __close_udp_socket(self): # private + self.__udp__socket.close() + self.__udp__socket = None + + def __init__(self, ssid, ssid_key, ssid_security, port=554, mode=network.WINC.MODE_STA, static_ip=None): # private + self.__winc = network.WINC(mode=mode) + if mode == network.WINC.MODE_STA: + if static_ip is not None: self.__winc.ifconfig(static_ip) + self.__winc.connect(ssid, key=ssid_key, security=ssid_security) + if not self.__winc.isconnected(): raise OSError("Failed to connect to network!") + elif mode == network.WINC.MODE_AP: self.__winc.start_ap(ssid, key=ssid_key, security=ssid_security) + else: raise ValueError("Invalid mode") + self.__myip = self.__winc.ifconfig()[0] + self.__myaddr = (self.__myip, port) + self.__tcp__socket = None + self.__udp__socket = None + self.__setup_cb = None + self.__play_cb = None + self.__pause_cb = None + self.__teardown_cb = None + self.__pathname = "" + self.__session = pyb.rng() + self.__transport_is_tcp = False + self.__client_rtp_addr = None + self.__playing = False + self.__sequence_number = pyb.rng() & 0xFFFF + self.__ssrc = pyb.rng() + print("IP Address:Port %s:%d\nRunning..." % self.__myaddr) + + def register_setup_cb(self, cb): # public + self.__setup_cb = cb + + def register_play_cb(self, cb): # public + self.__play_cb = cb + + def register_pause_cb(self, cb): # public + self.__pause_cb = cb + + def register_teardown_cb(self, cb): # public + self.__teardown_cb = cb + + def __send_rtsp_response(self, code, name, extra=""): # private + self.__tcp__socket.send("RTSP/1.0 %d %s\r\n%s\r\n" % (code, name, extra)) + + def __send_rtsp_response_cseq(self, code, name, seq, extra=""): # private + self.__send_rtsp_response(code, name, "CSeq: %d\r\n%s" % (seq, extra)) + + def __send_rtsp_response_ok(self, seq, extra=""): # private + self.__send_rtsp_response_cseq(200, "OK", seq, extra) + + def __parse_rtsp_request(self, data): # private + if data[0] == 0x24: return # Interleaved RTCP Packet + if data[0] >= 0x80: return # Ressambled TCP + s = list(filter(lambda x: x and not x.startswith("User-Agent"), data.decode().splitlines())) + if s and len(s) >= 2: + line0 = s[0].split(' ') + request = line0[0] + self.__pathname = re.sub("rtsp(u)?://[a-zA-Z0-9\-\.]+(:\d+)?(/)?", '/', line0[1]) + if self.__pathname != '/' and self.__pathname.endswith('/'): + self.__pathname = self.__pathname[:-1] + if line0[2] == "RTSP/1.0": + line1 = s[1].split(' ') + if line1[0] == "CSeq:": + seq = int(line1[1]) + if request == "OPTIONS": + if len(s) >= 3: + m = re.match("(Proxy-)?Require: (\S)+", s[2]) + if m: + self.__send_rtsp_response_cseq(551, "Option not supported", seq, "Unsupported: %s\r\n" % m.group(2)) + return + self.__send_rtsp_response_ok(seq, "Public: DESCRIBE, SETUP, PLAY, PAUSE, TEARDOWN\r\n") + return + elif request == "DESCRIBE": + payload = "v=0\r\no=- %d %d IN IP4 %s\r\ns=OpenMV Video\r\nt=0 0\r\nm=video 0 RTP/AVP 26" % (pyb.rng(), pyb.rng(), self.__myip) + self.__send_rtsp_response_ok(seq, "Content-Type: application/sdp\r\nContent-Length: %d\r\n\r\n%s" % (len(payload) + 2, payload)) + return + elif request == "SETUP": + if len(s) >= 3: + m = re.match("Transport: RTP/AVP(/UDP)?;unicast;client_port=(\d+)-(\d+)", s[2]) + if m: + self.__transport_is_tcp = False + self.__client_rtp_port = int(m.group(2)) + self.__client_rtcp_port = int(m.group(3)) + self.__client_rtp_addr = (self.__client_addr[0], self.__client_rtp_port) + self.__client_rtcp_addr = (self.__client_addr[0], self.__client_rtcp_port) + self.__session = pyb.rng() + self.__ssrc = pyb.rng() + self.__send_rtsp_response_ok(seq, "%s;server_port=%d-%d;ssrc=%d\r\nSession: %d\r\n" % (s[2], self.__client_rtp_port, self.__client_rtcp_port, self.__ssrc, self.__session)) + if self.__setup_cb: self.__setup_cb(self.__pathname, self.__session) + return + m = re.match("Transport: RTP/AVP/TCP;unicast;interleaved=(\d+)-(\d+)", s[2]) + if m: + self.__transport_is_tcp = True + self.__client_rtp_channel = int(m.group(1)) + self.__client_rtcp_channel = int(m.group(2)) + self.__session = pyb.rng() + self.__ssrc = pyb.rng() + self.__send_rtsp_response_ok(seq, "%s\r\nSession: %d\r\n" % (s[2], self.__session)) + if self.__setup_cb: self.__setup_cb(self.__pathname, self.__session) + return + elif request == "PLAY": + if len(s) >= 3: + m = re.match("Session: (\d+)", s[2]) + if m: + self.__playing = True + self.__send_rtsp_response_ok(seq, "RTP-Info: url=%s;seq=%d\r\n" % (line0[1], self.__sequence_number)) + if self.__play_cb: self.__play_cb(self.__pathname, int(m.group(1))) + return + elif request == "PAUSE": + if len(s) >= 3: + m = re.match("Session: (\d+)", s[2]) + if m: + self.__playing = False + self.__send_rtsp_response_ok(seq) + if self.__pause_cb: self.__pause_cb(self.__pathname, int(m.group(1))) + return + elif request == "TEARDOWN": + if len(s) >= 3: + m = re.match("Session: (\d+)", s[2]) + if m: + self.__playing = False + self.__send_rtsp_response_ok(seq) + if self.__teardown_cb: self.__teardown_cb(self.__pathname, int(m.group(1))) + return + else: + self.__send_rtsp_response_cseq(501, "Not Implemented", seq) + return + self.__send_rtsp_response(400, "Bad Request") + + def __valid_socket(self): # private + if self.__transport_is_tcp: return self.__client_rtp_channel is not None + else: return self.__valid_udp_socket() and self.__client_rtp_addr is not None + + def __settimeout(self, timeout): # private + if self.__transport_is_tcp: self.__tcp__socket.settimeout(timeout) + else: self.__udp__socket.settimeout(timeout) + + def __send(self, data): # private + if self.__transport_is_tcp: return self.__tcp__socket.send(struct.pack(">BBH", 0x24, self.__client_rtp_channel, len(data)) + data) + else: return self.__udp__socket.sendto(data, self.__client_rtp_addr) + + def __close_socket(self): # private + if self.__transport_is_tcp: pass + else: self.__close_udp_socket() + + def __send_rtp(self, image_callback, quality): # private + img = image_callback(self.__pathname, self.__session).compress(quality) + if img.width() >= 2040: raise ValueError("Maximum width is 2040") + if img.height() >= 2040: raise ValueError("Maximum height is 2040") + i = 0 + l = img.size() + rtp_header_size = 12 + jpeg_header_size = 8 + max_packet_size = 1400 - rtp_header_size - jpeg_header_size + if self.__transport_is_tcp: max_packet_size -= 4 + if self.__valid_socket(): + try: + self.__settimeout(0.1) + timestamp = (pyb.millis() * 90) & 0xFFFFFFFF + while l: + rtp_header = struct.pack(">BBHII", + 0x80, 0x9A if l <= max_packet_size else 0x1A, + self.__sequence_number, timestamp, self.__ssrc) + self.__sequence_number = (self.__sequence_number + 1) & 0xFFFF + jpeg_header = struct.pack(">IBBBB", i, 0, quality, int(img.width() // 8), int(img.height() // 8)) + img_data = img.bytearray()[i:i+min(l, max_packet_size)] + img_data_len = len(img_data) + data_len = self.__send(rtp_header + jpeg_header + img_data) + if not data_len: break + i += img_data_len + l -= img_data_len + if l: self.__close_socket() + except OSError: self.__close_socket() + + def stream(self, image_callback, quality=90): # public + while True: + if self.__valid_tcp_socket(): + try: + self.__tcp__socket.settimeout(0.1) + while True: + data = self.__tcp__socket.recv(1400) + if data and len(data): self.__parse_rtsp_request(data) + if self.__playing: self.__send_rtp(image_callback, quality) + except OSError: self.__close_tcp_socket()