Skip to content

Commit c84e8ba

Browse files
Merge pull request #6 from YimingZhanshen/copilot/add-screen-mirror-detection
Auto-launch ffplay on mirroring, fix reconnection port leak, fix audio seq wraparound, optimize CI
2 parents 6039e9b + f0a3a1a commit c84e8ba

11 files changed

Lines changed: 389 additions & 135 deletions

File tree

.github/workflows/build.yml

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,40 @@ jobs:
3131
- name: Publish
3232
run: dotnet publish AirPlay/AirPlay.csproj --configuration Release --runtime win-x64 --self-contained true --output ./publish/win-x64
3333

34+
- name: Cache FFmpeg
35+
id: cache-ffmpeg
36+
uses: actions/cache@v4
37+
with:
38+
path: ffmpeg-cache
39+
key: ffmpeg-win64-gpl-v1
40+
3441
- name: Download FFmpeg
42+
if: steps.cache-ffmpeg.outputs.cache-hit != 'true'
3543
shell: pwsh
3644
run: |
3745
$ffmpegUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"
3846
Invoke-WebRequest -Uri $ffmpegUrl -OutFile ffmpeg.zip
3947
Expand-Archive -Path ffmpeg.zip -DestinationPath ffmpeg-tmp
40-
# Copy ffmpeg.exe and ffplay.exe to publish directory
4148
$binDir = Get-ChildItem -Path ffmpeg-tmp -Recurse -Directory -Filter "bin" | Select-Object -First 1
42-
Copy-Item "$($binDir.FullName)\ffmpeg.exe" -Destination ./publish/win-x64/
43-
Copy-Item "$($binDir.FullName)\ffplay.exe" -Destination ./publish/win-x64/
49+
New-Item -ItemType Directory -Force -Path ffmpeg-cache
50+
Copy-Item "$($binDir.FullName)\ffmpeg.exe" -Destination ffmpeg-cache/
51+
Copy-Item "$($binDir.FullName)\ffplay.exe" -Destination ffmpeg-cache/
52+
53+
- name: Copy FFmpeg to publish
54+
shell: pwsh
55+
run: |
56+
Copy-Item "ffmpeg-cache\ffmpeg.exe" -Destination ./publish/win-x64/
57+
Copy-Item "ffmpeg-cache\ffplay.exe" -Destination ./publish/win-x64/
58+
59+
- name: Cache libfdk-aac
60+
id: cache-fdkaac
61+
uses: actions/cache@v4
62+
with:
63+
path: fdkaac-cache
64+
key: fdkaac-win64-v1
4465

4566
- name: Build libfdk-aac from source
67+
if: steps.cache-fdkaac.outputs.cache-hit != 'true'
4668
shell: bash
4769
run: |
4870
# Build FDK-AAC from official source using MSYS2 (pre-installed on windows-latest)
@@ -61,16 +83,21 @@ jobs:
6183
cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON ..
6284
cmake --build . --config Release
6385
64-
# Find and copy the DLL
86+
# Find and copy the DLL to cache
6587
echo "Built files:"
6688
find . -name "*.dll" -type f
6789
DLL_PATH=$(find . -name "fdk-aac*.dll" -o -name "libfdk-aac*.dll" | head -1)
6890
if [ -z "$DLL_PATH" ]; then
6991
echo "ERROR: No FDK-AAC DLL found after build"
7092
exit 1
7193
fi
72-
cp -v "$DLL_PATH" ../../publish/win-x64/libfdk-aac-2.dll
73-
echo "Successfully built and copied libfdk-aac-2.dll"
94+
mkdir -p ../../fdkaac-cache
95+
cp -v "$DLL_PATH" ../../fdkaac-cache/libfdk-aac-2.dll
96+
echo "Successfully built and cached libfdk-aac-2.dll"
97+
98+
- name: Copy libfdk-aac to publish
99+
shell: bash
100+
run: cp -v fdkaac-cache/libfdk-aac-2.dll publish/win-x64/libfdk-aac-2.dll
74101

75102
- name: Upload build artifacts
76103
uses: actions/upload-artifact@v4

AirPlay/AirPlayReceiver.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class AirPlayReceiver : IRtspReceiver, IAirPlayReceiver, IDisposable
1919
public event EventHandler<H264Data> OnH264DataReceived;
2020
public event EventHandler<PcmData> OnPCMDataReceived;
2121
public event EventHandler OnAudioFlushReceived;
22+
public event EventHandler OnMirroringStartedReceived;
23+
public event EventHandler OnMirroringStoppedReceived;
2224

