Files
live_recorder/src/LiveRecorder.Application/Services/BandwidthStatisticsService.cs
T
nanxunandClaude Opus 4.8 noreply@anthropic.com cbee29bef9 feat: add dashboard, file preview metadata, and bandwidth statistics
Dashboard:
- Add DashboardDto, DashboardService with SQL-level aggregate queries
- Add GET /api/dashboard endpoint with real-time system status
- Add repository aggregate methods (CountByAvailability, SumDuration, etc.)
- Add DashboardView.vue as new landing page with KPI cards, storage status, recent sessions, top rooms
- Update router to make dashboard the new / route, add nav item in sidebar

File Preview:
- Add IVideoMetadataService + FfmpegVideoMetadataService for video metadata extraction
- Extend MediaBrowserItemDto with Metadata and ThumbnailUrl fields
- Add includeMetadata param to media browser API, add thumbnail endpoint
- Add SessionPlaylistDto and GET /api/record-sessions/{id}/playlist for continuous playback

Bandwidth Statistics:
- Add -progress pipe:1 to live recording ffmpeg args for bitrate output
- Parse total_size/bitrate/speed from ffmpeg progress lines during recording
- Write bandwidth samples as SystemLogEntry (Category=Bandwidth) every 30s
- Add BandwidthStatisticsService, BandwidthController with session timeline + daily summary
- Add bandwidth TypeScript types

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
2026-06-04 00:30:02 +08:00

136 lines
4.6 KiB
C#

using System.Text.Json;
using LiveRecorder.Application.Abstractions.Persistence;
using LiveRecorder.Application.Models.RecordTasks;
using LiveRecorder.Domain.Enums;
namespace LiveRecorder.Application.Services;
public sealed class BandwidthStatisticsService
{
private const string BandwidthCategory = "Bandwidth";
private const string SampleMessage = "bandwidth_sample";
private readonly ISystemLogRepository _systemLogRepository;
public BandwidthStatisticsService(ISystemLogRepository systemLogRepository)
{
_systemLogRepository = systemLogRepository;
}
public async Task<BandwidthTimelineDto?> GetSessionTimelineAsync(Guid recordSessionId, CancellationToken cancellationToken = default)
{
// Query all bandwidth sample log entries for this session
// Since there's no dedicated method, we use ListAsync with category filter
var allLogs = await _systemLogRepository.ListAllAsync(cancellationToken);
var bandwidthLogs = allLogs
.Where(item => item.Category == BandwidthCategory
&& item.Message == SampleMessage
&& item.RecordSessionId == recordSessionId)
.OrderBy(item => item.CreatedAt)
.ToList();
if (bandwidthLogs.Count == 0)
{
return null;
}
var points = new List<BandwidthPointDto>(bandwidthLogs.Count);
foreach (var entry in bandwidthLogs)
{
if (string.IsNullOrWhiteSpace(entry.Detail))
{
continue;
}
try
{
using var doc = JsonDocument.Parse(entry.Detail);
long bytesDownloaded = 0;
double? bitrateKbps = null;
if (doc.RootElement.TryGetProperty("bytesDownloaded", out var bd) && bd.TryGetInt64(out var bv))
bytesDownloaded = bv;
if (doc.RootElement.TryGetProperty("bitrateKbps", out var br) && br.TryGetDouble(out var bkv))
bitrateKbps = bkv;
points.Add(new BandwidthPointDto
{
Timestamp = entry.CreatedAt,
BytesDownloaded = bytesDownloaded,
BitrateKbps = bitrateKbps
});
}
catch
{
// Skip malformed entries
}
}
return new BandwidthTimelineDto
{
RecordSessionId = recordSessionId,
Points = points
};
}
public async Task<BandwidthSummaryDto?> GetDailySummaryAsync(DateOnly date, int utcOffsetMinutes, CancellationToken cancellationToken = default)
{
var windowStartUtc = new DateTimeOffset(date.ToDateTime(TimeOnly.MinValue), TimeSpan.FromMinutes(utcOffsetMinutes));
var windowEndUtc = new DateTimeOffset(date.ToDateTime(TimeOnly.MaxValue), TimeSpan.FromMinutes(utcOffsetMinutes));
var allLogs = await _systemLogRepository.ListAllAsync(cancellationToken);
var bandwidthLogs = allLogs
.Where(item => item.Category == BandwidthCategory
&& item.Message == SampleMessage
&& item.CreatedAt >= windowStartUtc
&& item.CreatedAt <= windowEndUtc)
.OrderBy(item => item.CreatedAt)
.ToList();
if (bandwidthLogs.Count == 0)
{
return null;
}
var bitrates = new List<double>();
long maxBytes = 0;
long finalBytes = 0;
foreach (var entry in bandwidthLogs)
{
if (string.IsNullOrWhiteSpace(entry.Detail))
continue;
try
{
using var doc = JsonDocument.Parse(entry.Detail);
if (doc.RootElement.TryGetProperty("bytesDownloaded", out var bd) && bd.TryGetInt64(out var bv))
{
if (bv > maxBytes) maxBytes = bv;
finalBytes = bv;
}
if (doc.RootElement.TryGetProperty("bitrateKbps", out var br) && br.TryGetDouble(out var bkv))
{
bitrates.Add(bkv);
}
}
catch
{
// Skip
}
}
var avgBitrate = bitrates.Count > 0 ? bitrates.Average() : 0;
var peakBitrate = bitrates.Count > 0 ? bitrates.Max() : 0;
return new BandwidthSummaryDto
{
TotalTrafficMB = finalBytes / (1024.0 * 1024.0),
AverageBitrateKbps = avgBitrate,
PeakBitrateKbps = peakBitrate
};
}
}