Refactor room stats

This commit is contained in:
genteure 2022-04-09 16:43:05 +08:00
parent 95e4f1d5dd
commit 42cbf55fe2
21 changed files with 657 additions and 130 deletions

View File

@ -4,25 +4,43 @@ namespace BililiveRecorder.Core.Event
{ {
public class IOStatsEventArgs : EventArgs public class IOStatsEventArgs : EventArgs
{ {
/// <summary>
/// 当前统计区间的开始时间
/// </summary>
public DateTimeOffset StartTime { get; set; } public DateTimeOffset StartTime { get; set; }
/// <summary>
/// 当前统计区间的结束时间
/// </summary>
public DateTimeOffset EndTime { get; set; } public DateTimeOffset EndTime { get; set; }
/// <summary>
/// 当前统计区间的时长
/// </summary>
public TimeSpan Duration { get; set; } public TimeSpan Duration { get; set; }
/// <summary>
/// 下载了的数据量
/// </summary>
public int NetworkBytesDownloaded { get; set; } public int NetworkBytesDownloaded { get; set; }
/// <summary> /// <summary>
/// mibi-bits per seconds /// 平均下载速度,mibi-bits per second
/// </summary> /// </summary>
public double NetworkMbps { get; set; } public double NetworkMbps { get; set; }
public TimeSpan DiskWriteTime { get; set; } /// <summary>
/// 统计区间内的磁盘写入耗时
/// </summary>
public TimeSpan DiskWriteDuration { get; set; }
/// <summary>
/// 统计区间内写入磁盘的数据量
/// </summary>
public int DiskBytesWritten { get; set; } public int DiskBytesWritten { get; set; }
/// <summary> /// <summary>
/// mibi-bytes per seconds /// 平均写入速度,mibi-bytes per second
/// </summary> /// </summary>
public double DiskMBps { get; set; } public double DiskMBps { get; set; }
} }

View File

@ -4,28 +4,103 @@ namespace BililiveRecorder.Core.Event
{ {
public class RecordingStatsEventArgs : EventArgs public class RecordingStatsEventArgs : EventArgs
{ {
public long InputVideoByteCount { get; set; } /// <summary>
public long InputAudioByteCount { get; set; } /// 从录制开始到现在一共经过的时间,毫秒
/// </summary>
public double SessionDuration { get; set; }
public int OutputVideoFrameCount { get; set; } /// <summary>
public int OutputAudioFrameCount { get; set; } /// 总接受字节数
public long OutputVideoByteCount { get; set; } /// </summary>
public long OutputAudioByteCount { get; set; } public long TotalInputBytes { get; set; }
public long TotalInputVideoByteCount { get; set; } /// <summary>
public long TotalInputAudioByteCount { get; set; } /// 总写入字节数
/// </summary>
public int TotalOutputVideoFrameCount { get; set; } public long TotalOutputBytes { get; set; }
public int TotalOutputAudioFrameCount { get; set; }
public long TotalOutputVideoByteCount { get; set; }
public long TotalOutputAudioByteCount { get; set; }
/// <summary>
/// 当前文件的大小
/// </summary>
public long CurrentFileSize { get; set; } public long CurrentFileSize { get; set; }
public double AddedDuration { get; set; } /// <summary>
public double PassedTime { get; set; } /// 本次直播流收到的最大时间戳(已修复过,相当于总时长,毫秒)
public double DurationRatio { get; set; } /// </summary>
public int SessionMaxTimestamp { get; set; } public int SessionMaxTimestamp { get; set; }
/// <summary>
/// 当前文件的最大时间戳(相当于总时长,毫秒)
/// </summary>
public int FileMaxTimestamp { get; set; } public int FileMaxTimestamp { get; set; }
/// <summary>
/// 当前这一个统计区间的直播数据时长,毫秒
/// </summary>
public double AddedDuration { get; set; }
/// <summary>
/// 当前这一个统计区间所经过的时间长度,毫秒
/// </summary>
public double PassedTime { get; set; }
/// <summary>
/// 录制速度比例
/// </summary>
public double DurationRatio { get; set; }
//----------------------------------------
/// <summary>
/// 当前统计区间新收到的视频数据大小
/// </summary>
public long InputVideoBytes { get; set; }
/// <summary>
/// 当前统计区间新收到的音频数据大小
/// </summary>
public long InputAudioBytes { get; set; }
/// <summary>
/// 当前统计区间新写入的视频帧数量
/// </summary>
public int OutputVideoFrames { get; set; }
/// <summary>
/// 当前统计区间新写入的音频帧数量
/// </summary>
public int OutputAudioFrames { get; set; }
/// <summary>
/// 当前统计区间新写入的视频数据大小
/// </summary>
public long OutputVideoBytes { get; set; }
/// <summary>
/// 当前统计区间新写入的音频数据大小
/// </summary>
public long OutputAudioBytes { get; set; }
/// <summary>
/// 总共收到的视频数据大小
/// </summary>
public long TotalInputVideoBytes { get; set; }
/// <summary>
/// 总共收到的音频数据大小
/// </summary>
public long TotalInputAudioBytes { get; set; }
/// <summary>
/// 总共写入的视频帧数量
/// </summary>
public int TotalOutputVideoFrames { get; set; }
/// <summary>
/// 总共写入的音频帧数量
/// </summary>
public int TotalOutputAudioFrames { get; set; }
/// <summary>
/// 总共写入的视频数据大小
/// </summary>
public long TotalOutputVideoBytes { get; set; }
/// <summary>
/// 总共写入的音频数据大小
/// </summary>
public long TotalOutputAudioBytes { get; set; }
} }
} }

View File

@ -22,7 +22,7 @@ namespace BililiveRecorder.Core
bool Streaming { get; } bool Streaming { get; }
bool DanmakuConnected { get; } bool DanmakuConnected { get; }
bool AutoRecordForThisSession { get; } bool AutoRecordForThisSession { get; }
RecordingStats Stats { get; } RoomStats Stats { get; }
event EventHandler<RecordSessionStartedEventArgs>? RecordSessionStarted; event EventHandler<RecordSessionStartedEventArgs>? RecordSessionStarted;
event EventHandler<RecordSessionEndedEventArgs>? RecordSessionEnded; event EventHandler<RecordSessionEndedEventArgs>? RecordSessionEnded;

View File

@ -13,6 +13,11 @@ namespace BililiveRecorder.Core.ProcessingRules
{ {
public const string SkipStatsKey = nameof(SkipStatsKey); public const string SkipStatsKey = nameof(SkipStatsKey);
public StatsRule(DateTimeOffset? RecordingStart = null)
{
this.RecordingStart = RecordingStart ?? DateTimeOffset.Now;
}
public event EventHandler<RecordingStatsEventArgs>? StatsUpdated; public event EventHandler<RecordingStatsEventArgs>? StatsUpdated;
public long TotalInputVideoByteCount { get; private set; } public long TotalInputVideoByteCount { get; private set; }
@ -29,10 +34,14 @@ namespace BililiveRecorder.Core.ProcessingRules
public int CurrentFileMaxTimestamp { get; private set; } public int CurrentFileMaxTimestamp { get; private set; }
public DateTimeOffset LastWriteTime { get; private set; } public DateTimeOffset LastWriteTime { get; private set; }
public DateTimeOffset RecordingStart { get; }
public void Run(FlvProcessingContext context, Action next) public void Run(FlvProcessingContext context, Action next)
{ {
var e = new RecordingStatsEventArgs(); var e = new RecordingStatsEventArgs
{
SessionDuration = (DateTimeOffset.Now - this.RecordingStart).TotalMilliseconds
};
{ {
static IEnumerable<PipelineDataAction> FilterDataActions(IEnumerable<PipelineAction> actions) static IEnumerable<PipelineDataAction> FilterDataActions(IEnumerable<PipelineAction> actions)
@ -42,11 +51,13 @@ namespace BililiveRecorder.Core.ProcessingRules
yield return dataAction; yield return dataAction;
} }
e.TotalInputVideoByteCount = this.TotalInputVideoByteCount += e.InputVideoByteCount = e.TotalInputVideoBytes = this.TotalInputVideoByteCount += e.InputVideoBytes =
FilterDataActions(context.Actions).ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfVideoData, x => x, x => x); FilterDataActions(context.Actions).ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfVideoData, x => x, x => x);
e.TotalInputAudioByteCount = this.TotalInputAudioByteCount += e.InputAudioByteCount = e.TotalInputAudioBytes = this.TotalInputAudioByteCount += e.InputAudioBytes =
FilterDataActions(context.Actions).ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfAudioData, x => x, x => x); FilterDataActions(context.Actions).ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfAudioData, x => x, x => x);
e.TotalInputBytes = e.TotalInputVideoBytes + e.TotalInputAudioBytes;
} }
next(); next();
@ -82,7 +93,7 @@ namespace BililiveRecorder.Core.ProcessingRules
} }
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
e.PassedTime = (now - this.LastWriteTime).TotalSeconds; e.PassedTime = (now - this.LastWriteTime).TotalMilliseconds;
this.LastWriteTime = now; this.LastWriteTime = now;
e.DurationRatio = e.AddedDuration / e.PassedTime; e.DurationRatio = e.AddedDuration / e.PassedTime;
@ -93,26 +104,28 @@ namespace BililiveRecorder.Core.ProcessingRules
{ {
if (dataActions.Count > 0) if (dataActions.Count > 0)
{ {
e.TotalOutputVideoFrameCount = this.TotalOutputVideoFrameCount += e.OutputVideoFrameCount = e.TotalOutputVideoFrames = this.TotalOutputVideoFrameCount += e.OutputVideoFrames =
dataActions.ToStructEnumerable().Sum(ref LinqFunctions.CountVideoTags, x => x, x => x); dataActions.ToStructEnumerable().Sum(ref LinqFunctions.CountVideoTags, x => x, x => x);
e.TotalOutputAudioFrameCount = this.TotalOutputAudioFrameCount += e.OutputAudioFrameCount = e.TotalOutputAudioFrames = this.TotalOutputAudioFrameCount += e.OutputAudioFrames =
dataActions.ToStructEnumerable().Sum(ref LinqFunctions.CountAudioTags, x => x, x => x); dataActions.ToStructEnumerable().Sum(ref LinqFunctions.CountAudioTags, x => x, x => x);
e.TotalOutputVideoByteCount = this.TotalOutputVideoByteCount += e.OutputVideoByteCount = e.TotalOutputVideoBytes = this.TotalOutputVideoByteCount += e.OutputVideoBytes =
dataActions.ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfVideoDataByNalu, x => x, x => x); dataActions.ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfVideoDataByNalu, x => x, x => x);
e.TotalOutputAudioByteCount = this.TotalOutputAudioByteCount += e.OutputAudioByteCount = e.TotalOutputAudioBytes = this.TotalOutputAudioByteCount += e.OutputAudioBytes =
dataActions.ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfAudioData, x => x, x => x); dataActions.ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfAudioData, x => x, x => x);
e.CurrentFileSize = this.CurrentFileSize += e.OutputVideoByteCount + e.OutputAudioByteCount; e.TotalOutputBytes = e.TotalOutputAudioBytes + e.TotalOutputVideoBytes;
e.CurrentFileSize = this.CurrentFileSize += e.OutputVideoBytes + e.OutputAudioBytes;
foreach (var action in dataActions) foreach (var action in dataActions)
{ {
var tags = action.Tags; var tags = action.Tags;
if (tags.Count > 0) if (tags.Count > 0)
{ {
e.AddedDuration += (tags[tags.Count - 1].Timestamp - tags[0].Timestamp) / 1000d; e.AddedDuration += (tags[tags.Count - 1].Timestamp - tags[0].Timestamp);
this.CurrentFileMaxTimestamp = e.FileMaxTimestamp = tags[tags.Count - 1].Timestamp; this.CurrentFileMaxTimestamp = e.FileMaxTimestamp = tags[tags.Count - 1].Timestamp;
} }
} }

