Is MP4 audio-video muxing possible on the OpenMV N6 (firmware v5.0.0)?

Hello,

I am currently using the OpenMV N6 running firmware v5.0.0, and I have a question regarding audio-video recording capabilities.

I can already record MJPEG video separately using the built-in mjpeg module, and capture raw PCM audio separately using the audio module. Both work as expected on their own.

However, I would like to combine both audio and video into a single playable MP4 (or another standard container) file directly on the N6 board, rather than doing post-processing on a computer.

I noticed that the product page states the N6 has a hardware H.264 encoder and supports MP4 recording. I also found the VC8000 H.264 driver source code in the official GitHub repository under drivers/vc8000/src/h264/. However, when I tried import h264 in my MicroPython script, the module was not found.

So I would like to ask:

  1. Is there currently a built-in MicroPython API or module to mux audio and video into a single MP4 file directly on the N6?
  2. Is the hardware H.264 encoder accessible via Python in the current v5.0.0 official firmware?
  3. If on-device MP4 muxing is not yet supported, what is the officially recommended workflow to produce a video file with synchronized audio?

Thank you very much for your help.

Hi, we will be adding support for H.264 output before the end of the year. If you want to rush ahead of us, you may edit the firmware yourself. An AI coder can probably get you most of what you need rather quickly. I am focused on other tasks right now than H.264 support.

Please see the docs for how to build the firmware. As for making H.264 work… it should be straightforward from the C API. The only thing to worry about is that you will need to inject cache cleaning operations into the VENC driver, as it doesn’t include these.

The AI coder will likely get everything right except the need to add these into the library. I added the required one for JPEG support. However, for H.264 support, you have to use different paths.

Thank you very much for your reply. I have one follow-up question:

Putting aside the H.264 encoding topic for now, I would like to know if it is possible with the current firmware to generatean MP4 file stored on the SD card by muxingseparately recorded MJPEG video and raw PCM audio together. I don’t specifically require H.264 compression; Ijust want to combine the existing MJPEG video track and PCM audio track into a single MP4 container file directly on the device. Is this functionality achievable with the current firmware?Has anyone already implemented this module?

I currently handle this merging process on my PC using FFmpeg.Is there any way to port FFmpeg to the OpenMV N6 board to complete this task directly.

My AI developer told me this cannot be done.

The N6 cannot run desktop applications, so you’d just want to create two separate files for right now and merge them on the PC.

Putting aside the H.264 encoding topic for now, I would like to know if it is possible with the current firmware to generatean MP4 file stored on the SD card by muxingseparately recorded MJPEG video and raw PCM audio together. I don’t specifically require H.264 compression; Ijust want to combine the existing MJPEG video track and PCM audio track into a single MP4 container file directly on the device. Is this functionality achievable with the current firmware?Has anyone already implemented this module?

Ask your AI to write the code to output an AVI file. AVI is Audio Video Interleave. Its job is to explicitly do this type of stuff. Then for the audio bit, you want to record the audio part as PCM audio. You should be able to record JPEG video and PCM audio in an AVI container.

Claude came up with this:

# avi_recorder.py - MJPEG + PCM audio AVI recorder for the OpenMV N6
#
# Records camera frames as Motion-JPEG video and the on-board MEMS
# microphone as signed-16-bit PCM audio, interleaved into a single
# standard RIFF/AVI file that plays in VLC, ffmpeg, QuickTime, etc.
#
# Tested against the OpenMV N6 board interface:
#   https://docs.openmv.io/v5.0.0/openmvcam/quickref/openmv-n6.html
#
# --------------------------------------------------------------------------
# Quick start
# --------------------------------------------------------------------------
#   import csi, time
#   from avi_recorder import AviWriter
#
#   cam = csi.CSI()
#   cam.reset()
#   cam.pixformat(csi.RGB565)      # writer JPEG-compresses each frame
#   cam.framesize(csi.QVGA)        # 320x240
#
#   avi = AviWriter("recording.avi", 320, 240, quality=90,
#                   audio_frequency=16000, audio_channels=1)
#
#   clock = time.clock()
#   for _ in range(300):           # ~record until you decide to stop
#       clock.tick()
#       avi.add_frame(cam.snapshot())
#
#   avi.close()                    # finalizes header + index, stops audio
#
# Or as a context manager:
#   with AviWriter("clip.avi", 320, 240) as avi:
#       for _ in range(300):
#           avi.add_frame(cam.snapshot())
#
# --------------------------------------------------------------------------
# Notes
# --------------------------------------------------------------------------
# * Video FPS is *measured* from wall-clock time between the first and last
#   frame, then written back into the header on close(). You do not need to
#   know the frame rate in advance.
# * Audio is captured asynchronously via audio.start_streaming(); buffers are
#   queued and flushed (interleaved) just before each video frame is written,
#   so A/V stays roughly in sync. Any tail audio is flushed on close().
# * add_frame() accepts either an OpenMV image.Image (RGB565/GRAYSCALE - it is
#   JPEG compressed for you) or a bytes/bytearray of an already-encoded JPEG
#   (e.g. when the sensor is configured for csi.JPEG pixformat: pass
#   img.bytearray()).
# * Only 16-bit PCM is emitted (that is what the audio module produces).
 
import struct
import time
 
try:
    import audio
except ImportError:  # allows importing the module on a board without audio
    audio = None
 
 
