I regrettably have 8 Zmodo cameras with an NVR but they were dirty cheap when I bought them. The model is NP-NJ18-S. I have never been able to integrate them. Although I did try the cloud integration from this post here Zmodo Cloud (non-NVR). However, the cloud integration stops after 24 hours. Even if you try to automate the token refresh. The stream is also slow going through the cloud first even though you are at home.
So I started digging old posts and searching the internet for anything with local integration. I am surprised that no one has been able to get into the ZMODO cameras or integrate them directly. I think this mainly because they use a proprietary protocol called ZSP. I ran into this project Zmodopipe project while I was watching this video https://www.youtube.com/watch?v=3L9_in6LciY&t=46s. I gave this a try and quickly learnt it is not going to work. So …
I started re-writing the code in Python. With help from Wireshark and ChatGPT, I am now able to read the stream directly from the cameras. Below is my rough code for one camera. You will notice that I have some hardcoded Hex values. With Wireshark and Zviewer, I discovered that all my cameras are actually accessible directly in the local network. I also discovered that all cameras are using Username: admin and Password: 111111 if you have not set one for them. So you can hardcode that in the hex. I basically copied the values from Wirshark and it worked!! I also scanned the NVR and Cameras IP addresses for opened ports and found mine only has 8000 opened. So, this is also hardcoded for now.
import os
import socket
import time
from time import sleep
IP = "192.168.1.x"
PORT = 8000
def connect():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((IP, PORT))
print("âś… Connected")
return sock
def send_packet(sock, data, label):
try:
sock.send(data)
print(f" Sent {len(data)} bytes - {label}")
time.sleep(0.2)
except BrokenPipeError:
print(f" {label}: Broken pipe (remote closed connection)")
def recv_response(sock, label):
try:
response = sock.recv(1024)
if response:
print(f" {label}: {len(response)} bytes")
print(" ".join(f"{b:02x}" for b in response))
else:
print(f" {label}: No data received")
except Exception as e:
print(f" {label}: {e}")
def output_to_file():
global data, e
sps = b'\x67\x42\xC0\x1F\xE5\x88\x80\x50\x05\xBA\x13\x00\x00\x03\x00\x02\x00\x00\x03\x00\x32\x0F\x18\x31\x96'
pps = b'\x68\xCE\x06\xE2'
nal_prefix = b'\x00\x00\x00\x01'
try:
with open("/tmp/output_with_sps_pps.h264", "wb") as out_file:
seen_idr = False
while True:
data = sock.recv(4096)
time.sleep(0.2)
if data:
print(" ".join(f"{b:02x}" for b in data))
# Check if the data contains an IDR NAL unit (nal_type == 5)
# NAL start prefix can be 0x00000001 or 0x000001
if not seen_idr:
for i in range(len(data) - 4):
if data[i:i + 4] == b'\x00\x00\x00\x01' or data[i:i + 3] == b'\x00\x00\x01':
nal_start = i + (4 if data[i:i + 4] == b'\x00\x00\x00\x01' else 3)
nal_type = data[nal_start] & 0x1F
if nal_type == 5:
out_file.write(nal_prefix + sps)
out_file.write(nal_prefix + pps)
seen_idr = True
break
out_file.write(data)
except Exception as e:
print(f" Error while receiving stream: {e}")
finally:
sock.close()
def pipe_output():
global data, e
fifo_path = "/tmp/zmodo0"
# Create FIFO pipe (only if it doesn't already exist)
if not os.path.exists(fifo_path):
os.mkfifo(fifo_path, 0o666)
print(f"FIFO created at {fifo_path}")
else:
print(f"FIFO already exists at {fifo_path}")
# Open the FIFO for writing (this will block until a reader opens it)
print("⏳ Waiting for a reader (e.g. ffplay, ZoneMinder)...")
with open(fifo_path, "wb") as fifo:
print("📡 Writing to FIFO...")
try:
while True:
data = sock.recv(4096)
time.sleep(0.2)
if not data:
print("Stream ended or socket closed by camera.")
break
fifo.write(data)
fifo.flush()
print(f"{len(data)} bytes written to FIFO")
except Exception as e:
print(f"Error during stream: {e}")
finally:
sock.close()
print("Socket closed")
if __name__ == "__main__":
sock = connect()
# Handshake packets from 157cam/157cam2 PCAP
packet1 = bytes.fromhex("5555aaaa0000000000003696")
packet2 = bytes.fromhex("5555aaaa000400000000369630393539443041333242433934324332393045373841393933343432424143310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
packet3 = bytes.fromhex("5555aaaa000000000000a290")
packet4 = bytes.fromhex("5555aaaa040000000000a29000000000")
login = bytes.fromhex("5555aaaa200000000000009a61646d696e000000000000000000000031313131313100000000000000000000")
send_packet(sock, login, "Login")
recv_response(sock, "Login Response")
send_packet(sock, packet1, "Handshake 1")
recv_response(sock, "Initial Response")
send_packet(sock, packet2, "Handshake 2")
recv_response(sock, "Handshake 2 response")
send_packet(sock, packet3, "Handshake 3")
recv_response(sock, "Handshake 3 response")
# send_packet(sock, packet4, "Handshake 4")
# recv_response(sock, "Stream Response")
print("Handshake complete, starting to receive stream data...")
output_to_file()
I have created two methods to handle the received stream. One for storing it to file and one for piping it to a FIFO pipe so another service can consume it. I have one issue which I would like to get help with. The stream coming from the camera is encoded for Zmodo and I am not able to play it back using FFMPEG. I would love to get help here if possible.
Next Step will be:
- Decode the Zmodo stream
- Get to code to work for multiple cameras
- Use Zoneminder to read the stream pipe
- Integrate Zoneminder with Home Assistant to show the stream from the cameras locally
I will keep plugging away but any suggestions of improvements, please feel free to reply or send me private messages.