View File

@ -71,7 +71,7 @@ namespace BililiveRecorder.Core.Recording
lock (this.ioDiskStatsLock) lock (this.ioDiskStatsLock)
{ {
this.ioDiskWriteTime += this.ioDiskStopwatch.Elapsed; this.ioDiskWriteDuration += this.ioDiskStopwatch.Elapsed;
this.ioDiskWrittenBytes += bytesRead; this.ioDiskWrittenBytes += bytesRead;
} }
this.ioDiskStopwatch.Reset(); this.ioDiskStopwatch.Reset();

View File

@ -42,7 +42,7 @@ namespace BililiveRecorder.Core.Recording
protected Stopwatch ioDiskStopwatch = new(); protected Stopwatch ioDiskStopwatch = new();
protected object ioDiskStatsLock = new(); protected object ioDiskStatsLock = new();
protected TimeSpan ioDiskWriteTime; protected TimeSpan ioDiskWriteDuration;
protected int ioDiskWrittenBytes; protected int ioDiskWrittenBytes;
private DateTimeOffset ioDiskWarningTimeout; private DateTimeOffset ioDiskWarningTimeout;
@ -130,7 +130,7 @@ namespace BililiveRecorder.Core.Recording
private void Timer_Elapsed_TriggerIOStats(object sender, ElapsedEventArgs e) private void Timer_Elapsed_TriggerIOStats(object sender, ElapsedEventArgs e)
{ {
int networkDownloadBytes, diskWriteBytes; int networkDownloadBytes, diskWriteBytes;
TimeSpan durationDiff, diskWriteTime; TimeSpan durationDiff, diskWriteDuration;
DateTimeOffset startTime, endTime; DateTimeOffset startTime, endTime;
@ -148,15 +148,15 @@ namespace BililiveRecorder.Core.Recording
// disks // disks
lock (this.ioDiskStatsLock) // 锁硬盘统计 lock (this.ioDiskStatsLock) // 锁硬盘统计
{ {
diskWriteTime = this.ioDiskWriteTime; diskWriteDuration = this.ioDiskWriteDuration;
diskWriteBytes = this.ioDiskWrittenBytes; diskWriteBytes = this.ioDiskWrittenBytes;
this.ioDiskWriteTime = TimeSpan.Zero; this.ioDiskWriteDuration = TimeSpan.Zero;
this.ioDiskWrittenBytes = 0; this.ioDiskWrittenBytes = 0;
} }
} }
var netMbps = networkDownloadBytes * (8d / 1024d / 1024d) / durationDiff.TotalSeconds; var netMbps = networkDownloadBytes * (8d / 1024d / 1024d) / durationDiff.TotalSeconds;
var diskMBps = diskWriteBytes / (1024d * 1024d) / diskWriteTime.TotalSeconds; var diskMBps = diskWriteBytes / (1024d * 1024d) / diskWriteDuration.TotalSeconds;
this.OnIOStats(new IOStatsEventArgs this.OnIOStats(new IOStatsEventArgs
{ {
@ -166,12 +166,12 @@ namespace BililiveRecorder.Core.Recording
EndTime = endTime, EndTime = endTime,
NetworkMbps = netMbps, NetworkMbps = netMbps,
DiskBytesWritten = diskWriteBytes, DiskBytesWritten = diskWriteBytes,
DiskWriteTime = diskWriteTime, DiskWriteDuration = diskWriteDuration,
DiskMBps = diskMBps, DiskMBps = diskMBps,
}); });
var now = DateTimeOffset.Now; var now = DateTimeOffset.Now;
if (diskWriteBytes > 0 && this.ioDiskWarningTimeout < now && (diskWriteTime.TotalSeconds > 1d || diskMBps < 2d)) if (diskWriteBytes > 0 && this.ioDiskWarningTimeout < now && (diskWriteDuration.TotalSeconds > 1d || diskMBps < 2d))
{ {
// 硬盘 IO 可能不能满足录播 // 硬盘 IO 可能不能满足录播
this.ioDiskWarningTimeout = now + TimeSpan.FromMinutes(2); // 最多每 2 分钟提醒一次 this.ioDiskWarningTimeout = now + TimeSpan.FromMinutes(2); // 最多每 2 分钟提醒一次

View File

@ -177,7 +177,7 @@ namespace BililiveRecorder.Core.Recording
lock (this.ioDiskStatsLock) lock (this.ioDiskStatsLock)
{ {
this.ioDiskWriteTime += this.ioDiskStopwatch.Elapsed; this.ioDiskWriteDuration += this.ioDiskStopwatch.Elapsed;
this.ioDiskWrittenBytes += bytesWritten; this.ioDiskWrittenBytes += bytesWritten;
} }
this.ioDiskStopwatch.Reset(); this.ioDiskStopwatch.Reset();
@ -257,7 +257,7 @@ namespace BililiveRecorder.Core.Recording
this.splitFileRule.SetSplitBeforeFlag(); this.splitFileRule.SetSplitBeforeFlag();
break; break;
case CuttingMode.BySize: case CuttingMode.BySize:
if ((e.CurrentFileSize + (e.OutputVideoByteCount * 1.1) + e.OutputAudioByteCount) / (1024d * 1024d) > this.room.RoomConfig.CuttingNumber) if ((e.CurrentFileSize + (e.OutputVideoBytes * 1.1) + e.OutputAudioBytes) / (1024d * 1024d) > this.room.RoomConfig.CuttingNumber)
this.splitFileRule.SetSplitBeforeFlag(); this.splitFileRule.SetSplitBeforeFlag();
break; break;
} }

