Two cameras, two files, one program: the plan is sound, and so is the usual failure mode. The first camera opens and frames reach its file; start the second while the first still runs, and it throws a device-not-accessible error, or the first stream freezes. The instinct is to blame the file writer. The writer is almost never the cause: an error raised when the second device opens means the camera, its driver or the shared link refused the second stream. This guide shows how to confirm that in two minutes, how to wire the host so both cameras get real bandwidth, and the code pattern — one handle, one queue and one writer thread per camera — that records both streams to separate files in a single process.
Why the second camera refuses to open
Record each camera on its own before anything else. If either fails alone, fix that first; everything below assumes both pass solo. When both work alone but not together, three causes cover nearly every case, in this order of probability:
- Both capture objects target the same device. Two objects built on the same index or serial mean the second open hits a camera that already holds an exclusive handle, and the library reports the device as busy or inaccessible — the same story as Camera device already in use: one object, two buttons.
- The second open cannot reserve link bandwidth. A host reserves each camera's share of the link when the stream starts. Two cameras on one controller mean the second reservation fails while the first runs, and the tell is that start order decides: whichever camera starts second is the one that fails (USB hub drops camera frame rate: budget the bandwidth first).
- The driver allows one stream per device class. Some capture drivers accept a single active stream no matter how the objects are arranged. The fix is a different backend or SDK layer, not different application code; GenTL-style libraries solve it with one acquisition object per camera, as Multiple GenTL cameras in one process: one acquisition object each shows.
The two-minute isolation test: set both cameras to log to memory only and start them in that state. If the second camera still refuses to start, no file code is involved — work down the list above. An error raised at open or start is the device or driver refusing the second stream; the writer never ran.
What you need
A working rig needs four things, none of them the writer:
- Two cameras with distinct device indices or serials, confirmed by an enumeration print from your capture library.
- Two usable link paths on the host: separate root USB controllers, or one NIC port per GigE camera. Ports behind one external hub count as a single path.
- Short cables with strain relief at the camera end.
- A recorder disk whose sustained write speed clears the sum of both stream rates, measured over a full run rather than claimed on the box.
The host matters more than usual in this rig: you want two separate controllers, or a free slot for an expansion card. The industrial mini PC collection is one place to source a fanless recorder for the line.
The worked example below budgets a 12 MP USB3 camera at 4000 × 3000 and 30 fps:
- Sensor format
- 1/1.7"
- Interface
- USB3
- Resolution
- 12 MP
- Shutter
- Rolling
For bench wiring, keep both runs as short as the rig allows:
Budget the link bandwidth first
Every capture device reserves its share of the link when it opens, so the wiring decides success before the first frame is written. Three planning numbers cover most rigs:
- USB 3.0: a 5 Gbit/s port moves about 400 MB/s of payload on a real host. Line coding takes a fifth of the raw 5 Gbit/s, which leaves 500 MB/s, and packet overhead eats the rest. Plan on 400 MB/s per root controller.
- GigE: one 1 Gbit/s port carries 125 MB/s of line rate, and protocol overhead leaves about 118 MB/s of payload with jumbo frames enabled. Two cameras share a port only while their combined rate stays under that, and a switch adds no capacity.
- Hubs: every camera behind one hub shares the hub's upstream link, whatever the downstream ports promise.
The rule that decides the wiring: the sum of both stream rates must fit the slowest shared link with headroom. Compute it from delivered width, height, pixel depth and frame rate — measured values, not a snapshot mode. How many USB cameras one controller can feed before drops runs the same budget for bigger fleets.
Worked example: two 12 MP USB3 cameras at 30 fps on one host
Inputs, with their sources:
- Resolution and frame rate: 4000 × 3000 px and 30 fps, as the camera carded above delivers.
- Pixel depth: 8-bit mono at 1 byte per pixel, the assumption for this example; a 10- or 12-bit camera multiplies every line below by 1.25 or 1.5.
- Link: one shared USB 3.0 root controller at 400 MB/s usable, the planning figure from the previous section.
The rule in words: multiply delivered pixels by bytes per pixel for frame size, by frame rate for one camera's stream rate, double it for the pair, then subtract from the usable link rate:
frame size = 4000 px × 3000 px × 1 B = 12,000,000 B = 12.0 MB
one camera = 12.0 MB × 30 frames/s = 360 MB/s
pair demand = 2 × 360 MB/s = 720 MB/s
margin = 400 MB/s - 720 MB/s = -320 MB/s
The pair demands 1.8 times what one controller can move, so the second camera will not open while the first streams — the exact symptom we started from. Three decisions follow:
- Split the controllers. One camera per root controller puts each at 360 MB/s against 400 MB/s usable: 10% headroom. That runs, but it is thin, and no hub may sit in either path.
- Cut the data. Halve the frame rate to 15 fps and the pair drops to 360 MB/s total, still 90% of one controller. Add 2×2 binning as well and the pair falls to 180 MB/s, 45% of the controller — comfortable, traded against resolution.
- Change the link. 360 MB/s is three times a GigE port's 118 MB/s, so this pair never fits GigE at full rate. Full-rate pairs belong on 10GigE or CoaXPress, and the area scan cameras shop filters by interface.
Decision for this case: two dedicated controllers at 30 fps when the resolution matters; one controller at 15 fps with binning when it does not.
Open, record and close each camera on its own thread
- Open each camera alone and measure. Grab 10 s from camera A, release it, then camera B. Write down the delivered frame rate of each run; that measured number, not a data-sheet maximum, drives the writer.
- Assign distinct handles. Print the device list and bind each camera by serial or port path. Identical twin cameras that report the same name are told apart by port, as Two identical USB cameras: same name, open one by port shows.
- Separate the link paths. One camera per root USB controller, or one NIC port each, per the budget above.
- Give each camera its own queue and writer thread. Depth of at least three frames (Camera streaming buffer count: why three frames beat one), and never one shared queue — interleaved frames cannot be told apart afterwards.
- Start one, confirm frames, then start the other. Run the sequence once more in reverse order: order should not matter, and if it does, the bandwidth budget is the suspect again.
- Stop in order. Signal the grabbers to end, drain each queue into its writer, release the writer, then release the camera.
The pattern below uses OpenCV's documented capture and writer calls with standard-library threads. Vendor SDKs differ in the calls but not in the shape: one acquisition object, one queue, one writer per camera. The values are illustrative — set yours to the real device indices, delivered frame size and measured rate.
import cv2
import queue
import threading
# Illustrative values: set yours to real device index, delivered frame size and measured rate.
DEVICES = [(0, 'cam0.avi'), (1, 'cam1.avi')] # (device index, output file) per camera
FRAME_SIZE = (4000, 3000) # (width, height) as each camera delivers
FPS_OUT = 30.0 # rate measured in step 1
def record(index: int, path: str) -> None:
frames: queue.Queue = queue.Queue(maxsize=128) # one queue per camera
cap = cv2.VideoCapture(index) # one handle per camera
writer = cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*'MJPG'), FPS_OUT, FRAME_SIZE)
stats = {'grabbed': 0, 'written': 0}
def grab() -> None:
while True:
ok, frame = cap.read()
if not ok:
break
frames.put(frame)
stats['grabbed'] += 1
grabber = threading.Thread(target=grab, daemon=True)
grabber.start()
while grabber.is_alive() or not frames.empty(): # one writer owns one file
writer.write(frames.get())
stats['written'] += 1
writer.release()
cap.release()
print(path, 'grabbed', stats['grabbed'], 'written', stats['written'])
workers = [threading.Thread(target=record, args=dev) for dev in DEVICES]
for w in workers:
w.start()
for w in workers:
w.join()
The test that proves it: record 60 s from both cameras and read the two output lines. At 30 fps the target is about 30 × 60 = 1,800 frames grabbed per camera, with grabbed minus written equal to 0. Any gap is a drop; re-check the link budget first and the disk second.
Verify before the real run
- Soak, then count. Record 10 minutes from both cameras and compare grabbed with written for each. Counts that hold across the whole run are the pass condition; a count drifting behind means a writer falling back.
- Check timestamp spacing. Even inter-frame intervals mean camera and link are healthy; a sawtooth pattern points at writer stalls rather than camera drops. v4l2 buffer timestamp: know when a frame was really captured explains reading true capture times.
- Measure the disk, not its peak. Divide each file's size by its recording time and compare the pair's sum with what the drive sustains over the whole run; drives that peak fast still throttle once the write cache fills.
- Watch the cable that passes preview and fails recording. A link that carries a preview but dies under full-rate logging is a real pattern; USB extension cable fails only when you record the depth stream walks one to ground.
- Leave CPU headroom. Compressed writers spend cores, and a pegged core shows up as grabbed counts falling short. A lighter codec or an extra core frees the grabbers.
Symptoms and the fix that fits each
| Symptom | Likely cause | Fix |
|---|---|---|
| Second camera errors at start, and start order decides | Shared link out of bandwidth | One camera per controller, or cut the rate per the worked example |
| Second camera errors at start, same index in both objects | Both objects bound to one device | Assign by serial or port path |
| Both run with memory logging, fail once files are written | Disk or writer path too slow | One writer thread per camera and a disk sized for the sum |
| One file contains frames from both cameras | A single shared queue | One queue and one writer per camera |
| Runs alone, drops only when both record | Budget fits one stream, not the sum | Re-run the bandwidth arithmetic with both cameras' rates |
Every row is one story at a different layer, and GigE camera open fails in a second tool: exclusive control is the exclusive-handle version at the SDK level: one handle per device, wherever the opens come from.
When the budget says no
Splitting the load beats fighting it:
- Add a controller. A PCIe USB expansion card adds root controllers a hub cannot, and a dual-port NIC does the same for a GigE pair.
- Cut data at the source. Binning, a smaller ROI or a lower frame rate each divide the stream rate before it reaches the wire; halve any one and the rate halves with it.
- Move full-rate pairs to a faster link. 10GigE and CoaXPress carry pairs no arrangement of USB ports will, and the camera finder filters by interface and rate.
- Synchronise instead of racing. When both files must show the same instants, trigger both cameras from one source, as Trigger two cameras simultaneously from one frame grabber input wires it.
For long runs past the bench, the LS-CBL-0117 RJ45 data cable reaches 15 m with locking studs.
- Type
- Camera data cables
- Length
- 15 m
Size the budget, then buy once
The numbers above tell you whether the pair shares a controller or gets one each; settle that before ordering anything. Match camera cables and lengths in the accessories finder, or let the configurator walk the chain from camera to recorder:
The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.
If your two cameras' resolutions and frame rates refuse the arithmetic, send them through the contact page and we will run the budget with you. The systems topic page collects the neighbouring fixes — exclusive handles, buffer counts, hub budgets — that keep multi-camera rigs recording for whole shifts.
Questions
Can two USB cameras share one external hub?
Only while the sum of both stream rates fits the hub's upstream link, which carries the same usable figure as one controller — about 400 MB/s on USB 3.0. Two 12 MP cameras at 30 fps demand 720 MB/s together and will not fit. Two 720p cameras at 30 fps draw about 28 MB/s each, 55 MB/s together, and share one hub without trouble. Near the limit, give each camera its own root port instead.
Do the two recordings need two separate processes?
No. One process with one acquisition object per camera is simpler and keeps both frame streams in one clock domain for matching. Two processes also work, but each still needs its own device handle and its own bandwidth; splitting processes creates no new capacity. Process layout matters only when one SDK refuses to open two devices at once, which is a driver limit, not an operating-system one.
How fast must the recording disk be?
Fast enough to sustain the sum of both camera rates for the whole run, uncompressed. Add the two stream rates, then compare with the drive's sustained sequential write speed, measured by writing for 60 s and dividing — not the peak figure on the box. For long dual recordings at 4K and 30 fps, plan on an NVMe drive; a drive that sustains less than the pair demand drops frames once its cache fills.
Can two GigE cameras share one network card?
One 1 Gbit/s port carries about 118 MB/s of payload with jumbo frames enabled, so two cameras share a port only while their combined rate stays under that figure. A dual-port NIC gives each camera its own port and its own budget, and a switch in between adds no capacity. Enable jumbo frames on the NIC, the switch and the camera together, or fragmentation quietly eats the margin.



