From 475c72b241ea5160879b5b468b1538010988e746 Mon Sep 17 00:00:00 2001 From: Alano Terblanche Date: Sun, 11 Aug 2019 21:06:21 +0200 Subject: [PATCH] Added proxy support. Bug fixes in APIHandler. RTSP support (adding) Proxy support allows contacting the camera behind a proxy (GET and POST requests). Adding RTSP support - still in progress. --- APIHandler.py | 45 ++++++++++++++++++++++++--- Camera.py | 14 ++++++++- RtspClient.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ resthandle.py | 16 +++++++--- test.py | 6 ++-- 5 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 RtspClient.py diff --git a/APIHandler.py b/APIHandler.py index bd35b67..c52b624 100644 --- a/APIHandler.py +++ b/APIHandler.py @@ -2,10 +2,14 @@ import io import json import random import string -from urllib.request import urlopen +import sys +from urllib import request +import numpy +import rtsp from PIL import Image +from RtspClient import RtspClient from resthandle import Request @@ -19,17 +23,23 @@ class APIHandler: """ - def __init__(self, ip: str, username: str, password: str): + def __init__(self, ip: str, username: str, password: str, **kwargs): """ Initialise the Camera API Handler (maps api calls into python) :param ip: :param username: :param password: + :param proxy: Add a proxy dict for requests to consume. + eg: {"http":"socks5://[username]:[password]@[host]:[port], "https": ...} + More information on proxies in requests: https://stackoverflow.com/a/15661226/9313679 + """ + self.ip = ip self.url = "http://" + ip + "/cgi-bin/api.cgi" self.token = None self.username = username self.password = password + Request.proxies = kwargs.get("proxy") # Defaults to None if key isn't found ########### # Token @@ -380,7 +390,7 @@ class APIHandler: param = {"cmd": "GetDevInfo", "token": self.token} body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}] response = Request.post(self.url, data=body, params=param) - if response == 200: + if response.status_code == 200: return json.loads(response.text) print("Could not retrieve camera information. Status:", response.status_code) return None @@ -543,12 +553,15 @@ class APIHandler: :return: Image or None """ try: + randomstr = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) - snap = "?cmd=Snap&channel=0&rs=" \ + snap = self.url + "?cmd=Snap&channel=0&rs=" \ + randomstr \ + "&user=" + self.username \ + "&password=" + self.password - reader = urlopen(self.url + snap, timeout) + req = request.Request(snap) + req.set_proxy(Request.proxies, 'http') + reader = request.urlopen(req, timeout) if reader.status == 200: b = bytearray(reader.read()) return Image.open(io.BytesIO(b)) @@ -860,3 +873,25 @@ class APIHandler: return None except Exception as e: print("Could not get advanced recoding", e) + + ########### + # RTSP Stream + ########### + def open_video_stream(self, profile: str = "main") -> Image: + """ + profile is "main" or "sub" + https://support.reolink.com/hc/en-us/articles/360007010473-How-to-Live-View-Reolink-Cameras-via-VLC-Media-Player + :param profile: + :return: + """ + with RtspClient(ip=self.ip, username=self.username, password=self.password, + proxies={"host": "127.0.0.1", "port": 8000}) as rtsp_client: + rtsp_client.preview() + # with rtsp.Client( + # rtsp_server_uri="rtsp://" + # + self.username + ":" + # + self.password + "@" + # + self.ip + # + ":554//h264Preview_01_" + # + profile) as client: + # return client diff --git a/Camera.py b/Camera.py index 317840d..4a4ee79 100644 --- a/Camera.py +++ b/Camera.py @@ -4,7 +4,19 @@ from APIHandler import APIHandler class Camera(APIHandler): def __init__(self, ip, username="admin", password=""): - APIHandler.__init__(self, ip, username, password) + """ + Initialise the Camera object by passing the ip address. + The default details {"username":"admin", "password":""} will be used if nothing passed + :param ip: + :param username: + :param password: + """ + # For when you need to connect to a camera behind a proxy + APIHandler.__init__(self, ip, username, password, proxy={"http": "socks5://127.0.0.1:8000"}) + + # Normal call without proxy: + # APIHandler.__init__(self, ip, username, password) + self.ip = ip self.username = username self.password = password diff --git a/RtspClient.py b/RtspClient.py new file mode 100644 index 0000000..b3cb3fb --- /dev/null +++ b/RtspClient.py @@ -0,0 +1,85 @@ +import socket + +import cv2 +import numpy +import socks + + +class RtspClient: + + def __init__(self, ip, username, password, port=554, profile="main", **kwargs): + """ + + :param ip: + :param username: + :param password: + :param port: rtsp port + :param profile: "main" or "sub" + :param proxies: {"host": "localhost", "port": 8000} + """ + self.ip = ip + self.username = username + self.password = password + self.port = port + self.sockt = None + self.url = "rtsp://" + self.username + ":" + self.password + "@" + self.ip + ":" + str( + self.port) + "//h264Preview_01_" + profile + self.proxy = kwargs.get("proxies") + + def __enter__(self): + self.sockt = self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.sockt.close() + + def connect(self) -> socket: + try: + sockt = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) + sockt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.proxy is not None: + sockt.set_proxy(socks.SOCKS5, self.proxy["host"], self.proxy["port"]) + sockt.connect((self.ip, self.port)) + return sockt + except Exception as e: + print(e) + + def get_frame(self) -> bytearray: + try: + self.sockt.send(str.encode(self.url)) + data = b'' + while True: + try: + r = self.sockt.recv(90456) + if len(r) == 0: + break + a = r.find(b'END!') + if a != -1: + data += r[:a] + break + data += r + except Exception as e: + print(e) + continue + nparr = numpy.fromstring(data, numpy.uint8) + frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + return frame + except Exception as e: + print(e) + + def preview(self): + """ Blocking function. Opens OpenCV window to display stream. """ + self.connect() + win_name = 'RTSP' + cv2.namedWindow(win_name, cv2.WINDOW_AUTOSIZE) + cv2.moveWindow(win_name, 20, 20) + + while True: + cv2.imshow(win_name, self.get_frame()) + # if self._latest is not None: + # cv2.imshow(win_name,self._latest) + if cv2.waitKey(25) & 0xFF == ord('q'): + break + cv2.waitKey() + cv2.destroyAllWindows() + cv2.waitKey() diff --git a/resthandle.py b/resthandle.py index e632686..f68f406 100644 --- a/resthandle.py +++ b/resthandle.py @@ -1,9 +1,13 @@ import json import requests +import socket + +import socks class Request: + proxies = None @staticmethod def post(url: str, data, params=None) -> requests.Response or None: @@ -16,10 +20,11 @@ class Request: """ try: headers = {'content-type': 'application/json'} - if params is not None: - r = requests.post(url, params=params, json=data, headers=headers) - else: - r = requests.post(url, json=data) + r = requests.post(url, params=params, json=data, headers=headers, proxies=Request.proxies) + # if params is not None: + # r = requests.post(url, params=params, json=data, headers=headers, proxies=proxies) + # else: + # r = requests.post(url, json=data) if r.status_code == 200: return r else: @@ -38,7 +43,8 @@ class Request: :return: """ try: - data = requests.get(url=url, params=params, timeout=timeout) + data = requests.get(url=url, params=params, timeout=timeout, proxies=Request.proxies) + return data except Exception as e: print("Get Error\n", e) diff --git a/test.py b/test.py index 158ad3f..796f5a2 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,5 @@ from Camera import Camera -c = Camera("192.168.1.100", "admin", "jUa2kUzi") -c.get_wifi() -c.scan_wifi() +c = Camera("192.168.1.112", "admin", "jUa2kUzi") +# print("Getting information", c.get_information()) +c.open_video_stream()