Skip to content

Commit b22745a

Browse files
committed
capture: match Apple's frame queue admission boundary
The shared encoded queue checked the duration after adding the incoming frame, rejecting one picture before Apple's producer admission threshold. Check existing queued duration >= 67 ms instead. At 30 fps, two pending pictures occupy 66.7 ms, so a third is admitted and the fourth is rejected. Grounding: artifacts/26A5388g__MacOS/decompiled/AirPlaySender.c, vdsink_ShouldDropFrame (128485-128526). The queue is created at 188416, passed to APVirtualDisplaySinkCreate at 188571, and consumed by screenstream_dequeueAndProcessSampleBuffer at 319674. It holds encoded CMSampleBuffers. The byte/chunk bounds and shared-sink detachment policy remain unchanged; this does not port Apple's producer-feedback mechanism or resolve longer stalls reported in PR #36. Tests: failing-first 20/30/60 fps admission regressions, exact 67 ms equality, drain/re-enqueue ordering, byte/chunk bounds, and oversized-first-frame rejection. Full race suite, vet, and executable builds passed before commit.
1 parent 0dd466a commit b22745a

2 files changed

Lines changed: 71 additions & 13 deletions

File tree

internal/airplay/capture_broadcast.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ type BroadcastSink struct {
8181

8282
maxQueuedBytes int
8383
maxQueuedChunks int
84-
// Apple's ordinary virtual-display source bounds its upstream frame queue to
85-
// 67 ms and drops an incoming source frame at that limit. Doubletake derives
86-
// a downstream encoded-relay ceiling from that value and counts configured
87-
// sample durations. The byte and chunk limits remain independent safeguards.
84+
// Apple's vdsink_ShouldDropFrame checks whether the existing encoded sample
85+
// queue has reached 67 ms before signaling the producer to skip a frame.
86+
// Match that admission boundary using configured sample durations. Shared
87+
// relay overflow still detaches the sink; byte and chunk limits are separate.
8888
maxFrameQueueDuration time.Duration
8989
backpressure bool
9090
blockedProducers int // number waiting for queue handoff; guarded by mu
@@ -379,16 +379,16 @@ func (s *BroadcastSink) frameQueueExceedsLimitsLocked(frame VideoAccessUnit) boo
379379
return true
380380
}
381381
return len(s.frameQueue) > 0 && s.maxFrameQueueDuration > 0 &&
382-
s.queuedFrameDuration+s.frameDuration > s.maxFrameQueueDuration
382+
s.queuedFrameDuration >= s.maxFrameQueueDuration
383383
}
384384