class AviWriter:
    # AVIMAINHEADER.dwFlags
    _AVIF_HASINDEX = 0x00000010
    _AVIF_ISINTERLEAVED = 0x00000100
    # AVIINDEXENTRY.dwFlags
    _AVIIF_KEYFRAME = 0x00000010
 
    def __init__(self, path, width, height, quality=90,
                 record_audio=True, audio_channels=1,
                 audio_frequency=16000, audio_gain_db=24):
        self.path = path
        self.width = int(width)
        self.height = int(height)
        self.quality = int(quality)
 
        self.record_audio = bool(record_audio) and (audio is not None)
        self.audio_channels = int(audio_channels)
        self.audio_frequency = int(audio_frequency)
        self.audio_gain_db = int(audio_gain_db)
        self._bits = 16
        self._block_align = self.audio_channels * (self._bits // 8)
        self._avg_bps = self.audio_frequency * self._block_align
 
        # running state
        self.frames = 0
        self._audio_bytes = 0
        self._index = []          # list of (fourcc, flags, movi_offset, size)
        self._movi_off = 0        # bytes written into the movi payload so far
        self._audio_q = []        # queued PCM buffers from the streaming cb
        self._max_jpeg = 0
        self._max_audio = 0
        self._t0 = None
        self._t1 = None
        self._closed = False
 
        # Open file and reserve the (fixed-size) preamble. It is rewritten
        # with the final rates/counts in close().
        self._f = open(path, "wb")
        self._preamble_len = self._write_preamble(0.0, 0, 0)
        self._movi_data_start = self._f.tell()
 
        if self.record_audio:
            audio.init(channels=self.audio_channels,
                       frequency=self.audio_frequency,
                       gain_db=self.audio_gain_db)
            audio.start_streaming(self._audio_cb)
 
    # ------------------------------------------------------------------ #
    # Public API
    # ------------------------------------------------------------------ #
    def add_frame(self, img, quality=None):
        """Append one video frame. `img` is an OpenMV image (compressed here)
        or bytes/bytearray of an already-encoded JPEG."""
        if self._closed:
            raise OSError("AviWriter is closed")
 
        now = time.ticks_us()
        if self._t0 is None:
            self._t0 = now
        self._t1 = now
 
        if isinstance(img, (bytes, bytearray, memoryview)):
            data = img
        else:
            q = self.quality if quality is None else int(quality)
            # compress() JPEG-encodes the frame in place and returns it.
            data = img.compress(quality=q).bytearray()
 
        # Interleave: flush audio captured up to this point, then the frame.
        self._flush_audio()
        self._write_chunk(b"00dc", data, keyframe=True)
        self.frames += 1
        n = len(data)
        if n > self._max_jpeg:
            self._max_jpeg = n
 
    def close(self):
        """Stop audio, flush, write the index, patch the header, close file."""
        if self._closed:
            return
        self._closed = True
 
        if self.record_audio:
            try:
                audio.stop_streaming()
            except Exception:
                pass
            self._flush_audio()   # drain any tail buffers
 
        self._write_index()
        self._finalize()
        self._f.close()
 
    # context manager sugar
    def __enter__(self):
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
        return False
 
    # ------------------------------------------------------------------ #
    # Audio capture
    # ------------------------------------------------------------------ #
    def _audio_cb(self, pcmbuf):
        # The driver reuses pcmbuf, so copy the bytes out immediately.
        self._audio_q.append(bytes(pcmbuf))
 
    def _flush_audio(self):
        if not self._audio_q:
            return
        # Detach the queue so a concurrent callback appends to a fresh list.
        q = self._audio_q
        self._audio_q = []
        for buf in q:
            n = len(buf)
            if not n:
                continue
            self._write_chunk(b"01wb", buf, keyframe=False)
            self._audio_bytes += n
            if n > self._max_audio:
                self._max_audio = n
 
    # ------------------------------------------------------------------ #
    # RIFF chunk writing
    # ------------------------------------------------------------------ #
    def _write_chunk(self, fourcc, data, keyframe):
        n = len(data)
        # idx1 offset is relative to the 'movi' FOURCC (which sits 4 bytes
        # before the first payload chunk), hence +4.
        off = self._movi_off + 4
        f = self._f
        f.write(fourcc)
        f.write(struct.pack("<I", n))
        f.write(data)
        pad = n & 1               # chunks are word-aligned
        if pad:
            f.write(b"\x00")
        self._index.append(
            (fourcc, self._AVIIF_KEYFRAME if keyframe else 0, off, n))
        self._movi_off += 8 + n + pad
 
    def _write_index(self):
        f = self._f
        f.write(b"idx1")
        f.write(struct.pack("<I", 16 * len(self._index)))
        for fourcc, flags, off, size in self._index:
            f.write(fourcc)
            f.write(struct.pack("<III", flags, off, size))
 
    # ------------------------------------------------------------------ #
    # Header (preamble) - fixed size, written twice (placeholder + final)
    # ------------------------------------------------------------------ #
    def _finalize(self):
        file_size = self._f.tell()
        movi_list_size = 4 + self._movi_off      # 'movi' fourcc + payload
        riff_size = file_size - 8
 
        if self.frames >= 2 and self._t0 is not None:
            secs = time.ticks_diff(self._t1, self._t0) / 1000000.0
            fps = (self.frames - 1) / secs if secs > 0 else 30.0
        else:
            fps = 30.0
 
        self._f.seek(0)
        self._write_preamble(fps, movi_list_size, riff_size)
        self._f.seek(file_size)
 
    def _write_preamble(self, fps, movi_list_size, riff_size):
        hdrl = self._build_hdrl(fps)
        buf = bytearray()
        # RIFF header
        buf += b"RIFF" + struct.pack("<I", riff_size) + b"AVI "
        # hdrl LIST
        buf += b"LIST" + struct.pack("<I", len(hdrl) + 4) + b"hdrl" + hdrl
        # movi LIST header (payload follows this preamble)
        buf += b"LIST" + struct.pack("<I", movi_list_size) + b"movi"
        self._f.write(buf)
        return len(buf)
 
    def _build_hdrl(self, fps):
        streams = 2 if self.record_audio else 1
        micro = int(round(1000000.0 / fps)) if fps > 0 else 33333
        rate = int(round(fps * 1000)) if fps > 0 else 30000
        scale = 1000
        max_bytes_per_sec = (self._avg_bps +
                             int(self._max_jpeg * (fps if fps > 0 else 30)))
        suggested = max(self._max_jpeg, self._max_audio, 1)
 
        # --- avih (AVIMAINHEADER, 56 bytes body) ---
        avih_body = struct.pack(
            "<14I",
            micro,                       # dwMicroSecPerFrame
            max_bytes_per_sec,           # dwMaxBytesPerSec
            0,                           # dwPaddingGranularity
            self._AVIF_HASINDEX | self._AVIF_ISINTERLEAVED,  # dwFlags
            self.frames,                 # dwTotalFrames
            0,                           # dwInitialFrames
            streams,                     # dwStreams
            suggested,                   # dwSuggestedBufferSize
            self.width,                  # dwWidth
            self.height,                 # dwHeight
            0, 0, 0, 0)                  # dwReserved[4]
        avih = b"avih" + struct.pack("<I", len(avih_body)) + avih_body
 
        # --- video stream list (vids / MJPG) ---
        vid_strh_body = struct.pack(
            "<4s4sI2H8I4h",
            b"vids", b"MJPG",
            0,                           # dwFlags
            0, 0,                        # wPriority, wLanguage
            0,                           # dwInitialFrames
            scale,                       # dwScale
            rate,                        # dwRate  (rate/scale = fps)
            0,                           # dwStart
            self.frames,                 # dwLength (frames)
            max(self._max_jpeg, 1),      # dwSuggestedBufferSize
            0,                           # dwQuality
            0,                           # dwSampleSize (0 = variable)
            0, 0, self.width, self.height)   # rcFrame
        vid_strh = b"strh" + struct.pack("<I", len(vid_strh_body)) + vid_strh_body
 
        vid_strf_body = struct.pack(
            "<IiiHH4sIiiII",
            40,                          # biSize
            self.width,                  # biWidth
            self.height,                 # biHeight
            1,                           # biPlanes
            24,                          # biBitCount
            b"MJPG",                     # biCompression
            self.width * self.height * 3,  # biSizeImage
            0, 0,                        # biX/YPelsPerMeter
            0, 0)                        # biClrUsed, biClrImportant
        vid_strf = b"strf" + struct.pack("<I", len(vid_strf_body)) + vid_strf_body
 
        vid_strl_body = b"strl" + vid_strh + vid_strf
        vid_strl = b"LIST" + struct.pack("<I", len(vid_strl_body)) + vid_strl_body
 
        hdrl = avih + vid_strl
 
        # --- audio stream list (auds / PCM) ---
        if self.record_audio:
            aud_blocks = self._audio_bytes // self._block_align
            aud_strh_body = struct.pack(
                "<4s4sI2H8I4h",
                b"auds", b"\x00\x00\x00\x00",
                0,                       # dwFlags
                0, 0,                    # wPriority, wLanguage
                0,                       # dwInitialFrames
                self._block_align,       # dwScale
                self._avg_bps,           # dwRate (rate/scale = samples/sec)
                0,                       # dwStart
                aud_blocks,              # dwLength (in blocks/samples)
                max(self._max_audio, 1),  # dwSuggestedBufferSize
                0,                       # dwQuality
                self._block_align,       # dwSampleSize
                0, 0, 0, 0)              # rcFrame (unused for audio)
            aud_strh = b"strh" + struct.pack("<I", len(aud_strh_body)) + aud_strh_body
 
            aud_strf_body = struct.pack(
                "<HHIIHH",
                1,                       # wFormatTag = WAVE_FORMAT_PCM
                self.audio_channels,     # nChannels
                self.audio_frequency,    # nSamplesPerSec
                self._avg_bps,           # nAvgBytesPerSec
                self._block_align,       # nBlockAlign
                self._bits)              # wBitsPerSample
            aud_strf = b"strf" + struct.pack("<I", len(aud_strf_body)) + aud_strf_body
 
            aud_strl_body = b"strl" + aud_strh + aud_strf
            aud_strl = b"LIST" + struct.pack("<I", len(aud_strl_body)) + aud_strl_body
            hdrl += aud_strl
 
        return hdrl

Which you then use like:

import csi, time
from avi_recorder import AviWriter

cam = csi.CSI()
cam.reset()
cam.pixformat(csi.RGB565)     # writer JPEG-compresses each frame
cam.framesize(csi.QVGA)       # 320x240

avi = AviWriter("recording.avi", 320, 240, quality=90,
                audio_frequency=16000, audio_channels=1)

record_secs = 10
target_fps = 30
frame_ms = 1000 // target_fps

start = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start) < record_secs * 1000:
    t = time.ticks_ms()
    avi.add_frame(cam.snapshot())
    dt = time.ticks_diff(time.ticks_ms(), t)
    if dt < frame_ms:
        time.sleep_ms(frame_ms - dt)

