Getting Access to Zmodo Proprietry Cameras

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.

2 Likes

Just pasting the output from the script so far:

Following… I have zmodo as well and hate they’re proprietary. The removed RTSP shortly after I bought mine but I’d love to be able to view them through other methods.

You might has to use the device’s aes_key to decode the stream. On my brand of Zmodo cameras, I have 3 tcp ports open (21, 4444, 8000). I used Filezilla to ftp into the camera and have been looking at what I can use in there for this project. There were also 12 udp ports “open” but filtered. I used nmap to do an intense scan and that is what it returned.

Still no luck with this! It is now my weekend hackthon project.

However, I have implemented the following solution which I found on this forum but I could not get to work out of the box (Zmodo Cloud (non-NVR)). But here it is slightly modified and working. See steps below. This will depend on your upload and download speed. I have good internet so it is working well for me.

  1. Login to Zmodo web interface: https://user.zmodo.com/. Use your username and passwords then go to the list of cameras and click on one of them.

  2. Right click on the browser and click on inspect (or F12).

  3. Go to Network and Filter requests by Fetch/XHR

  4. Get the token like below

  5. In configuration.yaml, store the initial token in a cookie in HA (NOTE: your need to update it here if it ever expires by re-login). you will also need and the DevID and AES Key for each camera for Step 8.

# Set cookie
input_text:
  zmodo_cookie:
    name: Zmodo token cookie
    initial: "****87**********5f4d"
  1. Create a Shell Command in HA to constantly renew the token in configuration.yaml
shell_command:
  # Calls the refresh API with the cookie header
  refresh_zmodo_token: >-
    curl -s -X GET -H "Content-Type: application/json"
    -b "tokenid={{ states('input_text.zmodo_cookie') }}"
    "https://user.zmodo.com/api/refresh?tokenid={{ states('input_text.zmodo_cookie') }}"
  refresh_meshare_token: >-
    curl -s -X GET -H "Content-Type: application/json"
    -b "tokenid={{ states('input_text.zmodo_cookie') }}"
    "https://user.meshare.com/api/refresh?tokenid={{ states('input_text.zmodo_cookie') }}"
  1. Create an automation that will renew the token. You might have to add this in automations.yaml
- id: '1ZMODO169'
  alias: Refresh Zmodo Token
  description: ''
  triggers:
  - trigger: time_pattern
    minutes: /1
  conditions: []
  actions:
  - action: shell_command.refresh_zmodo_token
  - action: shell_command.refresh_meshare_token
  1. Create camera controllers in HA in configuration.yaml. The annoying :unamused: thing here is that you cannot set the value of these parameters at the top of the configuration. You will need to explicitly have them in the URLs.
camera:
  - platform: ffmpeg
    name: Back Yard
    input: >- 
      https://flv.meshare.com/live?devid=ZMD*********&token=****87**********5f4d&media_type=2&aes_key=095*********BAC1
  - platform: ffmpeg
    name: Veranda
    input: >- 
      https://flv.meshare.com/live?devid=ZMD*********&token=****87**********5f4d&media_type=2&aes_key=AB1*******79B1

more cameras
  1. Now add your cameras to your dashboard
show_state: true
show_name: true
camera_view: live
fit_mode: cover
type: picture-entity
entity: camera.front_door
camera_image: camera.front_door_camera
tap_action:
  action: more-info

  1. Enjoy! :grinning:

Bonus!
If you have Homekit Bridge setup you can view the cameras from your HomeKit App on Mac, iPhone and iPad.

Nice work! I created the other guide, but my latest comment says that the initial guide is now wrong because they changed all their API’s!

And, it is no longer necessary to do the https://user.zmodo.com/api/refresh?tokenid as that endpoint does not exist anymore.

Just grab the URL as you did, and paste it into the ffmpeg camera. It will automatically take a screenshot every 10 seconds and works well. I set my camera up on August 6, 2025, and my two cameras are still working without any token refresh (but this is because of the ffmpeg camera always doing something with the stream).

2 Likes

Great find! This is definitely getting added to my dashboard.

Yes, Thank you. I realised that it was you after I posted :smiley:

So I set this up over the weekend and can’t get the cameras to persist on the dashboard for more than an hour. Any secrets to your success?

This used to happen to me too. It continued to work when I used the input_text to store the token then do both CURLS on user.zmodo and user.meshare.But @iamdoubz thinks you can safely remove user.zmodo. I have not tried to remove it. Also make sure you have AES Keys in the URLs and make sure your automation script to renew the token is running.