385385
// enqueueFrame appends an immutable complete access unit without copying it.
386386
// The same backing bytes can safely be referenced by every receiver queue.
387387
// An explicitly single-destination sink waits once one AU is pending, rather
388388
// than accumulating encoded references which cannot safely be dropped. Shared
389389
// fan-out is nonblocking and detaches only the sink which exceeds Doubletake's
390-
// nominal-duration relay budget. This policy is distinct from Apple's upstream
391-
// source-frame dropping behavior.
390+
// nominal-duration relay budget. Detachment is distinct from Apple's producer
391+
// admission signal and does not discard individual encoded references.
392392
func (s *BroadcastSink) enqueueFrame(frame VideoAccessUnit) error {
393393
if len(frame.AnnexB) == 0 {
394394
return nil

internal/airplay/capture_broadcast_test.go

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,15 @@ func TestBroadcastSinkNonblockingFrameQueueUsesNominalDuration(t *testing.T) {
192192
base := time.Now()
193193
// A large or backward PTS gap can be caused by the upstream leaky queue; it
194194
// must not turn one queued picture into an artificial duration overflow.
195-
for i, offset := range []time.Duration{0, time.Second} {
195+
for i, offset := range []time.Duration{0, time.Second, -time.Second} {
196196
frame := VideoAccessUnit{AnnexB: []byte{byte(i + 1)}, PTS: base.Add(offset)}
197197
if err := sink.enqueueFrame(frame); err != nil {
198198
t.Fatalf("enqueue frame %d at %v: %v", i, offset, err)
199199
}
200200
}
201-
third := VideoAccessUnit{AnnexB: []byte{3}, PTS: base.Add(-time.Second)}
202-
if err := sink.enqueueFrame(third); !errors.Is(err, errBroadcastSinkBacklog) {
203-
t.Fatalf("enqueue third nominal 30fps frame = %v, want backlog error", err)
201+
fourth := VideoAccessUnit{AnnexB: []byte{4}, PTS: base.Add(2 * time.Second)}
202+
if err := sink.enqueueFrame(fourth); !errors.Is(err, errBroadcastSinkBacklog) {
203+
t.Fatalf("enqueue fourth nominal 30fps frame = %v, want backlog error", err)
204204
}
205205
}
206206

@@ -210,8 +210,9 @@ func TestBroadcastSinkNominalDurationUsesConfiguredFrameRate(t *testing.T) {
210210
acceptedFrames int
211211
rejectedOrdinal int
212212
}{
213-
{fps: 20, acceptedFrames: 1, rejectedOrdinal: 2},
214-
{fps: 60, acceptedFrames: 4, rejectedOrdinal: 5},
213+
{fps: 20, acceptedFrames: 2, rejectedOrdinal: 3},
214+
{fps: 30, acceptedFrames: 3, rejectedOrdinal: 4},
215+
{fps: 60, acceptedFrames: 5, rejectedOrdinal: 6},
215216
} {
216217
t.Run(fmt.Sprintf("%dfps", test.fps), func(t *testing.T) {
217218
broadcast := NewBroadcastCaptureWithFrameRate(nil, test.fps)
@@ -229,6 +230,63 @@ func TestBroadcastSinkNominalDurationUsesConfiguredFrameRate(t *testing.T) {
229230
}
230231
}
231232

233+
func TestBroadcastSinkRejectsFrameAtExactDurationThreshold(t *testing.T) {
234+
sink := newBroadcastSink(nil)
235+
defer sink.Close()
236+
// Apple's admission check compares the existing queue to the threshold.
237+
// Two samples land exactly on 67 ms; equality must reject the next sample.
238+
sink.frameDuration = sink.maxFrameQueueDuration / 2
239+
for _, value := range []byte{1, 2} {
240+
if err := sink.enqueueFrame(VideoAccessUnit{AnnexB: []byte{value}}); err != nil {
241+
t.Fatalf("enqueue frame %d: %v", value, err)
242+
}
243+
}
244+
third := VideoAccessUnit{AnnexB: []byte{3}}
245+
if err := sink.enqueueFrame(third); !errors.Is(err, errBroadcastSinkBacklog) {
246+
t.Fatalf("enqueue at exact threshold = %v, want backlog error", err)
247+
}
248+
first, err := sink.ReadVideoAccessUnit()
249+
if err != nil || !bytes.Equal(first.AnnexB, []byte{1}) {
250+
t.Fatalf("first queued frame after rejected enqueue = (%x, %v), want 01", first.AnnexB, err)
251+
}
252+
if err := sink.enqueueFrame(third); err != nil {
253+
t.Fatalf("enqueue after draining below threshold: %v", err)
254+
}
255+
for _, want := range []byte{2, 3} {
256+
frame, err := sink.ReadVideoAccessUnit()
257+
if err != nil || !bytes.Equal(frame.AnnexB, []byte{want}) {
258+
t.Fatalf("queued frame = (%x, %v), want %02x", frame.AnnexB, err, want)
259+
}
260+
}
261+
}
262+
263+
func TestBroadcastSinkFrameQueueRetainsByteAndChunkLimits(t *testing.T) {
264+
for _, test := range []struct {
265+
name string
266+
maxBytes, maxChunks int
267+
prefill bool
268+
}{
269+
{name: "bytes", maxBytes: 3, maxChunks: 10, prefill: true},
270+
{name: "chunks", maxBytes: 100, maxChunks: 1, prefill: true},
271+
{name: "oversized first frame", maxBytes: 1, maxChunks: 10},
272+
} {
273+
t.Run(test.name, func(t *testing.T) {
274+
sink := newBroadcastSink(nil)
275+
defer sink.Close()
276+
sink.maxQueuedBytes, sink.maxQueuedChunks = test.maxBytes, test.maxChunks
277+
frame := VideoAccessUnit{AnnexB: []byte{1, 2}}
278+
if test.prefill {
279+
if err := sink.enqueueFrame(frame); err != nil {
280+
t.Fatalf("enqueue initial frame: %v", err)
281+
}
282+
}
283+
if err := sink.enqueueFrame(frame); !errors.Is(err, errBroadcastSinkBacklog) {
284+
t.Fatalf("enqueue below duration threshold = %v, want capacity error", err)
285+
}
286+
})
287+
}
288+
}
289+
232290
func TestBackpressuredSinkRegistrationIsExclusive(t *testing.T) {
233291
sharedBroadcast := NewBroadcastCapture(nil)
234292
shared := sharedBroadcast.AddSink()

0 commit comments

Comments
 (0)