avi.close()                   # finalizes header + index, stops audio

A few design points worth flagging:

FPS is measured, not assumed. add_frame() timestamps with time.ticks_us(), and close() seeks back and rewrites the fixed-size preamble with the real frame rate, total frame count, and audio length — so you don’t have to declare a frame rate up front and it stays honest even if capture rate drifts.

Audio comes in through audio.start_streaming(self._audio_cb); the callback copies each buffer (bytes(pcmbuf), since the driver reuses it) into a queue, and the queue is flushed as 01wb chunks immediately before each 00dc video chunk. That keeps the interleave roughly time-ordered, and any tail buffers are drained after stop_streaming() in close(). Video length is derived from wall-clock time while audio length comes from the actual sample count, so the two durations line up.

add_frame() takes either an image.Image (RGB565/GRAYSCALE — it calls img.compress(quality=...).bytearray() for you) or raw JPEG bytes.

The index (idx1) is written with offsets relative to the movi FOURCC and AVIIF_KEYFRAME set on every JPEG frame, which is what makes it seekable in VLC/QuickTime rather than just linearly playable.

One thing to sanity-check on real hardware: I stubbed the audio module for the host test, so confirm your firmware build’s exact audio API (audio.init(channels=, frequency=, gain_db=) / start_streaming / stop_streaming) matches — the docs snippet I pulled shows exactly that signature, but if stop_streaming is named differently in v5.0.0 the close() call is the only spot to adjust.