2325
public const string AirPlayType = "_airplay._tcp";
2426
public const string AirTunesType = "_raop._tcp";
@@ -147,6 +149,16 @@ public void OnAudioFlush()
147149
OnAudioFlushReceived?.Invoke(this, EventArgs.Empty);
148150
}
149151

152+
public void OnMirroringStarted()
153+
{
154+
OnMirroringStartedReceived?.Invoke(this, EventArgs.Empty);
155+
}
156+
157+
public void OnMirroringStopped()
158+
{
159+
OnMirroringStoppedReceived?.Invoke(this, EventArgs.Empty);
160+
}
161+
150162
public void Dispose()
151163
{
152164
_mdns?.Stop();

AirPlay/AirPlayService.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,9 @@ public async Task StartAsync(CancellationToken cancellationToken)
9696
}
9797
}
9898

99-
// Initialize video output (named pipe for external player)
100-
try
101-
{
102-
_videoOutput = new VideoOutputService();
103-
_videoOutput.Initialize();
104-
Console.WriteLine("Video output initialized successfully");
105-
}
106-
catch (Exception ex)
107-
{
108-
Console.WriteLine($"Failed to initialize video output: {ex.Message}");
109-
Console.WriteLine("Continuing without video output...");
110-
}
99+
// Initialize video output service (will be started/stopped per mirroring session)
100+
_videoOutput = new VideoOutputService();
101+
Console.WriteLine("Video output service ready (will auto-launch ffplay when mirroring starts)");
111102

112103
await _airPlayReceiver.StartListeners(cancellationToken);
113104
await _airPlayReceiver.StartMdnsAsync().ConfigureAwait(false);
@@ -129,6 +120,24 @@ public async Task StartAsync(CancellationToken cancellationToken)
129120
}
130121
};
131122

123+
_airPlayReceiver.OnMirroringStartedReceived += (s, e) =>
124+
{
125+
Console.WriteLine("Mirroring started - launching video player...");
126+
lock (_videoOutputLock)
127+
{
128+
_videoOutput?.StartMirroring();
129+
}
130+
};
131+
132+
_airPlayReceiver.OnMirroringStoppedReceived += (s, e) =>
133+
{
134+
Console.WriteLine("Mirroring stopped - closing video player...");
135+
lock (_videoOutputLock)
136+
{
137+
_videoOutput?.StopMirroring();
138+
}
139+
};
140+
132141
// H264 VIDEO OUTPUT
133142
_airPlayReceiver.OnH264DataReceived += (s, e) =>
134143
{

AirPlay/IAirPlayReceiver.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public interface IAirPlayReceiver
1111
event EventHandler<H264Data> OnH264DataReceived;
1212
event EventHandler<PcmData> OnPCMDataReceived;
1313
event EventHandler OnAudioFlushReceived;
14+
event EventHandler OnMirroringStartedReceived;
15+
event EventHandler OnMirroringStoppedReceived;
1416

1517
Task StartListeners(CancellationToken cancellationToken);
1618

AirPlay/IRtspReceiver.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ public interface IRtspReceiver
1111
void OnData(H264Data data);
1212
void OnPCMData(PcmData data);
1313
void OnAudioFlush();
14+
void OnMirroringStarted();
15+
void OnMirroringStopped();
1416
}
1517
}

AirPlay/Listeners/AirTunesListener.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
450450
await mirroring.StartAsync(cancellationToken).ConfigureAwait(false);
451451

