Skip to content

Commit 5d03c62

Browse files
committed
adding blocking and non-blocking rtsp stream
This might fix the stream errors in issue #25 Also refactored the code a bit since we do not want to display a window from within the API (offload to the person implementing it).
1 parent 25ce2cb commit 5d03c62

File tree

5 files changed

+191
-32
lines changed

5 files changed

+191
-32
lines changed

RtspClient.py

+78-14
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import os
2+
from threading import ThreadError
3+
24
import cv2
35

6+
from util import threaded
7+
48

59
class RtspClient:
10+
"""
11+
Inspiration from:
12+
- https://benhowell.github.io/guide/2015/03/09/opencv-and-web-cam-streaming
13+
- https://stackoverflow.com/questions/19846332/python-threading-inside-a-class
14+
- https://stackoverflow.com/questions/55828451/video-streaming-from-ip-camera-in-python-using-opencv-cv2-videocapture
15+
"""
616

7-
def __init__(self, ip, username, password, port=554, profile="main", use_udp=True, **kwargs):
17+
def __init__(self, ip, username, password, port=554, profile="main", use_udp=True, callback=None, **kwargs):
818
"""
19+
RTSP client is used to retrieve frames from the camera in a stream
920
1021
:param ip: Camera IP
1122
:param username: Camera Username
@@ -15,33 +26,86 @@ def __init__(self, ip, username, password, port=554, profile="main", use_udp=Tru
1526
:param use_upd: True to use UDP, False to use TCP
1627
:param proxies: {"host": "localhost", "port": 8000}
1728
"""
29+
self.capture = None
30+
self.thread_cancelled = False
31+
self.callback = callback
32+
1833
capture_options = 'rtsp_transport;'
1934
self.ip = ip
2035
self.username = username
2136
self.password = password
2237
self.port = port
2338
self.proxy = kwargs.get("proxies")
2439
self.url = "rtsp://" + self.username + ":" + self.password + "@" + \
25-
self.ip + ":" + str(self.port) + "//h264Preview_01_" + profile
40+
self.ip + ":" + str(self.port) + "//h264Preview_01_" + profile
2641
if use_udp:
2742
capture_options = capture_options + 'udp'
2843
else:
2944
capture_options = capture_options + 'tcp'
3045

3146
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = capture_options
3247

33-
def preview(self):
34-
""" Blocking function. Opens OpenCV window to display stream. """
35-
win_name = self.ip
36-
cap = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG)
37-
ret, frame = cap.read()
48+
# opens the stream capture, but does not retrieve any frames yet.
49+
self._open_video_capture()
50+
51+
def _open_video_capture(self):
52+
# To CAP_FFMPEG or not To ?
53+
self.capture = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG)
54+
55+
def _stream_blocking(self):
56+
while True:
57+
try:
58+
if self.capture.isOpened():
59+
ret, frame = self.capture.read()
60+
if ret:
61+
yield frame
62+
else:
63+
print("stream closed")
64+
self.capture.release()
65+
return
66+
except Exception as e:
67+
print(e)
68+
self.capture.release()
69+
return
70+
71+
@threaded
72+
def _stream_non_blocking(self):
73+
while not self.thread_cancelled:
74+
try:
75+
if self.capture.isOpened():
76+
ret, frame = self.capture.read()
77+
if ret:
78+
self.callback(frame)
79+
else:
80+
print("stream is closed")
81+
self.stop_stream()
82+
except ThreadError as e:
83+
print(e)
84+
self.stop_stream()
3885

39-
while ret:
40-
cv2.imshow(win_name, frame)
86+
def stop_stream(self):
87+
self.capture.release()
88+
self.thread_cancelled = True
4189

42-
ret, frame = cap.read()
43-
if (cv2.waitKey(1) & 0xFF == ord('q')):
44-
break
90+
def open_stream(self):
91+
"""
92+
Opens OpenCV Video stream and returns the result according to the OpenCV documentation
93+
https://docs.opencv.org/3.4/d8/dfe/classcv_1_1VideoCapture.html#a473055e77dd7faa4d26d686226b292c1
94+
95+
:param callback: The function to callback the cv::mat frame to if required to be non-blocking. If this is left
96+
as None, then the function returns a generator which is blocking.
97+
"""
4598

46-
cap.release()
47-
cv2.destroyAllWindows()
99+
# Reset the capture object
100+
if self.capture is None or not self.capture.isOpened():
101+
self._open_video_capture()
102+
103+
print("opening stream")
104+
105+
if self.callback is None:
106+
return self._stream_blocking()
107+
else:
108+
# reset the thread status if the object was not re-created
109+
if not self.thread_cancelled:
110+
self.thread_cancelled = False
111+
return self._stream_non_blocking()

api/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .APIHandler import APIHandler
22

3-
__version__ = "0.0.4"
3+
__version__ = "0.0.5"
44
VERSION = __version__

api/recording.py