Sources: OpenMV N6 board interface

recording.zip (20.9 MB)

Thanks for your reply, this is very helpful. Video recording runs smoothly. I am currently attempting to transfer files directly to my computer using the Wi-Fi module. I have encountered an issue though: the Wi-Fi connection works normally using raw TCP, but the connection cannot be established when I switch to HTTP. Could you help me troubleshoot this?

import csi, time, os
import network, socket
from avi_recorder import AviWriter
from machine import LED

# ============================================================
# CONFIGURATION – fill in your actual values
# ============================================================
OUTPUT_FILE   = "/sdcard/hd_record.avi"   # Output file path on SD card
RECORD_SECS   = 15                        # Recording duration (seconds)
QUALITY       = 70                        # JPEG quality 1-100 (lower = smaller file, higher fps)

WIFI_SSID     = "YOUR_WIFI_SSID"          # 2.4 GHz Wi‑Fi SSID
WIFI_PASS     = "YOUR_WIFI_PASSWORD"      # Wi‑Fi password
PC_IP         = "YOUR_PC_IP"              # IPv4 address of the receiving PC
PC_PORT       = 12345                     # TCP port the PC listener is bound to
# ============================================================

# ============================================================
# Onboard RGB LED status definitions
# ============================================================
led_red   = LED("LED_RED")
led_green = LED("LED_GREEN")
led_blue  = LED("LED_BLUE")

def led(state):
    """Switch LEDs according to system state for quick visual feedback."""
    led_red.off(); led_green.off(); led_blue.off()
    if state == "ready":  led_green.on()                     # Green – ready
    elif state == "rec":  led_red.on()                       # Red – recording
    elif state == "wifi": led_green.on(); led_blue.on()      # Cyan – Wi‑Fi / sending
    elif state == "done": led_blue.on()                      # Blue – transfer finished
    elif state == "err":  led_red.on(); led_green.on()       # Yellow (red+green) – error

# ============================================================
# 1. Camera initialisation (no audio yet)
# ============================================================
led("ready")
cam = csi.CSI()
cam.reset()
cam.pixformat(csi.RGB565)          # 16-bit colour
cam.framesize(csi.HD)              # 1280×720
cam.snapshot(time=2000)            # 2-second warm‑up to stabilise auto exposure / white balance

# ============================================================
# 2. Countdown (audio not started yet to avoid capturing silence)
# ============================================================
print("Starting in 2 seconds...")
time.sleep(2)                      # Give the operator a moment to prepare
print("===== Recording started =====")

# ============================================================
# 3. Create AVI writer (audio stream starts now, in sync with video)
# ============================================================
try:
    avi = AviWriter(OUTPUT_FILE, width=1280, height=720,
                    quality=QUALITY, audio_frequency=16000, audio_channels=1)
except Exception as e:
    print("AVI creation failed:", e)
    led("err")
    while True: time.sleep(1)      # Halt with red+green LED, wait for reset

led("rec")                         # Recording indicator
frame_cnt = 0
start_ms = time.ticks_ms()

# Capture continuously without artificial delays to maximise throughput
while time.ticks_diff(time.ticks_ms(), start_ms) < RECORD_SECS * 1000:
    avi.add_frame(cam.snapshot())  # Grab a frame; audio is interleaved automatically
    frame_cnt += 1

# Calculate actual frame rate
actual_ms = time.ticks_diff(time.ticks_ms(), start_ms)
actual_fps = frame_cnt / (actual_ms / 1000) if actual_ms > 0 else 15

# ============================================================
# 4. Finalise recording – write true frame rate for perfect A/V sync
# ============================================================
avi.set_fps(actual_fps)            # Must be called, otherwise header defaults to 30 fps
avi.close()                        # Stop audio, write index, patch headers

print(f"Actual FPS: {actual_fps:.1f}, total frames: {frame_cnt}")
print(f"Recording duration: {actual_ms/1000:.1f}s (playback will match)")

# ============================================================
# 5. File verification – make sure the AVI was written to the SD card
# ============================================================
try:
    if "hd_record.avi" not in os.listdir("/sdcard"):
        print("Error: file not created")
        led("err")
        while True: time.sleep(1)
    file_size = os.stat(OUTPUT_FILE)[6]   # Get file size in bytes
    print(f"File size: {file_size} bytes")
