@@ -176,6 +176,47 @@ func (ac *AudioCapture) ReadFrame(buf []byte) (int, error) {
176176 return n , nil
177177}
178178
179+ // DrainStale discards any PCM that buffered in the OS pipe between capture
180+ // start and the first read. The capture pipeline starts producing audio
181+ // immediately, but streaming does not begin until the first video frame is
182+ // sent; during that gap the kernel pipe accumulates a FIFO backlog that would
183+ // otherwise be read in order forever, leaving every frame permanently stale and
184+ // audio lagging video. Draining once just before the read loop starts streaming
185+ // from the freshest sample. It removes whatever backlog actually accumulated —
186+ // no fixed latency value is assumed.
187+ func (ac * AudioCapture ) DrainStale () {
188+ type deadlineReader interface {
189+ SetReadDeadline (t time.Time ) error
190+ }
191+ dr , ok := ac .pcmPipe .(deadlineReader )
192+ if ! ok {
193+ return
194+ }
195+ buf := make ([]byte , 32 * 1024 )
196+ var discarded int
197+ for {
198+ // Re-arm a short idle timeout each read: while a backlog exists, reads
199+ // return buffered data immediately; once the pipe is empty the read
200+ // blocks and this deadline fires before the next live frame (~8ms)
201+ // arrives, ending the drain. This is a poll timeout, not a latency.
202+ if err := dr .SetReadDeadline (time .Now ().Add (2 * time .Millisecond )); err != nil {
203+ break
204+ }
205+ n , err := ac .pcmPipe .Read (buf )
206+ discarded += n
207+ if err != nil {
208+ break
209+ }
210+ }
211+ // Restore blocking reads for steady-state streaming.
212+ _ = dr .SetReadDeadline (time.Time {})
213+ if discarded > 0 {
214+ const bytesPerSecond = 44100 * 2 * 2 // 44.1kHz, stereo, S16LE
215+ dbg ("[AUDIO] drained %d bytes (~%.0fms) of startup backlog before streaming" ,
216+ discarded , float64 (discarded )/ bytesPerSecond * 1000 )
217+ }
218+ }
219+
179220func (ac * AudioCapture ) Stop () {
180221 if ac .stopped {
181222 return
@@ -413,7 +454,7 @@ func (s *MirrorSession) setupAudioStream(dataPort, controlPort int, aesKey, aesI
413454 as .chachaNonceMode .String (), as .chachaAADMode .String ())
414455 }
415456 if latencyOverride > 0 {
416- dbg ("[AUDIO] receiver audio latency override : %d samples" , latencySamples )
457+ dbg ("[AUDIO] audio latency: %d samples" , latencySamples )
417458 }
418459 dbg ("[AUDIO] local ports: data=%d (→remote %d) ctrl=%d (→remote %d)" ,
419460 dataLocalPort , dataPort , ctrlLocalPort , controlPort )
@@ -594,12 +635,18 @@ func (as *AudioStream) sendSyncPacket(ntpTime uint64, isFirst bool) error {
594635 latencySamples := as .latencySamples
595636 as .mu .Unlock ()
596637
638+ // anchorLatency is the playout lead time reported to the receiver: the newest
639+ // audio we have sent (rtpNow) plays anchorLatency/44100 seconds after "now".
640+ // This equals the negotiated session latency, the same forward bias video
641+ // frames carry, so audio and video captured at the same instant play together.
642+ anchorLatency := latencySamples
643+
597644 // Sync packet: 20 bytes total (8-byte RTP-like header + 12-byte payload)
598645 // Format observed from real Apple senders:
599646 // header: V=2, X=1(first)/0(subsequent), M=1, PT=84, seq=4 (constant)
600647 // RTP timestamp = current playback position (sync_rtp)
601648 // payload: NTP_hi(4) + NTP_lo(4) + next_rtp(4)
602- // next_rtp = sync_rtp + latencySamples
649+ // next_rtp = current receive head (rtpNow)
603650 packet := make ([]byte , 20 )
604651 if isFirst {
605652 packet [0 ] = 0x90 // V=2, X=1
@@ -609,16 +656,16 @@ func (as *AudioStream) sendSyncPacket(ntpTime uint64, isFirst bool) error {
609656 packet [1 ] = 0xd4 // M=1, PT=84
610657 // seq field is constant 4 in working pcap captures
611658 binary .BigEndian .PutUint16 (packet [2 :4 ], 4 )
612- // Bytes 4-7: sync_rtp = current playback position
659+ // Bytes 4-7: sync_rtp = current playback position = receive head - anchorLatency
613660 syncRtp := rtpNow
614- if rtpNow >= latencySamples {
615- syncRtp = rtpNow - latencySamples
661+ if rtpNow >= anchorLatency {
662+ syncRtp = rtpNow - anchorLatency
616663 }
617664 binary .BigEndian .PutUint32 (packet [4 :8 ], syncRtp )
618665 // Bytes 8-15: NTP timestamp (current wall-clock time)
619666 binary .BigEndian .PutUint64 (packet [8 :16 ], ntpTime )
620- // Bytes 16-19: next_rtp = sync_rtp + latencySamples
621- binary .BigEndian .PutUint32 (packet [16 :20 ], syncRtp + latencySamples )
667+ // Bytes 16-19: next_rtp = current receive head
668+ binary .BigEndian .PutUint32 (packet [16 :20 ], rtpNow )
622669
623670 _ , err := as .ctrlConn .WriteTo (packet , as .ctrlAddr )
624671 return err
@@ -744,6 +791,11 @@ func (s *MirrorSession) StreamAudio(ctx context.Context, capture *AudioCapture,
744791 burstDone := false
745792 frameBuf := make ([]byte , 8192 )
746793
794+ // Capture started before video did, so the OS pipe holds a backlog of stale
795+ // audio accumulated while we waited for the first video frame. Drop it so we
796+ // begin streaming from the freshest sample and audio lines up with video.
797+ capture .DrainStale ()
798+
747799 for {
748800 select {
749801 case <- ctx .Done ():
0 commit comments