From b23acb4da956a911ab7e5f9926166a58d87240bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:03:42 +0000 Subject: [PATCH 1/9] Initial plan From 7e29a44f6b06ec7d142151b5222d84031ee82df8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:08:17 +0000 Subject: [PATCH 2/9] Auto-show/hide mirroring window: add mirroring start/stop events, auto-launch/kill ffplay, support reconnection Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com> --- AirPlay/AirPlayReceiver.cs | 12 ++ AirPlay/AirPlayService.cs | 33 ++-- AirPlay/IAirPlayReceiver.cs | 2 + AirPlay/IRtspReceiver.cs | 2 + AirPlay/Listeners/AirTunesListener.cs | 2 + AirPlay/Services/VideoOutputService.cs | 238 ++++++++++++++++--------- 6 files changed, 197 insertions(+), 92 deletions(-) diff --git a/AirPlay/AirPlayReceiver.cs b/AirPlay/AirPlayReceiver.cs index 63baf2d..5dfffa2 100644 --- a/AirPlay/AirPlayReceiver.cs +++ b/AirPlay/AirPlayReceiver.cs @@ -19,6 +19,8 @@ public class AirPlayReceiver : IRtspReceiver, IAirPlayReceiver, IDisposable public event EventHandler OnH264DataReceived; public event EventHandler OnPCMDataReceived; public event EventHandler OnAudioFlushReceived; + public event EventHandler OnMirroringStartedReceived; + public event EventHandler OnMirroringStoppedReceived; public const string AirPlayType = "_airplay._tcp"; public const string AirTunesType = "_raop._tcp"; @@ -147,6 +149,16 @@ public void OnAudioFlush() OnAudioFlushReceived?.Invoke(this, EventArgs.Empty); } + public void OnMirroringStarted() + { + OnMirroringStartedReceived?.Invoke(this, EventArgs.Empty); + } + + public void OnMirroringStopped() + { + OnMirroringStoppedReceived?.Invoke(this, EventArgs.Empty); + } + public void Dispose() { _mdns?.Stop(); diff --git a/AirPlay/AirPlayService.cs b/AirPlay/AirPlayService.cs index 0d4781e..1194cf9 100644 --- a/AirPlay/AirPlayService.cs +++ b/AirPlay/AirPlayService.cs @@ -96,18 +96,9 @@ public async Task StartAsync(CancellationToken cancellationToken) } } - // Initialize video output (named pipe for external player) - try - { - _videoOutput = new VideoOutputService(); - _videoOutput.Initialize(); - Console.WriteLine("Video output initialized successfully"); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to initialize video output: {ex.Message}"); - Console.WriteLine("Continuing without video output..."); - } + // Initialize video output service (will be started/stopped per mirroring session) + _videoOutput = new VideoOutputService(); + Console.WriteLine("Video output service ready (will auto-launch ffplay when mirroring starts)"); await _airPlayReceiver.StartListeners(cancellationToken); await _airPlayReceiver.StartMdnsAsync().ConfigureAwait(false); @@ -129,6 +120,24 @@ public async Task StartAsync(CancellationToken cancellationToken) } }; + _airPlayReceiver.OnMirroringStartedReceived += (s, e) => + { + Console.WriteLine("Mirroring started - launching video player..."); + lock (_videoOutputLock) + { + _videoOutput?.StartMirroring(); + } + }; + + _airPlayReceiver.OnMirroringStoppedReceived += (s, e) => + { + Console.WriteLine("Mirroring stopped - closing video player..."); + lock (_videoOutputLock) + { + _videoOutput?.StopMirroring(); + } + }; + // H264 VIDEO OUTPUT _airPlayReceiver.OnH264DataReceived += (s, e) => { diff --git a/AirPlay/IAirPlayReceiver.cs b/AirPlay/IAirPlayReceiver.cs index 9f72683..bb34374 100644 --- a/AirPlay/IAirPlayReceiver.cs +++ b/AirPlay/IAirPlayReceiver.cs @@ -11,6 +11,8 @@ public interface IAirPlayReceiver event EventHandler OnH264DataReceived; event EventHandler OnPCMDataReceived; event EventHandler OnAudioFlushReceived; + event EventHandler OnMirroringStartedReceived; + event EventHandler OnMirroringStoppedReceived; Task StartListeners(CancellationToken cancellationToken); diff --git a/AirPlay/IRtspReceiver.cs b/AirPlay/IRtspReceiver.cs index 8097841..3094bd5 100644 --- a/AirPlay/IRtspReceiver.cs +++ b/AirPlay/IRtspReceiver.cs @@ -11,5 +11,7 @@ public interface IRtspReceiver void OnData(H264Data data); void OnPCMData(PcmData data); void OnAudioFlush(); + void OnMirroringStarted(); + void OnMirroringStopped(); } } diff --git a/AirPlay/Listeners/AirTunesListener.cs b/AirPlay/Listeners/AirTunesListener.cs index d55ae52..d9c1a57 100644 --- a/AirPlay/Listeners/AirTunesListener.cs +++ b/AirPlay/Listeners/AirTunesListener.cs @@ -450,6 +450,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons await mirroring.StartAsync(cancellationToken).ConfigureAwait(false); session.MirroringListener = mirroring; + _receiver.OnMirroringStarted(); } if (session.FairPlayReady && (!session.MirroringSession.HasValue || !session.MirroringSession.Value)) { @@ -612,6 +613,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons session.SpsPps = null; session.StreamConnectionId = null; session.MirroringSession = null; + _receiver.OnMirroringStopped(); } // If audio session if (type == 96) diff --git a/AirPlay/Services/VideoOutputService.cs b/AirPlay/Services/VideoOutputService.cs index 0600567..10e9463 100644 --- a/AirPlay/Services/VideoOutputService.cs +++ b/AirPlay/Services/VideoOutputService.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; @@ -10,19 +11,13 @@ namespace AirPlay.Services { /// /// Video output service that writes H.264 Annex B data to a named pipe. - /// External video players (e.g. ffplay, mpv, vlc) can connect to the pipe - /// to display the AirPlay mirrored screen in real-time. - /// - /// Usage on Windows: - /// ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay \\.\pipe\AirPlayVideo - /// Usage on Linux: - /// ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay /tmp/airplay_video + /// Automatically launches ffplay when mirroring starts and kills it when mirroring stops. + /// Supports repeated start/stop cycles without requiring a program restart. /// public class VideoOutputService : IDisposable { private const string PIPE_NAME = "AirPlayVideo"; private const string UNIX_PIPE_PATH = "/tmp/airplay_video"; - private const int CONNECT_TIMEOUT_MS = 100; private NamedPipeServerStream _pipeServer; private FileStream _unixPipeStream; @@ -32,32 +27,50 @@ public class VideoOutputService : IDisposable private long _frameCount = 0; private CancellationTokenSource _cts; private Task _acceptTask; + private Process _ffplayProcess; public event EventHandler OnStatusChanged; /// - /// Initialize the video output pipe. + /// Start a new mirroring session: create the pipe, launch ffplay, and wait for connection. + /// Can be called multiple times across mirroring sessions. /// - public void Initialize() + public void StartMirroring() { lock (_lock) { if (_disposed) return; + // Clean up any previous session + CleanupSession(); + _cts = new CancellationTokenSource(); + _frameCount = 0; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - InitializeWindowsPipe(); + StartWindowsSession(); } else { - InitializeUnixPipe(); + StartUnixSession(); } } } - private void InitializeWindowsPipe() + /// + /// Stop the current mirroring session: kill ffplay and clean up the pipe. + /// + public void StopMirroring() + { + lock (_lock) + { + Console.WriteLine($"Mirroring stopped ({_frameCount} frames written)"); + CleanupSession(); + } + } + + private void StartWindowsSession() { try { @@ -69,46 +82,164 @@ private void InitializeWindowsPipe() PipeOptions.Asynchronous); Console.WriteLine($"Video pipe created: \\\\.\\pipe\\{PIPE_NAME}"); - Console.WriteLine($" Connect with: ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay \\\\.\\pipe\\{PIPE_NAME}"); - // Start waiting for client connection in background + // Launch ffplay to connect to the pipe + LaunchFfplay($"\\\\.\\pipe\\{PIPE_NAME}"); + + // Wait for ffplay to connect in background _acceptTask = Task.Run(() => WaitForPipeConnection(_cts.Token)); } catch (Exception ex) { - Console.WriteLine($"Failed to create video pipe: {ex.Message}"); + Console.WriteLine($"Failed to start video session: {ex.Message}"); } } - private void InitializeUnixPipe() + private void StartUnixSession() { try { - // Create a FIFO (named pipe) on Unix if (File.Exists(UNIX_PIPE_PATH)) { File.Delete(UNIX_PIPE_PATH); } - // Use mkfifo to create a named pipe - var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + var mkfifo = Process.Start(new ProcessStartInfo { FileName = "mkfifo", Arguments = UNIX_PIPE_PATH, UseShellExecute = false, CreateNoWindow = true }); - process?.WaitForExit(); + mkfifo?.WaitForExit(); Console.WriteLine($"Video FIFO created: {UNIX_PIPE_PATH}"); - Console.WriteLine($" Connect with: ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay {UNIX_PIPE_PATH}"); - // Open the FIFO in a background task (blocks until reader connects) + // Launch ffplay to read from the FIFO + LaunchFfplay(UNIX_PIPE_PATH); + + // Open the FIFO for writing (blocks until reader connects) _acceptTask = Task.Run(() => WaitForUnixPipeConnection(_cts.Token)); } catch (Exception ex) { - Console.WriteLine($"Failed to create video FIFO: {ex.Message}"); + Console.WriteLine($"Failed to start video session: {ex.Message}"); + } + } + + private void LaunchFfplay(string pipePath) + { + try + { + var ffplayPath = FindFfplay(); + if (ffplayPath == null) + { + Console.WriteLine("ffplay not found. Please ensure ffplay is in the application directory or PATH."); + Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay {pipePath}"); + return; + } + + var psi = new ProcessStartInfo + { + FileName = ffplayPath, + Arguments = $"-f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay \"{pipePath}\"", + UseShellExecute = false, + CreateNoWindow = false + }; + + _ffplayProcess = Process.Start(psi); + if (_ffplayProcess != null) + { + Console.WriteLine($"ffplay launched (PID: {_ffplayProcess.Id})"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to launch ffplay: {ex.Message}"); + Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay {pipePath}"); + } + } + + private string FindFfplay() + { + // Check application directory first + var appDir = AppDomain.CurrentDomain.BaseDirectory; + var ffplayName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "ffplay.exe" : "ffplay"; + var localPath = Path.Combine(appDir, ffplayName); + if (File.Exists(localPath)) + { + return localPath; + } + + // Fall back to PATH + try + { + var whichCmd = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "where" : "which"; + var psi = new ProcessStartInfo + { + FileName = whichCmd, + Arguments = "ffplay", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + var proc = Process.Start(psi); + var output = proc?.StandardOutput.ReadLine(); + proc?.WaitForExit(); + if (proc?.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) + { + return output.Trim(); + } + } + catch { } + + return null; + } + + private void StopFfplay() + { + if (_ffplayProcess != null) + { + try + { + if (!_ffplayProcess.HasExited) + { + Console.WriteLine($"Stopping ffplay (PID: {_ffplayProcess.Id})..."); + _ffplayProcess.Kill(); + _ffplayProcess.WaitForExit(3000); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error stopping ffplay: {ex.Message}"); + } + finally + { + _ffplayProcess.Dispose(); + _ffplayProcess = null; + } + } + } + + private void CleanupSession() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + + _connected = false; + + try { _pipeServer?.Dispose(); } catch { } + _pipeServer = null; + + try { _unixPipeStream?.Dispose(); } catch { } + _unixPipeStream = null; + + StopFfplay(); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && File.Exists(UNIX_PIPE_PATH)) + { + try { File.Delete(UNIX_PIPE_PATH); } catch { } } } @@ -145,7 +276,6 @@ private void WaitForUnixPipeConnection(CancellationToken token) { try { - // This will block until a reader connects _unixPipeStream = new FileStream(UNIX_PIPE_PATH, FileMode.Open, FileAccess.Write); lock (_lock) { @@ -203,10 +333,9 @@ public void WriteFrame(H264Data data) } catch (IOException) { - // Pipe broken — reader disconnected Console.WriteLine("Video player disconnected"); _connected = false; - HandleDisconnect(); + OnStatusChanged?.Invoke(this, "disconnected"); } catch (Exception ex) { @@ -215,35 +344,6 @@ public void WriteFrame(H264Data data) } } - private void HandleDisconnect() - { - OnStatusChanged?.Invoke(this, "disconnected"); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - try - { - _pipeServer?.Disconnect(); - } - catch { } - - // Restart waiting for new connection - _acceptTask = Task.Run(() => WaitForPipeConnection(_cts.Token)); - } - } - - /// - /// Handle a mirroring session stop/flush. - /// - public void HandleFlush() - { - lock (_lock) - { - _frameCount = 0; - Console.WriteLine("Video output flushed"); - } - } - public void Dispose() { lock (_lock) @@ -251,31 +351,9 @@ public void Dispose() if (_disposed) return; _disposed = true; - _cts?.Cancel(); - _cts?.Dispose(); - _cts = null; - - try - { - _pipeServer?.Dispose(); - } - catch { } - - try - { - _unixPipeStream?.Dispose(); - } - catch { } - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && File.Exists(UNIX_PIPE_PATH)) - { - try { File.Delete(UNIX_PIPE_PATH); } catch { } - } - - _pipeServer = null; - _unixPipeStream = null; + CleanupSession(); - Console.WriteLine($"Video disposed ({_frameCount} frames total)"); + Console.WriteLine("Video output service disposed"); } } } From 2ea3093cfac0849d9380c0e776f326641860324b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:11:49 +0000 Subject: [PATCH 3/9] Address code review: fix potential deadlock in FindFfplay, add timeout check in StopFfplay, optimize CI workflow with caching Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com> --- .github/workflows/build.yml | 39 ++++++++++++++++++++++---- AirPlay/Services/VideoOutputService.cs | 13 ++++++--- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c66e837..8c59ed9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,18 +31,40 @@ jobs: - name: Publish run: dotnet publish AirPlay/AirPlay.csproj --configuration Release --runtime win-x64 --self-contained true --output ./publish/win-x64 + - name: Cache FFmpeg + id: cache-ffmpeg + uses: actions/cache@v4 + with: + path: ffmpeg-cache + key: ffmpeg-win64-gpl-v1 + - name: Download FFmpeg + if: steps.cache-ffmpeg.outputs.cache-hit != 'true' 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/ + New-Item -ItemType Directory -Force -Path ffmpeg-cache + Copy-Item "$($binDir.FullName)\ffmpeg.exe" -Destination ffmpeg-cache/ + Copy-Item "$($binDir.FullName)\ffplay.exe" -Destination ffmpeg-cache/ + + - name: Copy FFmpeg to publish + shell: pwsh + run: | + Copy-Item "ffmpeg-cache\ffmpeg.exe" -Destination ./publish/win-x64/ + Copy-Item "ffmpeg-cache\ffplay.exe" -Destination ./publish/win-x64/ + + - name: Cache libfdk-aac + id: cache-fdkaac + uses: actions/cache@v4 + with: + path: fdkaac-cache + key: fdkaac-win64-v1 - name: Build libfdk-aac from source + if: steps.cache-fdkaac.outputs.cache-hit != 'true' shell: bash run: | # Build FDK-AAC from official source using MSYS2 (pre-installed on windows-latest) @@ -61,7 +83,7 @@ jobs: cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON .. cmake --build . --config Release - # Find and copy the DLL + # Find and copy the DLL to cache echo "Built files:" find . -name "*.dll" -type f DLL_PATH=$(find . -name "fdk-aac*.dll" -o -name "libfdk-aac*.dll" | head -1) @@ -69,8 +91,13 @@ jobs: 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" + mkdir -p ../../fdkaac-cache + cp -v "$DLL_PATH" ../../fdkaac-cache/libfdk-aac-2.dll + echo "Successfully built and cached libfdk-aac-2.dll" + + - name: Copy libfdk-aac to publish + shell: bash + run: cp -v fdkaac-cache/libfdk-aac-2.dll publish/win-x64/libfdk-aac-2.dll - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/AirPlay/Services/VideoOutputService.cs b/AirPlay/Services/VideoOutputService.cs index 10e9463..ce14234 100644 --- a/AirPlay/Services/VideoOutputService.cs +++ b/AirPlay/Services/VideoOutputService.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.IO; using System.IO.Pipes; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -184,11 +185,12 @@ private string FindFfplay() CreateNoWindow = true }; var proc = Process.Start(psi); - var output = proc?.StandardOutput.ReadLine(); + var output = proc?.StandardOutput.ReadToEnd()?.Trim(); proc?.WaitForExit(); - if (proc?.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) + var firstLine = output?.Split('\n', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim(); + if (proc?.ExitCode == 0 && !string.IsNullOrWhiteSpace(firstLine)) { - return output.Trim(); + return firstLine; } } catch { } @@ -206,7 +208,10 @@ private void StopFfplay() { Console.WriteLine($"Stopping ffplay (PID: {_ffplayProcess.Id})..."); _ffplayProcess.Kill(); - _ffplayProcess.WaitForExit(3000); + if (!_ffplayProcess.WaitForExit(3000)) + { + Console.WriteLine("Warning: ffplay did not exit within timeout"); + } } } catch (Exception ex) From 3265eb7a83a9a355b4a7f5ac922d57d76953c815 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:33:25 +0000 Subject: [PATCH 4/9] Fix audio stuttering on sequence number wraparound and video mirroring connection issues Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com> --- AirPlay/Listeners/AudioListener.cs | 19 +++++-- AirPlay/Services/VideoOutputService.cs | 76 ++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs index 3f56dd6..279d4e1 100644 --- a/AirPlay/Listeners/AudioListener.cs +++ b/AirPlay/Listeners/AudioListener.cs @@ -444,8 +444,8 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, 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) + // Ignore, old (use wraparound-aware comparison for 16-bit sequence numbers) + if (!raop_buffer.IsEmpty && SeqBefore(seqnum, raop_buffer.FirstSeqNum)) { if (_queueCallCount <= 10) Console.WriteLine($"[DEBUG-QUEUE] #{_queueCallCount}: SKIP old seqnum={seqnum} < firstSeqNum={raop_buffer.FirstSeqNum}"); @@ -453,7 +453,8 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, } /* Check that there is always space in the buffer, otherwise flush */ - if (raop_buffer.FirstSeqNum + RAOP_BUFFER_LENGTH < seqnum || seqnum == 0) + /* Use wraparound-aware gap detection: if seqnum is more than RAOP_BUFFER_LENGTH ahead of FirstSeqNum, flush */ + if (!raop_buffer.IsEmpty && (ushort)(seqnum - raop_buffer.FirstSeqNum) >= RAOP_BUFFER_LENGTH) { RaopBufferFlush(raop_buffer, seqnum); } @@ -543,7 +544,7 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen, raop_buffer.IsEmpty = false; } - if (raop_buffer.LastSeqNum < seqnum) + if (SeqBefore(raop_buffer.LastSeqNum, seqnum)) { raop_buffer.LastSeqNum = seqnum; } @@ -686,6 +687,16 @@ private int RaopRtpResendCallback(Socket cSocket, ushort control_seqnum, ushort return 0; } + /// + /// Wraparound-aware comparison for 16-bit sequence numbers. + /// Returns true if s1 is strictly before s2 in the circular sequence space. + /// Uses the standard approach: (s1 - s2) interpreted as signed 16-bit > 0 means s2 is ahead. + /// + private static bool SeqBefore(ushort s1, ushort s2) + { + return ((short)(s1 - s2)) < 0; + } + private void InitializeDecoder (Session session) { lock (_decoderLock) diff --git a/AirPlay/Services/VideoOutputService.cs b/AirPlay/Services/VideoOutputService.cs index ce14234..fd0cdbd 100644 --- a/AirPlay/Services/VideoOutputService.cs +++ b/AirPlay/Services/VideoOutputService.cs @@ -29,6 +29,8 @@ public class VideoOutputService : IDisposable private CancellationTokenSource _cts; private Task _acceptTask; private Process _ffplayProcess; + private System.Collections.Generic.List _pendingFrames = new System.Collections.Generic.List(); + private bool _hasReceivedKeyFrame = false; public event EventHandler OnStatusChanged; @@ -136,14 +138,14 @@ private void LaunchFfplay(string pipePath) if (ffplayPath == null) { Console.WriteLine("ffplay not found. Please ensure ffplay is in the application directory or PATH."); - Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay {pipePath}"); + Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 2048000 -fflags nobuffer -flags low_delay -framedrop {pipePath}"); return; } var psi = new ProcessStartInfo { FileName = ffplayPath, - Arguments = $"-f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay \"{pipePath}\"", + Arguments = $"-f h264 -probesize 2048000 -fflags nobuffer -flags low_delay -framedrop \"{pipePath}\"", UseShellExecute = false, CreateNoWindow = false }; @@ -157,7 +159,7 @@ private void LaunchFfplay(string pipePath) catch (Exception ex) { Console.WriteLine($"Failed to launch ffplay: {ex.Message}"); - Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 32 -analyzeduration 0 -fflags nobuffer -flags low_delay {pipePath}"); + Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 2048000 -fflags nobuffer -flags low_delay -framedrop {pipePath}"); } } @@ -233,6 +235,8 @@ private void CleanupSession() _cts = null; _connected = false; + _pendingFrames.Clear(); + _hasReceivedKeyFrame = false; try { _pipeServer?.Dispose(); } catch { } _pipeServer = null; @@ -260,6 +264,7 @@ private async Task WaitForPipeConnection(CancellationToken token) lock (_lock) { _connected = true; + FlushPendingFrames(); } Console.WriteLine("Video player connected!"); OnStatusChanged?.Invoke(this, "connected"); @@ -285,6 +290,7 @@ private void WaitForUnixPipeConnection(CancellationToken token) lock (_lock) { _connected = true; + FlushPendingFrames(); } Console.WriteLine("Video player connected to FIFO!"); OnStatusChanged?.Invoke(this, "connected"); @@ -298,20 +304,78 @@ private void WaitForUnixPipeConnection(CancellationToken token) } } + /// + /// Flush buffered frames to the pipe. Must be called under _lock. + /// + private void FlushPendingFrames() + { + if (_pendingFrames.Count == 0) return; + + Console.WriteLine($"Flushing {_pendingFrames.Count} buffered frames to video player..."); + try + { + foreach (var frameData in _pendingFrames) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _pipeServer?.Write(frameData, 0, frameData.Length); + } + else + { + _unixPipeStream?.Write(frameData, 0, frameData.Length); + } + _frameCount++; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _pipeServer?.Flush(); + } + else + { + _unixPipeStream?.Flush(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error flushing buffered frames: {ex.Message}"); + } + _pendingFrames.Clear(); + } + /// /// Write H.264 frame data to the pipe. + /// Buffers frames before ffplay connects and replays them on connection. /// public void WriteFrame(H264Data data) { lock (_lock) { - if (_disposed || !_connected) return; + if (_disposed) return; if (data.Data == null || data.Length <= 0) return; - try + bool isKeyFrame = data.FrameType == 5; + + if (!_connected) { - bool isKeyFrame = data.FrameType == 5; + // Buffer frames until ffplay connects. + // On a new keyframe, discard older buffered frames (start fresh from latest IDR). + if (isKeyFrame) + { + _pendingFrames.Clear(); + _hasReceivedKeyFrame = true; + } + if (_hasReceivedKeyFrame) + { + var copy = new byte[data.Length]; + Array.Copy(data.Data, 0, copy, 0, data.Length); + _pendingFrames.Add(copy); + } + return; + } + try + { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { if (_pipeServer != null && _pipeServer.IsConnected) From 2f95e949787b68f58d5c261b6173dbe3dd45fdbe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:36:01 +0000 Subject: [PATCH 5/9] Address code review: fix comments, use List<> type alias, add Collections.Generic using Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com> --- AirPlay/Listeners/AudioListener.cs | 2 +- AirPlay/Services/VideoOutputService.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/AirPlay/Listeners/AudioListener.cs b/AirPlay/Listeners/AudioListener.cs index 279d4e1..1353363 100644 --- a/AirPlay/Listeners/AudioListener.cs +++ b/AirPlay/Listeners/AudioListener.cs @@ -690,7 +690,7 @@ private int RaopRtpResendCallback(Socket cSocket, ushort control_seqnum, ushort /// /// Wraparound-aware comparison for 16-bit sequence numbers. /// Returns true if s1 is strictly before s2 in the circular sequence space. - /// Uses the standard approach: (s1 - s2) interpreted as signed 16-bit > 0 means s2 is ahead. + /// When (s1 - s2) interpreted as signed 16-bit is negative, s1 is before s2. /// private static bool SeqBefore(ushort s1, ushort s2) { diff --git a/AirPlay/Services/VideoOutputService.cs b/AirPlay/Services/VideoOutputService.cs index fd0cdbd..136a80a 100644 --- a/AirPlay/Services/VideoOutputService.cs +++ b/AirPlay/Services/VideoOutputService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Pipes; @@ -29,7 +30,7 @@ public class VideoOutputService : IDisposable private CancellationTokenSource _cts; private Task _acceptTask; private Process _ffplayProcess; - private System.Collections.Generic.List _pendingFrames = new System.Collections.Generic.List(); + private List _pendingFrames = new List(); private bool _hasReceivedKeyFrame = false; public event EventHandler OnStatusChanged; @@ -359,7 +360,7 @@ public void WriteFrame(H264Data data) if (!_connected) { // Buffer frames until ffplay connects. - // On a new keyframe, discard older buffered frames (start fresh from latest IDR). + // On a new keyframe, discard older buffered frames (start fresh from latest keyframe). if (isKeyFrame) { _pendingFrames.Clear(); From 10e1d625e4b3cdb87cde427ddee27473dd969bff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:50:41 +0000 Subject: [PATCH 6/9] Fix mirroring port leak causing reconnection failure and reduce ffplay latency Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com> --- AirPlay/Listeners/Bases/BaseTcpListener.cs | 21 ++++++++++++++------- AirPlay/Services/VideoOutputService.cs | 6 +++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/AirPlay/Listeners/Bases/BaseTcpListener.cs b/AirPlay/Listeners/Bases/BaseTcpListener.cs index eaffd76..bf50f2f 100644 --- a/AirPlay/Listeners/Bases/BaseTcpListener.cs +++ b/AirPlay/Listeners/Bases/BaseTcpListener.cs @@ -42,6 +42,7 @@ public override Task StartAsync(CancellationToken cancellationToken) public override Task StopAsync() { _cancellationTokenSource.Cancel(); + try { _listener.Stop(); } catch { } return Task.CompletedTask; } @@ -59,17 +60,23 @@ private async Task AcceptClientsAsync(CancellationToken cancellationToken) { _listener.Start(); - while (!cancellationToken.IsCancellationRequested) + try { - var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false); - var task = HandleClientAsync(client, cancellationToken); - - var remoteEndpoint = client.Client.RemoteEndPoint.ToString(); - if (!_connections.TryAdd(remoteEndpoint, task)) + while (!cancellationToken.IsCancellationRequested) { - client.Close(); + var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false); + var task = HandleClientAsync(client, cancellationToken); + + var remoteEndpoint = client.Client.RemoteEndPoint.ToString(); + if (!_connections.TryAdd(remoteEndpoint, task)) + { + client.Close(); + } } } + catch (ObjectDisposedException) { } + catch (InvalidOperationException) { } + catch (System.Net.Sockets.SocketException) { } } private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken) diff --git a/AirPlay/Services/VideoOutputService.cs b/AirPlay/Services/VideoOutputService.cs index 136a80a..f5c92e7 100644 --- a/AirPlay/Services/VideoOutputService.cs +++ b/AirPlay/Services/VideoOutputService.cs @@ -139,14 +139,14 @@ private void LaunchFfplay(string pipePath) if (ffplayPath == null) { Console.WriteLine("ffplay not found. Please ensure ffplay is in the application directory or PATH."); - Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 2048000 -fflags nobuffer -flags low_delay -framedrop {pipePath}"); + Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 32768 -analyzeduration 1000000 -fflags nobuffer -flags low_delay -framedrop {pipePath}"); return; } var psi = new ProcessStartInfo { FileName = ffplayPath, - Arguments = $"-f h264 -probesize 2048000 -fflags nobuffer -flags low_delay -framedrop \"{pipePath}\"", + Arguments = $"-f h264 -probesize 32768 -analyzeduration 1000000 -fflags nobuffer -flags low_delay -framedrop \"{pipePath}\"", UseShellExecute = false, CreateNoWindow = false }; @@ -160,7 +160,7 @@ private void LaunchFfplay(string pipePath) catch (Exception ex) { Console.WriteLine($"Failed to launch ffplay: {ex.Message}"); - Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 2048000 -fflags nobuffer -flags low_delay -framedrop {pipePath}"); + Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 32768 -analyzeduration 1000000 -fflags nobuffer -flags low_delay -framedrop {pipePath}"); } } From dcead124604d08327c1dc1004537ffb13df83e57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:08:24 +0000 Subject: [PATCH 7/9] Fix video display latency: add -sync ext -avioflags direct -analyzeduration 0 to ffplay Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com> --- AirPlay/Services/VideoOutputService.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/AirPlay/Services/VideoOutputService.cs b/AirPlay/Services/VideoOutputService.cs index f5c92e7..5ac2252 100644 --- a/AirPlay/Services/VideoOutputService.cs +++ b/AirPlay/Services/VideoOutputService.cs @@ -133,20 +133,30 @@ private void StartUnixSession() private void LaunchFfplay(string pipePath) { + // Low-latency ffplay flags: + // -probesize 32768: small probe size (pre-buffered SPS/PPS+IDR makes this sufficient) + // -analyzeduration 0: don't wait to analyze stream duration + // -fflags nobuffer+discardcorrupt: no input buffering, discard corrupt frames + // -flags low_delay: enable low-delay decoding + // -framedrop: drop frames if display can't keep up + // -avioflags direct: bypass I/O buffering on pipe reads + // -sync ext: use external clock (display frames immediately without A/V sync delay) + var ffplayArgs = $"-f h264 -probesize 32768 -analyzeduration 0 -fflags nobuffer+discardcorrupt -flags low_delay -framedrop -avioflags direct -sync ext \"{pipePath}\""; + try { var ffplayPath = FindFfplay(); if (ffplayPath == null) { Console.WriteLine("ffplay not found. Please ensure ffplay is in the application directory or PATH."); - Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 32768 -analyzeduration 1000000 -fflags nobuffer -flags low_delay -framedrop {pipePath}"); + Console.WriteLine($" You can manually connect with: ffplay {ffplayArgs}"); return; } var psi = new ProcessStartInfo { FileName = ffplayPath, - Arguments = $"-f h264 -probesize 32768 -analyzeduration 1000000 -fflags nobuffer -flags low_delay -framedrop \"{pipePath}\"", + Arguments = ffplayArgs, UseShellExecute = false, CreateNoWindow = false }; @@ -160,7 +170,7 @@ private void LaunchFfplay(string pipePath) catch (Exception ex) { Console.WriteLine($"Failed to launch ffplay: {ex.Message}"); - Console.WriteLine($" You can manually connect with: ffplay -f h264 -probesize 32768 -analyzeduration 1000000 -fflags nobuffer -flags low_delay -framedrop {pipePath}"); + Console.WriteLine($" You can manually connect with: ffplay {ffplayArgs}"); } } From 43ddce0216f1123eeb6e2534704bd16880381b79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:33:29 +0000 Subject: [PATCH 8/9] Fix slow-motion video: replace -sync ext with -vf setpts=0 for immediate frame display Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com> --- AirPlay/Services/VideoOutputService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AirPlay/Services/VideoOutputService.cs b/AirPlay/Services/VideoOutputService.cs index 5ac2252..e26414a 100644 --- a/AirPlay/Services/VideoOutputService.cs +++ b/AirPlay/Services/VideoOutputService.cs @@ -140,8 +140,8 @@ private void LaunchFfplay(string pipePath) // -flags low_delay: enable low-delay decoding // -framedrop: drop frames if display can't keep up // -avioflags direct: bypass I/O buffering on pipe reads - // -sync ext: use external clock (display frames immediately without A/V sync delay) - var ffplayArgs = $"-f h264 -probesize 32768 -analyzeduration 0 -fflags nobuffer+discardcorrupt -flags low_delay -framedrop -avioflags direct -sync ext \"{pipePath}\""; + // -vf setpts=0: zero out PTS so frames display immediately without pacing delay + var ffplayArgs = $"-f h264 -probesize 32768 -analyzeduration 0 -fflags nobuffer+discardcorrupt -flags low_delay -framedrop -avioflags direct -vf setpts=0 \"{pipePath}\""; try { From f0a3a1a5899b6f87c5bbd9f238e9f88cd6be55cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:47:45 +0000 Subject: [PATCH 9/9] Update README.md and README_zh.md for release: auto-launch ffplay, Releases link, updated docs Co-authored-by: YimingZhanshen <76594627+YimingZhanshen@users.noreply.github.com> --- README.md | 36 ++++++++++++++++++++++++------------ README_zh.md | 34 +++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 930b54b..2f5ae84 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Open-source AirPlay 2 receiver for Windows, supporting **screen mirroring** (wit ## Features - **Screen Mirroring** — Mirror your iPhone/iPad/Mac screen to Windows with H.264 video and AAC-ELD audio +- **Auto Video Display** — Video player (ffplay) launches automatically when mirroring starts and closes when mirroring stops. Supports repeated connections without restarting the application. - **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 @@ -17,14 +18,23 @@ Open-source AirPlay 2 receiver for Windows, supporting **screen mirroring** (wit ### Download -Download the latest build from [GitHub Actions](https://github.com/YimingZhanshen/Airplay2OnWindows/actions) artifacts. The package includes all required dependencies (FFmpeg, libfdk-aac). +Download the latest release from [GitHub Releases](https://github.com/YimingZhanshen/Airplay2OnWindows/releases). The package includes all required dependencies (FFmpeg, libfdk-aac). -### Manual Setup +### Usage + +1. Extract the downloaded package +2. Run the application +3. On your Apple device, open Control Center → Screen Mirroring → select your PC +4. The mirroring window will appear automatically — no manual setup needed! + +> **Note**: For audio-only streaming (e.g., music), simply select your PC as the AirPlay output device from any audio app on your Apple device. + +### Build from Source #### Prerequisites - [.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 +- [FFmpeg](https://github.com/BtbN/FFmpeg-Builds/releases) — `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) #### Build @@ -37,15 +47,17 @@ dotnet build AirPlay.sln --configuration Release #### Run 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 +2. On your Apple device, open Control Center → Screen Mirroring → select your PC +3. The mirroring video window (ffplay) will launch automatically + +> **Fallback**: If ffplay is not found, you can manually open a video player: +> ```bash +> ffplay -f h264 -probesize 32768 -analyzeduration 0 -fflags nobuffer+discardcorrupt -flags low_delay -framedrop -avioflags direct -vf setpts=0 \\.\pipe\AirPlayVideo +> ``` ## Building libfdk-aac on Windows -该 `libfdk-aac-2.dll` is required for decoding AAC-ELD audio during screen mirroring. You can build it from source: +The `libfdk-aac-2.dll` is required for decoding AAC-ELD audio during screen mirroring. You can build it from source: 1. Install [MSYS2](https://www.msys2.org/) 2. Open MSYS2 MinGW 64-bit terminal and run: @@ -71,11 +83,11 @@ Apple Device ──AirPlay──► AirPlay Receiver (.NET 8) │ │ ┌─────┴───┐ │ ▼ ▼ ▼ - Named Pipe FDK-AAC NAudio - (ffplay) Decoder DirectSound + ffplay FDK-AAC NAudio + (auto-launch) Decoder DirectSound ``` -- **Video**: H.264 stream written to a named pipe (`\\.\pipe\AirPlayVideo`), consumed by ffplay or any compatible player +- **Video**: H.264 stream written to a named pipe, ffplay is auto-launched to display the mirroring window when a device connects - **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 diff --git a/README_zh.md b/README_zh.md index be8bf00..3ef4e2e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -9,6 +9,7 @@ ## 功能特性 - **屏幕镜像** — 将 iPhone/iPad/Mac 的屏幕投射到 Windows 上,支持 H.264 视频和 AAC-ELD 音频 +- **自动显示画面** — 镜像连接时自动启动视频播放器(ffplay),断开时自动关闭。支持重复连接,无需重启程序。 - **音频推送** — 通过 AirPlay 播放音乐和播客(支持 ALAC 和 AAC 编解码器) - **音量控制** — 支持从 Apple 设备远程调节音量 - **自动发现** — Bonjour/mDNS 服务广播,Windows 电脑自动显示为 AirPlay 接收器 @@ -17,14 +18,23 @@ ### 下载 -从 [GitHub Actions](https://github.com/YimingZhanshen/Airplay2OnWindows/actions) 的构建产物中下载最新版本。安装包已包含所有依赖(FFmpeg、libfdk-aac)。 +从 [GitHub Releases](https://github.com/YimingZhanshen/Airplay2OnWindows/releases) 下载最新版本。安装包已包含所有依赖(FFmpeg、libfdk-aac)。 -### 手动配置 +### 使用方法 + +1. 解压下载的安装包 +2. 运行程序 +3. 在 Apple 设备上,打开控制中心 → 屏幕镜像 → 选择你的电脑 +4. 投屏画面会自动显示,无需手动操作! + +> **提示**:如果只想推送音频(如播放音乐),在 Apple 设备的音频应用中选择你的电脑作为 AirPlay 输出设备即可。 + +### 从源码编译 #### 前置条件 - [.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 中 +- [FFmpeg](https://github.com/BtbN/FFmpeg-Builds/releases) — 将 `ffplay.exe` 放在程序目录或 PATH 中 - [libfdk-aac](https://github.com/mstorsjo/fdk-aac) — 将 `libfdk-aac-2.dll` 放在程序目录中(屏幕镜像音频所需) #### 编译 @@ -37,11 +47,13 @@ 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 设备上,打开控制中心 → 屏幕镜像 → 选择你的电脑 +2. 在 Apple 设备上,打开控制中心 → 屏幕镜像 → 选择你的电脑 +3. 投屏视频窗口(ffplay)会自动启动 + +> **备选方案**:如果未找到 ffplay,可以手动打开视频播放器: +> ```bash +> ffplay -f h264 -probesize 32768 -analyzeduration 0 -fflags nobuffer+discardcorrupt -flags low_delay -framedrop -avioflags direct -vf setpts=0 \\.\pipe\AirPlayVideo +> ``` ## 在 Windows 上编译 libfdk-aac @@ -71,11 +83,11 @@ Apple 设备 ──AirPlay──► AirPlay 接收器 (.NET 8) │ │ ┌─────┴───┐ │ ▼ ▼ ▼ - 命名管道 FDK-AAC NAudio - (ffplay) 解码器 DirectSound + ffplay FDK-AAC NAudio + (自动启动) 解码器 DirectSound ``` -- **视频**:H.264 码流写入命名管道(`\\.\pipe\AirPlayVideo`),由 ffplay 或其他兼容播放器消费 +- **视频**:H.264 码流写入命名管道,设备连接时自动启动 ffplay 显示投屏画面 - **镜像音频**:AAC-ELD 通过原生 FDK-AAC 库(P/Invoke)解码,经 NAudio DirectSound 输出 - **推送音频**:ALAC/AAC 解码后通过 NAudio DirectSound 输出