fix(packet_pool): don't drop a payload that is flushed by the same packet that completes a single-packet PES - #78
Merged
asticode merged 2 commits intoSep 18, 2026
Conversation
…ete single-packet PES When a packet both flushes the previous payload and completes a PES on its own (bounded PES that fits in one packet, added in asticode#72), the second assignment to ps overwrote the first one, so the previous payload was silently dropped. This happens in practice with GStreamer's mpegtsmux, which writes PES length 0 only for video PES larger than 65535 bytes: a large IDR access unit followed by a tiny P frame makes the IDR disappear, with no demuxer error. Keep the newly started payload queued in that case; it is flushed on the next payload start, as before asticode#72 for this specific sequence.
asticode
requested changes
Sep 17, 2026
asticode
left a comment
Owner
There was a problem hiding this comment.
Thanks for the PR ❤️
A few minor changes needed 👍
Owner
There was a problem hiding this comment.
Can you move those tests in packet_pool_test.go instead?
| // GStreamer's mpegtsmux) must not be dropped when the PES that starts right | ||
| // after it is complete within that single packet. | ||
| func TestPacketPoolUnboundedPESFollowedByOnePacketPES(t *testing.T) { | ||
| pp := newPacketPool(nil) |
Owner
There was a problem hiding this comment.
since both pesPacketPayload and tsPacket are only used in TestPacketPoolUnboundedPESFollowedByOnePacketPES, could you declare them inside TestPacketPoolUnboundedPESFollowedByOnePacketPES instead ?
func TestPacketPoolUnboundedPESFollowedByOnePacketPES(t *testing.T) {
// pesPacketPayload builds a PES payload: start code, stream id, packet length
// (0 = unbounded), minimal optional header, then n data bytes.
pesPacketPayload := func(packetLength uint16, n int) []byte {
p := []byte{0, 0, 1, 0xe0, byte(packetLength >> 8), byte(packetLength), 0x80, 0x00, 0x00}
return append(p, make([]byte, n)...)
}
...
...
}| return append(p, make([]byte, n)...) | ||
| } | ||
|
|
||
| func tsPacket(cc uint8, pusi bool, payload []byte) *Packet { |
Address review: keep the test next to the other packet pool tests, declare its helpers inside the test and rename tsPacket to packet.
jhcho-hanvision
force-pushed
the
fix/packet-pool-dropped-payload
branch
from
September 18, 2026 02:03
4bf577d to
3256154
Compare
Contributor
Author
|
Thanks for the review! Done in 3256154:
The test still fails without the fix in |
Owner
|
Thanks for the PR ❤️ Let me know whether you need a tag 👍 |
Contributor
Author
|
No tag needed on our side for now — we only use it as an indirect dependency. Thanks for merging! 🙏 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a TS packet both starts a new payload (PUSI) and completes a bounded PES on its own,
packetAccumulator.addassigns topstwice, so the payload accumulated so far is silently dropped — it is neither returned nor kept in the queue, and no error is reported.The immediate flush of a complete single-packet PES was added in #72 to reduce latency for sparse streams (e.g. DVB teletext).
How we hit it
GStreamer's
mpegtsmuxwritesPES_packet_length = 0for video only when the PES exceeds 65535 bytes, and the real length otherwise. So a large IDR access unit (length 0, spanning many TS packets) followed by a tiny P frame that fits into a single TS packet (length set) makes the IDR disappear.Downstream this shows up as video corruption every few seconds with no demuxer error and no transport-level loss — we chased it for a while in an SRT ingest (mediamtx, which uses go-astits). FFmpeg always writes 0 for video PES, which is why streams produced by FFmpeg never trigger it. Both writers are spec compliant: ISO/IEC 13818-1 allows 0 only for video PES, but does not require it.
Fix
If this call is already returning the previous payload, keep the newly started payload queued; it is flushed on the next payload start, as before #72 for this particular sequence. The latency improvement of #72 is therefore unaffected for sparse streams, where a payload start does not coincide with a flush.
A complete fix would let
addreturn several payloads, but that changes the internal API and all of its call sites, so this keeps the change minimal.Test
TestPacketPoolUnboundedPESFollowedByOnePacketPESreproduces the sequence with synthetic packets. It fails on master (only the one-packet PES is returned,len=1instead oflen=2) and passes with this change. The rest of the test suite is unaffected.