except Exception as e:
    print("File check failed:", e)
    led("err")
    while True: time.sleep(1)

# ============================================================
# 6. Reliable send helper – guarantees all bytes are transmitted
# ============================================================
def safe_send(sock, data):
    total = len(data)
    sent = 0
    while sent < total:
        n = sock.send(data[sent:])
        if n == 0:
            raise OSError("Connection closed")
        sent += n
    return sent

# ============================================================
# 7. Connect to Wi‑Fi (2.4 GHz)
# ============================================================
led("wifi")
print("Connecting to Wi‑Fi...")
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASS)

wifi_cnt = 0
while not wlan.isconnected() and wifi_cnt < 20:   # Timeout after ~10 seconds
    time.sleep_ms(500)
    print(".", end="")
    wifi_cnt += 1

if not wlan.isconnected():
    print("\nWi‑Fi connection failed. File remains on SD card – transfer it manually.")
    led("err")
    while True: time.sleep(1)

print(f"\nWi‑Fi connected. Device IP: {wlan.ifconfig()[0]}")
print(f"Connecting to PC {PC_IP}:{PC_PORT} ...")

# ============================================================
# 8. Transfer the AVI file to the PC via raw TCP
#     The PC must be running the receiver script on the same port
# ============================================================
try:
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.settimeout(30)                           # Longer timeout for large files
    client.connect((PC_IP, PC_PORT))
    print("Connected to PC. Starting file transfer.")

    # Send file metadata: filename length (1 byte) + filename + file size (4 bytes, little-endian)
    file_name = OUTPUT_FILE.split("/")[-1]
    name_bytes = file_name.encode("utf-8")
    safe_send(client, len(name_bytes).to_bytes(1, "little"))
    safe_send(client, name_bytes)
    safe_send(client, file_size.to_bytes(4, "little"))

    # Send file contents in 4 KB chunks
    sent_total = 0
    chunk_size = 4096
    with open(OUTPUT_FILE, "rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            safe_send(client, chunk)
            sent_total += len(chunk)

    # Final integrity check
    if sent_total != file_size:
        raise Exception(f"Byte mismatch: sent {sent_total}, expected {file_size}")

    client.close()
    led("done")
    print(f"✅ Transfer complete. {sent_total} bytes sent, file intact.")

except Exception as e:
    print(f"Transfer failed: {e}")
    print("File remains on SD card – transfer it manually.")
    led("err")

# Idle forever
while True:
    time.sleep(1)
HTTP
import csi, time, os, gc
import network, socket
from avi_recorder import AviWriter
from machine import LED

# ============================================================
# CONFIGURATION – fill in your actual values
# ============================================================
OUTPUT_FILE   = "/sdcard/hd_record.avi"   # File path on SD card
RECORD_SECS   = 6                         # Recording duration (seconds)
QUALITY       = 70                        # JPEG quality 1-100 (lower = smaller file, higher fps)

WIFI_SSID     = "YOUR_WIFI_SSID"          # 2.4 GHz Wi‑Fi name
WIFI_PASS     = "YOUR_WIFI_PASSWORD"      # Wi‑Fi password
SERVER_URL    = "http://YOUR_PC_IP:12345/upload"  # HTTP upload endpoint (http:// only)
API_KEY       = ""                        # Bearer token if required, otherwise leave empty
# ============================================================

# ============================================================
# Onboard RGB LED status definitions
# ============================================================
led_red   = LED("LED_RED")
led_green = LED("LED_GREEN")
led_blue  = LED("LED_BLUE")

def led(state):
    """Set LED colours for quick visual feedback of system state."""
    led_red.off(); led_green.off(); led_blue.off()
    if state == "ready":   led_green.on()                     # Green – ready
    elif state == "rec":   led_red.on()                       # Red – recording
    elif state == "wifi":  led_green.on(); led_blue.on()      # Cyan – Wi‑Fi / uploading
    elif state == "done":  led_blue.on()                      # Blue – success
    elif state == "err":   led_red.on(); led_green.on()       # Yellow (red+green) – error

# ============================================================
# 1. Camera setup (no audio yet)
# ============================================================
led("ready")
cam = csi.CSI()
cam.reset()
cam.pixformat(csi.RGB565)          # 16-bit colour
cam.framesize(csi.HD)              # 1280×720
cam.snapshot(time=2000)            # 2-second warm‑up to stabilise exposure

# ============================================================
# 2. Countdown before recording
# ============================================================
print("Starting in 2 seconds...")
time.sleep(2)
print("===== Recording started =====")

# ============================================================
# 3. Create AVI writer (audio starts now) and record
# ============================================================
try:
    avi = AviWriter(OUTPUT_FILE, width=1280, height=720,
                    quality=QUALITY, audio_frequency=16000, audio_channels=1)
except Exception as e:
    print("AVI creation failed:", e)
    led("err")
    while True: time.sleep(1)

led("rec")
frame_cnt = 0
start_ms = time.ticks_ms()

while time.ticks_diff(time.ticks_ms(), start_ms) < RECORD_SECS * 1000:
    avi.add_frame(cam.snapshot())
    frame_cnt += 1

actual_ms = time.ticks_diff(time.ticks_ms(), start_ms)
actual_fps = frame_cnt / (actual_ms / 1000) if actual_ms > 0 else 15
avi.set_fps(actual_fps)   # write true frame rate to header → perfect A/V sync
avi.close()               # stop audio, finalise index and file headers

print(f"Actual FPS: {actual_fps:.1f}, total frames: {frame_cnt}")
print(f"Recording duration: {actual_ms/1000:.1f}s")

# ============================================================
# 4. Verify the file exists on SD card
# ============================================================
try:
    if "hd_record.avi" not in os.listdir("/sdcard"):
        print("Error: file not created")
        led("err")
        while True: time.sleep(1)
    file_size = os.stat(OUTPUT_FILE)[6]
    print(f"File size: {file_size} bytes")
except Exception as e:
    print("File check failed:", e)
    led("err")
    while True: time.sleep(1)

# ============================================================
# 5. Wi‑Fi connection (with memory cleanup and retry)
# ============================================================
led("wifi")
print("Connecting to Wi‑Fi...")

gc.collect()                # free memory before network stack initialisation
time.sleep(0.5)

wlan = network.WLAN(network.STA_IF)
wlan.active(False)          # reset interface
time.sleep_ms(200)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASS)