View File

@ -1,57 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
#nullable enable
namespace BililiveRecorder.Core
{
public class RecordingStats : INotifyPropertyChanged
{
private TimeSpan sessionMaxTimestamp;
private TimeSpan fileMaxTimestamp;
private TimeSpan sessionDuration;
private double networkMbps;
private long totalInputBytes;
private long totalOutputBytes;
private double durationRatio;
public RecordingStats()
{
this.Reset();
}
public TimeSpan SessionDuration { get => this.sessionDuration; set => this.SetField(ref this.sessionDuration, value); }
public TimeSpan SessionMaxTimestamp { get => this.sessionMaxTimestamp; set => this.SetField(ref this.sessionMaxTimestamp, value); }
public TimeSpan FileMaxTimestamp { get => this.fileMaxTimestamp; set => this.SetField(ref this.fileMaxTimestamp, value); }
public double DurationRatio { get => this.durationRatio; set => this.SetField(ref this.durationRatio, value); }
public long TotalInputBytes { get => this.totalInputBytes; set => this.SetField(ref this.totalInputBytes, value); }
public long TotalOutputBytes { get => this.totalOutputBytes; set => this.SetField(ref this.totalOutputBytes, value); }
public double NetworkMbps { get => this.networkMbps; set => this.SetField(ref this.networkMbps, value); }
public void Reset()
{
this.SessionDuration = TimeSpan.Zero;
this.SessionMaxTimestamp = TimeSpan.Zero;
this.FileMaxTimestamp = TimeSpan.Zero;
this.DurationRatio = double.NaN;
this.TotalInputBytes = 0;
this.TotalOutputBytes = 0;
this.NetworkMbps = 0;
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetField<T>(ref T location, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(location, value))
return false;
location = value;
this.OnPropertyChanged(propertyName);
return true;
}
}
}

