diff --git a/frontend/src/views/LiveRoomsView.vue b/frontend/src/views/LiveRoomsView.vue
index d7c8ccc..c1ab1ba 100644
--- a/frontend/src/views/LiveRoomsView.vue
+++ b/frontend/src/views/LiveRoomsView.vue
@@ -1,5 +1,5 @@
diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue
index b96c7bf..d455b1f 100644
--- a/frontend/src/views/SettingsView.vue
+++ b/frontend/src/views/SettingsView.vue
@@ -1094,19 +1094,27 @@ onMounted(loadSettings);
.settings-grid {
display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(320px, 0.92fr);
+ grid-template-columns: minmax(0, 1fr);
align-items: start;
- gap: 20px;
+ gap: 18px;
}
.settings-grid__full {
- grid-column: 1 / -1;
+ grid-column: auto;
}
.settings-card :deep(.el-card__body) {
padding-top: 20px;
}
+.settings-card :deep(.el-form) {
+ max-width: 1120px;
+}
+
+.settings-card :deep(.el-input-number) {
+ width: 100%;
+}
+
.helper-panel {
margin-top: 14px;
padding: 14px 16px;
@@ -1359,12 +1367,6 @@ onMounted(loadSettings);
line-height: 1.7;
}
-@media (max-width: 1100px) {
- .settings-grid {
- grid-template-columns: 1fr;
- }
-}
-
@media (max-width: 960px) {
.event-script-grid {
grid-template-columns: 1fr;
@@ -1391,6 +1393,10 @@ onMounted(loadSettings);
max-width: 100%;
}
+ .settings-card :deep(.el-form) {
+ max-width: none;
+ }
+
.event-script-example__row {
grid-template-columns: 1fr;
gap: 4px;
diff --git a/src/LiveRecorder.Infrastructure/Services/LiveRoomPollingBackgroundService.cs b/src/LiveRecorder.Infrastructure/Services/LiveRoomPollingBackgroundService.cs
index 1e5ee39..19c94f6 100644
--- a/src/LiveRecorder.Infrastructure/Services/LiveRoomPollingBackgroundService.cs
+++ b/src/LiveRecorder.Infrastructure/Services/LiveRoomPollingBackgroundService.cs
@@ -24,6 +24,8 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
private static readonly TimeSpan OfflineGracefulStopTimeout = TimeSpan.FromSeconds(20);
private static readonly TimeSpan OfflineForcedStopTimeout = TimeSpan.FromSeconds(8);
private static readonly TimeSpan ExceptionEmailCooldown = TimeSpan.FromHours(6);
+ private static readonly TimeSpan PollDispatchSpacing = TimeSpan.FromMilliseconds(400);
+ private const int MaxConcurrentLiveRoomPolls = 2;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger _logger;
@@ -73,15 +75,7 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
.Select(static item => item.Id)
.ToList();
- foreach (var liveRoomId in liveRoomIds)
- {
- if (stoppingToken.IsCancellationRequested)
- {
- break;
- }
-
- await PollLiveRoomAsync(liveRoomId, settings, stoppingToken);
- }
+ await PollLiveRoomsAsync(liveRoomIds, settings, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
@@ -130,6 +124,52 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
private static Task DelayAsync(int delaySeconds, CancellationToken cancellationToken) =>
Task.Delay(TimeSpan.FromSeconds(Math.Clamp(delaySeconds, 10, 3600)), cancellationToken);
+ private async Task PollLiveRoomsAsync(
+ IReadOnlyList liveRoomIds,
+ SystemSettingsDto settings,
+ CancellationToken cancellationToken)
+ {
+ if (liveRoomIds.Count == 0)
+ {
+ return;
+ }
+
+ using var semaphore = new SemaphoreSlim(Math.Min(MaxConcurrentLiveRoomPolls, liveRoomIds.Count));
+ var tasks = new List(liveRoomIds.Count);
+
+ for (var index = 0; index < liveRoomIds.Count; index++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await semaphore.WaitAsync(cancellationToken);
+ var liveRoomId = liveRoomIds[index];
+ tasks.Add(PollLiveRoomWithReleaseAsync(liveRoomId, settings, semaphore, cancellationToken));
+
+ if (index < liveRoomIds.Count - 1)
+ {
+ await Task.Delay(PollDispatchSpacing, cancellationToken);
+ }
+ }
+
+ await Task.WhenAll(tasks);
+ }
+
+ private async Task PollLiveRoomWithReleaseAsync(
+ Guid liveRoomId,
+ SystemSettingsDto settings,
+ SemaphoreSlim semaphore,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ await PollLiveRoomAsync(liveRoomId, settings, cancellationToken);
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }
+
private async Task PollLiveRoomAsync(Guid liveRoomId, SystemSettingsDto settings, CancellationToken cancellationToken)
{
using var scope = _serviceScopeFactory.CreateScope();