# First attempt – wait up to 15 seconds
wifi_cnt = 0
while not wlan.isconnected() and wifi_cnt < 30:
    time.sleep_ms(500)
    print(".", end="")
    wifi_cnt += 1

# Retry once if first attempt fails
if not wlan.isconnected():
    print("\nFirst attempt failed, retrying after 2s...")
    time.sleep(2)
    gc.collect()
    wlan.active(False)
    time.sleep_ms(200)
    wlan.active(True)
    wlan.connect(WIFI_SSID, WIFI_PASS)
    retry_cnt = 0
    while not wlan.isconnected() and retry_cnt < 30:
        time.sleep_ms(500)
        print(".", end="")
        retry_cnt += 1

    if not wlan.isconnected():
        print("\nWi‑Fi connection failed. File remains on SD card.")
        led("err")
        while True: time.sleep(1)

print(f"\nWi‑Fi connected. Device IP: {wlan.ifconfig()[0]}")
gc.collect()  # another cleanup before the upload

# ============================================================
# 6. Streaming HTTP upload (avoids loading the whole file into RAM)
# ============================================================
print(f"Uploading to {SERVER_URL} ...")

# ----- Parse the server URL -----
url = SERVER_URL.replace("http://", "")
if "/" in url:
    host_port, path = url.split("/", 1)
    path = "/" + path
else:
    host_port, path = url, "/"
if ":" in host_port:
    host, port_str = host_port.split(":", 1)
    port = int(port_str)
else:
    host, port = host_port, 80

# ----- Build multipart/form-data envelope -----
boundary = "----N6Up"
head = (
    f"--{boundary}\r\n"
    'Content-Disposition: form-data; name="file"; filename="hd_record.avi"\r\n'
    "Content-Type: application/octet-stream\r\n\r\n"
).encode()
tail = f"\r\n--{boundary}--\r\n".encode()
body_len = len(head) + file_size + len(tail)

header = (
    f"POST {path} HTTP/1.1\r\n"
    f"Host: {host}\r\n"
    f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
    f"Content-Length: {body_len}\r\n"
)
if API_KEY:
    header += f"Authorization: Bearer {API_KEY}\r\n"
header += "Connection: close\r\n\r\n"

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(30)
    sock.connect((host, port))

    # Send headers and multipart head
    sock.send(header.encode())
    sock.send(head)

    # Send file in 4 KB chunks (never loads the entire file into memory)
    with open(OUTPUT_FILE, "rb") as f:
        while True:
            chunk = f.read(4096)
            if not chunk:
                break
            sock.send(chunk)

    # Finalise multipart body
    sock.send(tail)

    # Read server response
    response = b""
    try:
        while True:
            chunk = sock.recv(1024)
            if not chunk:
                break
            response += chunk
    except:
        pass
    sock.close()

    # Check if the server signalled success (optional)
    if b"200" in response or b"ok" in response:
        led("done")
        print("✅ Upload successful")
    else:
        print("Server response:", response[:200])
        led("err")
except Exception as e:
    print(f"Upload failed: {e}")
    print("File remains on SD card – transfer it manually.")
    led("err")

# Keep the board idle after finishing
while True:
time.sleep(1)

Hi Kui,

HTTP runs on top of TCP — the connect() call in both of your scripts is doing exactly the same thing. So if the raw-TCP script connects and the HTTP script doesn’t, the difference isn’t the protocol, it’s what’s listening on the PC. Check these first:

  1. Is your HTTP server actually running when the camera connects? Your raw-TCP test used a custom receiver script on port 12345 — the HTTP test needs a different server program listening on that same port.
  2. Is it bound to 0.0.0.0 and not 127.0.0.1/localhost? A server bound to localhost accepts connections only from the PC itself; the camera will get “connection refused.”
  3. Windows Firewall prompts per-program. Your TCP receiver may have been allowed already while the new HTTP server got silently blocked. Allow Python through the firewall for private networks (or temporarily disable it to test).
  4. Note: python -m http.server won’t work — it only serves GET requests, it can’t accept uploads.

Here’s a minimal PC-side upload server that matches your camera script (save as upload_server.py, run with python upload_server.py):slight_smile:

from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers["Content-Length"])
        data = self.rfile.read(length)
        # Strip the multipart envelope to recover the raw AVI bytes
        boundary = self.headers["Content-Type"].split("boundary=")[1].encode()
        start = data.find(b"\r\n\r\n") + 4
        end = data.rfind(b"\r\n--" + boundary + b"--")
        with open("hd_record.avi", "wb") as f:
            f.write(data[start:end])
        print(f"Received {end - start} bytes")
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"ok")

print("Listening on 0.0.0.0:12345 ...")
HTTPServer(("0.0.0.0", 12345), Handler).serve_forever()

