Skip to content

Commit d169f00

Browse files
committed
Added ffmpeg player retry when source is invalid
1 parent a9d9df1 commit d169f00

3 files changed

Lines changed: 77 additions & 38 deletions

File tree

src/Application/Interfaces/Services/INativePlaceMusicProcessorService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ public interface INativePlaceMusicProcessorService
77
Task<Process> CreateStreamAsync(string audioUrl, CancellationToken cancellationToken);
88
event Func<Task>? OnExitProcess;
99
event Func<Task>? OnProcessStart;
10+
event Func<Task>? ErrorDataReceived;
1011
}

src/Infrastructure/Services/FfmpegProcessService.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ public class FfmpegProcessService(ILogger<FfmpegProcessService> logger, IMusicQu
1111
private bool _disposed;
1212
public event Func<Task>? OnExitProcess;
1313
public event Func<Task>? OnProcessStart;
14-
14+
public event Func<Task>? ErrorDataReceived;
15+
1516
public async Task<Process> CreateStreamAsync(string audioUrl, CancellationToken cancellationToken)
1617
{
1718
DisposeCurrentProcess();
@@ -33,17 +34,23 @@ public async Task<Process> CreateStreamAsync(string audioUrl, CancellationToken
3334
};
3435

3536
// Set up logging
36-
process.ErrorDataReceived += (_, e) =>
37+
process.ErrorDataReceived += async (_, e) =>
3738
{
3839
if (string.IsNullOrEmpty(e.Data)) return;
3940

4041
var level = e.Data.Contains("error", StringComparison.OrdinalIgnoreCase) ||
41-
e.Data.Contains("warning", StringComparison.OrdinalIgnoreCase) ||
4242
e.Data.Contains("failed", StringComparison.OrdinalIgnoreCase)
43-
? LogLevel.Warning
44-
: LogLevel.Debug;
43+
? LogLevel.Error
44+
: e.Data.Contains("warning", StringComparison.OrdinalIgnoreCase)
45+
? LogLevel.Warning
46+
: LogLevel.Debug;
4547

4648
logger.Log(level, "FFmpeg: {Message}", e.Data);
49+
50+
if (level == LogLevel.Error)
51+
{
52+
await (ErrorDataReceived?.Invoke() ?? Task.CompletedTask);
53+
}
4754
};
4855

4956
process.Exited += async (sender, _) =>
@@ -60,7 +67,7 @@ public async Task<Process> CreateStreamAsync(string audioUrl, CancellationToken
6067
logger.LogDebug(ex, "Error reading FFmpeg exit code");
6168
}
6269
};
63-
70+
6471
// Handle cancellation
6572
cancellationToken.Register(() =>
6673
{

src/Infrastructure/Services/NetCordAudioPlayerService.cs

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public class NetCordAudioPlayerService(
2121
public event Func<Task>? NotInVoiceChannelCallback;
2222
private Action<Func<Task>> OnDisconnectAsync { get; set; } = _ => { };
2323
private StringMenuInteractionContext Context { get; set; } = null!;
24+
25+
private int _retryCount = 0;
2426

2527
public async Task Play<T>(T ctx, Action<Func<Task>> onDisconnectAsync)
2628
{
@@ -80,39 +82,9 @@ async Task HandleVoiceStream()
8082
}
8183

8284
var selectedValue = currentTrack.Context.SelectedValues[0];
83-
string sourceUrl;
85+
var sourceUrl = await GetSourceUrl(selectedValue, radioSourceService, youTubeService, currentTrack);
8486

85-
if (Guid.TryParse(selectedValue, out var radioId))
86-
{
87-
sourceUrl = (await radioSourceService.GetRadioSourceByIdAsync(radioId)).SourceUrl;
88-
}
89-
else
90-
{
91-
sourceUrl = await youTubeService.GetAudioStreamUrlAsync(selectedValue,
92-
playerState.SkipCts?.Token ?? CancellationToken.None);
93-
94-
var song = new SongDtoBase
95-
{
96-
Url = selectedValue,
97-
Title = await youTubeService.GetVideoTitleAsync(selectedValue,
98-
playerState.SkipCts?.Token ?? CancellationToken.None),
99-
UserId = currentTrack.Context.User.Id
100-
};
101-
ffmpegProcessService.OnProcessStart += () => HandleOnProcessStartAsync(song);
102-
}
103-
104-
var ffmpeg =
105-
await ffmpegProcessService.CreateStreamAsync(sourceUrl,
106-
playerState.SkipCts?.Token ?? CancellationToken.None);
107-
108-
109-
ffmpegProcessService.OnExitProcess += HandleOnProcessExitAsync;
110-
111-
await ffmpeg.StandardOutput.BaseStream.CopyToAsync(stream,
112-
playerState.SkipCts?.Token ?? CancellationToken.None);
113-
114-
// Flush 'stream' to make sure all the data has been sent and to indicate to Discord that we have finished sending
115-
await stream.FlushAsync(playerState.SkipCts?.Token ?? CancellationToken.None);
87+
await StartFfmpegStream(sourceUrl, stream);
11688
}
11789

11890
async Task StartVoiceClientAsync()
@@ -146,9 +118,54 @@ await client.UpdateVoiceStateAsync(
146118
playerState.StopCts?.Token ?? CancellationToken.None);
147119
await playerState.CurrentVoiceClient.CloseAsync(
148120
cancellationToken: playerState.StopCts?.Token ?? CancellationToken.None);
121+
122+
_retryCount = 0;
149123
}
150124
}
151125

126+
private async Task StartFfmpegStream(string sourceUrl, OpusEncodeStream stream)
127+
{
128+
var ffmpeg =
129+
await ffmpegProcessService.CreateStreamAsync(sourceUrl,
130+
playerState.SkipCts?.Token ?? CancellationToken.None);
131+
132+
ffmpegProcessService.ErrorDataReceived += HandleFfmpegErrorAsync;
133+
ffmpegProcessService.OnExitProcess += HandleOnProcessExitAsync;
134+
135+
await ffmpeg.StandardOutput.BaseStream.CopyToAsync(stream,
136+
playerState.SkipCts?.Token ?? CancellationToken.None);
137+
138+
// Flush 'stream' to make sure all the data has been sent and to indicate to Discord that we have finished sending
139+
await stream.FlushAsync(playerState.SkipCts?.Token ?? CancellationToken.None);
140+
}
141+
142+
private async Task<string> GetSourceUrl(string selectedValue, IRadioSourceService radioSourceService,
143+
IStreamService youTubeService, PlayRequest<StringMenuInteractionContext> currentTrack)
144+
{
145+
string sourceUrl;
146+
147+
if (Guid.TryParse(selectedValue, out var radioId))
148+
{
149+
sourceUrl = (await radioSourceService.GetRadioSourceByIdAsync(radioId)).SourceUrl;
150+
}
151+
else
152+
{
153+
sourceUrl = await youTubeService.GetAudioStreamUrlAsync(selectedValue,
154+
playerState.SkipCts?.Token ?? CancellationToken.None);
155+
156+
var song = new SongDtoBase
157+
{
158+
Url = selectedValue,
159+
Title = await youTubeService.GetVideoTitleAsync(selectedValue,
160+
playerState.SkipCts?.Token ?? CancellationToken.None),
161+
UserId = currentTrack.Context.User.Id
162+
};
163+
ffmpegProcessService.OnProcessStart += () => HandleOnProcessStartAsync(song);
164+
}
165+
166+
return sourceUrl;
167+
}
168+
152169
private async Task HandleOnProcessStartAsync(SongDtoBase song)
153170
{
154171
using var scope = serviceProvider.CreateScope();
@@ -174,4 +191,18 @@ private async ValueTask HandleOnVoiceClientDisconnectedAsync(DisconnectEventArgs
174191
{
175192
await (DisconnectedVoiceClientEvent?.Invoke() ?? Task.CompletedTask);
176193
}
194+
195+
private async Task HandleFfmpegErrorAsync()
196+
{
197+
// Retry to get new stream URL and play again
198+
logger.LogWarning("Ffmpeg error received, retrying to play the stream");
199+
_retryCount++;
200+
if(_retryCount > 3)
201+
{
202+
logger.LogError("Ffmpeg error received, maximum retry count reached, stopping playback");
203+
await (DisconnectedVoiceClientEvent?.Invoke() ?? Task.CompletedTask);
204+
return;
205+
}
206+
await HandleMusicPlayingAsync();
207+
}
177208
}

0 commit comments

Comments
 (0)