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
This commit is contained in:
co-authored by
Claude Opus 4.8 noreply@anthropic.com
parent
a5c2cc3202
commit
cbee29bef9
@@ -0,0 +1,135 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user