A few fixes for your camera script too:

1. Use safe_send() for the HTTP upload as well. Your raw-TCP script correctly loops until all bytes are sent, but the HTTP script uses bare sock.send(), which is allowed to send fewer bytes than you gave it. On a large file you’ll eventually get a short send and a corrupted upload. Replace every sock.send(...) in section 6 with the same safe_send(sock, ...) helper from your TCP script.

2. Resolve the address with getaddrinfo(). It’s more portable and lets you use hostnames, not just IPs:

addr = socket.getaddrinfo(host, port)[0][-1]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(30)
sock.connect(addr)

3. Fix the indentation at the very end of the script — the last two lines as posted are:

while True:
time.sleep(1)

time.sleep(1) needs to be indented, otherwise the script dies with an IndentationError before it even starts (if this is just a copy-paste artifact, ignore this one).

With the server above running and the firewall open, your script should work — I’d suggest testing connectivity first with a short recording (RECORD_SECS = 2) so you’re not waiting on a big file each attempt.

kwagyeman,Thank you very much for your help!!! I have completed the first step as required`.# Fixed PC HTTP receiver service, listening on 0.0.0.0:12346, compatible with OpenMV upload format

Save this file as upload_server.py, run via command: python upload_server.py

from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
def do_POST(self):
try:
# Read total request payload length
content_len = int(self.headers["Content-Length"])
full_req = self.rfile.read(content_len)

        # Extract multipart boundary from request header
        boundary = self.headers["Content-Type"].split("boundary=")[1].encode()

        # Strip multipart wrapper headers and trailers, extract raw AVI binary data
        data_start = full_req.find(b"\r\n\r\n") + 4
        data_end = full_req.rfind(b"\r\n--" + boundary + b"--")
        video_raw = full_req[data_start:data_end]

        # Write binary video stream to local file
        with open("hd_record.avi", "wb") as f_out:
            f_out.write(video_raw)

        print(f"✅ Video received successfully, payload bytes: {len(video_raw)}")
        # Respond 200 OK to OpenMV device
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"ok")

    except Exception as err:
        print("Error parsing incoming request:", err)
        self.send_response(400)
        self.end_headers()

if name == "main":
# Critical fix: address tuple format (bind_address, port)
listen_addr = ("0.0.0.0", 12346)
http_server = HTTPServer(listen_addr, Handler)

print("📡 HTTP upload server started, listening on 0.0.0.0:12346")
print("⚠️ Allow private network access in firewall; disable old TCP receiver script")
http_server.serve_forever()`

the recorded code in the IDE # OpenMV N6 A/V recording → HTTP chunked streaming upload (Port updated to 12346)

# Workflow: Record AVI -> Save to SD Card -> WiFi STA -> HTTP POST multipart upload
# Dependency: avi_recorder.py in the same directory
import csi, time, os, gc
import network, socket
from avi_recorder import AviWriter
from machine import LED

# ============================================================
# Configuration Section - Modify according to your environment
# ============================================================
OUTPUT_FILE   = "/sdcard/hd_record.avi"
RECORD_SECS   = 180       # Use shorter duration for testing; extend after debugging
QUALITY       = 70

WIFI_SSID     = "YOUR_WIFI_NAME"
WIFI_PASS     = "YOUR_WIFI_PASSWORD"

# Upload target: PC IP + Port, server root path /
PC_HOST       = "YOUR_PC_IP"
PC_PORT       = 12346   # Port matched with HTTP server
API_KEY       = ""
BOUNDARY      = b"----OpenMVN6Upload" # Must match parser boundary on server side
CHUNK_SIZE    = 4096                  # Read/write chunk buffer size
# ============================================================

# ============================================================
# LED Status Indicator Control
# ============================================================
led_red   = LED("LED_RED")
led_green = LED("LED_GREEN")
led_blue  = LED("LED_BLUE")

def led(state):
    led_red.off(); led_green.off(); led_blue.off()
    if state == "ready":  led_green.on()
    elif state == "rec":  led_red.on()
    elif state == "wifi": led_green.on(); led_blue.on()
    elif state == "done": led_blue.on()
    elif state == "err":  led_red.on()

# ============================================================
# Reliable transmission wrapper; all socket writes use this function
# ============================================================
def safe_send(sock, data):
    total_len = len(data)
    send_ptr = 0
    while send_ptr < total_len:
        sent = sock.send(data[send_ptr:])
        if sent <= 0:
            raise OSError("TCP connection lost, transmission failed")
        send_ptr += sent
    return send_ptr

# ============================================================
# 1. Camera initialization and warm-up
# ============================================================
led("ready")
cam = csi.CSI()
cam.reset()
cam.pixformat(csi.RGB565)
cam.framesize(csi.HD)
cam.snapshot(time=2000)

# ============================================================
# 2. Recording countdown
# ============================================================
print("Recording starts in 2 seconds...")
time.sleep(2)
print("===== START RECORDING =====")

# ============================================================
# 3. AVI recording, print instantaneous frame rate per frame
# ============================================================
try:
    avi = AviWriter(OUTPUT_FILE, width=1280, height=720,
                    quality=QUALITY, audio_frequency=16000, audio_channels=1)
except Exception as e:
    print("Failed to create AVI file:", e)
    led("err")
    while True:
        time.sleep(1)

led("rec")
frame_cnt = 0
start_ms = time.ticks_ms()
last_frame_tick = time.ticks_ms()

