diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7de20f..c66e837 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,6 +31,47 @@ jobs: - name: Publish run: dotnet publish AirPlay/AirPlay.csproj --configuration Release --runtime win-x64 --self-contained true --output ./publish/win-x64 + - 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/ + + - 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 + + # Clone and build fdk-aac + git clone --depth 1 https://github.com/mstorsjo/fdk-aac.git fdk-aac-src + cd fdk-aac-src + + # Use CMake for a simpler build + mkdir build && cd build + cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON .. + cmake --build . --config Release + + # Find and copy the DLL + echo "Built files:" + find . -name "*.dll" -type f + DLL_PATH=$(find . -name "fdk-aac*.dll" -o -name "libfdk-aac*.dll" | head -1) + if [ -z "$DLL_PATH" ]; then + echo "ERROR: No FDK-AAC DLL found after build" + exit 1 + fi + cp -v "$DLL_PATH" ../../publish/win-x64/libfdk-aac-2.dll + echo "Successfully built and copied libfdk-aac-2.dll" + - name: Upload build artifacts uses: actions/upload-artifact@v4 with: 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 72bc7f7..6427ffc 100644 --- a/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs +++ b/AirPlay/Decoders/Implementations/FFmpegAacEldDecoder.cs @@ -1,15 +1,17 @@ /* - * AAC-ELD Decoder using FFmpeg as a subprocess. + * AAC-ELD Decoder using FFmpeg as a subprocess with LOAS/LATM framing. * * The fdk-aac NuGet package returns error 0x5 (AAC_DEC_UNSUPPORTED_ER_FORMAT) * 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 wraps raw AAC-ELD frames in LOAS (Low Overhead Audio Stream) + * format per ISO 14496-3 and pipes them through FFmpeg's LOAS demuxer (-f loas). + * Unlike ADTS which only supports AAC-Main/LC/SSR profiles (2-bit field), LOAS + * carries a full AudioSpecificConfig that can properly signal AAC-ELD (AOT 39). * - * Reference: UxPlay uses GStreamer's avdec_aac (which wraps FFmpeg) for the same purpose. + * AudioSpecificConfig (f8e85000): AOT=39 (ER AAC-ELD), 44100Hz, 2ch + * Reference: UxPlay uses this same ASC with GStreamer's avdec_aac decoder. */ using System; @@ -34,10 +36,13 @@ 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 int _decodeCallCount; + private int _totalFramesDecoded; + private bool _firstFrame = true; + + // Pre-built LOAS framing components + private byte[] _streamMuxConfigBits; // StreamMuxConfig as bit array + private int _streamMuxConfigBitLen; public AudioFormat Type => AudioFormat.AAC_ELD; @@ -52,21 +57,18 @@ public int Config(int sampleRate, int channels, int bitDepth, int frameLength) _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; + // Build AudioSpecificConfig for AAC-ELD + BuildStreamMuxConfig(sampleRate, channels, frameLength); try { // Start FFmpeg process: - // Input: AAC frames wrapped in ADTS headers via stdin pipe + // Input: LOAS-wrapped AAC-ELD frames 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 loas -i pipe:0 -f s16le -acodec pcm_s16le -ar {sampleRate} -ac {channels} pipe:1", UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, @@ -83,7 +85,15 @@ 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) + { + Console.WriteLine($"[DEBUG-FFMPEG-STDERR] {line}"); + } + } catch { /* ignore */ } }); stderrThread.IsBackground = true; @@ -109,19 +119,28 @@ 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 { - // 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); + _decodeCallCount++; - // Write ADTS header + raw AAC data to FFmpeg stdin - _ffmpegInput.Write(_adtsHeader, 0, 7); - _ffmpegInput.Write(input, 0, input.Length); + // Build LOAS frame containing the raw AAC-ELD data + byte[] loasFrame = BuildLoasFrame(input, _firstFrame); + _firstFrame = false; + + // Write LOAS frame to FFmpeg stdin + _ffmpegInput.Write(loasFrame, 0, loasFrame.Length); _ffmpegInput.Flush(); + if (_decodeCallCount <= 3) + Console.WriteLine($"[DEBUG-FFMPEG] Wrote LOAS frame #{_decodeCallCount}: inputLen={input.Length}, loasLen={loasFrame.Length}, expecting {_pcmOutputSize} bytes PCM output"); + // Read decoded PCM from FFmpeg stdout int bytesToRead = _pcmOutputSize; int totalRead = 0; @@ -139,32 +158,155 @@ 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; } } /// - /// Build an ADTS header for wrapping raw AAC frames. + /// Build a LOAS (Low Overhead Audio Stream) frame per ISO 14496-3. + /// First frame includes full StreamMuxConfig with AudioSpecificConfig. + /// Subsequent frames use useSameStreamMux=1 to reuse the config. + /// + private byte[] BuildLoasFrame(byte[] aacPayload, bool includeConfig) + { + // Build AudioMuxElement as bit stream + var muxBits = new BitList(); + + if (includeConfig) + { + // useSameStreamMux = 0 (send new StreamMuxConfig) + muxBits.Add(false); + + // Write pre-built StreamMuxConfig bits + for (int i = 0; i < _streamMuxConfigBitLen; i++) + { + muxBits.Add(_streamMuxConfigBits[i] != 0); + } + } + else + { + // useSameStreamMux = 1 (reuse previous config) + muxBits.Add(true); + } + + // PayloadLengthInfo (for frameLengthType == 0): + // Encode length as sequence of 255-byte chunks + remainder + int remaining = aacPayload.Length; + while (remaining >= 255) + { + WriteBits(muxBits, 255, 8); + remaining -= 255; + } + WriteBits(muxBits, remaining, 8); + + // PayloadMux: raw AAC frame data + for (int i = 0; i < aacPayload.Length; i++) + { + WriteBits(muxBits, aacPayload[i], 8); + } + + // Convert AudioMuxElement to bytes (pad to byte boundary) + byte[] muxBytes = BitsToBytes(muxBits); + + // Build complete LOAS frame: + // syncword (11 bits) = 0x2B7 + // audioMuxLengthBytes (13 bits) = length of AudioMuxElement in bytes + var frameBits = new BitList(); + WriteBits(frameBits, 0x2B7, 11); + WriteBits(frameBits, muxBytes.Length, 13); + + // Append AudioMuxElement bytes + for (int i = 0; i < muxBytes.Length; i++) + { + WriteBits(frameBits, muxBytes[i], 8); + } + + return BitsToBytes(frameBits); + } + + /// + /// Build the StreamMuxConfig containing AudioSpecificConfig for AAC-ELD. + /// Pre-computed once during Config() for reuse in every LOAS frame. /// - private static void BuildAdtsHeader(byte[] header, int packetLen, int profile, int freqIdx, int chanCfg) + private void BuildStreamMuxConfig(int sampleRate, int channels, int frameLength) { - 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; + var bits = new BitList(); + + // audioMuxVersion = 0 + bits.Add(false); + // allStreamsSameTimeFraming = 1 + bits.Add(true); + // numSubFrames = 0 (1 subframe) + WriteBits(bits, 0, 6); + // numProgram = 0 (1 program) + WriteBits(bits, 0, 4); + // numLayer = 0 (1 layer) + WriteBits(bits, 0, 3); + + // AudioSpecificConfig for AAC-ELD (ISO 14496-3) + // audioObjectType = 39 (ER AAC-ELD): escape(11111) + 7(000111) + WriteBits(bits, 0x1F, 5); // escape + WriteBits(bits, 39 - 32, 6); // 7 + + // samplingFrequencyIndex + int freqIdx = GetSampleRateIndex(sampleRate); + WriteBits(bits, freqIdx, 4); + + // channelConfiguration + WriteBits(bits, channels, 4); + + // ELD specific config (ISO 14496-3 section 4.4.2.5) + // frameLengthFlag: 0=480 samples, 1=512 samples + // Must match the actual frame length from iOS SETUP (spf field) + bits.Add(frameLength == 512); // frameLengthFlag + + // aacSectionDataResilienceFlag + bits.Add(false); + // aacScalefactorDataResilienceFlag + bits.Add(false); + // aacSpectralDataResilienceFlag + bits.Add(false); + + // ldSbrPresentFlag + bits.Add(false); + + // End of AudioSpecificConfig + + // frameLengthType = 0 (variable length) + WriteBits(bits, 0, 3); + // latmBufferFullness = 0xFF (VBR) + WriteBits(bits, 0xFF, 8); + // otherDataPresent = 0 + bits.Add(false); + // crcCheckPresent = 0 + bits.Add(false); + + // Store as bit array for reuse + _streamMuxConfigBitLen = bits.Count; + _streamMuxConfigBits = new byte[_streamMuxConfigBitLen]; + for (int i = 0; i < _streamMuxConfigBitLen; i++) + { + _streamMuxConfigBits[i] = bits[i] ? (byte)1 : (byte)0; + } } private static int GetSampleRateIndex(int sampleRate) @@ -178,6 +320,48 @@ private static int GetSampleRateIndex(int sampleRate) }; } + #region Bit manipulation helpers + + private static void WriteBits(BitList bits, int value, int numBits) + { + for (int i = numBits - 1; i >= 0; i--) + { + bits.Add(((value >> i) & 1) == 1); + } + } + + private static byte[] BitsToBytes(BitList bits) + { + // Pad to byte boundary + int padded = ((bits.Count + 7) / 8) * 8; + int byteCount = padded / 8; + var result = new byte[byteCount]; + + for (int i = 0; i < byteCount; i++) + { + int val = 0; + for (int j = 0; j < 8; j++) + { + int bitIdx = i * 8 + j; + val <<= 1; + if (bitIdx < bits.Count && bits[bitIdx]) + val |= 1; + } + result[i] = (byte)val; + } + + return result; + } + + /// + /// Simple growable bit list using System.Collections.Generic.List<bool>. + /// + private class BitList : System.Collections.Generic.List + { + } + + #endregion + public void Dispose() { if (!_disposed) @@ -185,12 +369,10 @@ public void Dispose() _disposed = true; try { - // Close stdin first for graceful FFmpeg shutdown _ffmpegInput?.Close(); if (_ffmpegProcess != null && !_ffmpegProcess.HasExited) { - // Wait for graceful exit, then force kill if needed if (!_ffmpegProcess.WaitForExit(PROCESS_EXIT_TIMEOUT_MS)) { _ffmpegProcess.Kill(); diff --git a/AirPlay/Decoders/Implementations/NativeFdkAacEldDecoder.cs b/AirPlay/Decoders/Implementations/NativeFdkAacEldDecoder.cs new file mode 100644 index 0000000..9cee74b --- /dev/null +++ b/AirPlay/Decoders/Implementations/NativeFdkAacEldDecoder.cs @@ -0,0 +1,284 @@ +/* + * AAC-ELD Decoder using the native FDK-AAC library via P/Invoke. + * + * This decoder feeds raw (decrypted) AAC-ELD frames directly to FDK-AAC + * using TT_MP4_RAW transport type with AudioSpecificConfig for AAC-ELD. + * No ADTS/LOAS framing is needed - the raw access units are passed directly. + * + * Requires libfdk-aac-2.dll (Windows) or libfdk-aac.so.2 (Linux) in the + * application directory or system PATH. + * + * AudioSpecificConfig: f8e85000 (AOT=39, 44100Hz, stereo, bitDepth=16) + * This matches both UxPlay and itskenny0/airplayreceiver reference implementations. + * + * Reference: itskenny0/airplayreceiver AACDecoder.cs + */ + +using System; +using System.Runtime.InteropServices; +using AirPlay.Models.Enums; + +namespace AirPlay.Decoders.Implementations +{ + public unsafe class NativeFdkAacEldDecoder : IDecoder, IDisposable + { + private const int TT_MP4_RAW = 0; + + private IntPtr _decoder; + private int _pcmPktSize; + private bool _disposed; + private int _decodeCallCount; + private int _fillErrorCount; + private int _decodeErrorCount; + private int _successCount; + + public AudioFormat Type => AudioFormat.AAC_ELD; + + public NativeFdkAacEldDecoder() + { + } + + public int Config(int sampleRate, int channels, int bitDepth, int frameLength) + { + _pcmPktSize = frameLength * channels * (bitDepth / 8); + + Console.WriteLine($"[DEBUG-FDK] Config called: sampleRate={sampleRate}, channels={channels}, bitDepth={bitDepth}, frameLength={frameLength}, pcmOutputSize={_pcmPktSize}"); + + // Open FDK-AAC decoder with TT_MP4_RAW transport (raw access units) + _decoder = aacDecoder_Open(TT_MP4_RAW, 1); + if (_decoder == IntPtr.Zero) + { + Console.WriteLine("[DEBUG-FDK] aacDecoder_Open returned null - library loaded but Open failed"); + return -1; + } + Console.WriteLine($"[DEBUG-FDK] aacDecoder_Open succeeded: handle=0x{_decoder:X}"); + + // Build AudioSpecificConfig for AAC-ELD + // Uses exact same format as itskenny0/airplayreceiver and UxPlay: f8e85000 + var asc = BuildAudioSpecificConfig(39, sampleRate, channels, frameLength); + + // Configure decoder with the ASC + int ret = ConfigRaw(asc); + if (ret != 0) + { + Console.WriteLine($"[DEBUG-FDK] aacDecoder_ConfigRaw FAILED: error=0x{ret:X} ({(AACDecoderError)ret})"); + aacDecoder_Close(_decoder); + _decoder = IntPtr.Zero; + return ret; + } + + Console.WriteLine($"[DEBUG-FDK] Decoder configured successfully: {sampleRate}Hz, {channels}ch, {bitDepth}bit, frameLength={frameLength}, pcmSize={_pcmPktSize}"); + return 0; + } + + public int GetOutputStreamLength() + { + return _pcmPktSize; + } + + public int DecodeFrame(byte[] input, ref byte[] output, int pcm_pkt_size) + { + if (_decoder == IntPtr.Zero) + { + _decodeCallCount++; + if (_decodeCallCount <= 3) + Console.WriteLine("[DEBUG-FDK] DecodeFrame called but decoder is null"); + return -1; + } + + _decodeCallCount++; + + // Log first few frames and periodically + bool shouldLog = _decodeCallCount <= 5 || _decodeCallCount % 200 == 0; + + if (shouldLog) + { + var hexDump = input.Length >= 16 + ? BitConverter.ToString(input, 0, Math.Min(16, input.Length)) + : BitConverter.ToString(input); + Console.WriteLine($"[DEBUG-FDK] DecodeFrame #{_decodeCallCount}: inputLen={input.Length}, first16={hexDump}"); + } + + int ret; + + // Fill the decoder's internal buffer with the raw AAC frame + ret = Fill(input); + if (ret != 0) + { + _fillErrorCount++; + if (_fillErrorCount <= 5 || _fillErrorCount % 200 == 0) + Console.WriteLine($"[DEBUG-FDK] aacDecoder_Fill error #{_fillErrorCount}: 0x{ret:X} ({(AACDecoderError)ret}), inputLen={input.Length}"); + return ret; + } + + // Decode one frame + ret = InternalDecodeFrame(ref output, pcm_pkt_size); + if (ret != 0) + { + _decodeErrorCount++; + if (_decodeErrorCount <= 5 || _decodeErrorCount % 200 == 0) + Console.WriteLine($"[DEBUG-FDK] aacDecoder_DecodeFrame error #{_decodeErrorCount}: 0x{ret:X} ({(AACDecoderError)ret}), pcmSize={pcm_pkt_size}"); + return ret; + } + + _successCount++; + if (_successCount <= 5 || _successCount % 200 == 0) + { + // Check if output is silence (all zeros) + bool isSilence = true; + for (int i = 0; i < Math.Min(output.Length, 64); i++) + { + if (output[i] != 0) { isSilence = false; break; } + } + Console.WriteLine($"[DEBUG-FDK] Decode SUCCESS #{_successCount}: outputLen={pcm_pkt_size}, isSilence={isSilence}, totalErrors(fill={_fillErrorCount},decode={_decodeErrorCount})"); + } + + return 0; + } + + private int Fill(byte[] pBuffer) + { + uint bufferSize = (uint)pBuffer.Length; + uint bytesValid = (uint)pBuffer.Length; + + 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); + } + } + + private int InternalDecodeFrame(ref byte[] output, int pcm_pkt_size) + { + IntPtr ptr = Marshal.AllocHGlobal(pcm_pkt_size); + try + { + int ret = (int)aacDecoder_DecodeFrame(_decoder, ptr, pcm_pkt_size, 0); + if (ret == 0) + { + Marshal.Copy(ptr, output, 0, pcm_pkt_size); + } + return ret; + } + finally + { + Marshal.FreeHGlobal(ptr); + } + } + + private int ConfigRaw(byte[] config) + { + uint length = (uint)config.Length; + + IntPtr ptr = Marshal.AllocHGlobal(config.Length); + try + { + Marshal.Copy(config, 0, ptr, config.Length); + + IntPtr* confPtr = stackalloc IntPtr[1]; + confPtr[0] = ptr; + + return (int)aacDecoder_ConfigRaw(_decoder, confPtr, &length); + } + finally + { + Marshal.FreeHGlobal(ptr); + } + } + + /// + /// Build AudioSpecificConfig for AAC-ELD per ISO 14496-3. + /// Uses the exact same format as itskenny0/airplayreceiver and UxPlay: + /// AOT(5+6) + FreqIdx(4) + Channels(4) + BitDepth(5) + padding(8) + /// For AAC-ELD 44100Hz stereo 16bit: f8 e8 50 00 + /// + private static byte[] BuildAudioSpecificConfig(int audioObjectType, int sampleRate, int channels, int frameLength) + { + int frequencyIndex = GetSampleRateIndex(sampleRate); + int bitDepth = 16; + + string bin; + if (audioObjectType >= 31) + { + // Extended AOT encoding: 5-bit escape (11111) + 6-bit AOT-32 + bin = Convert.ToString(31, 2).PadLeft(5, '0'); + bin += Convert.ToString(audioObjectType - 32, 2).PadLeft(6, '0'); + } + else + { + bin = Convert.ToString(audioObjectType, 2).PadLeft(5, '0'); + } + + bin += Convert.ToString(frequencyIndex, 2).PadLeft(4, '0'); + bin += Convert.ToString(channels, 2).PadLeft(4, '0'); + bin += Convert.ToString(bitDepth, 2).PadLeft(5, '0'); + bin += "00000000"; // padding byte + + int nBytes = bin.Length / 8; + byte[] bytes = new byte[nBytes]; + for (int i = 0; i < nBytes; i++) + { + bytes[i] = Convert.ToByte(bin.Substring(8 * i, 8), 2); + } + + Console.WriteLine($"[DEBUG-FDK] AudioSpecificConfig: {BitConverter.ToString(bytes)} (AOT={audioObjectType}, freq={sampleRate}[idx={frequencyIndex}], ch={channels}, bitDepth={bitDepth}, frameLen={frameLength})"); + return bytes; + } + + private static int GetSampleRateIndex(int sampleRate) + { + return sampleRate switch + { + 96000 => 0, 88200 => 1, 64000 => 2, 48000 => 3, + 44100 => 4, 32000 => 5, 24000 => 6, 22050 => 7, + 16000 => 8, 12000 => 9, 11025 => 10, 8000 => 11, + 7350 => 12, _ => 4, + }; + } + + public void Dispose() + { + if (!_disposed && _decoder != IntPtr.Zero) + { + _disposed = true; + Console.WriteLine($"[DEBUG-FDK] Disposing decoder: totalCalls={_decodeCallCount}, successes={_successCount}, fillErrors={_fillErrorCount}, decodeErrors={_decodeErrorCount}"); + aacDecoder_Close(_decoder); + _decoder = IntPtr.Zero; + } + } + + // ---- Native P/Invoke declarations ---- + // FDK-AAC library names vary by platform: + // Windows: libfdk-aac-2.dll + // Linux: libfdk-aac.so.2 + + private const string FDK_AAC_LIB = "libfdk-aac-2"; + + [DllImport(FDK_AAC_LIB, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr aacDecoder_Open(int transportFmt, uint nrOfLayers); + + [DllImport(FDK_AAC_LIB, CallingConvention = CallingConvention.Cdecl)] + private static extern int aacDecoder_ConfigRaw(IntPtr decoder, IntPtr* conf, uint* length); + + [DllImport(FDK_AAC_LIB, CallingConvention = CallingConvention.Cdecl)] + private static extern int aacDecoder_Fill(IntPtr decoder, IntPtr* pBuffer, uint* bufferSize, uint* pBytesValid); + + [DllImport(FDK_AAC_LIB, CallingConvention = CallingConvention.Cdecl)] + private static extern int aacDecoder_DecodeFrame(IntPtr decoder, IntPtr output, int pcm_pkt_size, uint flags); + + [DllImport(FDK_AAC_LIB, CallingConvention = CallingConvention.Cdecl)] + private static extern void aacDecoder_Close(IntPtr decoder); + } +} diff --git a/AirPlay/Listeners/AirTunesListener.cs b/AirPlay/Listeners/AirTunesListener.cs index 669ba1d..d55ae52 100644 --- a/AirPlay/Listeners/AirTunesListener.cs +++ b/AirPlay/Listeners/AirTunesListener.cs @@ -291,6 +291,16 @@ public override async Task OnDataReceivedAsync(Request request, Response respons // Always one foreach request var stream = (Dictionary)((object[])plist["streams"])[0]; var type = (short)stream["type"]; + Console.WriteLine($"[DEBUG-SETUP] Stream type={type}, keys=[{string.Join(", ", stream.Keys)}]"); + foreach (var kv in stream) + { + if (kv.Value is byte[] ba) + Console.WriteLine($"[DEBUG-SETUP] {kv.Key} = byte[{ba.Length}]"); + else if (kv.Value is short sv) + Console.WriteLine($"[DEBUG-SETUP] {kv.Key} = {sv} (unsigned={(ushort)sv})"); + else + Console.WriteLine($"[DEBUG-SETUP] {kv.Key} = {kv.Value}"); + } // If screen Mirroring if (type == 110) @@ -388,6 +398,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons } else { + Console.WriteLine($"[DEBUG-SETUP] Initial SETUP (keys/timing): et={plist.Contains("et")}, ekey={plist.Contains("ekey")}, eiv={plist.Contains("eiv")}, isScreenMirroringSession={plist.Contains("isScreenMirroringSession")}, timingPort={plist.Contains("timingPort")}"); // Read ekey and eiv used to decode video and audio data if (plist.Contains("et")) { @@ -454,14 +465,18 @@ 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); + bool isMirroring = session.MirroringSession.HasValue && session.MirroringSession.Value; + var control = new AudioListener(_receiver, session.SessionId, 7002, 7003, _dumpConfig, isMirroring); await control.StartAsync(cancellationToken).ConfigureAwait(false); + Console.WriteLine("[DEBUG-SETUP] AudioListener started successfully"); session.AudioControlListener = control; } @@ -480,6 +495,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons } if (request.Type == RequestType.RECORD) { + Console.WriteLine("[DEBUG-RTSP] RECORD request received"); response.Headers.Add("Audio-Latency", "0"); // 11025 // response.Headers.Add("Audio-Jack-Status", "connected; type=analog"); } @@ -530,11 +546,16 @@ public override async Task OnDataReceivedAsync(Request request, Response respons } if(request.Type == RequestType.OPTIONS) { - response.Headers.Add("Public", "SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, ANNOUNCE"); + response.Headers.Add("Public", "SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, ANNOUNCE, SETPEERS"); } if(request.Type == RequestType.ANNOUNCE) { + } + if(request.Type == RequestType.SETPEERS) + { + Console.WriteLine("[DEBUG-RTSP] SETPEERS request received - returning 200 OK"); + // Just acknowledge the peer list, no action needed } if(request.Type == RequestType.FLUSH) { @@ -564,6 +585,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 +596,12 @@ public override async Task OnDataReceivedAsync(Request request, Response respons // Always one foreach request var stream = (Dictionary)((object[])plist["streams"]).Last(); var type = (short)stream["type"]; + Console.WriteLine($"[DEBUG-TEARDOWN] Stream type: {type}"); // If screen Mirroring if (type == 110) { + Console.WriteLine("[DEBUG-TEARDOWN] Stopping mirroring session"); // Stop mirroring session if (session.MirroringListener != null) { @@ -592,6 +616,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons // If audio session if (type == 96) { + Console.WriteLine("[DEBUG-TEARDOWN] Stopping audio session"); // Stop audio session if (session.AudioControlListener != null) { diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs index e906a08..3f56dd6 100644 --- a/AirPlay/Listeners/AudioListener.cs +++ b/AirPlay/Listeners/AudioListener.cs @@ -23,10 +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; @@ -35,24 +36,30 @@ public class AudioListener : BaseUdpListener private readonly DumpConfig _dConfig; - public AudioListener(IRtspReceiver receiver, string sessionId, ushort cport, ushort dport, DumpConfig dConfig) : base(cport, dport) + private bool _isMirroring = false; + + public AudioListener(IRtspReceiver receiver, string sessionId, ushort cport, ushort dport, DumpConfig dConfig, bool isMirroring = false) : base(cport, dport) { _receiver = receiver ?? throw new ArgumentNullException(nameof(receiver)); _sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId)); _dConfig = dConfig ?? throw new ArgumentNullException(nameof(dConfig)); + _isMirroring = isMirroring; _raopBuffer = RaopBufferInit(); - _aesCbcDecrypt = CipherUtilities.GetCipher("AES/CBC/NoPadding"); } public override async Task OnRawCSocketAsync(Socket cSocket, CancellationToken cancellationToken) { - Console.WriteLine("Initializing recevie audio control from socket.."); + Console.WriteLine("[DEBUG-C] OnRawCSocketAsync started"); _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); + Console.WriteLine($"[DEBUG-C] Session loaded: AesKey={session.AesKey != null}, AesIv={session.AesIv != null}, EcdhShared={session.EcdhShared != null}, KeyMsg={session.KeyMsg != null}, AudioFormat={session.AudioFormat}"); // If we have not decripted session AesKey if (session.DecryptedAesKey == null) @@ -60,92 +67,170 @@ public override async Task OnRawCSocketAsync(Socket cSocket, CancellationToken c byte[] decryptedAesKey = new byte[16]; _omgHax.DecryptAesKey(session.KeyMsg, session.AesKey, decryptedAesKey); session.DecryptedAesKey = decryptedAesKey; + Console.WriteLine("[DEBUG-C] AES key decrypted"); } // Initialize decoder (needed for type 0x56 audio packets during mirroring) InitializeDecoder(session); + Console.WriteLine($"[DEBUG-C] Decoder initialized: type={_decoder?.Type}, outputLen={_decoder?.GetOutputStreamLength()}"); await SessionManager.Current.CreateOrUpdateSessionAsync(_sessionId, session); var packet = new byte[RAOP_PACKET_LENGTH]; + int cPacketCount = 0; + int c56Count = 0; + int c54Count = 0; + int cOtherCount = 0; + int cQueuedCount = 0; + int cDequeuedCount = 0; + int cPcmDelivered = 0; + int cSocketErrors = 0; + string exitReason = "loop-end"; + + Console.WriteLine("[DEBUG-C] Entering receive loop..."); 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); + cSocketErrors++; + if (cSocketErrors <= 5) + Console.WriteLine($"[DEBUG-C] Socket.Receive error: {error}"); + continue; + } - var ret = RaopBufferQueue(_raopBuffer, data, (ushort)data.Length, session); + cPacketCount++; + if (cPacketCount == 1) + Console.WriteLine($"[DEBUG-C] First packet received! size={cret}"); - // 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); + c56Count++; + InitAesCbcCipher(aesCbcDecrypt, session.DecryptedAesKey, session.EcdhShared, session.AesIv); + + mem.Position = 4; + var data = reader.ReadBytes(cret - 4); + + if (c56Count <= 3) + Console.WriteLine($"[DEBUG-C] 0x56 packet #{c56Count}: dataLen={data.Length}, seqNum={(ushort)((data[2] << 8) | data[3])}"); + + int ret; + lock (_bufferLock) + { + ret = RaopBufferQueue(_raopBuffer, data, (ushort)data.Length, session, aesCbcDecrypt); + } + + if (c56Count <= 3) + Console.WriteLine($"[DEBUG-C] RaopBufferQueue returned: {ret}"); + + if (ret > 0) cQueuedCount++; + + // 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; + lock (_bufferLock) + { + while ((audiobuf = RaopBufferDequeue(_raopBuffer, ref audiobuflen, ref timestamp, true)) != null) + { + if (audiobuf.Length == 0 || audiobuflen <= 0) + continue; + + cDequeuedCount++; + var pcmData = new PcmData(); + pcmData.Length = audiobuflen; + pcmData.Data = audiobuf; + pcmData.Pts = (ulong)(timestamp - _sync_timestamp) * 1000000UL / 44100 + _sync_time; + + pcmBatch.Add(pcmData); + } + } + + if (c56Count <= 3 && pcmBatch.Count > 0) + Console.WriteLine($"[DEBUG-C] Dequeued {pcmBatch.Count} PCM frames, first len={pcmBatch[0].Length}"); + + // Deliver PCM outside the lock to avoid blocking the data handler + foreach (var pcm in pcmBatch) + { + _receiver.OnPCMData(pcm); + cPcmDelivered++; + } + } + else if (type_c == 0x54) + { + c54Count++; + 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; + + if (c54Count <= 3) + Console.WriteLine($"[DEBUG-C] 0x54 sync packet #{c54Count}: rtp_ts={rtp_timestamp}"); + } + else + { + cOtherCount++; + if (cOtherCount <= 5) + Console.WriteLine($"[DEBUG-C] Unknown packet type: 0x{type_c:X2}, size={cret}"); } } - 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 + + // Log summary periodically + if (cPacketCount % 500 == 0) { - Console.WriteLine("Unknown packet"); + Console.WriteLine($"[DEBUG-C] Stats: packets={cPacketCount}, 0x56={c56Count}, 0x54={c54Count}, other={cOtherCount}, queued={cQueuedCount}, dequeued={cDequeuedCount}, pcmDelivered={cPcmDelivered}, socketErrors={cSocketErrors}"); } - } - Array.Fill(packet, 0); + Array.Fill(packet, 0); + } + catch (ObjectDisposedException) + { + exitReason = "ObjectDisposedException (socket closed)"; + break; + } + catch (SocketException ex) + { + exitReason = $"SocketException: {ex.SocketErrorCode} - {ex.Message}"; + break; + } + catch (Exception ex) + { + Console.WriteLine($"[DEBUG-C] Exception in receive loop: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[DEBUG-C] Stack trace: {ex.StackTrace}"); + } } while (!cancellationToken.IsCancellationRequested); - Console.WriteLine("Closing audio control socket.."); + if (cancellationToken.IsCancellationRequested) + exitReason = "CancellationToken requested"; + + Console.WriteLine($"[DEBUG-C] Closing audio control socket. Reason: {exitReason}"); + Console.WriteLine($"[DEBUG-C] Final stats: packets={cPacketCount}, 0x56={c56Count}, 0x54={c54Count}, other={cOtherCount}, queued={cQueuedCount}, dequeued={cDequeuedCount}, pcmDelivered={cPcmDelivered}, socketErrors={cSocketErrors}"); } public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken cancellationToken) { - Console.WriteLine("Initializing recevie audio data from socket.."); + Console.WriteLine("[DEBUG-D] OnRawDSocketAsync started"); + + // 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); + Console.WriteLine($"[DEBUG-D] Session loaded: AesKey={session.AesKey != null}, AesIv={session.AesIv != null}, EcdhShared={session.EcdhShared != null}, KeyMsg={session.KeyMsg != null}, AudioFormat={session.AudioFormat}"); // If we have not decripted session AesKey if (session.DecryptedAesKey == null) @@ -153,77 +238,150 @@ public override async Task OnRawDSocketAsync(Socket dSocket, CancellationToken c byte[] decryptedAesKey = new byte[16]; _omgHax.DecryptAesKey(session.KeyMsg, session.AesKey, decryptedAesKey); session.DecryptedAesKey = decryptedAesKey; + Console.WriteLine("[DEBUG-D] AES key decrypted"); } // Initialize decoder InitializeDecoder(session); + Console.WriteLine($"[DEBUG-D] Decoder initialized: type={_decoder?.Type}, outputLen={_decoder?.GetOutputStreamLength()}"); await SessionManager.Current.CreateOrUpdateSessionAsync(_sessionId, session); var packet = new byte[RAOP_PACKET_LENGTH]; + int dPacketCount = 0; + int dQueuedCount = 0; + int dDequeuedCount = 0; + int dPcmDelivered = 0; + int dSocketErrors = 0; + string exitReason = "loop-end"; + + Console.WriteLine("[DEBUG-D] Entering receive loop..."); 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) + { + dSocketErrors++; + if (dSocketErrors <= 5) + Console.WriteLine($"[DEBUG-D] Socket.Receive error: {error}"); + continue; + } - // RTP payload type - int type_d = packet[1] & ~0x80; + dPacketCount++; + if (dPacketCount == 1) + Console.WriteLine($"[DEBUG-D] First packet received! size={dret}"); - if (packet.Length >= 12) - { - InitAesCbcCipher(session.DecryptedAesKey, session.EcdhShared, session.AesIv); + // RTP payload type + int type_d = packet[1] & ~0x80; - bool no_resend = false; - int buf_ret; - byte[] audiobuf; - int audiobuflen = 0; - uint timestamp = 0; + if (dPacketCount <= 3) + 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) + { + InitAesCbcCipher(aesCbcDecrypt, session.DecryptedAesKey, session.EcdhShared, session.AesIv); + + // During screen mirroring, skip resend waiting (real-time audio can't wait) + bool no_resend = _isMirroring; + int buf_ret; + byte[] audiobuf; + int audiobuflen = 0; + uint timestamp = 0; + + lock (_bufferLock) + { + buf_ret = RaopBufferQueue(_raopBuffer, packet, (ushort)dret, session, aesCbcDecrypt); + } + + if (dPacketCount <= 3) + Console.WriteLine($"[DEBUG-D] RaopBufferQueue returned: {buf_ret}"); + + if (buf_ret > 0) dQueuedCount++; - //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) + var pcmBatch = new System.Collections.Generic.List(); + 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; + dDequeuedCount++; + 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); + pcmBatch.Add(pcmData); + } } - //} - /* Handle possible resend requests */ - if (!no_resend) + if (dPacketCount <= 3 && pcmBatch.Count > 0) + Console.WriteLine($"[DEBUG-D] Dequeued {pcmBatch.Count} PCM frames, first len={pcmBatch[0].Length}"); + + // Deliver PCM outside the lock to avoid blocking the control handler + foreach (var pcm in pcmBatch) + { + _receiver.OnPCMData(pcm); + dPcmDelivered++; + } + + /* Handle possible resend requests (not needed during mirroring) */ + if (!no_resend) + { + RaopBufferHandleResends(_raopBuffer, _cSocket, _controlSequenceNumber); + } + } + + // Log summary periodically + if (dPacketCount % 500 == 0) { - RaopBufferHandleResends(_raopBuffer, _cSocket, _controlSequenceNumber); + Console.WriteLine($"[DEBUG-D] Stats: packets={dPacketCount}, queued={dQueuedCount}, dequeued={dDequeuedCount}, pcmDelivered={dPcmDelivered}, socketErrors={dSocketErrors}"); } - } - Array.Clear(packet, 0, packet.Length); + Array.Clear(packet, 0, packet.Length); + } + catch (ObjectDisposedException) + { + exitReason = "ObjectDisposedException (socket closed)"; + break; + } + catch (SocketException ex) + { + exitReason = $"SocketException: {ex.SocketErrorCode} - {ex.Message}"; + break; + } + catch (Exception ex) + { + Console.WriteLine($"[DEBUG-D] Exception in receive loop: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[DEBUG-D] Stack trace: {ex.StackTrace}"); + } } while (!cancellationToken.IsCancellationRequested); - Console.WriteLine("Closing audio data socket.."); + if (cancellationToken.IsCancellationRequested) + exitReason = "CancellationToken requested"; + + Console.WriteLine($"[DEBUG-D] Closing audio data socket. Reason: {exitReason}"); + Console.WriteLine($"[DEBUG-D] Final stats: packets={dPacketCount}, queued={dQueuedCount}, dequeued={dDequeuedCount}, pcmDelivered={dPcmDelivered}, socketErrors={dSocketErrors}"); } public Task FlushAsync(int nextSequence) { - RaopBufferFlush(_raopBuffer, nextSequence); + Console.WriteLine($"[DEBUG-FLUSH] FlushAsync called: nextSequence={nextSequence}, isMirroring={_isMirroring}"); + lock (_bufferLock) + { + RaopBufferFlush(_raopBuffer, nextSequence); + } _receiver.OnAudioFlush(); 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); @@ -231,7 +389,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() @@ -258,26 +416,39 @@ private RaopBuffer RaopBufferInit() return raop_buffer; } - public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, Session session) + private int _queueCallCount = 0; + + public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, Session session, IBufferedCipher aesCbcDecrypt) { int encryptedlen; RaopBufferEntry entry; + _queueCallCount++; + /* Check packet data length is valid */ if (datalen < 12 || datalen > RAOP_PACKET_LENGTH) { + if (_queueCallCount <= 5) + Console.WriteLine($"[DEBUG-QUEUE] #{_queueCallCount}: REJECT invalid length={datalen}"); return -1; } var seqnum = (ushort)((data[2] << 8) | data[3]); if (datalen == 16 && data[12] == 0x0 && data[13] == 0x68 && data[14] == 0x34 && data[15] == 0x0) { + if (_queueCallCount <= 5) + Console.WriteLine($"[DEBUG-QUEUE] #{_queueCallCount}: no-data marker, seqnum={seqnum}"); return 0; } + if (_queueCallCount <= 10 || _queueCallCount % 500 == 0) + Console.WriteLine($"[DEBUG-QUEUE] #{_queueCallCount}: seqnum={seqnum}, datalen={datalen}, payloadSize={datalen - 12}, bufferEmpty={raop_buffer.IsEmpty}, firstSeq={raop_buffer.FirstSeqNum}, lastSeq={raop_buffer.LastSeqNum}"); + // Ignore, old if (!raop_buffer.IsEmpty && seqnum < raop_buffer.FirstSeqNum && seqnum != 0) { + if (_queueCallCount <= 10) + Console.WriteLine($"[DEBUG-QUEUE] #{_queueCallCount}: SKIP old seqnum={seqnum} < firstSeqNum={raop_buffer.FirstSeqNum}"); return 0; } @@ -309,7 +480,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); } @@ -321,6 +492,15 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, File.WriteAllBytes($"{fPath}raw_{seqnum}", raw); #endif /* RAW -> PCM */ + if (_queueCallCount <= 10 || _queueCallCount % 200 == 0) + { + // Log first bytes of decrypted payload for verification + // Valid AAC-ELD frames start with: 0x8c, 0x8d, 0x8e, 0x80, 0x81, 0x82, 0x20 + var hexDump = raw.Length >= 16 + ? BitConverter.ToString(raw, 0, Math.Min(16, raw.Length)) + : BitConverter.ToString(raw); + Console.WriteLine($"[DEBUG-DECRYPT] #{_queueCallCount}: raw[0]=0x{raw[0]:X2}, len={raw.Length}, first16={hexDump}"); + } var length = _decoder.GetOutputStreamLength(); var output = new byte[length]; @@ -328,7 +508,21 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, if (res != 0) { output = new byte[length]; - Console.WriteLine($"Decoding error. Decoder: {_decoder.Type} Code: {res}"); + if (_queueCallCount <= 10 || _queueCallCount % 200 == 0) + Console.WriteLine($"[DEBUG-DECODE] #{_queueCallCount}: ERROR decoder={_decoder.Type}, code=0x{res:X} ({res}), inputLen={raw.Length}, outputLen={length}"); + } + else + { + if (_queueCallCount <= 10 || _queueCallCount % 200 == 0) + { + // Check if PCM output is silence + bool isSilence = true; + for (int i = 0; i < Math.Min(output.Length, 64); i++) + { + if (output[i] != 0) { isSilence = false; break; } + } + Console.WriteLine($"[DEBUG-DECODE] #{_queueCallCount}: OK decoder={_decoder.Type}, inputLen={raw.Length}, outputLen={length}, silence={isSilence}"); + } } #if DUMP @@ -362,11 +556,11 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, public byte[] RaopBufferDequeue(RaopBuffer raop_buffer, ref int length, ref uint pts, bool noResend) { - short buflen; + int buflen; RaopBufferEntry entry; - /* Calculate number of entries in the current buffer */ - buflen = (short)(raop_buffer.LastSeqNum - raop_buffer.FirstSeqNum + 1); + /* Calculate number of entries in the current buffer (use ushort arithmetic to handle wraparound) */ + buflen = (ushort)(raop_buffer.LastSeqNum - raop_buffer.FirstSeqNum + 1); /* Cannot dequeue from empty buffer */ if (raop_buffer.IsEmpty || buflen <= 0) @@ -494,94 +688,130 @@ 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) + // Try native FDK-AAC first (direct P/Invoke, no framing needed), + // then FFmpeg subprocess with LOAS wrapping as fallback. + + var frameLength = spf > 0 ? spf : 480; + var numChannels = 2; + var bitDepth = 16; + var sampleRate = 44100; + + // Try 1: Native FDK-AAC library (same approach as itskenny0/airplayreceiver) + try { - _decoder = aacEldDecoder; + var nativeDecoder = new Decoders.Implementations.NativeFdkAacEldDecoder(); + var ret = nativeDecoder.Config(sampleRate, numChannels, bitDepth, frameLength); + if (ret == 0) + { + _decoder = nativeDecoder; + Console.WriteLine("[DEBUG] Using native FDK-AAC decoder for AAC-ELD"); + } + else + { + Console.WriteLine($"[DEBUG] Native FDK-AAC config error: 0x{ret:X}, trying FFmpeg..."); + nativeDecoder.Dispose(); + } } - else + catch (DllNotFoundException ex) { - 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); + Console.WriteLine($"[DEBUG] Native FDK-AAC library not found: {ex.Message}"); + Console.WriteLine("[DEBUG] To use native decoder, place libfdk-aac-2.dll (Windows) or libfdk-aac.so.2 (Linux) in app directory"); + } + catch (Exception ex) + { + Console.WriteLine($"[DEBUG] Native FDK-AAC failed: {ex.GetType().Name}: {ex.Message}"); + } + + // Try 2: FFmpeg subprocess with LOAS wrapping + if (_decoder == null) + { + try + { + var ffmpegDecoder = new Decoders.Implementations.FFmpegAacEldDecoder(); + var ret = ffmpegDecoder.Config(sampleRate, numChannels, bitDepth, frameLength); + if (ret == 0) + { + _decoder = ffmpegDecoder; + Console.WriteLine("[DEBUG] Using FFmpeg subprocess decoder for AAC-ELD"); + } + else + { + Console.WriteLine($"[DEBUG] FFmpeg decoder config failed (error {ret}), falling back to SharpJaad AAC-LC"); + ffmpegDecoder.Dispose(); + _decoder = new AACDecoder(TransportType.TT_MP4_RAW, AudioObjectType.AOT_AAC_LC, 1); + _decoder.Config(sampleRate, numChannels, bitDepth, frameLength); + } + } + catch (Exception ex) + { + Console.WriteLine($"[DEBUG] FFmpeg 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) - { - 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) + else if (audioFormat == AudioFormat.PCM) { - // 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(); + } } } } diff --git a/AirPlay/Listeners/Bases/BaseTcpListener.cs b/AirPlay/Listeners/Bases/BaseTcpListener.cs index b8bf448..eaffd76 100644 --- a/AirPlay/Listeners/Bases/BaseTcpListener.cs +++ b/AirPlay/Listeners/Bases/BaseTcpListener.cs @@ -134,22 +134,30 @@ private async Task ReadFormattedAsync(TcpClient client, NetworkStream stream, Ca // Because of the persistent connection we might receive more than one request at a time // I'm using a regex to find all request by 'magic numbers' (ex. GET, POST, SETUP, ecc) - var pattern = - $"^{RequestConst.GET}[.]*|" + - $"^{RequestConst.POST}[.]*|" + - $"^{RequestConst.SETUP}[.]*|" + - $"^{RequestConst.GET_PARAMETER}[.]*|" + - $"^{RequestConst.RECORD}[.]*|" + - $"^{RequestConst.SET_PARAMETER}[.]*|" + - $"^{RequestConst.ANNOUNCE}[.]*|" + - $"^{RequestConst.FLUSH}[.]*|" + - $"^{RequestConst.OPTIONS}[.]*|" + - $"^{RequestConst.PAUSE}[.]*|" + + var pattern = + $"^{RequestConst.GET}[.]*|" + + $"^{RequestConst.POST}[.]*|" + + $"^{RequestConst.SETUP}[.]*|" + + $"^{RequestConst.GET_PARAMETER}[.]*|" + + $"^{RequestConst.RECORD}[.]*|" + + $"^{RequestConst.SET_PARAMETER}[.]*|" + + $"^{RequestConst.ANNOUNCE}[.]*|" + + $"^{RequestConst.FLUSH}[.]*|" + + $"^{RequestConst.OPTIONS}[.]*|" + + $"^{RequestConst.PAUSE}[.]*|" + + $"^{RequestConst.SETPEERS}[.]*|" + $"^{RequestConst.TEARDOWN}[.]*"; var r = new Regex(pattern, RegexOptions.Multiline); var m = r.Matches(raw); + if (m.Count == 0 && raw.Length > 0) + { + // Log first 200 chars of unrecognized data for debugging + var preview = raw.Length > 200 ? raw.Substring(0, 200) : raw; + Console.WriteLine($"[DEBUG-RTSP] Unrecognized data (no regex match), len={raw.Length}, preview: {preview}"); + } + // Split requests and create models var requests = new List(); for (int i = 0; i < m.Count; i++) @@ -172,9 +180,13 @@ private async Task ReadFormattedAsync(TcpClient client, NetworkStream stream, Ca { var response = request.GetBaseResponse(); + var cseq = request.Headers.ContainsKey("CSeq") ? request.Headers["CSeq"] : "?"; + Console.WriteLine($"[DEBUG-RTSP] >>> {request.Type} CSeq={cseq}"); + await OnDataReceivedAsync(request, response, cancellationToken).ConfigureAwait(false); await SendResponseAsync(stream, response); - } + + Console.WriteLine($"[DEBUG-RTSP] <<< {response.StatusCode}"); } } // If we have read some bytes, leave connection open and wait for next message diff --git a/AirPlay/Listeners/Bases/BaseUdpListener.cs b/AirPlay/Listeners/Bases/BaseUdpListener.cs index 049d9bb..a16f3df 100644 --- a/AirPlay/Listeners/Bases/BaseUdpListener.cs +++ b/AirPlay/Listeners/Bases/BaseUdpListener.cs @@ -1,4 +1,5 @@ -using System.Net; +using System; +using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -6,15 +7,19 @@ namespace AirPlay.Listeners { public class BaseUdpListener : BaseListener - { - public const int CloseTimeout = 1000; - + { + public const int CloseTimeout = 1000; + private readonly Socket _cSocket; private readonly Socket _dSocket; + private readonly ushort _cPort; + private readonly ushort _dPort; private readonly CancellationTokenSource _cancellationTokenSource; public BaseUdpListener(ushort cPort, ushort dPort) { + _cPort = cPort; + _dPort = dPort; _cSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); _dSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); @@ -28,6 +33,7 @@ public override Task StartAsync(CancellationToken cancellationToken) { var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); + Console.WriteLine($"[DEBUG-UDP] StartAsync: cSocket on port {_cPort}, dSocket on port {_dPort}"); Task.Run(() => OnRawCSocketAsync(_cSocket, source.Token), source.Token); Task.Run(() => OnRawDSocketAsync(_dSocket, source.Token), source.Token); @@ -36,10 +42,12 @@ public override Task StartAsync(CancellationToken cancellationToken) public override Task StopAsync() { + Console.WriteLine("[DEBUG-UDP] StopAsync called - cancelling tokens and closing sockets"); _cancellationTokenSource.Cancel(); _cSocket.Close(CloseTimeout); _dSocket.Close(CloseTimeout); + Console.WriteLine("[DEBUG-UDP] StopAsync completed"); return Task.CompletedTask; } diff --git a/AirPlay/Models/Enums/RequestConst.cs b/AirPlay/Models/Enums/RequestConst.cs index a2d98d5..ac40f68 100644 --- a/AirPlay/Models/Enums/RequestConst.cs +++ b/AirPlay/Models/Enums/RequestConst.cs @@ -13,5 +13,6 @@ public class RequestConst public const string TEARDOWN = "54454152444F574E"; public const string OPTIONS = "4F5054494F4E53"; public const string PAUSE = "5041555345"; + public const string SETPEERS = "534554504545525320"; } } diff --git a/AirPlay/Models/Enums/RequestType.cs b/AirPlay/Models/Enums/RequestType.cs index b6865e1..788447e 100644 --- a/AirPlay/Models/Enums/RequestType.cs +++ b/AirPlay/Models/Enums/RequestType.cs @@ -12,6 +12,7 @@ public enum RequestType : ushort FLUSH = 7, TEARDOWN = 8, OPTIONS = 9, - PAUSE = 10 + PAUSE = 10, + SETPEERS = 11 } } \ No newline at end of file diff --git a/AirPlay/Models/TcpListeners/Request.cs b/AirPlay/Models/TcpListeners/Request.cs index c53fc28..aab7006 100644 --- a/AirPlay/Models/TcpListeners/Request.cs +++ b/AirPlay/Models/TcpListeners/Request.cs @@ -162,6 +162,10 @@ private void Initialize () { return RequestType.PAUSE; } + if (hex.StartsWith(RequestConst.SETPEERS, StringComparison.OrdinalIgnoreCase)) + { + return RequestType.SETPEERS; + } return null; } diff --git a/AirPlay/Services/AudioOutputService.cs b/AirPlay/Services/AudioOutputService.cs index af16922..b08cd9b 100644 --- a/AirPlay/Services/AudioOutputService.cs +++ b/AirPlay/Services/AudioOutputService.cs @@ -182,10 +182,23 @@ public void AddSamples(byte[] buffer, int offset, int count) _streamProvider.AddSamples(buffer, offset, count); _sampleCount++; + // Log first few samples for debugging + if (_sampleCount <= 5 || _sampleCount % 500 == 0) + { + // Check if data is all zeros (silence) + bool allZeros = true; + int checkLen = Math.Min(count, 100); + for (int i = offset; i < offset + checkLen; i++) + { + if (buffer[i] != 0) { allZeros = false; break; } + } + Console.WriteLine($"[DEBUG-AUDIO] AddSamples #{_sampleCount}: count={count}, allZeros={allZeros}, queue={_streamProvider.QueuedBuffers}, initialized={_initialized}, playing={_isPlaying}"); + } + // Initialize DirectSound AFTER prebuffering if (!_initialized && _sampleCount >= PREBUFFER_PACKETS) { - Console.WriteLine($"Prebuffered {_streamProvider.QueuedBuffers} packets, calling Init()..."); + Console.WriteLine($"[DEBUG-AUDIO] Prebuffered {_streamProvider.QueuedBuffers} packets, calling Init()..."); _waveOut.Init(_streamProvider); _initialized = true; } @@ -195,7 +208,7 @@ public void AddSamples(byte[] buffer, int offset, int count) { _waveOut.Play(); _isPlaying = true; - Console.WriteLine($"Audio playback started ({_streamProvider.QueuedBuffers} packets buffered)"); + Console.WriteLine($"[DEBUG-AUDIO] Audio playback started ({_streamProvider.QueuedBuffers} packets buffered)"); } // Log status periodically @@ -203,12 +216,12 @@ public void AddSamples(byte[] buffer, int offset, int count) { var state = _waveOut.PlaybackState; var queued = _streamProvider.QueuedBuffers; - Console.WriteLine($"Audio: {_sampleCount} packets | Queue: {queued} | State: {state}"); + Console.WriteLine($"[DEBUG-AUDIO] Audio: {_sampleCount} packets | Queue: {queued} | State: {state}"); } } catch (Exception ex) { - Console.WriteLine($"Audio error: {ex.Message}"); + Console.WriteLine($"[DEBUG-AUDIO] Audio error in AddSamples: {ex.GetType().Name}: {ex.Message}"); } } } diff --git a/README.md b/README.md index bad0543..9105115 100644 --- a/README.md +++ b/README.md @@ -1,277 +1,98 @@ -# AirPlay Receiver -Open source implementation of AirPlay 2 Mirroring / Audio protocol in C# and .NET. +# AirPlay Receiver for Windows -![Build Status](https://github.com/YimingZhanshen/Airplay2OnWindows/workflows/Build%20and%20Test/badge.svg) - -## Requirements - -- .NET 8.0 SDK or later -- C++ build tools for compiling AAC and ALAC codecs - -## Generic - -Tested on macOS with iPhone 12 Pro iOS14. - -The project is fully functional, but the AAC and ALAC libraries written in C++ must be built. - -## Building the Project - -### Prerequisites -1. Install [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) -2. Build the required codecs (see below) - -### Compile the Project -```bash -$ dotnet restore AirPlay.sln -$ dotnet build AirPlay.sln --configuration Release -``` - -## How To - -### Build AAC Codec -To download, build and install fdk-aac do the following: - -Clone the repository and cd into the folder: -``` -$ git clone https://github.com/mstorsjo/fdk-aac.git -$ cd fdk-aac -``` - -Configure the build and make the library: -``` -$ autoreconf -fi -$ ./configure -$ make -``` - -### Build ALAC Codec -To download, build and install alac do the following: - -Clone the repository and cd into the folder: -``` -$ git clone https://github.com/mikebrady/alac.git -$ cd alac -``` - -Download and paste 'GiteKat''s files in 'alac/codec' folder cloned before -``` -$ https://github.com/GiteKat/LibALAC/tree/master/LibALAC -``` - -The 'mikebrady''s source code does not contains 'extern' keyword. -We need external linkage so we use 'GiteKat''s source code files. - -
- -Edit makefile.original as follow - - -``` -# libalac make - -CFLAGS = -g -O3 -c -LFLAGS = -Wall -CC = g++ - -SRCDIR = . -OBJDIR = ./obj -INCLUDES = . - -HEADERS = \ -$(SRCDIR)/EndianPortable.h \ -$(SRCDIR)/aglib.h \ -$(SRCDIR)/ALACAudioTypes.h \ -$(SRCDIR)/ALACBitUtilities.h\ -$(SRCDIR)/ALACDecoder.h \ -$(SRCDIR)/ALACEncoder.h \ -$(SRCDIR)/LibALAC.h \ -$(SRCDIR)/dplib.h \ -$(SRCDIR)/matrixlib.h - -SOURCES = \ -$(SRCDIR)/EndianPortable.c \ -$(SRCDIR)/ALACBitUtilities.c \ -$(SRCDIR)/ALACDecoder.cpp \ -$(SRCDIR)/ALACEncoder.cpp \ -$(SRCDIR)/LibALAC.cpp \ -$(SRCDIR)/ag_dec.c \ -$(SRCDIR)/ag_enc.c \ -$(SRCDIR)/dp_dec.c \ -$(SRCDIR)/dp_enc.c \ -$(SRCDIR)/matrix_dec.c \ -$(SRCDIR)/matrix_enc.c - -OBJS = \ -EndianPortable.o \ -ALACBitUtilities.o \ -ALACDecoder.o \ -ALACEncoder.o \ -LibALAC.o \ -ag_dec.o \ -ag_enc.o \ -dp_dec.o \ -dp_enc.o \ -matrix_dec.o \ -matrix_enc.o +[中文文档](README_zh.md) -libalac.a: $(OBJS) - ar rcs libalac.a $(OBJS) +Open-source AirPlay 2 receiver for Windows, supporting **screen mirroring** (with audio) and **audio streaming** from Apple devices. Built with C# and .NET 8. -EndianPortable.o : EndianPortable.c - $(CC) -I $(INCLUDES) $(CFLAGS) EndianPortable.c +![Build Status](https://github.com/YimingZhanshen/Airplay2OnWindows/workflows/Build%20and%20Test/badge.svg) -ALACBitUtilities.o : ALACBitUtilities.c - $(CC) -I $(INCLUDES) $(CFLAGS) ALACBitUtilities.c +## Features -ALACDecoder.o : ALACDecoder.cpp - $(CC) -I $(INCLUDES) $(CFLAGS) ALACDecoder.cpp +- **Screen Mirroring** — Mirror your iPhone/iPad/Mac screen to Windows with H.264 video and AAC-ELD audio +- **Audio Streaming** — Play music and podcasts via AirPlay (ALAC and AAC codecs) +- **Volume Control** — Remote volume adjustment from your Apple device +- **Auto-Discovery** — Bonjour/mDNS service advertising, your Windows PC appears as an AirPlay receiver automatically -ALACEncoder.o : ALACEncoder.cpp - $(CC) -I $(INCLUDES) $(CFLAGS) ALACEncoder.cpp +## Quick Start -LibALAC.o : LibALAC.cpp - $(CC) -I $(INCLUDES) $(CFLAGS) LibALAC.cpp +### Download -ag_dec.o : ag_dec.c - $(CC) -I $(INCLUDES) $(CFLAGS) ag_dec.c +Download the latest build from [GitHub Actions](https://github.com/YimingZhanshen/Airplay2OnWindows/actions) artifacts. The package includes all required dependencies (FFmpeg, libfdk-aac). -ag_enc.o : ag_enc.c - $(CC) -I $(INCLUDES) $(CFLAGS) ag_enc.c +### Manual Setup -dp_dec.o : dp_dec.c - $(CC) -I $(INCLUDES) $(CFLAGS) dp_dec.c +#### Prerequisites -dp_enc.o : dp_enc.c - $(CC) -I $(INCLUDES) $(CFLAGS) dp_enc.c +- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or later +- [FFmpeg](https://github.com/BtbN/FFmpeg-Builds/releases) — `ffmpeg.exe` and `ffplay.exe` in the application directory or PATH +- [libfdk-aac](https://github.com/mstorsjo/fdk-aac) — `libfdk-aac-2.dll` in the application directory (required for screen mirroring audio) -matrix_dec.o : matrix_dec.c - $(CC) -I $(INCLUDES) $(CFLAGS) matrix_dec.c +#### Build -matrix_enc.o : matrix_enc.c - $(CC) -I $(INCLUDES) $(CFLAGS) matrix_enc.c - -clean: - -rm $(OBJS) libalac.a +```bash +dotnet restore AirPlay.sln +dotnet build AirPlay.sln --configuration Release ``` -
- -
- -Edit makefile.am as follow - - -``` -## Copyright (c) 2013 Tiancheng "Timothy" Gu -## Modifications copyright (c) 2016 Mike Brady -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. +#### Run -lib_LTLIBRARIES = libalac.la +1. Start the application +2. Open a video player to receive the mirroring stream: + ```bash + ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay \\.\pipe\AirPlayVideo + ``` +3. On your Apple device, open Control Center → Screen Mirroring → select your PC -libalac_la_CPPFLAGS = -Wno-multichar -libalac_la_LDFLAGS = -version-info @ALAC_VERSION@ +## Building libfdk-aac on Windows -libalac_la_SOURCES = \ - EndianPortable.c \ - ALACBitUtilities.c \ - ALACDecoder.cpp \ - ALACEncoder.cpp \ - LibALAC.cpp \ - ag_dec.c \ - ag_enc.c \ - dp_dec.c \ - dp_enc.c \ - matrix_dec.c \ - matrix_enc.c +The `libfdk-aac-2.dll` is required for decoding AAC-ELD audio during screen mirroring. You can build it from source: -pkgconfigdir = $(libdir)/pkgconfig -pkgconfig_DATA = alac.pc +1. Install [MSYS2](https://www.msys2.org/) +2. Open MSYS2 MinGW 64-bit terminal and run: + ```bash + pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake make + git clone https://github.com/mstorsjo/fdk-aac.git + cd fdk-aac + mkdir build && cd build + cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON .. + cmake --build . + ``` +3. Copy the resulting `libfdk-aac-2.dll` to the application directory -# Install to include/alac -alacincludedir = $(includedir)/alac +## Architecture -# Install everything -alacinclude_HEADERS = *.h ``` - -
- -Configure the build and make the library: +Apple Device ──AirPlay──► AirPlay Receiver (.NET 8) + │ + ┌─────────┼─────────┐ + ▼ ▼ ▼ + Screen Mirror Audio Volume + (H.264+AAC) (ALAC) Control + │ │ + ┌─────┴───┐ │ + ▼ ▼ ▼ + Named Pipe FDK-AAC NAudio + (ffplay) Decoder DirectSound ``` -$ autoreconf -fi -$ ./configure -$ make -``` - -### Linux -On terminal type the follow to install build tools -``` -apt-get install build-essential autoconf automake libtool -``` -Add compiled DLL path into 'appsettings_linux.json' file. - -### MacOS -On terminal type the follow to install build tools - -``` -brew install autoconf automake libtool -``` -Add compiled DLL path into 'appsettings_osx.json' file. - -### Windows -Use [this](http://www.gaia-gis.it/gaia-sins/mingw64_how_to.html#env) tutorial to understand how to install build tools and how to compile source code on Windows. -You need MinGW32 or MinGW64 based on arch. - -Put repo folders inside msys64 home folder ('C:\msys64\home\'). -![homefolder](https://user-images.githubusercontent.com/11635557/116857182-b0b9b180-abfc-11eb-8e75-5d1b23d7541f.PNG) - -Start an mingw32.exe or mingw64.exe shell based on arch and execute commands. -![mingwshell](https://user-images.githubusercontent.com/11635557/116857648-756bb280-abfd-11eb-8d6b-43d474f4a27b.PNG) - -The compiled dll will be saved in 'C:\\msys64\\home\\username\\fdk-aac-master\\.libs\\'. -Add compiled DLL path into 'appsettings_win.json' file. - -TIP 1: If the ALAC library gives you an error during the compilation try to insert the following arguments in the 'makefile.am' file: -``` -libalac_la_LDFLAGS = -version-info @ALAC_VERSION@ -no-undefined -static-libgcc -static-libstdc++ -``` -TIP 2: If the error code 126 appears when loading the dll, try to import all the dlls located in the C:\msys64\bin\ folder into the bin folder of the project. -TIP 3: If the error code 193 appears when loading the dll, it means that you are trying to load a dll with the wrong architecture, so you have to compile the dll with the other mingwXX.exe. - -## Wiki - -Here you will find an [Article](https://github.com/SteeBono/airplayreceiver/wiki/AirPlay2-Protocol) where I explain how the whole AirPlay 2 protocol works. - -## Disclamier - -All the resources in this repository are written using only opensource projects. -The code and related resources are meant for educational purposes only. -I do not take any responsibility for the use that will be made of it. +- **Video**: H.264 stream written to a named pipe (`\\.\pipe\AirPlayVideo`), consumed by ffplay or any compatible player +- **Mirroring Audio**: AAC-ELD decoded by native FDK-AAC library via P/Invoke, output through NAudio DirectSound +- **Streaming Audio**: ALAC/AAC decoded and output through NAudio DirectSound ## Credits -Inspired by others AirPlay open source projects. -Big ty to OmgHax.c's author 😱. +Based on open-source AirPlay protocol implementations. Special thanks to: +- [SteeBono/airplayreceiver](https://github.com/SteeBono/airplayreceiver) — Original C# AirPlay receiver +- [itskenny0/airplayreceiver](https://github.com/itskenny0/airplayreceiver) — FDK-AAC mirroring audio implementation +- [UxPlay](https://github.com/FDH2/UxPlay) — AirPlay protocol reference +- [mstorsjo/fdk-aac](https://github.com/mstorsjo/fdk-aac) — FDK-AAC codec library + +## Disclaimer + +All resources in this repository are written using only open-source projects. +The code and related resources are meant for educational purposes only. +The author does not take any responsibility for the use that will be made of it. -## If you want support me 🔥 +## License -If you appreciate my work, consider buying me a cup of coffee to keep me recharged ☕ - -Buy Me A Coffee - -... or ... crypto ... - -BTC: 1BXhfC5U75G2H8b99wk5AedGFxtqJ6xf8q -BCH: 1BXhfC5U75G2H8b99wk5AedGFxtqJ6xf8q -ETH: 0x4Fc12c7C71C581aBc77945Ab9cFBA8DF9692b713 (ERC20) +[MIT License](LICENSE) diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..be3e216 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,98 @@ +# AirPlay 投屏接收器(Windows) + +[English](README.md) + +基于 C# 和 .NET 8 的开源 AirPlay 2 接收器,支持 **屏幕镜像**(含音频)和 **音频推送** 功能。 + +![构建状态](https://github.com/YimingZhanshen/Airplay2OnWindows/workflows/Build%20and%20Test/badge.svg) + +## 功能特性 + +- **屏幕镜像** — 将 iPhone/iPad/Mac 的屏幕投射到 Windows 上,支持 H.264 视频和 AAC-ELD 音频 +- **音频推送** — 通过 AirPlay 播放音乐和播客(支持 ALAC 和 AAC 编解码器) +- **音量控制** — 支持从 Apple 设备远程调节音量 +- **自动发现** — Bonjour/mDNS 服务广播,Windows 电脑自动显示为 AirPlay 接收器 + +## 快速开始 + +### 下载 + +从 [GitHub Actions](https://github.com/YimingZhanshen/Airplay2OnWindows/actions) 的构建产物中下载最新版本。安装包已包含所有依赖(FFmpeg、libfdk-aac)。 + +### 手动配置 + +#### 前置条件 + +- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) 或更高版本 +- [FFmpeg](https://github.com/BtbN/FFmpeg-Builds/releases) — 将 `ffmpeg.exe` 和 `ffplay.exe` 放在程序目录或 PATH 中 +- [libfdk-aac](https://github.com/mstorsjo/fdk-aac) — 将 `libfdk-aac-2.dll` 放在程序目录中(屏幕镜像音频所需) + +#### 编译 + +```bash +dotnet restore AirPlay.sln +dotnet build AirPlay.sln --configuration Release +``` + +#### 运行 + +1. 启动应用程序 +2. 打开视频播放器接收投屏画面: + ```bash + ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay \\.\pipe\AirPlayVideo + ``` +3. 在 Apple 设备上,打开控制中心 → 屏幕镜像 → 选择你的电脑 + +## 在 Windows 上编译 libfdk-aac + +屏幕镜像时需要 `libfdk-aac-2.dll` 来解码 AAC-ELD 音频。编译步骤如下: + +1. 安装 [MSYS2](https://www.msys2.org/) +2. 打开 MSYS2 MinGW 64 位终端,执行: + ```bash + pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake make + git clone https://github.com/mstorsjo/fdk-aac.git + cd fdk-aac + mkdir build && cd build + cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON .. + cmake --build . + ``` +3. 将生成的 `libfdk-aac-2.dll` 复制到程序目录中 + +## 系统架构 + +``` +Apple 设备 ──AirPlay──► AirPlay 接收器 (.NET 8) + │ + ┌─────────┼─────────┐ + ▼ ▼ ▼ + 屏幕镜像 音频 音量 + (H.264+AAC) (ALAC) 控制 + │ │ + ┌─────┴───┐ │ + ▼ ▼ ▼ + 命名管道 FDK-AAC NAudio + (ffplay) 解码器 DirectSound +``` + +- **视频**:H.264 码流写入命名管道(`\\.\pipe\AirPlayVideo`),由 ffplay 或其他兼容播放器消费 +- **镜像音频**:AAC-ELD 通过原生 FDK-AAC 库(P/Invoke)解码,经 NAudio DirectSound 输出 +- **推送音频**:ALAC/AAC 解码后通过 NAudio DirectSound 输出 + +## 致谢 + +基于开源 AirPlay 协议实现。特别感谢: +- [SteeBono/airplayreceiver](https://github.com/SteeBono/airplayreceiver) — 原始 C# AirPlay 接收器 +- [itskenny0/airplayreceiver](https://github.com/itskenny0/airplayreceiver) — FDK-AAC 镜像音频实现 +- [UxPlay](https://github.com/FDH2/UxPlay) — AirPlay 协议参考 +- [mstorsjo/fdk-aac](https://github.com/mstorsjo/fdk-aac) — FDK-AAC 编解码器库 + +## 免责声明 + +本仓库中的所有资源均使用开源项目编写。 +代码及相关资源仅供教育目的使用。 +作者不对其使用方式承担任何责任。 + +## 许可证 + +[MIT 许可证](LICENSE)