Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -61,16 +83,21 @@ 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)
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"
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
Expand Down
12 changes: 12 additions & 0 deletions AirPlay/AirPlayReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class AirPlayReceiver : IRtspReceiver, IAirPlayReceiver, IDisposable
public event EventHandler<H264Data> OnH264DataReceived;
public event EventHandler<PcmData> 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";
Expand Down Expand Up @@ -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();
Expand Down
33 changes: 21 additions & 12 deletions AirPlay/AirPlayService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) =>
{
Expand Down
2 changes: 2 additions & 0 deletions AirPlay/IAirPlayReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface IAirPlayReceiver
event EventHandler<H264Data> OnH264DataReceived;
event EventHandler<PcmData> OnPCMDataReceived;
event EventHandler OnAudioFlushReceived;
event EventHandler OnMirroringStartedReceived;
event EventHandler OnMirroringStoppedReceived;

Task StartListeners(CancellationToken cancellationToken);

Expand Down
2 changes: 2 additions & 0 deletions AirPlay/IRtspReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ public interface IRtspReceiver
void OnData(H264Data data);
void OnPCMData(PcmData data);
void OnAudioFlush();
void OnMirroringStarted();
void OnMirroringStopped();
}
}
2 changes: 2 additions & 0 deletions AirPlay/Listeners/AirTunesListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 15 additions & 4 deletions AirPlay/Listeners/AudioListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -444,16 +444,17 @@ 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}");
return 0;
}

/* 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);
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -686,6 +687,16 @@ private int RaopRtpResendCallback(Socket cSocket, ushort control_seqnum, ushort
return 0;
}

/// <summary>
/// Wraparound-aware comparison for 16-bit sequence numbers.
/// Returns true if s1 is strictly before s2 in the circular sequence space.
/// When (s1 - s2) interpreted as signed 16-bit is negative, s1 is before s2.
/// </summary>
private static bool SeqBefore(ushort s1, ushort s2)
{
return ((short)(s1 - s2)) < 0;
}

private void InitializeDecoder (Session session)
{
lock (_decoderLock)
Expand Down
21 changes: 14 additions & 7 deletions AirPlay/Listeners/Bases/BaseTcpListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public override Task StartAsync(CancellationToken cancellationToken)
public override Task StopAsync()
{
_cancellationTokenSource.Cancel();
try { _listener.Stop(); } catch { }
return Task.CompletedTask;
}

Expand All @@ -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)
Expand Down
Loading