Fix screen mirroring audio: native FDK-AAC decoder, buffer overflow fix, CI packaging - #5
Conversation
…ace 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>
… condition Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
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>
… 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>
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>
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>
Trace the full RTSP request/response flow to identify why iOS tears down audio immediately during screen mirroring: - [DEBUG-RTSP] logs every request type and response status - [DEBUG-SETUP] logs all stream properties from SETUP requests - [DEBUG-QUEUE] logs RaopBufferQueue decisions (no-data markers, old seqnums) - [DEBUG-RTSP] logs unrecognized data that doesn't match any request pattern Also add SETPEERS support: - iOS sends SETPEERS requests for peer discovery - Without a handler, these were silently dropped (no regex match, no response) - This could cause iOS to think the connection is broken - Added to RequestConst, RequestType enum, Request parser, and regex pattern Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
…ecificConfig ADTS headers can only encode AAC-Main/LC/SSR profiles (2-bit field) and cannot represent AAC-ELD (AOT 39). FFmpeg was parsing AAC-ELD frames as AAC-LC, causing "skip_data_stream_element: Input buffer exhausted" errors. LOAS (Low Overhead Audio Stream, ISO 14496-3) carries a full AudioSpecificConfig that properly signals AAC-ELD. The ASC (f8e85000) matches UxPlay's reference implementation: - AOT 39 (ER AAC-ELD) - 44100 Hz - 2 channels (stereo) - frameLengthFlag = 1 FFmpeg demuxer changed from `-f aac` to `-f loas`. Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
… spf Add [DEBUG-DECRYPT] logging to show first bytes of decrypted AAC-ELD payload. Valid frames should start with 0x8c/0x8d/0x8e/0x80/0x81/0x82/0x20 (per UxPlay reference). This will help identify if AES-CBC decryption is producing valid AAC-ELD data. Also fix AudioSpecificConfig frameLengthFlag to match the actual frame length from iOS SETUP (spf=480 → frameLengthFlag=0). Previously hardcoded to 1 (512 samples) which mismatches the 480-sample frames iOS sends. Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
…bug logging Replace FFmpeg subprocess + fragile ADTS/LOAS wrapping with direct native FDK-AAC library calls via P/Invoke, matching the approach used by both itskenny0/airplayreceiver and UxPlay reference implementations. Key changes: - NativeFdkAacEldDecoder: P/Invoke to libfdk-aac-2.dll with TT_MP4_RAW transport. Raw decrypted AAC-ELD frames fed directly, no container framing. - AudioSpecificConfig: f8e85000 (matches UxPlay/itskenny0 exactly) - Decoder fallback chain: native FDK-AAC → FFmpeg subprocess → SharpJaad - Debug logging: [DEBUG-FDK] for decoder, [DEBUG-DECRYPT] for payload hex, [DEBUG-DECODE] for decode results with silence detection - SETUP debug: shows unsigned port values for clarity User needs libfdk-aac-2.dll in the application directory. Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
…g + CI deps Three fixes for screen mirroring audio stuttering: 1. RaopBufferDequeue: Change buflen from short to int (cast via ushort). When LastSeqNum - FirstSeqNum > 32767, short overflows negative, causing dequeue to think buffer is empty and stop processing. 2. Set no_resend=true during mirroring. Real-time audio should not wait for packet resends - this caused dequeue to get stuck on missing entries while the queue grew unboundedly, adding seconds of latency. 3. FlushAsync: Add _bufferLock synchronization for thread safety. CI workflow: Add FFmpeg (ffmpeg.exe + ffplay.exe) and libfdk-aac-2.dll to build artifacts so users don't need to download them separately. Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
1. CI workflow: Replace non-existent nicholastay/libfdk-aac-build with building FDK-AAC from source using MSYS2 (pre-installed on windows-latest runners). Uses mstorsjo/fdk-aac official source with CMake. 2. README.md: Complete rewrite reflecting current project state - FDK-AAC native decoder for mirroring audio, FFmpeg for video, simplified build instructions, architecture diagram. 3. README_zh.md: Chinese version of the README. Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses missing audio during AirPlay screen mirroring by reworking the AAC-ELD decoding path, improving RAOP buffering behavior during mirroring, and updating CI packaging/docs to include required native dependencies.
Changes:
- Added a native FDK-AAC AAC-ELD decoder (P/Invoke) with fallback to FFmpeg-based decoding.
- Fixed RAOP buffer dequeue overflow behavior and adjusted mirroring mode to skip resend waits.
- Updated CI to bundle FFmpeg + libfdk-aac, and refreshed English/Chinese READMEs.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 25 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Rewritten quick start, dependencies, and architecture notes (EN). |
| README_zh.md | Added Chinese documentation with build/run instructions. |
| AirPlay/Services/AudioOutputService.cs | Added more detailed audio output diagnostics. |
| AirPlay/Models/TcpListeners/Request.cs | Added SETPEERS request type detection. |
| AirPlay/Models/Enums/RequestType.cs | Added SETPEERS enum entry. |
| AirPlay/Models/Enums/RequestConst.cs | Added SETPEERS magic-number constant. |
| AirPlay/Listeners/Bases/BaseUdpListener.cs | Added UDP listener start/stop diagnostics and stored ports. |
| AirPlay/Listeners/Bases/BaseTcpListener.cs | Added SETPEERS to regex splitter + extra RTSP diagnostics. |
| AirPlay/Listeners/AudioListener.cs | Thread-safety improvements, mirroring resend behavior, buffer overflow fix, extensive debug logging. |
| AirPlay/Listeners/AirTunesListener.cs | Pass mirroring flag into AudioListener; RTSP logging; OPTIONS advertises SETPEERS. |
| AirPlay/Decoders/Implementations/NativeFdkAacEldDecoder.cs | New native FDK-AAC AAC-ELD decoder via P/Invoke. |
| AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs | Switched FFmpeg decoder to LOAS/LATM framing and improved diagnostics. |
| AirPlay/AirPlayService.cs | Added PCM receive diagnostics before pushing to audio output. |
| .github/workflows/build.yml | Downloads FFmpeg, builds libfdk-aac in CI, and bundles into artifacts. |
Comments suppressed due to low confidence (30)
AirPlay/Listeners/AudioListener.cs:646
- This comparison is always false.
for (seqnum = raop_buffer.FirstSeqNum; Utilities.SeqNumCmp(seqnum, raop_buffer.LastSeqNum) < 0; seqnum++)
AirPlay/AirPlayService.cs:22
- The contents of this container are never accessed.
private List<byte> _audiobuf;
AirPlay/Listeners/Bases/BaseTcpListener.cs:126
- Poor error handling: empty catch block.
catch (System.IO.IOException) { }
AirPlay/Services/AudioOutputService.cs:99
- Poor error handling: empty catch block.
catch { }
AirPlay/Services/AudioOutputService.cs:242
- Poor error handling: empty catch block.
catch { }
AirPlay/Services/AudioOutputService.cs:276
- Poor error handling: empty catch block.
catch { }
AirPlay/Listeners/AudioListener.cs:642
- Condition always evaluates to 'false'.
if (Utilities.SeqNumCmp(raop_buffer.FirstSeqNum, raop_buffer.LastSeqNum) < 0)
AirPlay/Listeners/AirTunesListener.cs:344
- Inefficient use of 'ContainsKey' and indexer.
if (stream.ContainsKey("audioFormat"))
AirPlay/Listeners/AirTunesListener.cs:352
- Inefficient use of 'ContainsKey' and indexer.
if (stream.ContainsKey("ct"))
AirPlay/Listeners/AirTunesListener.cs:356
- Inefficient use of 'ContainsKey' and indexer.
if (stream.ContainsKey("spf"))
AirPlay/Listeners/AirTunesListener.cs:360
- Inefficient use of 'ContainsKey' and indexer.
if (stream.ContainsKey("controlPort"))
AirPlay/Listeners/AirTunesListener.cs:504
- Inefficient use of 'ContainsKey' and indexer.
if (request.Headers.ContainsKey("Content-Type"))
AirPlay/Listeners/AirTunesListener.cs:564
- Inefficient use of 'ContainsKey' and indexer.
if (request.Headers.ContainsKey("RTP-Info"))
AirPlay/Models/TcpListeners/Request.cs:210
- Inefficient use of 'ContainsKey' and indexer.
if(_headers.ContainsKey("CSeq"))
AirPlay/Listeners/Bases/BaseTcpListener.cs:121
- String concatenation in loop: use 'StringBuilder'.
raw += string.Join(string.Empty, buffer.Take(readCount).Select(b => b.ToString("X2")));
AirPlay/Listeners/Bases/BaseTcpListener.cs:206
- String concatenation in loop: use 'StringBuilder'.
format += $"{header.Name}: {string.Join(",", header.Values)}\r\n";
AirPlay/Listeners/Bases/BaseTcpListener.cs:230
- This assignment to e is useless, since its value is never read.
catch (System.IO.IOException e)
AirPlay/Listeners/AirTunesListener.cs:201
- This assignment to signatureBuffer is useless, since its value is never read.
signatureBuffer = aesCtr128Encrypt.ProcessBytes(signatureBuffer);
AirPlay/Listeners/AirTunesListener.cs:227
- This assignment to mode is useless, since its value is never read.
var mode = body[14];
AirPlay/Listeners/AirTunesListener.cs:363
- This assignment to controlPort is useless, since its value is never read.
var controlPort = (ushort)((short)stream["controlPort"]);
AirPlay/Listeners/AirTunesListener.cs:423
- This assignment to timingPort is useless, since its value is never read.
var timingPort = (ushort)((short)plist["timingPort"]);
AirPlay/Listeners/AirTunesListener.cs:526
- This assignment to start is useless, since its value is never read.
var start = long.Parse(pVals[0]);
AirPlay/Listeners/AirTunesListener.cs:527
- This assignment to current is useless, since its value is never read.
var current = long.Parse(pVals[1]);
AirPlay/Listeners/AirTunesListener.cs:528
- This assignment to end is useless, since its value is never read.
var end = long.Parse(pVals[2]);
AirPlay/Listeners/AirTunesListener.cs:536
- This assignment to image is useless, since its value is never read.
var image = request.Body;
AirPlay/Listeners/AirTunesListener.cs:543
- This assignment to output is useless, since its value is never read.
var output = dmap.Decode(request.Body);
AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs:381
- These 'if' statements can be combined.
if (_ffmpegProcess != null && !_ffmpegProcess.HasExited)
{
if (!_ffmpegProcess.WaitForExit(PROCESS_EXIT_TIMEOUT_MS))
{
_ffmpegProcess.Kill();
_ffmpegProcess.WaitForExit(PROCESS_EXIT_TIMEOUT_MS);
}
}
AirPlay/Listeners/AudioListener.cs:34
- Field '_raopBuffer' can be 'readonly'.
private RaopBuffer _raopBuffer;
AirPlay/Models/TcpListeners/Request.cs:23
- Field '_headers' can be 'readonly'.
private HeadersCollection _headers;
AirPlay/Listeners/Bases/BaseTcpListener.cs:223
- Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
if (bodyBuffer?.Any() == true)
{
payload = formatBuffer.Concat(bodyBuffer).ToArray();
}
else
{
payload = formatBuffer;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| IntPtr ptr = Marshal.AllocHGlobal(pBuffer.Length); | ||
| try | ||
| { | ||
| Marshal.Copy(pBuffer, 0, ptr, pBuffer.Length); | ||
|
|
||
| IntPtr* pBufferPtr = stackalloc IntPtr[1]; | ||
| pBufferPtr[0] = ptr; | ||
|
|
||
| int ret = (int)aacDecoder_Fill(_decoder, pBufferPtr, &bufferSize, &bytesValid); | ||
| if (ret == 0 && _decodeCallCount <= 5) | ||
| Console.WriteLine($"[DEBUG-FDK] Fill OK: bufferSize={pBuffer.Length}, bytesValid after={bytesValid}"); | ||
| return ret; | ||
| } | ||
| finally | ||
| { | ||
| Marshal.FreeHGlobal(ptr); | ||
| } |
There was a problem hiding this comment.
Fill allocates and frees unmanaged memory (AllocHGlobal/FreeHGlobal) on every frame. In real-time audio this can become a hotspot and cause jitter/glitches. Consider pinning the managed input buffer (fixed) or reusing a single unmanaged buffer across calls to avoid per-frame allocations and copies.
| IntPtr ptr = Marshal.AllocHGlobal(pBuffer.Length); | |
| try | |
| { | |
| Marshal.Copy(pBuffer, 0, ptr, pBuffer.Length); | |
| IntPtr* pBufferPtr = stackalloc IntPtr[1]; | |
| pBufferPtr[0] = ptr; | |
| int ret = (int)aacDecoder_Fill(_decoder, pBufferPtr, &bufferSize, &bytesValid); | |
| if (ret == 0 && _decodeCallCount <= 5) | |
| Console.WriteLine($"[DEBUG-FDK] Fill OK: bufferSize={pBuffer.Length}, bytesValid after={bytesValid}"); | |
| return ret; | |
| } | |
| finally | |
| { | |
| Marshal.FreeHGlobal(ptr); | |
| } | |
| fixed (byte* bufferPtr = pBuffer) | |
| { | |
| IntPtr* pBufferPtr = stackalloc IntPtr[1]; | |
| pBufferPtr[0] = (IntPtr)bufferPtr; | |
| int ret = (int)aacDecoder_Fill(_decoder, pBufferPtr, &bufferSize, &bytesValid); | |
| if (ret == 0 && _decodeCallCount <= 5) | |
| Console.WriteLine($"[DEBUG-FDK] Fill OK: bufferSize={pBuffer.Length}, bytesValid after={bytesValid}"); | |
| return ret; | |
| } |
|
|
||
| private const string FDK_AAC_LIB = "libfdk-aac-2"; |
There was a problem hiding this comment.
The comments state Linux uses libfdk-aac.so.2, but FDK_AAC_LIB is hardcoded to libfdk-aac-2. On non-Windows platforms this name is unlikely to resolve without a matching symlink, so native decoding will fail even if the library is installed. Consider adding a platform-specific resolver (NativeLibrary.SetDllImportResolver) or updating the library name strategy/documentation to match actual SONAMEs.
| private const string FDK_AAC_LIB = "libfdk-aac-2"; | |
| #if WINDOWS | |
| private const string FDK_AAC_LIB = "libfdk-aac-2"; | |
| #elif LINUX | |
| private const string FDK_AAC_LIB = "libfdk-aac.so.2"; | |
| #else | |
| // Default to the common UNIX-like SONAME; adjust if targeting other platforms. | |
| private const string FDK_AAC_LIB = "libfdk-aac.so.2"; | |
| #endif |
| - name: Download FFmpeg | ||
| shell: pwsh | ||
| run: | | ||
| $ffmpegUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" | ||
| Invoke-WebRequest -Uri $ffmpegUrl -OutFile ffmpeg.zip | ||
| Expand-Archive -Path ffmpeg.zip -DestinationPath ffmpeg-tmp | ||
| # Copy ffmpeg.exe and ffplay.exe to publish directory | ||
| $binDir = Get-ChildItem -Path ffmpeg-tmp -Recurse -Directory -Filter "bin" | Select-Object -First 1 | ||
| Copy-Item "$($binDir.FullName)\ffmpeg.exe" -Destination ./publish/win-x64/ | ||
| Copy-Item "$($binDir.FullName)\ffplay.exe" -Destination ./publish/win-x64/ |
There was a problem hiding this comment.
The workflow downloads a moving latest FFmpeg ZIP without any integrity verification (checksum/signature) and then executes the binaries. For supply-chain safety and reproducibility, consider pinning to a specific release/build (or at least validating a published SHA256) before copying into artifacts.
| - name: Build libfdk-aac from source | ||
| shell: bash | ||
| run: | | ||
| # Build FDK-AAC from official source using MSYS2 (pre-installed on windows-latest) | ||
| export MSYSTEM=MINGW64 | ||
| export PATH="/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH" | ||
|
|
||
| # Install build dependencies | ||
| pacman -S --noconfirm --needed mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake make |
There was a problem hiding this comment.
This step assumes MSYS2 is already present at C:\msys64 and that pacman is available from the selected bash shell. To make CI more robust against runner image changes, consider installing MSYS2 explicitly via msys2/setup-msys2@v2 (and running the build in an MSYS2 shell), rather than relying on a preinstalled location.
| Console.WriteLine($"[DEBUG-D] Packet #{dPacketCount}: type=0x{type_d:X2}, size={dret}"); | ||
|
|
||
| buf_ret = RaopBufferQueue(_raopBuffer, packet, (ushort)dret, session); | ||
| if (packet.Length >= 12) |
There was a problem hiding this comment.
if (packet.Length >= 12) is always true because packet is a fixed 50k buffer. This should be checking the received byte count (dret >= 12) before reading header fields / decrypting / queueing, otherwise short/empty reads can still run through this path.
| if (packet.Length >= 12) | |
| if (dret >= 12) |
| { | ||
| var response = request.GetBaseResponse(); | ||
|
|
||
| var cseq = request.Headers.ContainsKey("CSeq") ? request.Headers["CSeq"] : "?"; |
| 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]); |
There was a problem hiding this comment.
This assignment to next_timestamp is useless, since its value is never read.
| uint next_timestamp = (uint)((packet[16] << 24) | (packet[17] << 16) | (packet[18] << 8) | packet[19]); |
| if (request.Type == RequestType.SET_PARAMETER) | ||
| { | ||
| if (request.Headers.ContainsKey("Content-Type")) | ||
| { | ||
| var contentType = request.Headers["Content-Type"]; | ||
|
|
||
| if (contentType.Equals("text/parameters", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| var body = Encoding.ASCII.GetString(request.Body); | ||
| var keyPair = body.Split(":", StringSplitOptions.RemoveEmptyEntries).Select(b => b.Trim(' ', '\r', '\n')).ToArray(); | ||
| if(keyPair.Length == 2) | ||
| { | ||
| var key = keyPair[0]; | ||
| var val = keyPair[1]; | ||
|
|
||
| if (key.Equals("volume", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| // request.Body contains 'volume: N.NNNNNN' | ||
| _receiver.OnSetVolume(decimal.Parse(val, CultureInfo.InvariantCulture)); | ||
| } | ||
| else if (key.Equals("progress", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| var pVals = val.Split("/", StringSplitOptions.RemoveEmptyEntries); | ||
|
|
||
| var start = long.Parse(pVals[0]); | ||
| var current = long.Parse(pVals[1]); | ||
| var end = long.Parse(pVals[2]); | ||
|
|
||
| // DO SOMETHING W/ PROGRESS | ||
| } | ||
| } | ||
| } | ||
| else if (contentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| var image = request.Body; | ||
|
|
||
| // DO SOMETHING W/ IMAGE | ||
| } | ||
| else if (contentType.Equals("application/x-dmap-tagged", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| var dmap = new DMapTagged(); | ||
| var output = dmap.Decode(request.Body); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
These 'if' statements can be combined.
| private readonly object _bufferLock = new object(); | ||
| private ulong _sync_time; | ||
| private ulong _sync_timestamp; | ||
| private ushort _controlSequenceNumber = 0; |
There was a problem hiding this comment.
Field '_controlSequenceNumber' can be 'readonly'.
| private ushort _controlSequenceNumber = 0; | |
| private readonly ushort _controlSequenceNumber = 0; |
| private readonly DumpConfig _dConfig; | ||
|
|
||
| public AudioListener(IRtspReceiver receiver, string sessionId, ushort cport, ushort dport, DumpConfig dConfig) : base(cport, dport) | ||
| private bool _isMirroring = false; |
There was a problem hiding this comment.
Field '_isMirroring' can be 'readonly'.
| private bool _isMirroring = false; | |
| private readonly bool _isMirroring = false; |
Screen mirroring had no audio output. Root cause was multi-layered: AAC-ELD frames were wrapped in ADTS/LOAS containers that FFmpeg couldn't parse correctly, the RaopBuffer dequeue had a
shortoverflow causing playback stalls, and the CI workflow was missing required native dependencies.Native FDK-AAC decoder
NativeFdkAacEldDecoderusing P/Invoke tolibfdk-aac-2.dllwithTT_MP4_RAWtransport, matching the approach in itskenny0/airplayreceiver and UxPlayf8e85000(AOT=39, 44100Hz, stereo) matches UxPlay exactlyBuffer overflow fix (audio stuttering)
RaopBufferDequeuecastbuflentoshort, overflowing negative when seq gap > 32767 → dequeue stops entirely, queue grows unboundedushortarithmetic to handle full 16-bit sequence number rangeno_resend=trueduring mirroring — real-time audio should skip missing packets, not waitThread safety
IBufferedCipherinstances per socket handler (CBC mode is stateful)_bufferLocksynchronizesRaopBufferaccess between concurrent control/data handlersFlushAsyncnow acquires lock before clearing bufferProtocol fixes
SETPEERSRTSP request handler (was silently dropped, no regex match)Convert.ToInt32()instead of direct(int)unbox onInt16CI & documentation
libfdk-aac-2.dll+ FFmpeg in artifactsREADME.md, addedREADME_zh.mdDebug logging
Comprehensive
[DEBUG-*]prefixed logging throughout the audio pipeline for future diagnostics:[DEBUG-FDK],[DEBUG-DECRYPT],[DEBUG-DECODE],[DEBUG-QUEUE],[DEBUG-RTSP],[DEBUG-SETUP],[DEBUG-TEARDOWN]Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.