diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..5c98b42
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,2 @@
+# Default ignored files
+/workspace.xml
\ No newline at end of file
diff --git a/.idea/ReolinkCameraAPI.iml b/.idea/ReolinkCameraAPI.iml
new file mode 100644
index 0000000..6711606
--- /dev/null
+++ b/.idea/ReolinkCameraAPI.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..3999087
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..8125795
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/APIHandler.py b/APIHandler.py
index 0522f5c..ee7472b 100644
--- a/APIHandler.py
+++ b/APIHandler.py
@@ -1,27 +1,59 @@
+import io
import json
+import random
+import string
+import sys
+from urllib import request
+import numpy
+import rtsp
+from PIL import Image
+
+from RtspClient import RtspClient
from resthandle import Request
class APIHandler:
+ """
+ The APIHandler class is the backend part of the API.
+ This handles communication directly with the camera.
+ Current camera's tested: RLC-411WS
- def __init__(self, ip, https=False):
+ All Code will try to follow the PEP 8 standard as described here: https://www.python.org/dev/peps/pep-0008/
+
+ """
+
+ def __init__(self, ip: str, username: str, password: str, https = False, **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
+ """
scheme = 'https' if https else 'http'
self.url = f"{scheme}://{ip}/cgi-bin/api.cgi"
+ self.ip = ip
self.token = None
+ self.username = username
+ self.password = password
+ Request.proxies = kwargs.get("proxy") # Defaults to None if key isn't found
+ ###########
# Token
+ ###########
- def login(self, username: str, password: str):
+ def login(self) -> bool:
"""
Get login token
Must be called first, before any other operation can be performed
- :param username:
- :param password:
- :return:
+ :return: bool
"""
try:
- body = [{"cmd": "Login", "action": 0, "param": {"User": {"userName": username, "password": password}}}]
+ body = [{"cmd": "Login", "action": 0,
+ "param": {"User": {"userName": self.username, "password": self.password}}}]
param = {"cmd": "Login", "token": "null"}
response = Request.post(self.url, data=body, params=param)
if response is not None:
@@ -30,9 +62,12 @@ class APIHandler:
if int(code) == 0:
self.token = data["value"]["Token"]["name"]
print("Login success")
+ return True
print(self.token)
+ return False
else:
print("Failed to login\nStatus Code:", response.status_code)
+ return False
except Exception as e:
print("Error Login\n", e)
raise
@@ -44,43 +79,46 @@ class APIHandler:
###########
# SET Network
###########
- def set_net_port(self, httpPort=80, httpsPort=443, mediaPort=9000, onvifPort=8000, rtmpPort=1935, rtspPort=554):
+ def set_net_port(self, http_port=80, https_port=443, media_port=9000, onvif_port=8000, rtmp_port=1935,
+ rtsp_port=554) -> bool:
"""
Set network ports
If nothing is specified, the default values will be used
- :param httpPort:
- :param httpsPort:
- :param mediaPort:
- :param onvifPort:
- :param rtmpPort:
- :param rtspPort:
- :return:
+ :param rtsp_port: int
+ :param rtmp_port: int
+ :param onvif_port: int
+ :param media_port: int
+ :param https_port: int
+ :type http_port: int
+ :return: bool
"""
try:
if self.token is None:
raise ValueError("Login first")
body = [{"cmd": "SetNetPort", "action": 0, "param": {"NetPort": {
- "httpPort": httpPort,
- "httpsPort": httpsPort,
- "mediaPort": mediaPort,
- "onvifPort": onvifPort,
- "rtmpPort": rtmpPort,
- "rtspPort": rtspPort
+ "httpPort": http_port,
+ "httpsPort": https_port,
+ "mediaPort": media_port,
+ "onvifPort": onvif_port,
+ "rtmpPort": rtmp_port,
+ "rtspPort": rtsp_port
}}}]
param = {"token": self.token}
response = Request.post(self.url, data=body, params=param)
if response is not None:
if response.status_code == 200:
print("Successfully Set Network Ports")
+ return True
else:
print("Something went wront\nStatus Code:", response.status_code)
-
+ return False
+ return False
except Exception as e:
print("Setting Network Port Error\n", e)
raise
- def set_wifi(self, ssid, password):
+ def set_wifi(self, ssid, password) -> json or None:
try:
if self.token is None:
raise ValueError("Login first")
@@ -99,7 +137,7 @@ class APIHandler:
###########
# GET
###########
- def get_net_ports(self):
+ def get_net_ports(self) -> json or None:
"""
Get network ports
:return:
@@ -113,7 +151,10 @@ class APIHandler:
{"cmd": "GetP2p", "action": 0, "param": {}}]
param = {"token": self.token}
response = Request.post(self.url, data=body, params=param)
- return json.loads(response.text)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not get network ports data. Status:", response.status_code)
+ return None
except Exception as e:
print("Get Network Ports", e)
@@ -130,7 +171,9 @@ class APIHandler:
body = [{"cmd": "GetLocalLink", "action": 1, "param": {}}]
param = {"cmd": "GetLocalLink", "token": self.token}
request = Request.post(self.url, data=body, params=param)
- return json.loads(request.text)
+ if request.status_code == 200:
+ return json.loads(request.text)
+ print("Could not get ")
except Exception as e:
print("Could not get Link Local", e)
raise
@@ -159,6 +202,113 @@ class APIHandler:
print("Could not Scan wifi\n", e)
raise
+ ###########
+ # Display
+ ###########
+
+ ###########
+ # GET
+ ###########
+ def get_osd(self) -> json or None:
+ """
+ Get OSD information.
+ Response data format is as follows:
+ [{"cmd" : "GetOsd","code" : 0, "initial" : {
+ "Osd" : {"bgcolor" : 0,"channel" : 0,"osdChannel" : {"enable" : 1,"name" : "Camera1","pos" : "Lower Right"},
+ "osdTime" : {"enable" : 1,"pos" : "Top Center"}
+ }},"range" : {"Osd" : {"bgcolor" : "boolean","channel" : 0,"osdChannel" : {"enable" : "boolean","name" : {"maxLen" : 31},
+ "pos" : ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
+ },
+ "osdTime" : {"enable" : "boolean","pos" : ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
+ }
+ }},"value" : {"Osd" : {"bgcolor" : 0,"channel" : 0,"osdChannel" : {"enable" : 0,"name" : "FarRight","pos" : "Lower Right"},
+ "osdTime" : {"enable" : 0,"pos" : "Top Center"}
+ }}}]
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetOsd", "token": self.token}
+ body = [{"cmd": "GetOsd", "action": 1, "param": {"channel": 0}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not retrieve OSD from camera successfully. Status:", response.status_code)
+ return None
+ except Exception as e:
+ print("Could not get OSD", e)
+ raise
+
+ def get_mask(self) -> json or None:
+ """
+ Get the camera mask information
+ Response data format is as follows:
+ [{"cmd" : "GetMask","code" : 0,"initial" : {
+ "Mask" : {
+ "area" : [{"block" : {"height" : 0,"width" : 0,"x" : 0,"y" : 0},"screen" : {"height" : 0,"width" : 0}}],
+ "channel" : 0,
+ "enable" : 0
+ }
+ },"range" : {"Mask" : {"channel" : 0,"enable" : "boolean","maxAreas" : 4}},"value" : {
+ "Mask" : {
+ "area" : null,
+ "channel" : 0,
+ "enable" : 0}
+ }
+ }]
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetMask", "token": self.token}
+ body = [{"cmd": "GetMask", "action": 1, "param": {"channel": 0}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not get Mask from camera successfully. Status:", response.status_code)
+ return None
+ except Exception as e:
+ print("Could not get mask", e)
+ raise
+
+ ###########
+ # SET
+ ###########
+ def set_osd(self, bg_color: bool = 0, channel: int = 0, osd_channel_enabled: bool = 0, osd_channel_name: str = "",
+ osd_channel_pos: str = "Lower Right", osd_time_enabled: bool = 0,
+ osd_time_pos: str = "Lower Right") -> bool:
+ """
+ Set OSD
+ :param bg_color: bool
+ :param channel: int channel id
+ :param osd_channel_enabled: bool
+ :param osd_channel_name: string channel name
+ :param osd_channel_pos: string channel position ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
+ :param osd_time_enabled: bool
+ :param osd_time_pos: string time position ["Upper Left","Top Center","Upper Right","Lower Left","Bottom Center","Lower Right"]
+ :return:
+ """
+ try:
+ param = {"cmd": "setOsd", "token": self.token}
+ body = [{"cmd": "SetOsd", "action": 1, "param":
+ {"Osd": {"bgcolor": bg_color, "channel": channel,
+ "osdChannel": {"enable": osd_channel_enabled, "name": osd_channel_name,
+ "pos": osd_channel_pos},
+ "osdTime": {"enable": osd_time_enabled, "pos": osd_time_pos}
+ }
+ }
+ }]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ r_data = json.loads(response.text)
+ if r_data["value"]["rspCode"] == "200":
+ return True
+ print("Could not set OSD. Camera responded with status:", r_data["value"])
+ return False
+ print("Could not set OSD. Status:", response.status_code)
+ return False
+ except Exception as e:
+ print("Could not set OSD", e)
+ raise
+
###########
# SYSTEM
###########
@@ -166,14 +316,582 @@ class APIHandler:
###########
# GET
###########
- def get_general_system(self):
+ def get_general_system(self) -> json or None:
try:
if self.token is None:
raise ValueError("Login first")
body = [{"cmd": "GetTime", "action": 1, "param": {}}, {"cmd": "GetNorm", "action": 1, "param": {}}]
param = {"token": self.token}
response = Request.post(self.url, data=body, params=param)
- return json.loads(response.text)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not retrieve general information from camera successfully. Status:", response.status_code)
+ return None
except Exception as e:
print("Could not get General System settings\n", e)
raise
+
+ def get_performance(self) -> json or None:
+ """
+ Get a snapshot of the current performance of the camera.
+ Response data format is as follows:
+ [{"cmd" : "GetPerformance",
+ "code" : 0,
+ "value" : {
+ "Performance" : {
+ "codecRate" : 2154,
+ "cpuUsed" : 14,
+ "netThroughput" : 0
+ }
+ }
+ }]
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetPerformance", "token": self.token}
+ body = [{"cmd": "GetPerformance", "action": 0, "param": {}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Cound not retrieve performance information from camera successfully. Status:", response.status_code)
+ return None
+ except Exception as e:
+ print("Could not get performance", e)
+ raise
+
+ def get_information(self) -> json or None:
+ """
+ Get the camera information
+ Response data format is as follows:
+ [{"cmd" : "GetDevInfo","code" : 0,"value" : {
+ "DevInfo" : {
+ "B485" : 0,
+ "IOInputNum" : 0,
+ "IOOutputNum" : 0,
+ "audioNum" : 0,
+ "buildDay" : "build 18081408",
+ "cfgVer" : "v2.0.0.0",
+ "channelNum" : 1,
+ "detail" : "IPC_3816M100000000100000",
+ "diskNum" : 1,
+ "firmVer" : "v2.0.0.1389_18081408",
+ "hardVer" : "IPC_3816M",
+ "model" : "RLC-411WS",
+ "name" : "Camera1_withpersonality",
+ "serial" : "00000000000000",
+ "type" : "IPC",
+ "wifi" : 1
+ }
+ }
+ }]
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetDevInfo", "token": self.token}
+ body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not retrieve camera information. Status:", response.status_code)
+ return None
+ except Exception as e:
+ print("Could not get device information", e)
+ raise
+
+ ##########
+ # User
+ ##########
+
+ ##########
+ # GET
+ ##########
+ def get_online_user(self) -> json or None:
+ """
+ Return a list of current logged-in users in json format
+ Response data format is as follows:
+ [{"cmd" : "GetOnline","code" : 0,"value" : {
+ "User" : [{
+ "canbeDisconn" : 0,
+ "ip" : "192.168.1.100",
+ "level" : "admin",
+ "sessionId" : 1000,
+ "userName" : "admin"
+ }]
+ }
+ }]
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetOnline", "token": self.token}
+ body = [{"cmd": "GetOnline", "action": 1, "param": {}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not retrieve online user from camera. Status:", response.status_code)
+ return None
+ except Exception as e:
+ print("Could not get online user", e)
+ raise
+
+ def get_users(self) -> json or None:
+ """
+ Return a list of user accounts from the camera in json format
+ Response data format is as follows:
+ [{"cmd" : "GetUser","code" : 0,"initial" : {
+ "User" : {
+ "level" : "guest"
+ }},
+ "range" : {"User" : {
+ "level" : [ "guest", "admin" ],
+ "password" : {
+ "maxLen" : 31,
+ "minLen" : 6
+ },
+ "userName" : {
+ "maxLen" : 31,
+ "minLen" : 1
+ }}
+ },"value" : {
+ "User" : [
+ {
+ "level" : "admin",
+ "userName" : "admin"
+ }]
+ }
+ }]
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetUser", "token": self.token}
+ body = [{"cmd": "GetUser", "action": 1, "param": {}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not retrieve users from camera. Status:", response.status_code)
+ return None
+ except Exception as e:
+ print("Could not get users", e)
+ raise
+
+ ##########
+ # SET
+ ##########
+ def add_user(self, username: str, password: str, level: str = "guest") -> bool:
+ """
+ Add a new user account to the camera
+ :param username: The user's username
+ :param password: The user's password
+ :param level: The privilege level 'guest' or 'admin'. Default is 'guest'
+ :return: bool
+ """
+ try:
+ param = {"cmd": "AddUser", "token": self.token}
+ body = [{"cmd": "AddUser", "action": 0,
+ "param": {"User": {"userName": username, "password": password, "level": level}}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ r_data = json.loads(response.text)
+ if r_data["value"]["rspCode"] == "200":
+ return True
+ print("Could not add user. Camera responded with:", r_data["value"])
+ return False
+ print("Something went wrong. Could not add user. Status:", response.status_code)
+ return False
+ except Exception as e:
+ print("Could not add user", e)
+ raise
+
+ def modify_user(self, username: str, password: str) -> bool:
+ """
+ Modify the user's password by specifying their username
+ :param username: The user which would want to be modified
+ :param password: The new password
+ :return: bool
+ """
+ try:
+ param = {"cmd": "ModifyUser", "token": self.token}
+ body = [{"cmd": "ModifyUser", "action": 0, "param": {"User": {"userName": username, "password": password}}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ r_data = json.loads(response.text)
+ if r_data["value"]["rspCode"] == "200":
+ return True
+ print("Could not modify user:", username, "\nCamera responded with:", r_data["value"])
+ print("Something went wrong. Could not modify user. Status:", response.status_code)
+ return False
+ except Exception as e:
+ print("Could not modify user", e)
+ raise
+
+ def delete_user(self, username: str) -> bool:
+ """
+ Delete a user by specifying their username
+ :param username: The user which would want to be deleted
+ :return: bool
+ """
+ try:
+ param = {"cmd": "DelUser", "token": self.token}
+ body = [{"cmd": "DelUser", "action": 0, "param": {"User": {"userName": username}}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ r_data = json.loads(response.text)
+ if r_data["value"]["rspCode"] == "200":
+ return True
+ print("Could not delete user:", username, "\nCamera responded with:", r_data["value"])
+ return False
+ except Exception as e:
+ print("Could not delete user", e)
+ raise
+
+ ##########
+ # Image Data
+ ##########
+ def get_snap(self, timeout: int = 3) -> Image or None:
+ """
+ Gets a "snap" of the current camera video data and returns a Pillow Image or None
+ :param timeout: Request timeout to camera in seconds
+ :return: Image or None
+ """
+ try:
+
+ randomstr = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
+ snap = self.url + "?cmd=Snap&channel=0&rs=" \
+ + randomstr \
+ + "&user=" + self.username \
+ + "&password=" + self.password
+ 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))
+ print("Could not retrieve data from camera successfully. Status:", reader.status)
+ return None
+
+ except Exception as e:
+ print("Could not get Image data\n", e)
+ raise
+
+ #########
+ # Device
+ #########
+ def get_hdd_info(self) -> json or None:
+ """
+ Gets all HDD and SD card information from Camera
+ Response data format is as follows:
+ [{"cmd" : "GetHddInfo",
+ "code" : 0,
+ "value" : {
+ "HddInfo" : [{
+ "capacity" : 15181,
+ "format" : 1,
+ "id" : 0,
+ "mount" : 1,
+ "size" : 15181
+ }]
+ }
+ }]
+
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetHddInfo", "token": self.token}
+ body = [{"cmd": "GetHddInfo", "action": 0, "param": {}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not retrieve HDD/SD info from camera successfully. Status:", response.status_code)
+ return None
+ except Exception as e:
+ print("Could not get HDD/SD card information", e)
+ raise
+
+ def format_hdd(self, hdd_id: [int] = [0]) -> bool:
+ """
+ Format specified HDD/SD cards with their id's
+ :param hdd_id: List of id's specified by the camera with get_hdd_info api. Default is 0 (SD card)
+ :return: bool
+ """
+ try:
+ param = {"cmd": "Format", "token": self.token}
+ body = [{"cmd": "Format", "action": 0, "param": {"HddInfo": {"id": hdd_id}}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ r_data = json.loads(response.text)
+ if r_data["value"]["rspCode"] == "200":
+ return True
+ print("Could not format HDD/SD. Camera responded with:", r_data["value"])
+ return False
+ print("Could not format HDD/SD. Status:", response.status_code)
+ return False
+ except Exception as e:
+ print("Could not format HDD/SD", e)
+ raise
+
+ ###########
+ # Recording
+ ###########
+
+ ###########
+ # SET
+ ###########
+
+ ###########
+ # GET
+ ###########
+ def get_recording_encoding(self) -> json or None:
+ """
+ Get the current camera encoding settings for "Clear" and "Fluent" profiles.
+ Response data format is as follows:
+ [{
+ "cmd" : "GetEnc",
+ "code" : 0,
+ "initial" : {
+ "Enc" : {
+ "audio" : 0,
+ "channel" : 0,
+ "mainStream" : {
+ "bitRate" : 4096,
+ "frameRate" : 15,
+ "profile" : "High",
+ "size" : "3072*1728"
+ },
+ "subStream" : {
+ "bitRate" : 160,
+ "frameRate" : 7,
+ "profile" : "High",
+ "size" : "640*360"
+ }
+ }
+ },
+ "range" : {
+ "Enc" : [
+ {
+ "audio" : "boolean",
+ "mainStream" : {
+ "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ],
+ "default" : {
+ "bitRate" : 4096,
+ "frameRate" : 15
+ },
+ "frameRate" : [ 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "3072*1728"
+ },
+ "subStream" : {
+ "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ],
+ "default" : {
+ "bitRate" : 160,
+ "frameRate" : 7
+ },
+ "frameRate" : [ 15, 10, 7, 4 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "640*360"
+ }
+ },
+ {
+ "audio" : "boolean",
+ "mainStream" : {
+ "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ],
+ "default" : {
+ "bitRate" : 4096,
+ "frameRate" : 15
+ },
+ "frameRate" : [ 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "2592*1944"
+ },
+ "subStream" : {
+ "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ],
+ "default" : {
+ "bitRate" : 160,
+ "frameRate" : 7
+ },
+ "frameRate" : [ 15, 10, 7, 4 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "640*360"
+ }
+ },
+ {
+ "audio" : "boolean",
+ "mainStream" : {
+ "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ],
+ "default" : {
+ "bitRate" : 3072,
+ "frameRate" : 15
+ },
+ "frameRate" : [ 30, 22, 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "2560*1440"
+ },
+ "subStream" : {
+ "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ],
+ "default" : {
+ "bitRate" : 160,
+ "frameRate" : 7
+ },
+ "frameRate" : [ 15, 10, 7, 4 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "640*360"
+ }
+ },
+ {
+ "audio" : "boolean",
+ "mainStream" : {
+ "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ],
+ "default" : {
+ "bitRate" : 3072,
+ "frameRate" : 15
+ },
+ "frameRate" : [ 30, 22, 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "2048*1536"
+ },
+ "subStream" : {
+ "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ],
+ "default" : {
+ "bitRate" : 160,
+ "frameRate" : 7
+ },
+ "frameRate" : [ 15, 10, 7, 4 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "640*360"
+ }
+ },
+ {
+ "audio" : "boolean",
+ "mainStream" : {
+ "bitRate" : [ 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192 ],
+ "default" : {
+ "bitRate" : 3072,
+ "frameRate" : 15
+ },
+ "frameRate" : [ 30, 22, 20, 18, 16, 15, 12, 10, 8, 6, 4, 2 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "2304*1296"
+ },
+ "subStream" : {
+ "bitRate" : [ 64, 128, 160, 192, 256, 384, 512 ],
+ "default" : {
+ "bitRate" : 160,
+ "frameRate" : 7
+ },
+ "frameRate" : [ 15, 10, 7, 4 ],
+ "profile" : [ "Base", "Main", "High" ],
+ "size" : "640*360"
+ }
+ }
+ ]
+ },
+ "value" : {
+ "Enc" : {
+ "audio" : 0,
+ "channel" : 0,
+ "mainStream" : {
+ "bitRate" : 2048,
+ "frameRate" : 20,
+ "profile" : "Main",
+ "size" : "3072*1728"
+ },
+ "subStream" : {
+ "bitRate" : 64,
+ "frameRate" : 4,
+ "profile" : "High",
+ "size" : "640*360"
+ }
+ }
+ }
+ }]
+
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetEnc", "token": self.token}
+ body = [{"cmd": "GetEnc", "action": 1, "param": {"channel": 0}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not retrieve recording encoding data. Status:", response.status_code)
+ return None
+ except Exception as e:
+ print("Could not get recording encoding", e)
+ raise
+
+ def get_recording_advanced(self) -> json or None:
+ """
+ Get recording advanced setup data
+ Response data format is as follows:
+ [{
+ "cmd" : "GetRec",
+ "code" : 0,
+ "initial" : {
+ "Rec" : {
+ "channel" : 0,
+ "overwrite" : 1,
+ "postRec" : "15 Seconds",
+ "preRec" : 1,
+ "schedule" : {
+ "enable" : 1,
+ "table" : "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
+ }
+ }
+ },
+ "range" : {
+ "Rec" : {
+ "channel" : 0,
+ "overwrite" : "boolean",
+ "postRec" : [ "15 Seconds", "30 Seconds", "1 Minute" ],
+ "preRec" : "boolean",
+ "schedule" : {
+ "enable" : "boolean"
+ }
+ }
+ },
+ "value" : {
+ "Rec" : {
+ "channel" : 0,
+ "overwrite" : 1,
+ "postRec" : "15 Seconds",
+ "preRec" : 1,
+ "schedule" : {
+ "enable" : 1,
+ "table" : "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ }
+ }
+ }
+ }]
+
+ :return: json or None
+ """
+ try:
+ param = {"cmd": "GetRec", "token": self.token}
+ body = [{"cmd": "GetRec", "action": 1, "param": {"channel": 0}}]
+ response = Request.post(self.url, data=body, params=param)
+ if response.status_code == 200:
+ return json.loads(response.text)
+ print("Could not retrieve advanced recording. Status:", response.status_code)
+ 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 63dca91..1e8a52b 100644
--- a/Camera.py
+++ b/Camera.py
@@ -4,9 +4,20 @@ from APIHandler import APIHandler
class Camera(APIHandler):
def __init__(self, ip, username="admin", password="", https=False):
- APIHandler.__init__(self, ip, https=https)
+ """
+ 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"}, https=https)
+
+ # Normal call without proxy:
+ # APIHandler.__init__(self, ip, username, password)
+
self.ip = ip
self.username = username
self.password = password
- self.https = https
- super().login(self.username, self.password)
+ super().login()
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 5b1187a..a5433b1 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, verify=False, params=params, json=data, headers=headers)
- else:
- r = requests.post(url, verify=False, json=data)
+ r = requests.post(url, verify=False, 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, verify=False, params=params, timeout=timeout)
+ data = requests.get(url=url, verify=False, 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()