From e3c53f9736edd1ff7f4a707d9370d4c308827e67 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 01:24:42 +0000
Subject: [PATCH 01/14] Initial plan
From 017f31cfe27fe5ac3e5acd79cbe4296c060810ce Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 01:33:40 +0000
Subject: [PATCH 02/14] Fix AAC-ELD decoder: switch from ADTS to LATM wrapping,
fix decoder race condition
ADTS format cannot represent AAC-ELD (its 2-bit profile field only supports
AAC-Main/LC/SSR). Using LATM/LOAS wrapping which supports all AAC profiles
including AAC-ELD (AOT 39).
Also added thread-safety lock on decoder initialization to prevent race
condition between concurrent OnRawCSocketAsync and OnRawDSocketAsync handlers
that was causing duplicate FFmpeg processes to be spawned.
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
.../Implementations/FFmpegAacEldDecoder.cs | 199 +++++++++++++++---
AirPlay/Listeners/AudioListener.cs | 140 ++++++------
2 files changed, 240 insertions(+), 99 deletions(-)
diff --git a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
index 72bc7f7..f74a849 100644
--- a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
+++ b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
@@ -5,14 +5,15 @@
* for AAC-ELD because the pre-built binary doesn't include ER format support.
* SharpJaad.AAC also doesn't support AAC-ELD.
*
- * This decoder pipes raw AAC-ELD frames (wrapped in ADTS) through FFmpeg for
- * decoding, which supports AAC-ELD natively. This approach works with any
- * FFmpeg version the user has installed.
+ * This decoder pipes raw AAC-ELD frames wrapped in LOAS/LATM through FFmpeg
+ * for decoding. ADTS format does NOT support AAC-ELD (its 2-bit profile field
+ * cannot represent AOT 39). LATM supports all AAC profiles including AAC-ELD.
*
* Reference: UxPlay uses GStreamer's avdec_aac (which wraps FFmpeg) for the same purpose.
*/
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
@@ -34,10 +35,8 @@ public class FFmpegAacEldDecoder : IDecoder, IDisposable
private int _frameLength;
private bool _disposed;
private bool _initialized;
- private readonly byte[] _adtsHeader = new byte[7];
- private int _adtsProfile;
- private int _adtsFreqIdx;
- private int _adtsChanCfg;
+ private bool _firstFrame = true;
+ private int _freqIdx;
public AudioFormat Type => AudioFormat.AAC_ELD;
@@ -51,22 +50,17 @@ public int Config(int sampleRate, int channels, int bitDepth, int frameLength)
_sampleRate = sampleRate;
_frameLength = frameLength;
_pcmOutputSize = frameLength * channels * (bitDepth / 8);
-
- // ADTS profile=2 (AAC-LC) since ADTS doesn't support AAC-ELD profile encoding.
- // FFmpeg's parser will still decode the content correctly based on the actual bitstream.
- _adtsProfile = 2;
- _adtsFreqIdx = GetSampleRateIndex(sampleRate);
- _adtsChanCfg = channels;
+ _freqIdx = GetSampleRateIndex(sampleRate);
try
{
// Start FFmpeg process:
- // Input: AAC frames wrapped in ADTS headers via stdin pipe
+ // Input: AAC-ELD frames wrapped in LOAS/LATM via stdin pipe
// Output: raw PCM S16LE via stdout pipe
var psi = new ProcessStartInfo
{
FileName = "ffmpeg",
- Arguments = $"-hide_banner -loglevel error -f aac -i pipe:0 -f s16le -acodec pcm_s16le -ar {sampleRate} -ac {channels} pipe:1",
+ Arguments = $"-hide_banner -loglevel error -f latm -i pipe:0 -f s16le -acodec pcm_s16le -ar {sampleRate} -ac {channels} pipe:1",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
@@ -90,7 +84,7 @@ public int Config(int sampleRate, int channels, int bitDepth, int frameLength)
stderrThread.Start();
_initialized = true;
- Console.WriteLine($"FFmpeg AAC-ELD decoder started: {sampleRate}Hz, {channels}ch, {bitDepth}bit, frameLength={frameLength}");
+ Console.WriteLine($"FFmpeg AAC-ELD decoder started (LATM): {sampleRate}Hz, {channels}ch, {bitDepth}bit, frameLength={frameLength}");
return 0;
}
catch (Exception ex)
@@ -113,13 +107,11 @@ public int DecodeFrame(byte[] input, ref byte[] output, int length)
try
{
- // Wrap the raw AAC frame in an ADTS header for FFmpeg to parse
- int frameLen = input.Length + 7; // ADTS header is 7 bytes
- BuildAdtsHeader(_adtsHeader, frameLen, _adtsProfile, _adtsFreqIdx, _adtsChanCfg);
+ // Build LOAS/LATM frame wrapping the raw AAC-ELD data
+ byte[] loasFrame = BuildLoasFrame(input, _firstFrame);
+ _firstFrame = false;
- // Write ADTS header + raw AAC data to FFmpeg stdin
- _ffmpegInput.Write(_adtsHeader, 0, 7);
- _ffmpegInput.Write(input, 0, input.Length);
+ _ffmpegInput.Write(loasFrame, 0, loasFrame.Length);
_ffmpegInput.Flush();
// Read decoded PCM from FFmpeg stdout
@@ -154,17 +146,124 @@ public int DecodeFrame(byte[] input, ref byte[] output, int length)
}
///
- /// Build an ADTS header for wrapping raw AAC frames.
+ /// Build a LOAS/LATM frame containing the raw AAC-ELD data.
+ /// LOAS (Low Overhead Audio Stream) sync layer wraps an AudioMuxElement.
+ /// The first frame includes the full StreamMuxConfig; subsequent frames
+ /// reuse it (useSameStreamMux=1).
+ ///
+ private byte[] BuildLoasFrame(byte[] aacFrame, bool includeConfig)
+ {
+ // Build the AudioMuxElement as a bitstream
+ var bits = new BitWriter();
+
+ if (includeConfig)
+ {
+ // useSameStreamMux = 0 (include StreamMuxConfig)
+ bits.WriteBits(0, 1);
+ WriteStreamMuxConfig(bits);
+ }
+ else
+ {
+ // useSameStreamMux = 1 (reuse previous config)
+ bits.WriteBits(1, 1);
+ }
+
+ // PayloadLengthInfo for allStreamsSameTimeFraming=1, progSIndx=0, laySIndx=0
+ // Length is encoded as: N bytes of 0xFF followed by a final byte < 0xFF
+ int remaining = aacFrame.Length;
+ while (remaining >= 255)
+ {
+ bits.WriteBits(255, 8);
+ remaining -= 255;
+ }
+ bits.WriteBits(remaining, 8);
+
+ // PayloadMux: the raw AAC-ELD frame data
+ for (int i = 0; i < aacFrame.Length; i++)
+ {
+ bits.WriteBits(aacFrame[i], 8);
+ }
+
+ // otherDataPresent = 0 already implied by StreamMuxConfig setting
+
+ byte[] audioMuxElement = bits.ToByteArray();
+
+ // LOAS sync layer: 0x56E0 | (length & 0x1FFF)
+ int audioMuxLength = audioMuxElement.Length;
+ byte[] loasFrame = new byte[3 + audioMuxLength];
+ loasFrame[0] = 0x56;
+ loasFrame[1] = (byte)(0xE0 | ((audioMuxLength >> 8) & 0x1F));
+ loasFrame[2] = (byte)(audioMuxLength & 0xFF);
+ Array.Copy(audioMuxElement, 0, loasFrame, 3, audioMuxLength);
+
+ return loasFrame;
+ }
+
+ ///
+ /// Write StreamMuxConfig for AAC-ELD into the bitstream.
+ /// ISO 14496-3 Table 1.42
+ ///
+ private void WriteStreamMuxConfig(BitWriter bits)
+ {
+ // audioMuxVersion = 0
+ bits.WriteBits(0, 1);
+ // allStreamsSameTimeFraming = 1
+ bits.WriteBits(1, 1);
+ // numSubFrames = 0 (1 subframe)
+ bits.WriteBits(0, 6);
+ // numProgram = 0 (1 program)
+ bits.WriteBits(0, 4);
+ // numLayer = 0 (1 layer)
+ bits.WriteBits(0, 3);
+
+ // AudioSpecificConfig for AAC-ELD (ISO 14496-3 Table 1.15)
+ WriteAudioSpecificConfig(bits);
+
+ // frameLengthType = 0 (variable frame length)
+ bits.WriteBits(0, 3);
+ // latmBufferFullness = 0xFF (variable bitrate)
+ bits.WriteBits(0xFF, 8);
+
+ // otherDataPresent = 0
+ bits.WriteBits(0, 1);
+ // crcCheckPresent = 0
+ bits.WriteBits(0, 1);
+ }
+
+ ///
+ /// Write AudioSpecificConfig for AAC-ELD.
+ /// ISO 14496-3 Table 1.15
///
- private static void BuildAdtsHeader(byte[] header, int packetLen, int profile, int freqIdx, int chanCfg)
+ private void WriteAudioSpecificConfig(BitWriter bits)
{
- header[0] = 0xFF;
- header[1] = 0xF1; // MPEG-4, Layer 0, no CRC
- header[2] = (byte)(((profile - 1) << 6) | (freqIdx << 2) | (chanCfg >> 2));
- header[3] = (byte)(((chanCfg & 3) << 6) | (packetLen >> 11));
- header[4] = (byte)((packetLen >> 3) & 0xFF);
- header[5] = (byte)(((packetLen & 7) << 5) | 0x1F);
- header[6] = 0xFC;
+ // audioObjectType = 39 (AAC-ELD)
+ // Since 39 >= 31, write 5 bits of 31 + 6 bits of (39-32) = 7
+ bits.WriteBits(31, 5);
+ bits.WriteBits(7, 6);
+
+ // samplingFrequencyIndex
+ bits.WriteBits(_freqIdx, 4);
+ // If freqIdx == 0xF, write 24-bit samplingFrequency (not needed for standard rates)
+
+ // channelConfiguration
+ bits.WriteBits(_channels, 4);
+
+ // SBR/PS extension: not present for AAC-ELD
+ // (AAC-ELD specific config follows)
+
+ // ELDSpecificConfig (ISO 14496-3 Table 4.180)
+ // frameLengthFlag: 0 = 512 samples (480 after windowing), 1 = 480 samples
+ bits.WriteBits(_frameLength == 480 ? 1 : 0, 1);
+ // aacSectionDataResilienceFlag = 0
+ bits.WriteBits(0, 1);
+ // aacScalefactorDataResilienceFlag = 0
+ bits.WriteBits(0, 1);
+ // aacSpectralDataResilienceFlag = 0
+ bits.WriteBits(0, 1);
+
+ // ldSbrPresentFlag = 0 (no LD-SBR)
+ bits.WriteBits(0, 1);
+ // No further extension data
}
private static int GetSampleRateIndex(int sampleRate)
@@ -202,5 +301,43 @@ public void Dispose()
catch { /* ignore cleanup errors */ }
}
}
+
+ ///
+ /// Helper class for writing individual bits into a byte array.
+ /// Used to construct LOAS/LATM bitstream fields.
+ ///
+ private class BitWriter
+ {
+ private readonly List _bytes = new List();
+ private int _currentByte = 0;
+ private int _bitsInCurrentByte = 0;
+
+ public void WriteBits(int value, int numBits)
+ {
+ for (int i = numBits - 1; i >= 0; i--)
+ {
+ _currentByte = (_currentByte << 1) | ((value >> i) & 1);
+ _bitsInCurrentByte++;
+
+ if (_bitsInCurrentByte == 8)
+ {
+ _bytes.Add((byte)_currentByte);
+ _currentByte = 0;
+ _bitsInCurrentByte = 0;
+ }
+ }
+ }
+
+ public byte[] ToByteArray()
+ {
+ if (_bitsInCurrentByte > 0)
+ {
+ // Pad remaining bits with zeros
+ _currentByte <<= (8 - _bitsInCurrentByte);
+ _bytes.Add((byte)_currentByte);
+ }
+ return _bytes.ToArray();
+ }
+ }
}
}
diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs
index e906a08..8fbbaf8 100644
--- a/AirPlay/Listeners/AudioListener.cs
+++ b/AirPlay/Listeners/AudioListener.cs
@@ -27,6 +27,7 @@ public class AudioListener : BaseUdpListener
private readonly OmgHax _omgHax = new OmgHax();
private IDecoder _decoder;
+ private readonly object _decoderLock = new object();
private ulong _sync_time;
private ulong _sync_timestamp;
private ushort _controlSequenceNumber = 0;
@@ -494,94 +495,97 @@ private int RaopRtpResendCallback(Socket cSocket, ushort control_seqnum, ushort
private void InitializeDecoder (Session session)
{
- if (_decoder != null) return;
-
- var audioFormat = session.AudioFormat;
- var spf = session.AudioSamplesPerFrame;
-
- if (audioFormat == AudioFormat.ALAC)
+ lock (_decoderLock)
{
- // RTP info: 96 AppleLossless, 96 352 0 16 40 10 14 2 255 0 0 44100
- // (ALAC -> PCM)
+ if (_decoder != null) return;
- var frameLength = spf > 0 ? spf : 352;
- var numChannels = 2;
- var bitDepth = 16;
- var sampleRate = 44100;
+ var audioFormat = session.AudioFormat;
+ var spf = session.AudioSamplesPerFrame;
- _decoder = new ALACDecoder();
- _decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
- }
- else if (audioFormat == AudioFormat.AAC)
- {
- // RTP info: 96 mpeg4-generic/44100/2, 96 mode=AAC-main; constantDuration=1024
- // (AAC-MAIN -> PCM)
+ if (audioFormat == AudioFormat.ALAC)
+ {
+ // RTP info: 96 AppleLossless, 96 352 0 16 40 10 14 2 255 0 0 44100
+ // (ALAC -> PCM)
- var frameLength = spf > 0 ? spf : 1024;
- var numChannels = 2;
- var bitDepth = 16;
- var sampleRate = 44100;
+ var frameLength = spf > 0 ? spf : 352;
+ var numChannels = 2;
+ var bitDepth = 16;
+ var sampleRate = 44100;
- _decoder = new AACDecoder(TransportType.TT_MP4_RAW, AudioObjectType.AOT_AAC_MAIN, 1);
- _decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
- }
- else if(audioFormat == AudioFormat.AAC_ELD)
- {
- // RTP info: 96 mpeg4-generic/44100/2, 96 mode=AAC-eld; constantDuration=480
- // (AAC-ELD -> PCM) using FFmpeg subprocess decoder
+ _decoder = new ALACDecoder();
+ _decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
+ }
+ else if (audioFormat == AudioFormat.AAC)
+ {
+ // RTP info: 96 mpeg4-generic/44100/2, 96 mode=AAC-main; constantDuration=1024
+ // (AAC-MAIN -> PCM)
- var frameLength = spf > 0 ? spf : 480;
- var numChannels = 2;
- var bitDepth = 16;
- var sampleRate = 44100;
+ var frameLength = spf > 0 ? spf : 1024;
+ var numChannels = 2;
+ var bitDepth = 16;
+ var sampleRate = 44100;
- try
+ _decoder = new AACDecoder(TransportType.TT_MP4_RAW, AudioObjectType.AOT_AAC_MAIN, 1);
+ _decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
+ }
+ else if(audioFormat == AudioFormat.AAC_ELD)
{
- var aacEldDecoder = new Decoders.Implementations.FFmpegAacEldDecoder();
- var ret = aacEldDecoder.Config(sampleRate, numChannels, bitDepth, frameLength);
- if (ret == 0)
+ // RTP info: 96 mpeg4-generic/44100/2, 96 mode=AAC-eld; constantDuration=480
+ // (AAC-ELD -> PCM) using FFmpeg subprocess decoder with LATM wrapping
+
+ var frameLength = spf > 0 ? spf : 480;
+ var numChannels = 2;
+ var bitDepth = 16;
+ var sampleRate = 44100;
+
+ try
{
- _decoder = aacEldDecoder;
+ var aacEldDecoder = new Decoders.Implementations.FFmpegAacEldDecoder();
+ var ret = aacEldDecoder.Config(sampleRate, numChannels, bitDepth, frameLength);
+ if (ret == 0)
+ {
+ _decoder = aacEldDecoder;
+ }
+ else
+ {
+ Console.WriteLine($"FFmpeg AAC-ELD decoder config failed (error {ret}), falling back to SharpJaad AAC-LC");
+ aacEldDecoder.Dispose();
+ _decoder = new AACDecoder(TransportType.TT_MP4_RAW, AudioObjectType.AOT_AAC_LC, 1);
+ _decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
+ }
}
- else
+ catch (Exception ex)
{
- Console.WriteLine($"FFmpeg AAC-ELD decoder config failed (error {ret}), falling back to SharpJaad AAC-LC");
- aacEldDecoder.Dispose();
+ Console.WriteLine($"FFmpeg AAC-ELD decoder unavailable ({ex.Message}), falling back to SharpJaad AAC-LC");
_decoder = new AACDecoder(TransportType.TT_MP4_RAW, AudioObjectType.AOT_AAC_LC, 1);
_decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
}
}
- catch (Exception ex)
+ else if (audioFormat == AudioFormat.PCM)
{
- Console.WriteLine($"FFmpeg AAC-ELD decoder unavailable ({ex.Message}), falling back to SharpJaad AAC-LC");
- _decoder = new AACDecoder(TransportType.TT_MP4_RAW, AudioObjectType.AOT_AAC_LC, 1);
- _decoder.Config(sampleRate, numChannels, bitDepth, frameLength);
- }
- }
- else if (audioFormat == AudioFormat.PCM)
- {
- // Raw PCM audio - no decoding needed
- _decoder = new PCMDecoder();
- }
- else
- {
- // Determine format from compression type if audioFormat is unknown
- if (session.AudioCompressionType == 1)
- {
- // ct=1 = ALAC
- var frameLength = spf > 0 ? spf : 352;
- _decoder = new ALACDecoder();
- _decoder.Config(44100, 2, 16, frameLength);
- }
- else if (session.AudioCompressionType == 0)
- {
- // ct=0 = PCM
+ // Raw PCM audio - no decoding needed
_decoder = new PCMDecoder();
}
else
{
- // Default fallback
- _decoder = new PCMDecoder();
+ // Determine format from compression type if audioFormat is unknown
+ if (session.AudioCompressionType == 1)
+ {
+ // ct=1 = ALAC
+ var frameLength = spf > 0 ? spf : 352;
+ _decoder = new ALACDecoder();
+ _decoder.Config(44100, 2, 16, frameLength);
+ }
+ else if (session.AudioCompressionType == 0)
+ {
+ // ct=0 = PCM
+ _decoder = new PCMDecoder();
+ }
+ else
+ {
+ // Default fallback
+ _decoder = new PCMDecoder();
+ }
}
}
}
From e37794672696c86bb90de0ec69ca2b273406a043 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 01:34:43 +0000
Subject: [PATCH 03/14] Address code review: fix BitWriter thread safety and
_firstFrame race condition
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
.../Decoders/Implementations/FFmpegAacEldDecoder.cs | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
index f74a849..3a76fe7 100644
--- a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
+++ b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
@@ -35,7 +35,7 @@ public class FFmpegAacEldDecoder : IDecoder, IDisposable
private int _frameLength;
private bool _disposed;
private bool _initialized;
- private bool _firstFrame = true;
+ private int _firstFrame = 1; // 1 = true, 0 = false (for thread-safe Interlocked access)
private int _freqIdx;
public AudioFormat Type => AudioFormat.AAC_ELD;
@@ -108,8 +108,8 @@ public int DecodeFrame(byte[] input, ref byte[] output, int length)
try
{
// Build LOAS/LATM frame wrapping the raw AAC-ELD data
- byte[] loasFrame = BuildLoasFrame(input, _firstFrame);
- _firstFrame = false;
+ bool includeConfig = Interlocked.Exchange(ref _firstFrame, 0) == 1;
+ byte[] loasFrame = BuildLoasFrame(input, includeConfig);
_ffmpegInput.Write(loasFrame, 0, loasFrame.Length);
_ffmpegInput.Flush();
@@ -330,13 +330,14 @@ public void WriteBits(int value, int numBits)
public byte[] ToByteArray()
{
+ var result = new List(_bytes);
if (_bitsInCurrentByte > 0)
{
// Pad remaining bits with zeros
- _currentByte <<= (8 - _bitsInCurrentByte);
- _bytes.Add((byte)_currentByte);
+ int padded = _currentByte << (8 - _bitsInCurrentByte);
+ result.Add((byte)padded);
}
- return _bytes.ToArray();
+ return result.ToArray();
}
}
}
From 0983bd0a8ac654c8be4d28698944bf99db47aa39 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 01:48:45 +0000
Subject: [PATCH 04/14] Fix FFmpeg AAC-ELD decoder: revert from LATM to ADTS
wrapping
The previous change used `-f latm` as FFmpeg input format, but FFmpeg has
no `latm` demuxer (only a muxer). This caused FFmpeg to exit immediately,
and every subsequent DecodeFrame call returned -1 (HasExited check),
producing the "Decoding error. Decoder: AAC_ELD Code: -1" error spam.
Revert to ADTS wrapping with `-f aac` which keeps FFmpeg alive. While
ADTS headers can only encode AAC-LC profile (2-bit field), FFmpeg's AAC
decoder handles the actual bitstream content. The ADTS header serves as
a framing/sync mechanism.
Also improved stderr handling to filter per-frame warnings and only log
critical errors with case-insensitive matching.
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
.../Implementations/FFmpegAacEldDecoder.cs | 214 ++++--------------
1 file changed, 43 insertions(+), 171 deletions(-)
diff --git a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
index 3a76fe7..c2f419a 100644
--- a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
+++ b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
@@ -5,15 +5,15 @@
* for AAC-ELD because the pre-built binary doesn't include ER format support.
* SharpJaad.AAC also doesn't support AAC-ELD.
*
- * This decoder pipes raw AAC-ELD frames wrapped in LOAS/LATM through FFmpeg
- * for decoding. ADTS format does NOT support AAC-ELD (its 2-bit profile field
- * cannot represent AOT 39). LATM supports all AAC profiles including AAC-ELD.
+ * This decoder pipes raw AAC-ELD frames wrapped in ADTS headers through FFmpeg
+ * for decoding. While ADTS headers can only encode AAC-LC profile (2-bit field),
+ * FFmpeg's AAC decoder handles the actual bitstream content regardless of the
+ * ADTS profile indicator. The ADTS header serves as a framing/sync mechanism.
*
* Reference: UxPlay uses GStreamer's avdec_aac (which wraps FFmpeg) for the same purpose.
*/
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
@@ -35,8 +35,8 @@ public class FFmpegAacEldDecoder : IDecoder, IDisposable
private int _frameLength;
private bool _disposed;
private bool _initialized;
- private int _firstFrame = 1; // 1 = true, 0 = false (for thread-safe Interlocked access)
- private int _freqIdx;
+ private readonly byte[] _adtsHeader = new byte[7];
+ private int _adtsFreqIdx;
public AudioFormat Type => AudioFormat.AAC_ELD;
@@ -50,17 +50,17 @@ public int Config(int sampleRate, int channels, int bitDepth, int frameLength)
_sampleRate = sampleRate;
_frameLength = frameLength;
_pcmOutputSize = frameLength * channels * (bitDepth / 8);
- _freqIdx = GetSampleRateIndex(sampleRate);
+ _adtsFreqIdx = GetSampleRateIndex(sampleRate);
try
{
// Start FFmpeg process:
- // Input: AAC-ELD frames wrapped in LOAS/LATM via stdin pipe
+ // Input: AAC frames wrapped in ADTS headers via stdin pipe
// Output: raw PCM S16LE via stdout pipe
var psi = new ProcessStartInfo
{
FileName = "ffmpeg",
- Arguments = $"-hide_banner -loglevel error -f latm -i pipe:0 -f s16le -acodec pcm_s16le -ar {sampleRate} -ac {channels} pipe:1",
+ Arguments = $"-hide_banner -loglevel error -f aac -i pipe:0 -f s16le -acodec pcm_s16le -ar {sampleRate} -ac {channels} pipe:1",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
@@ -77,14 +77,26 @@ public int Config(int sampleRate, int channels, int bitDepth, int frameLength)
// Drain stderr in background to prevent pipe deadlock
var stderrThread = new Thread(() =>
{
- try { _ffmpegProcess.StandardError.ReadToEnd(); }
+ try
+ {
+ using var reader = _ffmpegProcess.StandardError;
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ // Only log critical errors, not per-frame decode warnings
+ if (line.Contains("error", StringComparison.OrdinalIgnoreCase))
+ {
+ Console.WriteLine($"FFmpeg: {line}");
+ }
+ }
+ }
catch { /* ignore */ }
});
stderrThread.IsBackground = true;
stderrThread.Start();
_initialized = true;
- Console.WriteLine($"FFmpeg AAC-ELD decoder started (LATM): {sampleRate}Hz, {channels}ch, {bitDepth}bit, frameLength={frameLength}");
+ Console.WriteLine($"FFmpeg AAC-ELD decoder started: {sampleRate}Hz, {channels}ch, {bitDepth}bit, frameLength={frameLength}");
return 0;
}
catch (Exception ex)
@@ -107,11 +119,16 @@ public int DecodeFrame(byte[] input, ref byte[] output, int length)
try
{
- // Build LOAS/LATM frame wrapping the raw AAC-ELD data
- bool includeConfig = Interlocked.Exchange(ref _firstFrame, 0) == 1;
- byte[] loasFrame = BuildLoasFrame(input, includeConfig);
-
- _ffmpegInput.Write(loasFrame, 0, loasFrame.Length);
+ // Wrap the raw AAC frame in an ADTS header for FFmpeg to parse.
+ // ADTS profile is set to AAC-LC (profile=2) since ADTS only has a 2-bit
+ // profile field and cannot represent AAC-ELD. FFmpeg's decoder handles
+ // the actual bitstream content regardless of the ADTS profile indicator.
+ int frameLen = input.Length + 7; // ADTS header is 7 bytes
+ BuildAdtsHeader(_adtsHeader, frameLen, 2, _adtsFreqIdx, _channels);
+
+ // Write ADTS header + raw AAC data to FFmpeg stdin
+ _ffmpegInput.Write(_adtsHeader, 0, 7);
+ _ffmpegInput.Write(input, 0, input.Length);
_ffmpegInput.Flush();
// Read decoded PCM from FFmpeg stdout
@@ -146,124 +163,18 @@ public int DecodeFrame(byte[] input, ref byte[] output, int length)
}
///
- /// Build a LOAS/LATM frame containing the raw AAC-ELD data.
- /// LOAS (Low Overhead Audio Stream) sync layer wraps an AudioMuxElement.
- /// The first frame includes the full StreamMuxConfig; subsequent frames
- /// reuse it (useSameStreamMux=1).
- ///
- private byte[] BuildLoasFrame(byte[] aacFrame, bool includeConfig)
- {
- // Build the AudioMuxElement as a bitstream
- var bits = new BitWriter();
-
- if (includeConfig)
- {
- // useSameStreamMux = 0 (include StreamMuxConfig)
- bits.WriteBits(0, 1);
- WriteStreamMuxConfig(bits);
- }
- else
- {
- // useSameStreamMux = 1 (reuse previous config)
- bits.WriteBits(1, 1);
- }
-
- // PayloadLengthInfo for allStreamsSameTimeFraming=1, progSIndx=0, laySIndx=0
- // Length is encoded as: N bytes of 0xFF followed by a final byte < 0xFF
- int remaining = aacFrame.Length;
- while (remaining >= 255)
- {
- bits.WriteBits(255, 8);
- remaining -= 255;
- }
- bits.WriteBits(remaining, 8);
-
- // PayloadMux: the raw AAC-ELD frame data
- for (int i = 0; i < aacFrame.Length; i++)
- {
- bits.WriteBits(aacFrame[i], 8);
- }
-
- // otherDataPresent = 0 already implied by StreamMuxConfig setting
-
- byte[] audioMuxElement = bits.ToByteArray();
-
- // LOAS sync layer: 0x56E0 | (length & 0x1FFF)
- int audioMuxLength = audioMuxElement.Length;
- byte[] loasFrame = new byte[3 + audioMuxLength];
- loasFrame[0] = 0x56;
- loasFrame[1] = (byte)(0xE0 | ((audioMuxLength >> 8) & 0x1F));
- loasFrame[2] = (byte)(audioMuxLength & 0xFF);
- Array.Copy(audioMuxElement, 0, loasFrame, 3, audioMuxLength);
-
- return loasFrame;
- }
-
- ///
- /// Write StreamMuxConfig for AAC-ELD into the bitstream.
- /// ISO 14496-3 Table 1.42
- ///
- private void WriteStreamMuxConfig(BitWriter bits)
- {
- // audioMuxVersion = 0
- bits.WriteBits(0, 1);
- // allStreamsSameTimeFraming = 1
- bits.WriteBits(1, 1);
- // numSubFrames = 0 (1 subframe)
- bits.WriteBits(0, 6);
- // numProgram = 0 (1 program)
- bits.WriteBits(0, 4);
- // numLayer = 0 (1 layer)
- bits.WriteBits(0, 3);
-
- // AudioSpecificConfig for AAC-ELD (ISO 14496-3 Table 1.15)
- WriteAudioSpecificConfig(bits);
-
- // frameLengthType = 0 (variable frame length)
- bits.WriteBits(0, 3);
- // latmBufferFullness = 0xFF (variable bitrate)
- bits.WriteBits(0xFF, 8);
-
- // otherDataPresent = 0
- bits.WriteBits(0, 1);
- // crcCheckPresent = 0
- bits.WriteBits(0, 1);
- }
-
- ///
- /// Write AudioSpecificConfig for AAC-ELD.
- /// ISO 14496-3 Table 1.15
+ /// Build a 7-byte ADTS header for wrapping raw AAC frames.
+ /// Used as a framing/sync mechanism for FFmpeg's AAC demuxer.
///
- private void WriteAudioSpecificConfig(BitWriter bits)
+ private static void BuildAdtsHeader(byte[] header, int packetLen, int profile, int freqIdx, int chanCfg)
{
- // audioObjectType = 39 (AAC-ELD)
- // Since 39 >= 31, write 5 bits of 31 + 6 bits of (39-32) = 7
- bits.WriteBits(31, 5);
- bits.WriteBits(7, 6);
-
- // samplingFrequencyIndex
- bits.WriteBits(_freqIdx, 4);
- // If freqIdx == 0xF, write 24-bit samplingFrequency (not needed for standard rates)
-
- // channelConfiguration
- bits.WriteBits(_channels, 4);
-
- // SBR/PS extension: not present for AAC-ELD
- // (AAC-ELD specific config follows)
-
- // ELDSpecificConfig (ISO 14496-3 Table 4.180)
- // frameLengthFlag: 0 = 512 samples (480 after windowing), 1 = 480 samples
- bits.WriteBits(_frameLength == 480 ? 1 : 0, 1);
- // aacSectionDataResilienceFlag = 0
- bits.WriteBits(0, 1);
- // aacScalefactorDataResilienceFlag = 0
- bits.WriteBits(0, 1);
- // aacSpectralDataResilienceFlag = 0
- bits.WriteBits(0, 1);
-
- // ldSbrPresentFlag = 0 (no LD-SBR)
- bits.WriteBits(0, 1);
- // No further extension data
+ header[0] = 0xFF;
+ header[1] = 0xF1; // MPEG-4, Layer 0, no CRC
+ header[2] = (byte)(((profile - 1) << 6) | (freqIdx << 2) | (chanCfg >> 2));
+ header[3] = (byte)(((chanCfg & 3) << 6) | (packetLen >> 11));
+ header[4] = (byte)((packetLen >> 3) & 0xFF);
+ header[5] = (byte)(((packetLen & 7) << 5) | 0x1F);
+ header[6] = 0xFC;
}
private static int GetSampleRateIndex(int sampleRate)
@@ -301,44 +212,5 @@ public void Dispose()
catch { /* ignore cleanup errors */ }
}
}
-
- ///
- /// Helper class for writing individual bits into a byte array.
- /// Used to construct LOAS/LATM bitstream fields.
- ///
- private class BitWriter
- {
- private readonly List _bytes = new List();
- private int _currentByte = 0;
- private int _bitsInCurrentByte = 0;
-
- public void WriteBits(int value, int numBits)
- {
- for (int i = numBits - 1; i >= 0; i--)
- {
- _currentByte = (_currentByte << 1) | ((value >> i) & 1);
- _bitsInCurrentByte++;
-
- if (_bitsInCurrentByte == 8)
- {
- _bytes.Add((byte)_currentByte);
- _currentByte = 0;
- _bitsInCurrentByte = 0;
- }
- }
- }
-
- public byte[] ToByteArray()
- {
- var result = new List(_bytes);
- if (_bitsInCurrentByte > 0)
- {
- // Pad remaining bits with zeros
- int padded = _currentByte << (8 - _bitsInCurrentByte);
- result.Add((byte)padded);
- }
- return result.ToArray();
- }
- }
}
}
From b6f299c16454655f066a8b0947063f7be2096f53 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 09:47:18 +0000
Subject: [PATCH 05/14] Fix audio threading: separate cipher per handler, add
buffer locking, add exception handling
The AES-CBC cipher is stateful (tracks IV internally). Sharing one instance
between the concurrent control and data socket handlers caused cipher state
corruption, leading to decryption failures and crashes.
Fix: Each handler now creates its own cipher instance. RaopBuffer access is
synchronized with a lock. Try-catch blocks prevent socket errors from
silently killing the handler tasks.
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/Listeners/AudioListener.cs | 235 +++++++++++++++++------------
1 file changed, 140 insertions(+), 95 deletions(-)
diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs
index 8fbbaf8..8a02d16 100644
--- a/AirPlay/Listeners/AudioListener.cs
+++ b/AirPlay/Listeners/AudioListener.cs
@@ -23,11 +23,11 @@ public class AudioListener : BaseUdpListener
private readonly IRtspReceiver _receiver;
private readonly string _sessionId;
- private IBufferedCipher _aesCbcDecrypt;
private readonly OmgHax _omgHax = new OmgHax();
private IDecoder _decoder;
private readonly object _decoderLock = new object();
+ private readonly object _bufferLock = new object();
private ulong _sync_time;
private ulong _sync_timestamp;
private ushort _controlSequenceNumber = 0;
@@ -43,7 +43,6 @@ public AudioListener(IRtspReceiver receiver, string sessionId, ushort cport, ush
_dConfig = dConfig ?? throw new ArgumentNullException(nameof(dConfig));
_raopBuffer = RaopBufferInit();
- _aesCbcDecrypt = CipherUtilities.GetCipher("AES/CBC/NoPadding");
}
public override async Task OnRawCSocketAsync(Socket cSocket, CancellationToken cancellationToken)
@@ -52,6 +51,9 @@ public override async Task OnRawCSocketAsync(Socket cSocket, CancellationToken c
_cSocket = cSocket;
+ // Each handler gets its own cipher instance (cipher is stateful, not thread-safe)
+ var aesCbcDecrypt = CipherUtilities.GetCipher("AES/CBC/NoPadding");
+
// Get session by active-remove header value
var session = await SessionManager.Current.GetSessionAsync(_sessionId);
@@ -72,70 +74,90 @@ public override async Task OnRawCSocketAsync(Socket cSocket, CancellationToken c
do
{
- var cret = cSocket.Receive(packet, 0, RAOP_PACKET_LENGTH, SocketFlags.None, out SocketError error);
- if(error != SocketError.Success)
- {
- continue;
- }
-
- var mem = new MemoryStream(packet);
- using (var reader = new BinaryReader(mem))
+ try
{
- mem.Position = 1;
- int type_c = reader.ReadByte() & ~0x80;
- if (type_c == 0x56)
+ var cret = cSocket.Receive(packet, 0, RAOP_PACKET_LENGTH, SocketFlags.None, out SocketError error);
+ if(error != SocketError.Success)
{
- InitAesCbcCipher(session.DecryptedAesKey, session.EcdhShared, session.AesIv);
-
- mem.Position = 4;
- var data = reader.ReadBytes(cret - 4);
-
- var ret = RaopBufferQueue(_raopBuffer, data, (ushort)data.Length, session);
+ continue;
+ }
- // Dequeue and play audio received on control socket (used during screen mirroring)
- byte[] audiobuf;
- int audiobuflen = 0;
- uint timestamp = 0;
- while ((audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, true)) != null)
+ var mem = new MemoryStream(packet);
+ using (var reader = new BinaryReader(mem))
+ {
+ mem.Position = 1;
+ int type_c = reader.ReadByte() & ~0x80;
+ if (type_c == 0x56)
{
- if (audiobuf.Length == 0 || audiobuflen <= 0)
- continue;
-
- var pcmData = new PcmData();
- pcmData.Length = audiobuflen;
- pcmData.Data = audiobuf;
- pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
-
- _receiver.OnPCMData(pcmData);
+ InitAesCbcCipher(aesCbcDecrypt, session.DecryptedAesKey, session.EcdhShared, session.AesIv);
+
+ mem.Position = 4;
+ var data = reader.ReadBytes(cret - 4);
+
+ int ret;
+ lock (_bufferLock)
+ {
+ ret = RaopBufferQueue(_raopBuffer, data, (ushort)data.Length, session, aesCbcDecrypt);
+ }
+
+ // Dequeue and play audio received on control socket (used during screen mirroring)
+ byte[] audiobuf;
+ int audiobuflen = 0;
+ uint timestamp = 0;
+ lock (_bufferLock)
+ {
+ while ((audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, true)) != null)
+ {
+ if (audiobuf.Length == 0 || audiobuflen <= 0)
+ continue;
+
+ var pcmData = new PcmData();
+ pcmData.Length = audiobuflen;
+ pcmData.Data = audiobuf;
+ pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
+
+ _receiver.OnPCMData(pcmData);
+ }
+ }
+ }
+ else if (type_c == 0x54)
+ {
+ /**
+ * packetlen = 20
+ * bytes description
+ 8 RTP header without SSRC
+ 8 current NTP time
+ 4 RTP timestamp for the next audio packet
+ */
+
+ mem.Position = 8;
+ uint ntp_seconds = (uint)reader.ReadInt32();
+ uint ntp_fraction = (uint)reader.ReadInt32();
+ ulong ntp_time = ((ulong)ntp_seconds * 1000000UL) + (((ulong)ntp_fraction * 1000000UL) >> 32);
+ uint rtp_timestamp = (uint)((packet[4] << 24) | (packet[5] << 16) | (packet[6] << 8) | packet[7]);
+ uint next_timestamp = (uint)((packet[16] << 24) | (packet[17] << 16) | (packet[18] << 8) | packet[19]);
+
+ _sync_time = ntp_time - OFFSET_1900_TO_1970 * 1000000UL;
+ _sync_timestamp = rtp_timestamp;
}
}
- else if (type_c == 0x54)
- {
- /**
- * packetlen = 20
- * bytes description
- 8 RTP header without SSRC
- 8 current NTP time
- 4 RTP timestamp for the next audio packet
- */
-
- mem.Position = 8;
- uint ntp_seconds = (uint)reader.ReadInt32();
- uint ntp_fraction = (uint)reader.ReadInt32();
- ulong ntp_time = ((ulong)ntp_seconds * 1000000UL) + (((ulong)ntp_fraction * 1000000UL) >> 32);
- uint rtp_timestamp = (uint)((packet[4] << 24) | (packet[5] << 16) | (packet[6] << 8) | packet[7]);
- uint next_timestamp = (uint)((packet[16] << 24) | (packet[17] << 16) | (packet[18] << 8) | packet[19]);
-
- _sync_time = ntp_time - OFFSET_1900_TO_1970 * 1000000UL;
- _sync_timestamp = rtp_timestamp;
- }
- else
- {
- Console.WriteLine("Unknown packet");
- }
- }
- Array.Fill(packet, 0);
+ Array.Fill(packet, 0);
+ }
+ catch (ObjectDisposedException)
+ {
+ // Socket was closed (StopAsync called)
+ break;
+ }
+ catch (SocketException)
+ {
+ // Socket error (e.g., ICMP port unreachable on Windows)
+ break;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Audio control socket error: {ex.Message}");
+ }
} while (!cancellationToken.IsCancellationRequested);
Console.WriteLine("Closing audio control socket..");
@@ -145,6 +167,9 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
{
Console.WriteLine("Initializing recevie audio data from socket..");
+ // Each handler gets its own cipher instance (cipher is stateful, not thread-safe)
+ var aesCbcDecrypt = CipherUtilities.GetCipher("AES/CBC/NoPadding");
+
// Get current session
var session = await SessionManager.Current.GetSessionAsync(_sessionId);
@@ -165,53 +190,73 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
do
{
- var dret = dSocket.Receive(packet, 0, RAOP_PACKET_LENGTH, SocketFlags.None, out SocketError error);
- if (error != SocketError.Success)
+ try
{
- continue;
- }
+ var dret = dSocket.Receive(packet, 0, RAOP_PACKET_LENGTH, SocketFlags.None, out SocketError error);
+ if (error != SocketError.Success)
+ {
+ continue;
+ }
- // RTP payload type
- int type_d = packet[1] & ~0x80;
+ // RTP payload type
+ int type_d = packet[1] & ~0x80;
- if (packet.Length >= 12)
- {
- InitAesCbcCipher(session.DecryptedAesKey, session.EcdhShared, session.AesIv);
+ if (packet.Length >= 12)
+ {
+ InitAesCbcCipher(aesCbcDecrypt, session.DecryptedAesKey, session.EcdhShared, session.AesIv);
- bool no_resend = false;
- int buf_ret;
- byte[] audiobuf;
- int audiobuflen = 0;
- uint timestamp = 0;
+ bool no_resend = false;
+ int buf_ret;
+ byte[] audiobuf;
+ int audiobuflen = 0;
+ uint timestamp = 0;
- buf_ret = RaopBufferQueue(_raopBuffer, packet, (ushort)dret, session);
+ lock (_bufferLock)
+ {
+ buf_ret = RaopBufferQueue(_raopBuffer, packet, (ushort)dret, session, aesCbcDecrypt);
+ }
- //if(_raopBuffer.LastSeqNum - _raopBuffer.FirstSeqNum > (RAOP_BUFFER_LENGTH / 8))
- //{
// Dequeue all available frames from buffer
- while ((audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, no_resend)) != null)
+ lock (_bufferLock)
{
- if (audiobuf.Length == 0 || audiobuflen <= 0)
- continue;
+ while ((audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, no_resend)) != null)
+ {
+ if (audiobuf.Length == 0 || audiobuflen <= 0)
+ continue;
- var pcmData = new PcmData();
- pcmData.Length = audiobuflen;
- pcmData.Data = audiobuf;
+ var pcmData = new PcmData();
+ pcmData.Length = audiobuflen;
+ pcmData.Data = audiobuf;
- pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
+ pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
- _receiver.OnPCMData(pcmData);
+ _receiver.OnPCMData(pcmData);
+ }
}
- //}
- /* Handle possible resend requests */
- if (!no_resend)
- {
- RaopBufferHandleResends(_raopBuffer, _cSocket, _controlSequenceNumber);
+ /* Handle possible resend requests */
+ if (!no_resend)
+ {
+ RaopBufferHandleResends(_raopBuffer, _cSocket, _controlSequenceNumber);
+ }
}
- }
- Array.Clear(packet, 0, packet.Length);
+ Array.Clear(packet, 0, packet.Length);
+ }
+ catch (ObjectDisposedException)
+ {
+ // Socket was closed (StopAsync called)
+ break;
+ }
+ catch (SocketException)
+ {
+ // Socket error (e.g., ICMP port unreachable on Windows)
+ break;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Audio data socket error: {ex.Message}");
+ }
} while (!cancellationToken.IsCancellationRequested);
Console.WriteLine("Closing audio data socket..");
@@ -224,7 +269,7 @@ public Task FlushAsync(int nextSequence)
return Task.CompletedTask;
}
- private void InitAesCbcCipher(byte[] aesKey, byte[] ecdhShared, byte[] aesIv)
+ private void InitAesCbcCipher(IBufferedCipher aesCbcDecrypt, byte[] aesKey, byte[] ecdhShared, byte[] aesIv)
{
byte[] hash = Utilities.Hash(aesKey, ecdhShared);
byte[] eaesKey = Utilities.CopyOfRange(hash, 0, 16);
@@ -232,7 +277,7 @@ private void InitAesCbcCipher(byte[] aesKey, byte[] ecdhShared, byte[] aesIv)
var keyParameter = ParameterUtilities.CreateKeyParameter("AES", eaesKey);
var cipherParameters = new ParametersWithIV(keyParameter, aesIv, 0, aesIv.Length);
- _aesCbcDecrypt.Init(false, cipherParameters);
+ aesCbcDecrypt.Init(false, cipherParameters);
}
private RaopBuffer RaopBufferInit()
@@ -259,7 +304,7 @@ private RaopBuffer RaopBufferInit()
return raop_buffer;
}
- public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, Session session)
+ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, Session session, IBufferedCipher aesCbcDecrypt)
{
int encryptedlen;
RaopBufferEntry entry;
@@ -310,7 +355,7 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen,
if (encryptedlen > 0)
{
- _aesCbcDecrypt.ProcessBytes(data, 12, encryptedlen, data, 12);
+ aesCbcDecrypt.ProcessBytes(data, 12, encryptedlen, data, 12);
Array.Copy(data, 12, raw, 0, encryptedlen);
}
From 383b580db947c5ab955a3854e0f79e919161e5da Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 09:49:51 +0000
Subject: [PATCH 06/14] Move OnPCMData calls outside buffer lock to avoid
blocking
Dequeue PCM data into a batch while holding the lock, then deliver
outside the lock. This prevents the receiver's processing time from
blocking the other socket handler.
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/Listeners/AudioListener.cs | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs
index 8a02d16..45824e0 100644
--- a/AirPlay/Listeners/AudioListener.cs
+++ b/AirPlay/Listeners/AudioListener.cs
@@ -101,6 +101,7 @@ public override async Task OnRawCSocketAsync(Socket cSocket, CancellationToken c
}
// Dequeue and play audio received on control socket (used during screen mirroring)
+ var pcmBatch = new System.Collections.Generic.List();
byte[] audiobuf;
int audiobuflen = 0;
uint timestamp = 0;
@@ -116,9 +117,15 @@ public override async Task OnRawCSocketAsync(Socket cSocket, CancellationToken c
pcmData.Data = audiobuf;
pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
- _receiver.OnPCMData(pcmData);
+ pcmBatch.Add(pcmData);
}
}
+
+ // Deliver PCM outside the lock to avoid blocking the data handler
+ foreach (var pcm in pcmBatch)
+ {
+ _receiver.OnPCMData(pcm);
+ }
}
else if (type_c == 0x54)
{
@@ -217,6 +224,7 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
}
// Dequeue all available frames from buffer
+ var pcmBatch = new System.Collections.Generic.List();
lock (_bufferLock)
{
while ((audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, no_resend)) != null)
@@ -230,10 +238,16 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c
pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time;
- _receiver.OnPCMData(pcmData);
+ pcmBatch.Add(pcmData);
}
}
+ // Deliver PCM outside the lock to avoid blocking the control handler
+ foreach (var pcm in pcmBatch)
+ {
+ _receiver.OnPCMData(pcm);
+ }
+
/* Handle possible resend requests */
if (!no_resend)
{
From 7c9b8d91af5da00221ea60f51b38186a37bedda3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 10:02:49 +0000
Subject: [PATCH 07/14] Add comprehensive debug logging throughout audio
pipeline
All log lines prefixed with [DEBUG-*] tags for easy filtering:
- [DEBUG-C] Control socket (port 7002): packet types, counts, exit reason
- [DEBUG-D] Data socket (port 7003): packet counts, queue/dequeue stats
- [DEBUG-FFMPEG] FFmpeg decoder: frame I/O, process health, ALL stderr
- [DEBUG-FFMPEG-STDERR] Raw FFmpeg stderr output
- [DEBUG-PCM] PCM data delivery with silence detection
- [DEBUG-AUDIO] AudioOutputService sample delivery and queue state
- [DEBUG-SETUP] AudioListener creation/stop lifecycle
- [DEBUG-TEARDOWN] Session teardown with stream type
- [DEBUG-UDP] Socket start/stop with port numbers
This will show exactly where audio data flow stops.
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
---
AirPlay/AirPlayService.cs | 13 ++
.../Implementations/FFmpegAacEldDecoder.cs | 30 +++-
AirPlay/Listeners/AirTunesListener.cs | 9 +-
AirPlay/Listeners/AudioListener.cs | 130 +++++++++++++++---
AirPlay/Listeners/Bases/BaseUdpListener.cs | 16 ++-
AirPlay/Services/AudioOutputService.cs | 21 ++-
6 files changed, 184 insertions(+), 35 deletions(-)
diff --git a/AirPlay/AirPlayService.cs b/AirPlay/AirPlayService.cs
index dea74d3..0d4781e 100644
--- a/AirPlay/AirPlayService.cs
+++ b/AirPlay/AirPlayService.cs
@@ -147,8 +147,21 @@ public async Task StartAsync(CancellationToken cancellationToken)
};
_audiobuf = new List();
+ int pcmReceivedCount = 0;
_airPlayReceiver.OnPCMDataReceived += (s, e) =>
{
+ pcmReceivedCount++;
+ if (pcmReceivedCount <= 5 || pcmReceivedCount % 500 == 0)
+ {
+ bool allZeros = true;
+ int checkLen = Math.Min(e.Length, 100);
+ for (int i = 0; i < checkLen; i++)
+ {
+ if (e.Data[i] != 0) { allZeros = false; break; }
+ }
+ Console.WriteLine($"[DEBUG-PCM] OnPCMDataReceived #{pcmReceivedCount}: len={e.Length}, allZeros={allZeros}");
+ }
+
// Play audio through speakers
lock (_audioOutputLock)
{
diff --git a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
index c2f419a..4d5caae 100644
--- a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
+++ b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs
@@ -37,6 +37,8 @@ public class FFmpegAacEldDecoder : IDecoder, IDisposable
private bool _initialized;
private readonly byte[] _adtsHeader = new byte[7];
private int _adtsFreqIdx;
+ private int _decodeCallCount;
+ private int _totalFramesDecoded;
public AudioFormat Type => AudioFormat.AAC_ELD;
@@ -83,11 +85,8 @@ public int Config(int sampleRate, int channels, int bitDepth, int frameLength)
string line;
while ((line = reader.ReadLine()) != null)
{
- // Only log critical errors, not per-frame decode warnings
- if (line.Contains("error", StringComparison.OrdinalIgnoreCase))
- {
- Console.WriteLine($"FFmpeg: {line}");
- }
+ // Log all FFmpeg stderr output for debugging
+ Console.WriteLine($"[DEBUG-FFMPEG-STDERR] {line}");
}
}
catch { /* ignore */ }
@@ -115,10 +114,17 @@ public int GetOutputStreamLength()
public int DecodeFrame(byte[] input, ref byte[] output, int length)
{
if (!_initialized || _ffmpegProcess == null || _ffmpegProcess.HasExited)
+ {
+ _decodeCallCount++;
+ if (_decodeCallCount <= 3 || _decodeCallCount % 500 == 0)
+ Console.WriteLine($"[DEBUG-FFMPEG] DecodeFrame called but process not ready: initialized={_initialized}, process={_ffmpegProcess != null}, hasExited={_ffmpegProcess?.HasExited}");
return -1;
+ }
try
{
+ _decodeCallCount++;
+
// Wrap the raw AAC frame in an ADTS header for FFmpeg to parse.
// ADTS profile is set to AAC-LC (profile=2) since ADTS only has a 2-bit
// profile field and cannot represent AAC-ELD. FFmpeg's decoder handles
@@ -131,6 +137,9 @@ public int DecodeFrame(byte[] input, ref byte[] output, int length)
_ffmpegInput.Write(input, 0, input.Length);
_ffmpegInput.Flush();
+ if (_decodeCallCount <= 3)
+ Console.WriteLine($"[DEBUG-FFMPEG] Wrote frame #{_decodeCallCount}: inputLen={input.Length}, adtsFrameLen={frameLen}, expecting {_pcmOutputSize} bytes PCM output");
+
// Read decoded PCM from FFmpeg stdout
int bytesToRead = _pcmOutputSize;
int totalRead = 0;
@@ -148,16 +157,25 @@ public int DecodeFrame(byte[] input, ref byte[] output, int length)
totalRead += read;
}
+ _totalFramesDecoded++;
+
+ if (_decodeCallCount <= 5 || _decodeCallCount % 500 == 0)
+ Console.WriteLine($"[DEBUG-FFMPEG] Frame #{_decodeCallCount}: read {totalRead}/{bytesToRead} bytes (attemptsLeft={maxAttempts}), totalDecoded={_totalFramesDecoded}");
+
if (totalRead < bytesToRead)
{
// Partial read - zero fill the rest
Array.Clear(output, totalRead, bytesToRead - totalRead);
+ if (_decodeCallCount <= 10)
+ Console.WriteLine($"[DEBUG-FFMPEG] WARNING: Partial read! Only {totalRead} of {bytesToRead} bytes, zero-filled remainder");
}
return 0;
}
- catch (Exception)
+ catch (Exception ex)
{
+ if (_decodeCallCount <= 5 || _decodeCallCount % 500 == 0)
+ Console.WriteLine($"[DEBUG-FFMPEG] DecodeFrame exception #{_decodeCallCount}: {ex.GetType().Name}: {ex.Message}");
return -1;
}
}
diff --git a/AirPlay/Listeners/AirTunesListener.cs b/AirPlay/Listeners/AirTunesListener.cs
index 669ba1d..82f463b 100644
--- a/AirPlay/Listeners/AirTunesListener.cs
+++ b/AirPlay/Listeners/AirTunesListener.cs
@@ -454,14 +454,17 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
// (ports 7002/7003 must be released first)
if (session.AudioControlListener != null)
{
+ Console.WriteLine("[DEBUG-SETUP] Stopping existing AudioListener before creating new one...");
try { await session.AudioControlListener.StopAsync(); }
- catch (Exception ex) { Console.WriteLine($"Error stopping old audio listener: {ex.Message}"); }
+ catch (Exception ex) { Console.WriteLine($"[DEBUG-SETUP] Error stopping old audio listener: {ex.Message}"); }
session.AudioControlListener = null;
}
+ Console.WriteLine($"[DEBUG-SETUP] Creating new AudioListener: format={session.AudioFormat}, ct={session.AudioCompressionType}, spf={session.AudioSamplesPerFrame}, mirroring={session.MirroringSession}");
// Start 'AudioListener' (handle PCM/AAC/ALAC data received from iOS/macOS
var control = new AudioListener(_receiver, session.SessionId, 7002, 7003, _dumpConfig);
await control.StartAsync(cancellationToken).ConfigureAwait(false);
+ Console.WriteLine("[DEBUG-SETUP] AudioListener started successfully");
session.AudioControlListener = control;
}
@@ -564,6 +567,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
}
if (request.Type == RequestType.TEARDOWN)
{
+ Console.WriteLine("[DEBUG-TEARDOWN] TEARDOWN request received");
var plistReader = new BinaryPlistReader();
using (var mem = new MemoryStream(request.Body))
{
@@ -574,10 +578,12 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
// Always one foreach request
var stream = (Dictionary