View File

@ -94,7 +94,7 @@ namespace BililiveRecorder.Core
public bool Recording => this.recordTask != null; public bool Recording => this.recordTask != null;
public RoomConfig RoomConfig { get; } public RoomConfig RoomConfig { get; }
public RecordingStats Stats { get; } = new RecordingStats(); public RoomStats Stats { get; } = new RoomStats();
public Guid ObjectId { get; } = Guid.NewGuid(); public Guid ObjectId { get; } = Guid.NewGuid();
@ -353,7 +353,14 @@ namespace BililiveRecorder.Core
{ {
this.logger.Verbose("IO stats: {@stats}", e); this.logger.Verbose("IO stats: {@stats}", e);
this.Stats.StartTime = e.StartTime;
this.Stats.EndTime = e.EndTime;
this.Stats.Duration = e.Duration;
this.Stats.NetworkBytesDownloaded = e.NetworkBytesDownloaded;
this.Stats.NetworkMbps = e.NetworkMbps; this.Stats.NetworkMbps = e.NetworkMbps;
this.Stats.DiskWriteDuration = e.DiskWriteDuration;
this.Stats.DiskBytesWritten = e.DiskBytesWritten;
this.Stats.DiskMBps = e.DiskMBps;
IOStats?.Invoke(this, e); IOStats?.Invoke(this, e);
} }
@ -363,14 +370,31 @@ namespace BililiveRecorder.Core
{ {
this.logger.Verbose("Recording stats: {@stats}", e); this.logger.Verbose("Recording stats: {@stats}", e);
var diff = DateTimeOffset.UtcNow - this.recordTaskStartTime; this.Stats.SessionDuration = TimeSpan.FromMilliseconds(e.SessionDuration);
this.Stats.SessionDuration = TimeSpan.FromSeconds(Math.Round(diff.TotalSeconds)); this.Stats.TotalInputBytes = e.TotalInputBytes;
this.Stats.FileMaxTimestamp = TimeSpan.FromMilliseconds(e.FileMaxTimestamp); this.Stats.TotalOutputBytes = e.TotalOutputBytes;
this.Stats.CurrentFileSize = e.CurrentFileSize;
this.Stats.SessionMaxTimestamp = TimeSpan.FromMilliseconds(e.SessionMaxTimestamp); this.Stats.SessionMaxTimestamp = TimeSpan.FromMilliseconds(e.SessionMaxTimestamp);
this.Stats.FileMaxTimestamp = TimeSpan.FromMilliseconds(e.FileMaxTimestamp);
this.Stats.AddedDuration = e.AddedDuration;
this.Stats.PassedTime = e.PassedTime;
this.Stats.DurationRatio = e.DurationRatio; this.Stats.DurationRatio = e.DurationRatio;
this.Stats.TotalInputBytes = e.TotalInputVideoByteCount + e.TotalInputAudioByteCount; this.Stats.InputVideoBytes = e.InputVideoBytes;
this.Stats.TotalOutputBytes = e.TotalOutputVideoByteCount + e.TotalOutputAudioByteCount; this.Stats.InputAudioBytes = e.InputAudioBytes;
this.Stats.OutputVideoFrames = e.OutputVideoFrames;
this.Stats.OutputAudioFrames = e.OutputAudioFrames;
this.Stats.OutputVideoBytes = e.OutputVideoBytes;
this.Stats.OutputAudioBytes = e.OutputAudioBytes;
this.Stats.TotalInputVideoBytes = e.TotalInputVideoBytes;
this.Stats.TotalInputAudioBytes = e.TotalInputAudioBytes;
this.Stats.TotalOutputVideoFrames = e.TotalOutputVideoFrames;
this.Stats.TotalOutputAudioFrames = e.TotalOutputAudioFrames;
this.Stats.TotalOutputVideoBytes = e.TotalOutputVideoBytes;
this.Stats.TotalOutputAudioBytes = e.TotalOutputAudioBytes;
RecordingStats?.Invoke(this, e); RecordingStats?.Invoke(this, e);
} }

View File

