6
\$\begingroup\$

I worked with IP cameras for some time and noticed that the camera uses a lot of processing power because of multiple connections. Writing a streaming server seemed to be a nice solution, so I decided to write my own. This code is posted to my GitHub page.

Main.py

import sys
import socket
import _thread
import time
import signal
from Libs import Connection

# Create socket and listen on port 5005
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(("", 5005))
server_socket.listen(5)

connections = []
opened_cameras = {}


def signal_handler(signal=None, frame=None):
    exit(0)

# Loop and check for new connections
while 1:
    try:
        client_socket, address = server_socket.accept()
        print
        "Conencted to - ", address, "\n"
        cam_url = client_socket.recv(1024)
        # if camera url does not exsists in oppened camera, open new connection,
        # or else just append client params and pass to Connection thread
        if cam_url not in opened_cameras:
            # opened_cameras.append(cam_url)
            client = Connection.Connection([client_socket, cam_url])
            opened_cameras[cam_url] = client
            _thread.start_new_thread(client.capture, (opened_cameras,))

        else:
            opened_cameras[cam_url].addConnection(client_socket)
        connections.append([client_socket, cam_url])

    except socket.timeout:
        continue
    except KeyboardInterrupt:
        server_socket.close()

        del connections
        exit(0)

Libs/Connection.py

import cv2
import socket
import signal

class Connection(object):

    def __init__(self, connections):
        print(connections)
        self.url = connections[1].decode("utf-8")
        print(self.url)
        self.socket = []
        self.socket.append(connections[0])
        self.connections = connections
        # signal.signal(signal.SIGINT, signal_handler)
        self.connect()
        pass



    def connect(self):
        self.connection = cv2.VideoCapture(self.url)
        return self.connection

    def addConnection(self, client):
        self.socket.append(client)

    def capture(self, opened_cameras):
        self.opened_cameras = opened_cameras
        while 1:
            try:
                ret, frame = self.connection.read()
                data = cv2.imencode('.jpg', frame)[1].tostring()
                if len(self.socket):
                    for c in self.socket:
                        self.send(c,data)
                else:
                    self.connection.release()
                    del self.opened_cameras[self.connections[1]]
                    exit(0)

                    # self.connections[1].close()
            except KeyboardInterrupt:
                self.signal_handler()

    def send(self,c, data):
        try:
            c.send(data)
            c.send(b"END!") # send param to end loop in client
        except socket.error:
            self.socket.remove(c)

This will check for user to connect and if same camera url is passed as parameter, Connection class will handle passing captured images to users that request from that camera.

client/recv.py

#!/usr/bin/python
import socket
import cv2
import numpy
import random
import sys

host = sys.argv[1] # e.g. localhost, 192.168.1.123
cam_url = sys.argv[2] # rtsp://user:pass@url/live.sdp , http://url/video.mjpg ...
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
client_socket.connect((host, 5005))
name = str(random.random()) # gives random name to create window

client_socket.send(str.encode(cam_url))

def rcv():
    data = b''
    while 1:
        
        try:
            r = client_socket.recv(90456)
            if len(r) == 0:
                exit(0)
            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)
    if type(frame) is type(None):
        pass
    else:
        try:
            cv2.imshow(name,frame)
            if cv2.waitKey(10) == ord('q'):
                client_socket.close()
                sys.exit()
        except:
            client_socket.close()
            exit(0)

while 1:
    rcv()

A unique window name is required because there will not be two windows if two clients are opened from the same PC. CPU usage is very low. On my PC, CPU usage is 4% when server has a connection, and on every other connection which uses the same params, there is no update in CPU usage.

Basic server usage:

python3 Main.py

Client usage:

cd client
python3 recv.py localhost rtsp://192.168.1.123/live2.sdp

Params for recv.py are

  1. Host where server is running
  2. Camera video address
\$\endgroup\$
2
  • \$\begingroup\$ How do you see the results? what about cannot connect to X server? how about no matches found error? \$\endgroup\$ Commented Apr 5, 2018 at 16:08
  • \$\begingroup\$ Make sure that you are using python3 and have opencv and other imports installed. Once you start server, start the client where 1st param is host (eg localhost,192.168.1.123...) and the second is the actual path to ip camera stream (mjpg,rtsp...). Results are displayed in client as window displaying live stream. Cannot connect to X server error? Are you ssh to client or server script? If that's the case, use export DISPLAY=:0 (or what your display is) before running. And I dont understand last part, no matches found? Did you mean no found for host info? Just provide right host address. \$\endgroup\$ Commented Apr 8, 2018 at 6:52

1 Answer 1

1
\$\begingroup\$

Documentation

The PEP 8 style guide recommends adding docstrings for classes and functions. Also, Main.py could have a docstring at the top of the code summarizing its purpose. For example:

"""
Video streaming server and client.

As in the text of your question, specify the problem
this code solves.
"""

Exit

The code uses inconsistent ways to exit. I recommend using sys.exit() everywhere.

It is good that you close sockets before exiting. However, I think it it not necessary to use del on variables before doing so. This implies that the code will continue, but it does not.

Tools

You could run code development tools to automatically find some style issues with your code.

ruff identifies several unused imports:

import signal
import time

It also identifies this:

E722 Do not use bare `except`
 |
 |                 client_socket.close()
 |                 sys.exit()
 |         except:
 |         ^^^^^^ E722
 |             client_socket.close()
 |             exit(0)

While loop

It is more common to use the boolean True value instead of 1 in infinite loops:

while True

Simpler

There is no need for pass in the __init__ function. It can be deleted.

In this line:

class Connection(object):

object is not needed and can be deleted:

class Connection():

This code:

if type(frame) is type(None):
    pass
else:
    try:

can be simplified as:

if type(frame) is not type(None):
    try:

Comments

Delete commented-put code to reduce clutter:

# signal.signal(signal.SIGINT, signal_handler)

# self.connections[1].close()

Naming

The PEP 8 style guide recommends snake_case for function and variable names.

addConnection would be add_connection

rcv is a cryptic name for a function. It would be better to spell the word out and to specify what is being received. For example, receive_video.

In the rcv function, the variable name a is not very descriptive. Give it a name that describes what it represents.

Magic numbers

In code like this:

client_socket.connect((host, 5005))

r = client_socket.recv(90456)

you could assign the numbers to named constants to make the code more meaningful.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.