141 protected override async Task
ExecuteAsync(CancellationToken stoppingToken)
144 while (!stoppingToken.IsCancellationRequested)
146 if (!
string.IsNullOrWhiteSpace(
_config[
"Agent:Email"]) &&
147 !
string.IsNullOrWhiteSpace(
_config[
"Agent:Password"]))
150 _logger.LogInformation(
"[Agent] Agent:Email/Password 미설정 — 30초 후 재확인.");
151 await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
154 if (stoppingToken.IsCancellationRequested)
return;
157 _logger.LogInformation(
"[Agent] 세그먼트 업로드 시작. 루트: {Root}", outputRoot);
160 var runningCameras =
new Dictionary<string, CancellationTokenSource>();
162 while (!stoppingToken.IsCancellationRequested)
165 if (
_api.Token is
null) { await Task.Delay(5000, stoppingToken);
continue; }
171 .Where(c => c.Enabled && !c.IsDirectHls)
176 foreach (var
id in runningCameras.Keys.Except(currentIds).ToList())
178 runningCameras[id].Cancel();
179 runningCameras.Remove(
id);
180 _logger.LogInformation(
"[Agent] 카메라 루프 중지: {Id}",
id);
184 foreach (var
id in currentIds.Except(runningCameras.Keys))
186 var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
187 runningCameras[id] = cts;
189 _logger.LogInformation(
"[Agent] 카메라 루프 시작: {Id}",
id);
192 await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
196 foreach (var cts
in runningCameras.Values) cts.Cancel();
241 var pushed =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
242 var camDir = Path.Combine(outputRoot, cameraId);
243 byte[]? lastPushedM3u8 =
null;
245 while (!ct.IsCancellationRequested)
249 if (
_api.Token is
null)
252 if (
_api.Token is
null)
254 await Task.Delay(2000, ct);
261 if (!Directory.Exists(camDir))
263 await Task.Delay(1000, ct);
267 var m3u8Path = Path.Combine(camDir,
"index.m3u8");
268 if (!File.Exists(m3u8Path))
270 await Task.Delay(500, ct);
278 m3u8Bytes = await File.ReadAllBytesAsync(m3u8Path, ct);
283 await Task.Delay(100, ct);
289 await Task.Delay(500, ct);
293 bool allRefsPushed =
true;
294 foreach (
string filename
in refs)
296 if (ct.IsCancellationRequested ||
_api.Token is
null)
298 allRefsPushed =
false;
302 if (pushed.Contains(filename))
307 string segmentPath = Path.Combine(camDir, filename);
308 if (!File.Exists(segmentPath))
310 allRefsPushed =
false;
317 pushed.Add(filename);
321 allRefsPushed =
false;
326 if (allRefsPushed && !m3u8Bytes.AsSpan().SequenceEqual(lastPushedM3u8))
328 bool ok = await
_api.PushSegmentAsync(cameraId,
"index.m3u8", m3u8Bytes);
331 lastPushedM3u8 = m3u8Bytes;
335 if (pushed.Count > 200)
337 pushed.RemoveWhere(filename => !refs.Contains(filename, StringComparer.OrdinalIgnoreCase));
340 catch (OperationCanceledException) {
break; }
343 _logger.LogWarning(ex,
"[Agent] [{Cam}] 스캔 중 오류", cameraId);
346 await Task.Delay(200, ct);
451 .Where(c => c.Enabled && !c.IsDirectHls)
454 Id = c.Id, Name = c.Name, Host = c.Host,
455 RtspUrl = c.RtspUrl, AutoReconnect = c.AutoReconnect, IsPublic = c.IsPublic
458 var ok = await
_api.SyncCamerasAsync(localCams);
459 _logger.LogInformation(
"[Agent] 카메라 {Count}개 서버 동기화 {Result}.",
460 localCams.Count, ok ?
"성공" :
"실패");