@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
#nullable enable
namespace BililiveRecorder.Core
{
public class RoomStats : INotifyPropertyChanged
{
#region IO Stats Fields
private DateTimeOffset ___StartTime;
private DateTimeOffset ___EndTime;
private TimeSpan ___Duration;
private int ___NetworkBytesDownloaded;
private double ___NetworkMbps;
private TimeSpan ___DiskWriteDuration;
private int ___DiskBytesWritten;
private double ___DiskMBps;
#endregion
#region Recording Stats Fields
private TimeSpan ___SessionDuration;
private long ___TotalInputBytes;
private long ___TotalOutputBytes;
private long ___CurrentFileSize;
private TimeSpan ___SessionMaxTimestamp;
private TimeSpan ___FileMaxTimestamp;
private double ___AddedDuration;
private double ___PassedTime;
private double ___DurationRatio;
private long ___InputVideoBytes;
private long ___InputAudioBytes;
private int ___OutputVideoFrames;
private int ___OutputAudioFrames;
private long ___OutputVideoBytes;
private long ___OutputAudioBytes;
private long ___TotalInputVideoBytes;
private long ___TotalInputAudioBytes;
private int ___TotalOutputVideoFrames;
private int ___TotalOutputAudioFrames;
private long ___TotalOutputVideoBytes;
private long ___TotalOutputAudioBytes;
#endregion
public RoomStats()
{
this.Reset();
}
#region IO Stats Properties
/// <summary>
/// 当前统计区间的开始时间
/// </summary>
public DateTimeOffset StartTime { get => this.___StartTime; set => this.SetField(ref this.___StartTime, value); }
/// <summary>
/// 当前统计区间的结束时间
/// </summary>
public DateTimeOffset EndTime { get => this.___EndTime; set => this.SetField(ref this.___EndTime, value); }
/// <summary>
/// 当前统计区间的时长
/// </summary>
public TimeSpan Duration { get => this.___Duration; set => this.SetField(ref this.___Duration, value); }
/// <summary>
/// 下载了的数据量
/// </summary>
public int NetworkBytesDownloaded { get => this.___NetworkBytesDownloaded; set => this.SetField(ref this.___NetworkBytesDownloaded, value); }
/// <summary>
/// 平均下载速度mibi-bits per second
/// </summary>
public double NetworkMbps { get => this.___NetworkMbps; set => this.SetField(ref this.___NetworkMbps, value); }
/// <summary>
/// 统计区间内的磁盘写入耗时
/// </summary>
public TimeSpan DiskWriteDuration { get => this.___DiskWriteDuration; set => this.SetField(ref this.___DiskWriteDuration, value); }
/// <summary>
/// 统计区间内写入磁盘的数据量
/// </summary>
public int DiskBytesWritten { get => this.___DiskBytesWritten; set => this.SetField(ref this.___DiskBytesWritten, value); }
/// <summary>
/// 平均写入速度mibi-bytes per second
/// </summary>
public double DiskMBps { get => this.___DiskMBps; set => this.SetField(ref this.___DiskMBps, value); }
#endregion
#region Recording Stats Properties
/// <summary>
/// 从录制开始到现在一共经过的时间
/// </summary>
public TimeSpan SessionDuration { get => this.___SessionDuration; set => this.SetField(ref this.___SessionDuration, value); }
/// <summary>
/// 总接受字节数
/// </summary>
public long TotalInputBytes { get => this.___TotalInputBytes; set => this.SetField(ref this.___TotalInputBytes, value); }
/// <summary>
/// 总写入字节数
/// </summary>
public long TotalOutputBytes { get => this.___TotalOutputBytes; set => this.SetField(ref this.___TotalOutputBytes, value); }
/// <summary>
/// 当前文件的大小
/// </summary>
public long CurrentFileSize { get => this.___CurrentFileSize; set => this.SetField(ref this.___CurrentFileSize, value); }
/// <summary>
/// 本次直播流收到的最大时间戳(已修复过,相当于总时长)
/// </summary>
public TimeSpan SessionMaxTimestamp { get => this.___SessionMaxTimestamp; set => this.SetField(ref this.___SessionMaxTimestamp, value); }
/// <summary>
/// 当前文件的最大时间戳(相当于总时长)
/// </summary>
public TimeSpan FileMaxTimestamp { get => this.___FileMaxTimestamp; set => this.SetField(ref this.___FileMaxTimestamp, value); }
/// <summary>
/// 当前这一个统计区间的直播数据时长
/// </summary>
public double AddedDuration { get => this.___AddedDuration; set => this.SetField(ref this.___AddedDuration, value); }
/// <summary>
/// 当前这一个统计区间所经过的时间长度
/// </summary>
public double PassedTime { get => this.___PassedTime; set => this.SetField(ref this.___PassedTime, value); }
/// <summary>
/// 录制速度比例
/// </summary>
public double DurationRatio { get => this.___DurationRatio; set => this.SetField(ref this.___DurationRatio, value); }
//----------------------------------------
/// <summary>
/// 当前统计区间新收到的视频数据大小
/// </summary>
public long InputVideoBytes { get => this.___InputVideoBytes; set => this.SetField(ref this.___InputVideoBytes, value); }
/// <summary>
/// 当前统计区间新收到的音频数据大小
/// </summary>
public long InputAudioBytes { get => this.___InputAudioBytes; set => this.SetField(ref this.___InputAudioBytes, value); }
/// <summary>
/// 当前统计区间新写入的视频帧数量
/// </summary>
public int OutputVideoFrames { get => this.___OutputVideoFrames; set => this.SetField(ref this.___OutputVideoFrames, value); }
/// <summary>
/// 当前统计区间新写入的音频帧数量
/// </summary>
public int OutputAudioFrames { get => this.___OutputAudioFrames; set => this.SetField(ref this.___OutputAudioFrames, value); }
/// <summary>
/// 当前统计区间新写入的视频数据大小
/// </summary>
public long OutputVideoBytes { get => this.___OutputVideoBytes; set => this.SetField(ref this.___OutputVideoBytes, value); }
/// <summary>
/// 当前统计区间新写入的音频数据大小
/// </summary>
public long OutputAudioBytes { get => this.___OutputAudioBytes; set => this.SetField(ref this.___OutputAudioBytes, value); }
/// <summary>
/// 总共收到的视频数据大小
/// </summary>
public long TotalInputVideoBytes { get => this.___TotalInputVideoBytes; set => this.SetField(ref this.___TotalInputVideoBytes, value); }
/// <summary>
/// 总共收到的音频数据大小
/// </summary>
public long TotalInputAudioBytes { get => this.___TotalInputAudioBytes; set => this.SetField(ref this.___TotalInputAudioBytes, value); }
/// <summary>
/// 总共写入的视频帧数量
/// </summary>
public int TotalOutputVideoFrames { get => this.___TotalOutputVideoFrames; set => this.SetField(ref this.___TotalOutputVideoFrames, value); }
/// <summary>
/// 总共写入的音频帧数量
/// </summary>
public int TotalOutputAudioFrames { get => this.___TotalOutputAudioFrames; set => this.SetField(ref this.___TotalOutputAudioFrames, value); }
/// <summary>
/// 总共写入的视频数据大小
/// </summary>
public long TotalOutputVideoBytes { get => this.___TotalOutputVideoBytes; set => this.SetField(ref this.___TotalOutputVideoBytes, value); }
/// <summary>
/// 总共写入的音频数据大小
/// </summary>
public long TotalOutputAudioBytes { get => this.___TotalOutputAudioBytes; set => this.SetField(ref this.___TotalOutputAudioBytes, value); }
#endregion
public void Reset()
{
this.SessionDuration = TimeSpan.Zero;
this.SessionMaxTimestamp = TimeSpan.Zero;
this.FileMaxTimestamp = TimeSpan.Zero;
//this.DurationRatio = double.NaN;
// ------------------------------
this.StartTime = default;
this.EndTime = default;
this.Duration = default;
this.NetworkBytesDownloaded = default;
this.NetworkMbps = default;
this.DiskWriteDuration = default;
this.DiskBytesWritten = default;
this.DiskMBps = default;
// ------------------------------
this.SessionDuration = default;
this.TotalInputBytes = default;
this.TotalOutputBytes = default;
this.CurrentFileSize = default;
this.SessionMaxTimestamp = default;
this.FileMaxTimestamp = default;
this.AddedDuration = default;
this.PassedTime = default;
this.DurationRatio = double.NaN;
this.InputVideoBytes = default;
this.InputAudioBytes = default;
this.OutputVideoFrames = default;
this.OutputAudioFrames = default;
this.OutputVideoBytes = default;
this.OutputAudioBytes = default;
this.TotalInputVideoBytes = default;
this.TotalInputAudioBytes = default;
this.TotalOutputVideoFrames = default;
this.TotalOutputAudioFrames = default;
this.TotalOutputVideoBytes = default;
this.TotalOutputAudioBytes = default;
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetField<T>(ref T location, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(location, value))
return false;
location = value;
this.OnPropertyChanged(propertyName);
return true;
}
}
}