# Recording loop, output instantaneous FPS for each captured frame
while time.ticks_diff(time.ticks_ms(), start_ms) < RECORD_SECS * 1000:
    avi.add_frame(cam.snapshot())
    frame_cnt += 1
    # Calculate instantaneous frame rate
    curr_tick = time.ticks_ms()
    frame_gap = time.ticks_diff(curr_tick, last_frame_tick)
    instant_fps = 1000 / frame_gap if frame_gap > 0 else 0
    print(f"Instant FPS:{instant_fps:.1f} | Frames captured:{frame_cnt}")
    last_frame_tick = curr_tick

# Finalize recording, write actual FPS into file header
actual_ms = time.ticks_diff(time.ticks_ms(), start_ms)
actual_fps = frame_cnt / (actual_ms / 1000) if actual_ms > 0 else 15
avi.set_fps(actual_fps)
avi.close()
gc.collect()

print("\n===== RECORD SUMMARY =====")
print(f"Average FPS:{actual_fps:.1f} Total frames:{frame_cnt}")
print(f"Recording duration:{actual_ms/1000:.1f}s")

# ============================================================
# 4. SD card file integrity verification
# ============================================================
try:
    file_list = os.listdir("/sdcard")
    if "hd_record.avi" not in file_list:
        print("Error: Recorded file not found on SD card")
        led("err")
        while True:
            time.sleep(1)
    file_size = os.stat(OUTPUT_FILE)[6]
    print(f"AVI file total bytes:{file_size}")
except Exception as e:
    print("File validation error:", e)
    led("err")
    while True:
        time.sleep(1)

# ============================================================
# 5. WiFi STA connection (hardware reset to reduce connection failures)
# ============================================================
led("wifi")
print("Connecting to WiFi...")
wlan = network.WLAN(network.STA_IF)
# Reset WiFi to release resources, fixes repeated connection failures
wlan.active(False)
time.sleep_ms(800)
wlan.active(True)
wlan.connect(WIFI_SSID, WIFI_PASS)

wifi_cnt = 0
while not wlan.isconnected() and wifi_cnt < 30: # Max wait ~15 seconds
    time.sleep_ms(500)
    print(".", end="")
    wifi_cnt += 1

if not wlan.isconnected():
    print("\nWiFi connection failed, video remains stored on SD card")
    led("err")
    while True:
        time.sleep(1)

print(f"\nWiFi connected, device IP:{wlan.ifconfig()[0]}")

# ============================================================
# 6. HTTP chunked streaming upload (fixed multi-line syntax issue)
# ============================================================
def upload_avi_stream(host, port, file_path, timeout=60):
    # Resolve address via getaddrinfo
    addr_info = socket.getaddrinfo(host, port)[0][-1]
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(timeout)
    sock.connect(addr_info)

    file_name = file_path.split("/")[-1]

    # Build multipart header, avoid MicroPython multi-line bracket syntax error
    preamble_text = "--" + BOUNDARY.decode() + "\r\n"
    preamble_text += 'Content-Disposition: form-data; name="file"; filename="%s"\r\n' % file_name
    preamble_text += "Content-Type: video/x-msvideo\r\n\r\n"
    preamble = preamble_text.encode("utf-8")

    epilogue_text = "\r\n--" + BOUNDARY.decode() + "--\r\n"
    epilogue = epilogue_text.encode("utf-8")

    # Calculate total body length
    f_stat = os.stat(file_path)
    total_body_len = len(preamble) + f_stat[6] + len(epilogue)

    # Construct HTTP request header line by line to avoid MicroPython syntax bugs
    http_header = b"POST / HTTP/1.1\r\n"
    http_header += b"Host: %s\r\n" % host.encode()
    http_header += b"Content-Type: multipart/form-data; boundary=%s\r\n" % BOUNDARY
    http_header += b"Content-Length: %d\r\n" % total_body_len
    http_header += b"Connection: close\r\n\r\n"

    safe_send(sock, http_header)
    safe_send(sock, preamble)

    # Read and transmit SD file in chunks
    with open(file_path, "rb") as f:
        while True:
            chunk = f.read(CHUNK_SIZE)
            if not chunk:
                break
            safe_send(sock, chunk)
            gc.collect()

    safe_send(sock, epilogue)

    resp_buf = b""
    while True:
        recv_data = sock.recv(1024)
        if not recv_data:
            break
        resp_buf += recv_data
    sock.close()
    gc.collect()
    return resp_buf

# ============================================================
# 7. Execute upload procedure
# ============================================================
print(f"Start HTTP push to {PC_HOST}:{PC_PORT}")
try:
    server_resp = upload_avi_stream(PC_HOST, PC_PORT, OUTPUT_FILE)
    print("Raw server response:", server_resp[:300].decode("utf-8", "ignore"))
    if b"200" in server_resp:
        led("done")
        print("✅ HTTP file upload completed successfully!")
    else:
        raise Exception("Server did not return 200 OK status")
except Exception as e:
    print(f"HTTP upload failed: {e}")
    print("AVI file saved locally on SD card, you can copy it manually")
    led("err")

# Idle infinite loop after workflow completes
while True:
    time.sleep(1)

Finally, I also have a question: the transfer method is extremely slow – it takes over 20 minutes to transfer a 3‑minute video. Is there something wrong with the way I wrote it?

The code I shared this time isn’t polished. I’ll send you a standardized version tomorrow for your review.

Hi, you can use this script to test transfer performance: micropython-lib/python-ecosys/iperf3/iperf3.py at master · micropython/micropython-lib · GitHub

You can run it on linux and micropython. With it you can test max transfer speeds. Yes, how you write code matters. Note that TCP performance isn’t extremely high out of the box as it requires massive LWIP buffers.