← Back to Writings

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.

March 14, 2025 Updated May 28, 2026
SwiftmacOSVLCKitRTSPDebuggingH265HEVCVideoToolboxTuya CameraError -12906

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.

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