Edited — H.265 MSE Streaming Bug Report
Date: 2026-04-10
Affected versions: All versions including v1.9.13 (embedded in Frigate 0.18.0-dev) and v1.9.14+dev (skrashevich fork, commit ad2c09a)
Repositories: AlexxIT/go2rtc · skrashevich/go2rtc
Symptom: H.265 (HEVC) cameras fail all MSE playback. Browser receives ~168 bytes and the WebSocket closes within 3 seconds. H.264 cameras on the same server are unaffected.
Environment (discovery context)
- Frigate version: 0.18.0-8f13932c (dev branch)
- go2rtc versions tested: embedded
1.9.13 (/usr/local/go2rtc/bin/go2rtc) and custom fork 1.9.14+dev.ad2c09a.dirty (/config/go2rtc)
- Camera: Dahua IPC-HDW2849H-S-IL, 4000×3000, H.265 Main profile, Level 6.0
- Access path: Chrome 146 (Windows 10) → Cloudflare → Traefik → Frigate nginx → go2rtc
- go2rtc stream config:
exec:ffmpeg -rtsp_transport tcp -i rtsp://.../cam/realmonitor?channel=1&subtype=0 -c copy -f rtsp -rtsp_transport tcp {output}
- go2rtc stream API confirms: codec
hevc, SDP a=rtpmap:96 H265/90000, all three parameter sets present (sprop-vps, sprop-sps, sprop-pps)
- Confirmed that the bug exists in BOTH go2rtc versions — the MSE/H.265 code in
pkg/mp4/, pkg/iso/, and internal/mp4/ is byte-for-byte identical between v1.9.13 and ad2c09a
Related open issues
None of the above identified the root cause described below.
Bug A — hev1 box written in fMP4, but hvc1 advertised in MIME type
Severity
High — confirmed root cause of MSE failures. This is a protocol-level mismatch. Chrome 107–110 silently tolerated it; Chrome 120+ rejects the init segment, causing MSE playback to fail for all H.265 cameras.
Files
pkg/iso/codecs.go — line 14
Description
When go2rtc builds the fMP4 init segment for an H.265 stream, it writes an hev1 sample entry box:
// pkg/iso/codecs.go:14
case core.CodecH265:
m.StartAtom("hev1") // ← writes hev1 box
However, the MIME type returned to the browser and used in addSourceBuffer() advertises hvc1:
// pkg/mp4/mime.go:10
MimeH265 = "hvc1.1.6.L153.B0" // ← declares hvc1
hvc1 and hev1 are not interchangeable. Per ISO 14496-15 §8.4.1:
| Box name |
Meaning |
hvc1 |
H.265 parameter sets stored exclusively out-of-band in the hvcC box |
hev1 |
H.265 parameter sets may be in-band (in the stream) or in hvcC |
When a browser creates a SourceBuffer with video/mp4; codecs="hvc1.1.6.L153.B0" and then receives an init segment with an hev1 sample entry box, it has received a different codec type than declared. In Chrome 120+ this inconsistency causes appendBuffer to fire a QuotaExceededError or InvalidStateError, which the video-rtc.js error handler interprets as stream failure, closing the WebSocket and falling back to jsmpeg.
Note: go2rtc already correctly writes parameter sets to the hvcC box (not in-band), so changing the sample entry box name from hev1 to hvc1 is the correct and safe fix — the delivery mode matches the hvc1 semantics.
Fix (applied)
Change pkg/iso/codecs.go line 14 to write hvc1 instead of hev1:
case core.CodecH265:
- m.StartAtom("hev1")
+ m.StartAtom("hvc1")
With this change, the declared MIME codec string (hvc1.1.6.L153.B0) matches the fMP4 sample entry box (hvc1). This also improves Safari compatibility — Safari requires hvc1 and does not accept hev1.
Verification
After deploying the patched binary, probing the MSE WebSocket from inside the container confirms:
Text: {'type': 'mse', 'value': 'video/mp4; codecs="hvc1.1.6.L153.B0"'}
Binary: 759 bytes (init segment)
Box name at offset 421: hvc1
The hev1 init segment was 168 bytes and caused immediate close; the hvc1 init segment is 759 bytes (full ftyp + moov) and the connection stays open for video frames.
Bug B — H.265 codec level hardcoded at 5.1 (investigated, not applied as fix)
Status
Investigated. Technically inaccurate, but cannot be corrected in the server response without client changes. No fix applied.
Description
For H.264, go2rtc derives the profile and level from the stream's SPS:
// pkg/mp4/mime.go
case core.CodecH264:
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
For H.265, the codec string is hardcoded regardless of the actual stream:
MimeH265 = "hvc1.1.6.L153.B0" // Level 5.1, hardcoded
case core.CodecH265:
s += MimeH265
L153 encodes H.265 Level 5.1. The Dahua IPC-HDW2849H-S-IL at 4000×3000 is actually Level 6.0 (general_level_idc = 180, since 4000×3000 = 12,000,000 luma samples > Level 5.1 max of 8,912,896).
Why a "derive from SPS" fix does not work here
The natural fix would mirror H.264: decode the H.265 SPS and use the actual level in the response. A GetProfileLevelID() function for H.265 was implemented (added to pkg/h265/helper.go) and tested.
Problem: The codec string in the server response must match what the browser offered, not the actual stream level.
video-rtc.js hardcodes its CODECS list and offers only hvc1.1.6.L153.B0 (Level 5.1) to go2rtc. The browser checks MediaSource.isTypeSupported('video/mp4; codecs="hvc1.1.6.L153.B0"') before sending the request — that's the level the browser has confirmed it supports.
When go2rtc responds with video/mp4; codecs="hvc1.1.6.L180.B0" (Level 6.0, derived from SPS), the browser tries:
mediaSource.addSourceBuffer('video/mp4; codecs="hvc1.1.6.L180.B0"')
Level 6.0 support depends on hardware and driver. On systems where L180 is not in the supported codec list, addSourceBuffer throws and MSE fails — the same symptom as before but for a different reason. This was confirmed during testing: deploying the "derive from SPS" fix did not resolve the 168-byte close pattern.
What would actually fix it
The level mismatch is a client-server contract issue. The options are:
-
Update video-rtc.js to include higher levels in its CODECS list (e.g. hvc1.1.6.L180.B0, hvc1.1.6.L186.B0). This is the correct long-term fix but requires a frontend change in go2rtc/Frigate.
-
In go2rtc: Use the codec string from the browser's offer as the response, rather than deriving from the stream. This requires threading the offer codec string through the ParseCodecs → AddConsumer → ContentType path.
-
Accept the current behaviour: Level 5.1 is declared but Level 6.0 content is delivered. The actual SPS inside the hvcC box has the correct level; most HEVC decoders use the SPS for decoding decisions rather than the MIME type level. Level 5.1 declaration for Level 6.0 content is technically wrong but benign in practice once the browser successfully creates the SourceBuffer.
The GetProfileLevelID() function added to pkg/h265/helper.go is retained in the patch for completeness and potential future use (e.g., in ParseCodecs matching or logging), but it is not called from MimeCodecs().
Summary
|
Bug A |
Bug B |
| File |
pkg/iso/codecs.go:14 |
pkg/mp4/mime.go:10 |
| Issue |
fMP4 box is hev1, MIME declares hvc1 |
H.265 codec level hardcoded at 5.1 for all streams |
| Impact |
Chrome 120+ rejects init segment; MSE always fails for H.265 |
Level mismatch between MIME and actual stream |
| Fix |
Change hev1 → hvc1 in codecs.go:14 |
Cannot fix response without client changes |
| Applied |
Yes |
No |
| Present in v1.9.13 |
Yes |
Yes |
| Present in v1.9.14+dev |
Yes |
Yes |
| Related issues |
#1302, #1598, #1711, #1789 |
#1302, #1711 |
Workarounds (no code change required)
1. Switch live stream to an H.264 sub-stream
If the camera has a configured H.264 sub-stream in go2rtc (e.g. via NVENC re-encode), configure the Frigate live.streams to default to that stream:
# config.yaml
cameras:
dahua_test:
live:
streams:
Detect Stream: dahua_test_sub # H.264 — MSE works
Record Stream: dahua_test # H.265 — MSE fails until bugs are fixed
In the Frigate UI, select "Detect Stream" for live view on affected cameras.
2. Add an H.264 restream in go2rtc for the affected camera
# go2rtc streams config
streams:
dahua_test_h264:
- exec:ffmpeg -rtsp_transport tcp -hwaccel cuda -i rtsp://user:pass@camera_ip/stream
-an -c:v h264_nvenc -preset p2 -tune ll -rc cbr -b:v 4M
-f rtsp -rtsp_transport tcp {output}
Then point the Frigate live view at dahua_test_h264.
3. Test with embedded go2rtc
Note: this does not fix the bug. The pkg/mp4/ and pkg/iso/ MSE/H.265 code is identical in v1.9.13 and v1.9.14+dev. Switching binaries will produce the same MSE failure for H.265 streams.
Edited — H.265 MSE Streaming Bug Report
Date: 2026-04-10
Affected versions: All versions including v1.9.13 (embedded in Frigate 0.18.0-dev) and v1.9.14+dev (skrashevich fork, commit
ad2c09a)Repositories: AlexxIT/go2rtc · skrashevich/go2rtc
Symptom: H.265 (HEVC) cameras fail all MSE playback. Browser receives ~168 bytes and the WebSocket closes within 3 seconds. H.264 cameras on the same server are unaffected.
Environment (discovery context)
1.9.13(/usr/local/go2rtc/bin/go2rtc) and custom fork1.9.14+dev.ad2c09a.dirty(/config/go2rtc)exec:ffmpeg -rtsp_transport tcp -i rtsp://.../cam/realmonitor?channel=1&subtype=0 -c copy -f rtsp -rtsp_transport tcp {output}hevc, SDPa=rtpmap:96 H265/90000, all three parameter sets present (sprop-vps,sprop-sps,sprop-pps)pkg/mp4/,pkg/iso/, andinternal/mp4/is byte-for-byte identical between v1.9.13 andad2c09aRelated open issues
None of the above identified the root cause described below.
Bug A —
hev1box written in fMP4, buthvc1advertised in MIME typeSeverity
High — confirmed root cause of MSE failures. This is a protocol-level mismatch. Chrome 107–110 silently tolerated it; Chrome 120+ rejects the init segment, causing MSE playback to fail for all H.265 cameras.
Files
pkg/iso/codecs.go— line 14Description
When go2rtc builds the fMP4 init segment for an H.265 stream, it writes an
hev1sample entry box:However, the MIME type returned to the browser and used in
addSourceBuffer()advertiseshvc1:hvc1andhev1are not interchangeable. Per ISO 14496-15 §8.4.1:hvc1hvcCboxhev1hvcCWhen a browser creates a SourceBuffer with
video/mp4; codecs="hvc1.1.6.L153.B0"and then receives an init segment with anhev1sample entry box, it has received a different codec type than declared. In Chrome 120+ this inconsistency causesappendBufferto fire aQuotaExceededErrororInvalidStateError, which thevideo-rtc.jserror handler interprets as stream failure, closing the WebSocket and falling back to jsmpeg.Note: go2rtc already correctly writes parameter sets to the
hvcCbox (not in-band), so changing the sample entry box name fromhev1tohvc1is the correct and safe fix — the delivery mode matches thehvc1semantics.Fix (applied)
Change
pkg/iso/codecs.goline 14 to writehvc1instead ofhev1:With this change, the declared MIME codec string (
hvc1.1.6.L153.B0) matches the fMP4 sample entry box (hvc1). This also improves Safari compatibility — Safari requireshvc1and does not accepthev1.Verification
After deploying the patched binary, probing the MSE WebSocket from inside the container confirms:
The
hev1init segment was 168 bytes and caused immediate close; thehvc1init segment is 759 bytes (fullftyp+moov) and the connection stays open for video frames.Bug B — H.265 codec level hardcoded at 5.1 (investigated, not applied as fix)
Status
Investigated. Technically inaccurate, but cannot be corrected in the server response without client changes. No fix applied.
Description
For H.264, go2rtc derives the profile and level from the stream's SPS:
For H.265, the codec string is hardcoded regardless of the actual stream:
L153encodes H.265 Level 5.1. The Dahua IPC-HDW2849H-S-IL at 4000×3000 is actually Level 6.0 (general_level_idc = 180, since 4000×3000 = 12,000,000 luma samples > Level 5.1 max of 8,912,896).Why a "derive from SPS" fix does not work here
The natural fix would mirror H.264: decode the H.265 SPS and use the actual level in the response. A
GetProfileLevelID()function for H.265 was implemented (added topkg/h265/helper.go) and tested.Problem: The codec string in the server response must match what the browser offered, not the actual stream level.
video-rtc.jshardcodes its CODECS list and offers onlyhvc1.1.6.L153.B0(Level 5.1) to go2rtc. The browser checksMediaSource.isTypeSupported('video/mp4; codecs="hvc1.1.6.L153.B0"')before sending the request — that's the level the browser has confirmed it supports.When go2rtc responds with
video/mp4; codecs="hvc1.1.6.L180.B0"(Level 6.0, derived from SPS), the browser tries:Level 6.0 support depends on hardware and driver. On systems where L180 is not in the supported codec list,
addSourceBufferthrows and MSE fails — the same symptom as before but for a different reason. This was confirmed during testing: deploying the "derive from SPS" fix did not resolve the 168-byte close pattern.What would actually fix it
The level mismatch is a client-server contract issue. The options are:
Update
video-rtc.jsto include higher levels in its CODECS list (e.g.hvc1.1.6.L180.B0,hvc1.1.6.L186.B0). This is the correct long-term fix but requires a frontend change in go2rtc/Frigate.In go2rtc: Use the codec string from the browser's offer as the response, rather than deriving from the stream. This requires threading the offer codec string through the
ParseCodecs→AddConsumer→ContentTypepath.Accept the current behaviour: Level 5.1 is declared but Level 6.0 content is delivered. The actual SPS inside the
hvcCbox has the correct level; most HEVC decoders use the SPS for decoding decisions rather than the MIME type level. Level 5.1 declaration for Level 6.0 content is technically wrong but benign in practice once the browser successfully creates the SourceBuffer.The
GetProfileLevelID()function added topkg/h265/helper.gois retained in the patch for completeness and potential future use (e.g., inParseCodecsmatching or logging), but it is not called fromMimeCodecs().Summary
pkg/iso/codecs.go:14pkg/mp4/mime.go:10hev1, MIME declareshvc1hev1→hvc1incodecs.go:14Workarounds (no code change required)
1. Switch live stream to an H.264 sub-stream
If the camera has a configured H.264 sub-stream in go2rtc (e.g. via NVENC re-encode), configure the Frigate
live.streamsto default to that stream:In the Frigate UI, select "Detect Stream" for live view on affected cameras.
2. Add an H.264 restream in go2rtc for the affected camera
Then point the Frigate live view at
dahua_test_h264.3. Test with embedded go2rtc
Note: this does not fix the bug. The
pkg/mp4/andpkg/iso/MSE/H.265 code is identical in v1.9.13 and v1.9.14+dev. Switching binaries will produce the same MSE failure for H.265 streams.