View File

@ -152,12 +152,12 @@ namespace BililiveRecorder.Web.Api
[HttpGet("{roomId:int}/stats")] [HttpGet("{roomId:int}/stats")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)]
public ActionResult<RoomStatsDto> GetRoomStats(int roomId) public ActionResult<RoomRecordingStatsDto> GetRoomStats(int roomId)
{ {
var room = this.FetchRoom(roomId); var room = this.FetchRoom(roomId);
if (room is null) if (room is null)
return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" });
return this.mapper.Map<RoomStatsDto>(room.Stats); return this.mapper.Map<RoomRecordingStatsDto>(room.Stats);
} }
/// <summary> /// <summary>
@ -168,12 +168,12 @@ namespace BililiveRecorder.Web.Api
[HttpGet("{objectId:guid}/stats")] [HttpGet("{objectId:guid}/stats")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)]
public ActionResult<RoomStatsDto> GetRoomStats(Guid objectId) public ActionResult<RoomRecordingStatsDto> GetRoomStats(Guid objectId)
{ {
var room = this.FetchRoom(objectId); var room = this.FetchRoom(objectId);
if (room is null) if (room is null)
return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" });
return this.mapper.Map<RoomStatsDto>(room.Stats); return this.mapper.Map<RoomRecordingStatsDto>(room.Stats);
} }
#endregion #endregion

View File

@ -20,7 +20,7 @@ namespace BililiveRecorder.Web
public event EventHandler<AggregatedRoomEventArgs<RecordSessionEndedEventArgs>>? RecordSessionEnded; public event EventHandler<AggregatedRoomEventArgs<RecordSessionEndedEventArgs>>? RecordSessionEnded;
public event EventHandler<AggregatedRoomEventArgs<RecordFileOpeningEventArgs>>? RecordFileOpening; public event EventHandler<AggregatedRoomEventArgs<RecordFileOpeningEventArgs>>? RecordFileOpening;
public event EventHandler<AggregatedRoomEventArgs<RecordFileClosedEventArgs>>? RecordFileClosed; public event EventHandler<AggregatedRoomEventArgs<RecordFileClosedEventArgs>>? RecordFileClosed;
public event EventHandler<AggregatedRoomEventArgs<NetworkingStatsEventArgs>>? NetworkingStats; public event EventHandler<AggregatedRoomEventArgs<IOStatsEventArgs>>? IOStats;
public event EventHandler<AggregatedRoomEventArgs<RecordingStatsEventArgs>>? RecordingStats; public event EventHandler<AggregatedRoomEventArgs<RecordingStatsEventArgs>>? RecordingStats;
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
#pragma warning restore CS0067 #pragma warning restore CS0067

View File

@ -0,0 +1,20 @@
using BililiveRecorder.Core;
using GraphQL.Types;
namespace BililiveRecorder.Web.Models.Graphql
{
public class IOStatsType : ObjectGraphType<RoomStats>
{
public IOStatsType()
{
this.Field(x => x.StartTime);
this.Field(x => x.EndTime);
this.Field(x => x.Duration, type: typeof(TimeSpanMillisecondsGraphType));
this.Field(x => x.NetworkBytesDownloaded);
this.Field(x => x.NetworkMbps);
this.Field(x => x.DiskWriteDuration, type: typeof(TimeSpanMillisecondsGraphType));
this.Field(x => x.DiskBytesWritten);
this.Field(x => x.DiskMBps);
}
}
}