+19-17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
class RecordingAPIMixin:
1111
"""API calls for recording/streaming image or video."""
12+
1213
def get_recording_encoding(self) -> object:
1314
"""
1415
Get the current camera encoding settings for "Clear" and "Fluent" profiles.
@@ -53,34 +54,35 @@ def set_recording_encoding(self,
5354
body = [{"cmd": "SetEnc",
5455
"action": 0,
5556
"param":
56-
{"Enc":
57-
{"audio": audio,
58-
"channel": 0,
59-
"mainStream": {
60-
"bitRate": main_bit_rate,
61-
"frameRate": main_frame_rate,
62-
"profile": main_profile,
63-
"size": main_size},
64-
"subStream": {
65-
"bitRate": sub_bit_rate,
66-
"frameRate": sub_frame_rate,
67-
"profile": sub_profile,
68-
"size": sub_size}}
69-
}}]
57+
{"Enc":
58+
{"audio": audio,
59+
"channel": 0,
60+
"mainStream": {
61+
"bitRate": main_bit_rate,
62+
"frameRate": main_frame_rate,
63+
"profile": main_profile,
64+
"size": main_size},
65+
"subStream": {
66+
"bitRate": sub_bit_rate,
67+
"frameRate": sub_frame_rate,
68+
"profile": sub_profile,
69+
"size": sub_size}}
70+
}}]
7071
return self._execute_command('SetEnc', body)
7172

7273
###########
7374
# RTSP Stream
7475
###########
75-
def open_video_stream(self, profile: str = "main", proxies=None) -> None:
76+
def open_video_stream(self, callback=None, profile: str = "main", proxies=None):
7677
"""
7778
'https://support.reolink.com/hc/en-us/articles/360007010473-How-to-Live-View-Reolink-Cameras-via-VLC-Media-Player'
79+
Blocking function creates a generator and returns the frames as it is spawned
7880
:param profile: profile is "main" or "sub"
7981
:param proxies: Default is none, example: {"host": "localhost", "port": 8000}
8082
"""
8183
rtsp_client = RtspClient(
82-
ip=self.ip, username=self.username, password=self.password, proxies=proxies)
83-
rtsp_client.preview()
84+
ip=self.ip, username=self.username, password=self.password, proxies=proxies, callback=callback)
85+
return rtsp_client.open_stream()
8486

8587
def get_snap(self, timeout: int = 3, proxies=None) -> Image or None:
8688
"""

examples/streaming_video.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import cv2
2+
3+
from Camera import Camera
4+
5+
6+
def non_blocking():
7+
print("calling non-blocking")
8+
9+
def inner_callback(img):
10+
cv2.imshow("name", maintain_aspect_ratio_resize(img, width=600))
11+
print("got the image non-blocking")
12+
key = cv2.waitKey(1)
13+
if key == ord('q'):
14+
cv2.destroyAllWindows()
15+
exit(1)
16+
17+
c = Camera("192.168.1.112", "admin", "jUa2kUzi")
18+
# t in this case is a thread
19+
t = c.open_video_stream(callback=inner_callback)
20+
21+
print(t.isAlive())
22+
while True:
23+
if not t.isAlive():
24+
print("continuing")
25+
break
26+
# stop the stream
27+
# client.stop_stream()
28+
29+
30+
def blocking():
31+
c = Camera("192.168.1.112", "admin", "jUa2kUzi")
32+
# stream in this case is a generator returning an image (in mat format)
33+
stream = c.open_video_stream()
34+
35+
# using next()
36+
# while True:
37+
# img = next(stream)
38+
# cv2.imshow("name", maintain_aspect_ratio_resize(img, width=600))
39+
# print("got the image blocking")
40+
# key = cv2.waitKey(1)
41+
# if key == ord('q'):
42+
# cv2.destroyAllWindows()
43+
# exit(1)
44+
45+
# or using a for loop
46+
for img in stream:
47+
cv2.imshow("name", maintain_aspect_ratio_resize(img, width=600))
48+
print("got the image blocking")
49+
key = cv2.waitKey(1)
50+
if key == ord('q'):
51+
cv2.destroyAllWindows()
52+
exit(1)
53+
54+
55+
# Resizes a image and maintains aspect ratio
56+
def maintain_aspect_ratio_resize(image, width=None, height=None, inter=cv2.INTER_AREA):
57+
# Grab the image size and initialize dimensions
58+
dim = None
59+
(h, w) = image.shape[:2]
60+
61+
# Return original image if no need to resize
62+
if width is None and height is None:
63+
return image
64+
65+
# We are resizing height if width is none
66+
if width is None:
67+
# Calculate the ratio of the height and construct the dimensions
68+
r = height / float(h)
69+
dim = (int(w * r), height)
70+
# We are resizing width if height is none
71+
else:
72+
# Calculate the ratio of the 0idth and construct the dimensions
73+
r = width / float(w)
74+
dim = (width, int(h * r))
75+
76+
# Return the resized image
77+
return cv2.resize(image, dim, interpolation=inter)
78+
79+
80+
# Call the methods. Either Blocking (using generator) or Non-Blocking using threads
81+
# non_blocking()
82+
blocking()

util.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from threading import Thread
2+
3+
4+
def threaded(fn):
5+
def wrapper(*args, **kwargs):
6+
thread = Thread(target=fn, args=args, kwargs=kwargs)
7+
thread.daemon = True
8+
thread.start()
9+
return thread
10+
11+
return wrapper

0 commit comments

Comments
 (0)