Fixing H265 Stream Freeze on Tuya Cameras
How I tracked down a VideoToolbox -12906 error causing RTSP stream freezes and reconnect loops in my macOS CCTV app.
Series: Building a CCTV App for macOS
- Building CCTV RTSP Live: A macOS App for Monitoring IP Cameras
- The Math Behind Cursor-Centered Zoom
- Building a Recording Engine with FFmpeg on macOS
- Fixing H265 Stream Freeze on Tuya Cameras
I spent an embarrassing amount of time staring at a frozen camera feed before I figured out what was actually going on.
The symptom was simple: some cameras would play for a few seconds, freeze, then reconnect - over and over in an infinite loop. Other cameras worked fine. The difference turned out to be H265.
The Error
Buried in the VLC logs was this:
VTDecompressionSessionCreate: err -12906
-12906 is kVTVideoDecoderNotAvailableNowError from VideoToolbox. It means the hardware decoder couldn’t handle the stream - not because the hardware doesn’t support H265 in general, but because the specific H265 profile coming from the Tuya camera wasn’t compatible with VideoToolbox’s expectations.
Tuya cameras tend to output H265 streams with profiles that VideoToolbox is picky about. When the decoder stalls, VLC doesn’t gracefully fall back - it just hangs, eventually times out, and triggers a reconnect. Then the whole thing repeats.
Fix A: Per-Camera Hardware Acceleration Toggle
The first fix was giving users control at the per-camera level. I added a hardwareAcceleration setting with three options:
- Follow Global - uses whatever the global default is
- Enabled - forces VideoToolbox
- Disabled - forces software decoding
For H265 Tuya cameras, the recommendation is Disabled. Software decoding is slower but it actually works, and for a 2K/15fps stream on a modern Mac it’s not a problem.
The tricky part was making the stream restart when this setting changes. I added lastEffectiveHW tracking - storing the hardware acceleration value that was actually used when the stream started. On settings change, I compare the new value against lastEffectiveHW. If they differ, restart the stream. If they’re the same (e.g., toggling “Follow Global” when the global is already “Disabled”), skip the restart.
if newEffectiveHW != lastEffectiveHW {
restartStream()
}
Fix B: Change the Global Default
The global default was videotoolbox. That’s fine for H264 cameras, but it’s the wrong default when you have a mix of cameras.
I changed it to any. With any, VLC picks the best available decoder and falls back gracefully if hardware decoding fails. In practice this means H264 cameras still get hardware acceleration, and H265 Tuya cameras fall back to software without the freeze-reconnect loop.
Fix D: Resource Leak in Polling Timer
While debugging this I also found an unrelated resource leak. There was a playbackPollingTimer that kept firing even after the stream had successfully started playing (hasEverPlayed = true). The timer was checking whether playback had begun, but it never stopped itself once the answer was yes.
// Before: timer kept running forever
playbackPollingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.player.isPlaying {
self.hasEverPlayed = true
}
}
// After: stop once we know it's playing
playbackPollingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self else { return }
if self.player.isPlaying {
self.hasEverPlayed = true
self.playbackPollingTimer?.invalidate()
self.playbackPollingTimer = nil
}
}
Small thing, but with multiple camera cells each running their own timer it adds up.
What I Learned
VideoToolbox is not forgiving about H265 profiles. If you’re building anything that plays RTSP streams from consumer cameras, don’t assume hardware decoding will just work. Give users a way to disable it per-camera, and make your global default permissive (any over videotoolbox).
The freeze-reconnect loop was the worst kind of bug - it looked like a network issue, a camera firmware issue, a VLC bug. It took reading the raw VLC logs to find the actual error code. Always check the logs first.
Series: Building a CCTV App for macOS
- Building CCTV RTSP Live: A macOS App for Monitoring IP Cameras
- The Math Behind Cursor-Centered Zoom
- Building a Recording Engine with FFmpeg on macOS
- Fixing H265 Stream Freeze on Tuya Cameras
Stuck on a video pipeline bug?
I debug nasty media playback issues on macOS for a living. If your project is grinding to a halt on something like this, get in touch.
Work with me