452452
session.MirroringListener = mirroring;
453+
_receiver.OnMirroringStarted();
453454
}
454455
if (session.FairPlayReady && (!session.MirroringSession.HasValue || !session.MirroringSession.Value))
455456
{
@@ -612,6 +613,7 @@ public override async Task OnDataReceivedAsync(Request request, Response respons
612613
session.SpsPps = null;
613614
session.StreamConnectionId = null;
614615
session.MirroringSession = null;
616+
_receiver.OnMirroringStopped();
615617
}
616618
// If audio session
617619
if (type == 96)

AirPlay/Listeners/AudioListener.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -444,16 +444,17 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen,
444444
if (_queueCallCount <= 10 || _queueCallCount % 500 == 0)
445445
Console.WriteLine($"[DEBUG-QUEUE] #{_queueCallCount}: seqnum={seqnum}, datalen={datalen}, payloadSize={datalen - 12}, bufferEmpty={raop_buffer.IsEmpty}, firstSeq={raop_buffer.FirstSeqNum}, lastSeq={raop_buffer.LastSeqNum}");
446446

447-
// Ignore, old
448-
if (!raop_buffer.IsEmpty && seqnum < raop_buffer.FirstSeqNum && seqnum != 0)
447+
// Ignore, old (use wraparound-aware comparison for 16-bit sequence numbers)
448+
if (!raop_buffer.IsEmpty && SeqBefore(seqnum, raop_buffer.FirstSeqNum))
449449
{
450450
if (_queueCallCount <= 10)
451451
Console.WriteLine($"[DEBUG-QUEUE] #{_queueCallCount}: SKIP old seqnum={seqnum} < firstSeqNum={raop_buffer.FirstSeqNum}");
452452
return 0;
453453
}
454454

455455
/* Check that there is always space in the buffer, otherwise flush */
456-
if (raop_buffer.FirstSeqNum + RAOP_BUFFER_LENGTH < seqnum || seqnum == 0)
456+
/* Use wraparound-aware gap detection: if seqnum is more than RAOP_BUFFER_LENGTH ahead of FirstSeqNum, flush */
457+
if (!raop_buffer.IsEmpty && (ushort)(seqnum - raop_buffer.FirstSeqNum) >= RAOP_BUFFER_LENGTH)
457458
{
458459
RaopBufferFlush(raop_buffer, seqnum);
459460
}
@@ -543,7 +544,7 @@ public int RaopBufferQueue(RaopBuffer raop_buffer, byte[] data, ushort datalen,
543544
raop_buffer.IsEmpty = false;
544545
}
545546

546-
if (raop_buffer.LastSeqNum < seqnum)
547+
if (SeqBefore(raop_buffer.LastSeqNum, seqnum))
547548
{
548549
raop_buffer.LastSeqNum = seqnum;
549550
}
@@ -686,6 +687,16 @@ private int RaopRtpResendCallback(Socket cSocket, ushort control_seqnum, ushort
686687
return 0;
687688
}
688689

690+
/// <summary>
691+
/// Wraparound-aware comparison for 16-bit sequence numbers.
692+
/// Returns true if s1 is strictly before s2 in the circular sequence space.
693+
/// When (s1 - s2) interpreted as signed 16-bit is negative, s1 is before s2.
694+
/// </summary>
695+
private static bool SeqBefore(ushort s1, ushort s2)
696+
{
697+
return ((short)(s1 - s2)) < 0;
698+
}
699+
689700
private void InitializeDecoder (Session session)
690701
{
691702
lock (_decoderLock)

AirPlay/Listeners/Bases/BaseTcpListener.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public override Task StartAsync(CancellationToken cancellationToken)
4242
public override Task StopAsync()
4343
{
4444
_cancellationTokenSource.Cancel();
45+
try { _listener.Stop(); } catch { }
4546
return Task.CompletedTask;
4647
}
4748

@@ -59,17 +60,23 @@ private async Task AcceptClientsAsync(CancellationToken cancellationToken)
5960
{
6061
_listener.Start();
6162

62-
while (!cancellationToken.IsCancellationRequested)
63+
try
6364
{
64-
var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false);
65-
var task = HandleClientAsync(client, cancellationToken);
66-
67-
var remoteEndpoint = client.Client.RemoteEndPoint.ToString();
68-
if (!_connections.TryAdd(remoteEndpoint, task))
65+
while (!cancellationToken.IsCancellationRequested)
6966
{
70-
client.Close();
67+
var client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false);
68+
var task = HandleClientAsync(client, cancellationToken);
69+
70+
var remoteEndpoint = client.Client.RemoteEndPoint.ToString();
71+
if (!_connections.TryAdd(remoteEndpoint, task))
72+
{
73+
client.Close();
74+
}
7175
}
7276
}
77+
catch (ObjectDisposedException) { }
78+
catch (InvalidOperationException) { }
79+
catch (System.Net.Sockets.SocketException) { }
7380
}
7481

7582
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)

0 commit comments

Comments
 (0)