View File

@ -3,17 +3,31 @@ using GraphQL.Types;
namespace BililiveRecorder.Web.Models.Graphql namespace BililiveRecorder.Web.Models.Graphql
{ {
public class RecordingStatsType : ObjectGraphType<RecordingStats> public class RecordingStatsType : ObjectGraphType<RoomStats>
{ {
public RecordingStatsType() public RecordingStatsType()
{ {
this.Field(x => x.NetworkMbps); this.Field(x => x.SessionDuration, type: typeof(TimeSpanMillisecondsGraphType));
this.Field(x => x.SessionDuration);
this.Field(x => x.FileMaxTimestamp);
this.Field(x => x.SessionMaxTimestamp);
this.Field(x => x.DurationRatio);
this.Field(x => x.TotalInputBytes); this.Field(x => x.TotalInputBytes);
this.Field(x => x.TotalOutputBytes); this.Field(x => x.TotalOutputBytes);
this.Field(x => x.CurrentFileSize);
this.Field(x => x.SessionMaxTimestamp, type: typeof(TimeSpanMillisecondsGraphType));
this.Field(x => x.FileMaxTimestamp, type: typeof(TimeSpanMillisecondsGraphType));
this.Field(x => x.AddedDuration);
this.Field(x => x.PassedTime);
this.Field(x => x.DurationRatio);
this.Field(x => x.InputVideoBytes);
this.Field(x => x.InputAudioBytes);
this.Field(x => x.OutputVideoFrames);
this.Field(x => x.OutputAudioFrames);
this.Field(x => x.OutputVideoBytes);
this.Field(x => x.OutputAudioBytes);
this.Field(x => x.TotalInputVideoBytes);
this.Field(x => x.TotalInputAudioBytes);
this.Field(x => x.TotalOutputVideoFrames);
this.Field(x => x.TotalOutputAudioFrames);
this.Field(x => x.TotalOutputVideoBytes);
this.Field(x => x.TotalOutputAudioBytes);
} }
} }
} }

View File

@ -18,7 +18,8 @@ namespace BililiveRecorder.Web.Models.Graphql
this.Field(x => x.Streaming); this.Field(x => x.Streaming);
this.Field(x => x.AutoRecordForThisSession); this.Field(x => x.AutoRecordForThisSession);
this.Field(x => x.DanmakuConnected); this.Field(x => x.DanmakuConnected);
this.Field(x => x.Stats, type: typeof(RecordingStatsType)); this.Field("ioStats", x => x.Stats, type: typeof(IOStatsType));
this.Field("recordingStats", x => x.Stats, type: typeof(RecordingStatsType));
} }
} }
} }

View File

@ -1,6 +1,7 @@
using AutoMapper; using AutoMapper;
using BililiveRecorder.Core; using BililiveRecorder.Core;
using BililiveRecorder.Core.Config.V3; using BililiveRecorder.Core.Config.V3;
using BililiveRecorder.Core.Event;
namespace BililiveRecorder.Web.Models.Rest namespace BililiveRecorder.Web.Models.Rest
{ {
@ -11,14 +12,23 @@ namespace BililiveRecorder.Web.Models.Rest
this.CreateMap<IRoom, RoomDto>() this.CreateMap<IRoom, RoomDto>()
.ForMember(x => x.RoomId, x => x.MapFrom(s => s.RoomConfig.RoomId)) .ForMember(x => x.RoomId, x => x.MapFrom(s => s.RoomConfig.RoomId))
.ForMember(x => x.AutoRecord, x => x.MapFrom(s => s.RoomConfig.AutoRecord)) .ForMember(x => x.AutoRecord, x => x.MapFrom(s => s.RoomConfig.AutoRecord))
.ForMember(x => x.IoStats, x => x.MapFrom(s => s.Stats))
.ForMember(x => x.RecordingStats, x => x.MapFrom(s => s.Stats))
; ;
this.CreateMap<RecordingStats, RoomStatsDto>() this.CreateMap<RoomStats, RoomRecordingStatsDto>()
.ForMember(x => x.SessionDuration, x => x.MapFrom(s => s.SessionDuration.TotalMilliseconds)) .ForMember(x => x.SessionDuration, x => x.MapFrom(s => s.SessionDuration.TotalMilliseconds))
.ForMember(x => x.SessionMaxTimestamp, x => x.MapFrom(s => s.SessionMaxTimestamp.TotalMilliseconds)) .ForMember(x => x.SessionMaxTimestamp, x => x.MapFrom(s => s.SessionMaxTimestamp.TotalMilliseconds))
.ForMember(x => x.FileMaxTimestamp, x => x.MapFrom(s => s.FileMaxTimestamp.TotalMilliseconds)) .ForMember(x => x.FileMaxTimestamp, x => x.MapFrom(s => s.FileMaxTimestamp.TotalMilliseconds))
; ;
this.CreateMap<RoomStats, RoomIOStatsDto>()
.ForMember(x => x.Duration, x => x.MapFrom(s => s.Duration.TotalMilliseconds))
.ForMember(x => x.DiskWriteDuration, x => x.MapFrom(s => s.DiskWriteDuration.TotalMilliseconds))
;
this.CreateMap<RecordingStatsEventArgs, RoomRecordingStatsDto>();
this.CreateMap<RoomConfig, RoomConfigDto>(); this.CreateMap<RoomConfig, RoomConfigDto>();
this.CreateMap<GlobalConfig, GlobalConfigDto>(); this.CreateMap<GlobalConfig, GlobalConfigDto>();

View File

@ -16,6 +16,7 @@ namespace BililiveRecorder.Web.Models.Rest
public bool Streaming { get; set; } public bool Streaming { get; set; }
public bool DanmakuConnected { get; set; } public bool DanmakuConnected { get; set; }
public bool AutoRecordForThisSession { get; set; } public bool AutoRecordForThisSession { get; set; }
public RoomStatsDto Stats { get; set; } = default!; public RoomRecordingStatsDto RecordingStats { get; set; } = default!;
public RoomIOStatsDto IoStats { get; set; } = default!;
} }
} }

