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
136 lines
4.6 KiB
C#
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
|
|
};
|
|
}
|
|
}
|