View File

@ -0,0 +1,47 @@
using System;
namespace BililiveRecorder.Web.Models.Rest
{
public class RoomIOStatsDto
{
/// <summary>
/// 当前统计区间的开始时间
/// </summary>
public DateTimeOffset StartTime { get; set; }
/// <summary>
/// 当前统计区间的结束时间
/// </summary>
public DateTimeOffset EndTime { get; set; }
/// <summary>
/// 当前统计区间的时长,毫秒
/// </summary>
public double Duration { get; set; }
/// <summary>
/// 下载了的数据量
/// </summary>
public int NetworkBytesDownloaded { get; set; }
/// <summary>
/// 平均下载速度mibi-bits per second
/// </summary>
public double NetworkMbps { get; set; }
/// <summary>
/// 统计区间内的磁盘写入耗时,毫秒
/// </summary>
public double DiskWriteDuration { get; set; }
/// <summary>
/// 统计区间内写入磁盘的数据量
/// </summary>
public int DiskBytesWritten { get; set; }
/// <summary>
/// 平均写入速度mibi-bytes per second
/// </summary>
public double DiskMBps { get; set; }
}
}

View File

@ -0,0 +1,104 @@
namespace BililiveRecorder.Web.Models.Rest
{
public class RoomRecordingStatsDto
{
/// <summary>
/// 从录制开始到现在一共经过的时间,毫秒
/// </summary>
public double SessionDuration { get; set; }
/// <summary>
/// 总接受字节数
/// </summary>
public long TotalInputBytes { get; set; }
/// <summary>
/// 总写入字节数
/// </summary>
public long TotalOutputBytes { get; set; }
/// <summary>
/// 当前文件的大小
/// </summary>
public long CurrentFileSize { get; set; }
/// <summary>
/// 本次直播流收到的最大时间戳(已修复过,相当于总时长,毫秒)
/// </summary>
public int SessionMaxTimestamp { get; set; }
/// <summary>
/// 当前文件的最大时间戳(相当于总时长,毫秒)
/// </summary>
public int FileMaxTimestamp { get; set; }
/// <summary>
/// 当前这一个统计区间的直播数据时长,毫秒
/// </summary>
public double AddedDuration { get; set; }
/// <summary>
/// 当前这一个统计区间所经过的时间长度,毫秒
/// </summary>
public double PassedTime { get; set; }
/// <summary>
/// 录制速度比例
/// </summary>
public double DurationRatio { get; set; }
//----------------------------------------
/// <summary>
/// 当前统计区间新收到的视频数据大小
/// </summary>
public long InputVideoBytes { get; set; }
/// <summary>
/// 当前统计区间新收到的音频数据大小
/// </summary>
public long InputAudioBytes { get; set; }
/// <summary>
/// 当前统计区间新写入的视频帧数量
/// </summary>
public int OutputVideoFrames { get; set; }
/// <summary>
/// 当前统计区间新写入的音频帧数量
/// </summary>
public int OutputAudioFrames { get; set; }
/// <summary>
/// 当前统计区间新写入的视频数据大小
/// </summary>
public long OutputVideoBytes { get; set; }
/// <summary>
/// 当前统计区间新写入的音频数据大小
/// </summary>
public long OutputAudioBytes { get; set; }
/// <summary>
/// 总共收到的视频数据大小
/// </summary>
public long TotalInputVideoBytes { get; set; }
/// <summary>
/// 总共收到的音频数据大小
/// </summary>
public long TotalInputAudioBytes { get; set; }
/// <summary>
/// 总共写入的视频帧数量
/// </summary>
public int TotalOutputVideoFrames { get; set; }
/// <summary>
/// 总共写入的音频帧数量
/// </summary>
public int TotalOutputAudioFrames { get; set; }
/// <summary>
/// 总共写入的视频数据大小
/// </summary>
public long TotalOutputVideoBytes { get; set; }
/// <summary>
/// 总共写入的音频数据大小
/// </summary>
public long TotalOutputAudioBytes { get; set; }
}
}

View File

@ -1,13 +0,0 @@
namespace BililiveRecorder.Web.Models.Rest
{
public class RoomStatsDto
{
public double SessionDuration { get; set; }
public double SessionMaxTimestamp { get; set; }
public double FileMaxTimestamp { get; set; }
public double DurationRatio { get; set; }
public long TotalInputBytes { get; set; }
public long TotalOutputBytes { get; set; }
public double NetworkMbps { get; set; }
}
}

View File

@ -2,6 +2,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoMapper;
using BililiveRecorder.Core; using BililiveRecorder.Core;
using BililiveRecorder.Web.Graphql; using BililiveRecorder.Web.Graphql;
using BililiveRecorder.Web.Models.Rest; using BililiveRecorder.Web.Models.Rest;
@ -53,6 +54,15 @@ namespace BililiveRecorder.Web
// 实际运行时在 BililiveRecorder.Web.Program 里会加上真的 IRecorder // 实际运行时在 BililiveRecorder.Web.Program 里会加上真的 IRecorder
services.TryAddSingleton<IRecorder>(new FakeRecorderForWeb()); services.TryAddSingleton<IRecorder>(new FakeRecorderForWeb());
#if DEBUG
// TODO 移动到一个单独的测试项目里
if (Debugger.IsAttached)
{
var configuration = new MapperConfiguration(cfg => cfg.AddProfile<DataMappingProfile>());
configuration.AssertConfigurationIsValid();
}
#endif
services services
.AddAutoMapper(c => c.AddProfile<DataMappingProfile>()) .AddAutoMapper(c => c.AddProfile<DataMappingProfile>())
.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())) .AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()))