diff --git a/BililiveRecorder.Core/Event/IOStatsEventArgs.cs b/BililiveRecorder.Core/Event/IOStatsEventArgs.cs index 428510a..11fa4a2 100644 --- a/BililiveRecorder.Core/Event/IOStatsEventArgs.cs +++ b/BililiveRecorder.Core/Event/IOStatsEventArgs.cs @@ -4,25 +4,43 @@ namespace BililiveRecorder.Core.Event { public class IOStatsEventArgs : EventArgs { + /// + /// 当前统计区间的开始时间 + /// public DateTimeOffset StartTime { get; set; } + /// + /// 当前统计区间的结束时间 + /// public DateTimeOffset EndTime { get; set; } + /// + /// 当前统计区间的时长 + /// public TimeSpan Duration { get; set; } + /// + /// 下载了的数据量 + /// public int NetworkBytesDownloaded { get; set; } /// - /// mibi-bits per seconds + /// 平均下载速度,mibi-bits per second /// public double NetworkMbps { get; set; } - public TimeSpan DiskWriteTime { get; set; } + /// + /// 统计区间内的磁盘写入耗时 + /// + public TimeSpan DiskWriteDuration { get; set; } + /// + /// 统计区间内写入磁盘的数据量 + /// public int DiskBytesWritten { get; set; } /// - /// mibi-bytes per seconds + /// 平均写入速度,mibi-bytes per second /// public double DiskMBps { get; set; } } diff --git a/BililiveRecorder.Core/Event/RecordingStatsEventArgs.cs b/BililiveRecorder.Core/Event/RecordingStatsEventArgs.cs index 439b964..3107b6f 100644 --- a/BililiveRecorder.Core/Event/RecordingStatsEventArgs.cs +++ b/BililiveRecorder.Core/Event/RecordingStatsEventArgs.cs @@ -4,28 +4,103 @@ namespace BililiveRecorder.Core.Event { public class RecordingStatsEventArgs : EventArgs { - public long InputVideoByteCount { get; set; } - public long InputAudioByteCount { get; set; } + /// + /// 从录制开始到现在一共经过的时间,毫秒 + /// + public double SessionDuration { get; set; } - public int OutputVideoFrameCount { get; set; } - public int OutputAudioFrameCount { get; set; } - public long OutputVideoByteCount { get; set; } - public long OutputAudioByteCount { get; set; } + /// + /// 总接受字节数 + /// + public long TotalInputBytes { get; set; } - public long TotalInputVideoByteCount { get; set; } - public long TotalInputAudioByteCount { get; set; } - - public int TotalOutputVideoFrameCount { get; set; } - public int TotalOutputAudioFrameCount { get; set; } - public long TotalOutputVideoByteCount { get; set; } - public long TotalOutputAudioByteCount { get; set; } + /// + /// 总写入字节数 + /// + public long TotalOutputBytes { get; set; } + /// + /// 当前文件的大小 + /// public long CurrentFileSize { get; set; } - public double AddedDuration { get; set; } - public double PassedTime { get; set; } - public double DurationRatio { get; set; } + /// + /// 本次直播流收到的最大时间戳(已修复过,相当于总时长,毫秒) + /// public int SessionMaxTimestamp { get; set; } + + /// + /// 当前文件的最大时间戳(相当于总时长,毫秒) + /// public int FileMaxTimestamp { get; set; } + + /// + /// 当前这一个统计区间的直播数据时长,毫秒 + /// + public double AddedDuration { get; set; } + + /// + /// 当前这一个统计区间所经过的时间长度,毫秒 + /// + public double PassedTime { get; set; } + + /// + /// 录制速度比例 + /// + public double DurationRatio { get; set; } + + //---------------------------------------- + + /// + /// 当前统计区间新收到的视频数据大小 + /// + public long InputVideoBytes { get; set; } + /// + /// 当前统计区间新收到的音频数据大小 + /// + public long InputAudioBytes { get; set; } + + /// + /// 当前统计区间新写入的视频帧数量 + /// + public int OutputVideoFrames { get; set; } + /// + /// 当前统计区间新写入的音频帧数量 + /// + public int OutputAudioFrames { get; set; } + /// + /// 当前统计区间新写入的视频数据大小 + /// + public long OutputVideoBytes { get; set; } + /// + /// 当前统计区间新写入的音频数据大小 + /// + public long OutputAudioBytes { get; set; } + + /// + /// 总共收到的视频数据大小 + /// + public long TotalInputVideoBytes { get; set; } + /// + /// 总共收到的音频数据大小 + /// + public long TotalInputAudioBytes { get; set; } + + /// + /// 总共写入的视频帧数量 + /// + public int TotalOutputVideoFrames { get; set; } + /// + /// 总共写入的音频帧数量 + /// + public int TotalOutputAudioFrames { get; set; } + /// + /// 总共写入的视频数据大小 + /// + public long TotalOutputVideoBytes { get; set; } + /// + /// 总共写入的音频数据大小 + /// + public long TotalOutputAudioBytes { get; set; } } } diff --git a/BililiveRecorder.Core/IRoom.cs b/BililiveRecorder.Core/IRoom.cs index fb44ce0..3f75644 100644 --- a/BililiveRecorder.Core/IRoom.cs +++ b/BililiveRecorder.Core/IRoom.cs @@ -22,7 +22,7 @@ namespace BililiveRecorder.Core bool Streaming { get; } bool DanmakuConnected { get; } bool AutoRecordForThisSession { get; } - RecordingStats Stats { get; } + RoomStats Stats { get; } event EventHandler? RecordSessionStarted; event EventHandler? RecordSessionEnded; diff --git a/BililiveRecorder.Core/ProcessingRules/StatsRule.cs b/BililiveRecorder.Core/ProcessingRules/StatsRule.cs index 35ef9d4..1fbfb72 100644 --- a/BililiveRecorder.Core/ProcessingRules/StatsRule.cs +++ b/BililiveRecorder.Core/ProcessingRules/StatsRule.cs @@ -13,6 +13,11 @@ namespace BililiveRecorder.Core.ProcessingRules { public const string SkipStatsKey = nameof(SkipStatsKey); + public StatsRule(DateTimeOffset? RecordingStart = null) + { + this.RecordingStart = RecordingStart ?? DateTimeOffset.Now; + } + public event EventHandler? StatsUpdated; public long TotalInputVideoByteCount { get; private set; } @@ -29,10 +34,14 @@ namespace BililiveRecorder.Core.ProcessingRules public int CurrentFileMaxTimestamp { get; private set; } public DateTimeOffset LastWriteTime { get; private set; } + public DateTimeOffset RecordingStart { get; } public void Run(FlvProcessingContext context, Action next) { - var e = new RecordingStatsEventArgs(); + var e = new RecordingStatsEventArgs + { + SessionDuration = (DateTimeOffset.Now - this.RecordingStart).TotalMilliseconds + }; { static IEnumerable FilterDataActions(IEnumerable actions) @@ -42,11 +51,13 @@ namespace BililiveRecorder.Core.ProcessingRules 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); - e.TotalInputAudioByteCount = this.TotalInputAudioByteCount += e.InputAudioByteCount = + e.TotalInputAudioBytes = this.TotalInputAudioByteCount += e.InputAudioBytes = FilterDataActions(context.Actions).ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfAudioData, x => x, x => x); + + e.TotalInputBytes = e.TotalInputVideoBytes + e.TotalInputAudioBytes; } next(); @@ -82,7 +93,7 @@ namespace BililiveRecorder.Core.ProcessingRules } var now = DateTimeOffset.UtcNow; - e.PassedTime = (now - this.LastWriteTime).TotalSeconds; + e.PassedTime = (now - this.LastWriteTime).TotalMilliseconds; this.LastWriteTime = now; e.DurationRatio = e.AddedDuration / e.PassedTime; @@ -93,26 +104,28 @@ namespace BililiveRecorder.Core.ProcessingRules { 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); - e.TotalOutputAudioFrameCount = this.TotalOutputAudioFrameCount += e.OutputAudioFrameCount = + e.TotalOutputAudioFrames = this.TotalOutputAudioFrameCount += e.OutputAudioFrames = 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); - e.TotalOutputAudioByteCount = this.TotalOutputAudioByteCount += e.OutputAudioByteCount = + e.TotalOutputAudioBytes = this.TotalOutputAudioByteCount += e.OutputAudioBytes = 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) { var tags = action.Tags; 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; } } diff --git a/BililiveRecorder.Core/Recording/RawDataRecordTask.cs b/BililiveRecorder.Core/Recording/RawDataRecordTask.cs index 4517b88..37aa620 100644 --- a/BililiveRecorder.Core/Recording/RawDataRecordTask.cs +++ b/BililiveRecorder.Core/Recording/RawDataRecordTask.cs @@ -71,7 +71,7 @@ namespace BililiveRecorder.Core.Recording lock (this.ioDiskStatsLock) { - this.ioDiskWriteTime += this.ioDiskStopwatch.Elapsed; + this.ioDiskWriteDuration += this.ioDiskStopwatch.Elapsed; this.ioDiskWrittenBytes += bytesRead; } this.ioDiskStopwatch.Reset(); diff --git a/BililiveRecorder.Core/Recording/RecordTaskBase.cs b/BililiveRecorder.Core/Recording/RecordTaskBase.cs index a19f16f..a666c11 100644 --- a/BililiveRecorder.Core/Recording/RecordTaskBase.cs +++ b/BililiveRecorder.Core/Recording/RecordTaskBase.cs @@ -42,7 +42,7 @@ namespace BililiveRecorder.Core.Recording protected Stopwatch ioDiskStopwatch = new(); protected object ioDiskStatsLock = new(); - protected TimeSpan ioDiskWriteTime; + protected TimeSpan ioDiskWriteDuration; protected int ioDiskWrittenBytes; private DateTimeOffset ioDiskWarningTimeout; @@ -130,7 +130,7 @@ namespace BililiveRecorder.Core.Recording private void Timer_Elapsed_TriggerIOStats(object sender, ElapsedEventArgs e) { int networkDownloadBytes, diskWriteBytes; - TimeSpan durationDiff, diskWriteTime; + TimeSpan durationDiff, diskWriteDuration; DateTimeOffset startTime, endTime; @@ -148,15 +148,15 @@ namespace BililiveRecorder.Core.Recording // disks lock (this.ioDiskStatsLock) // 锁硬盘统计 { - diskWriteTime = this.ioDiskWriteTime; + diskWriteDuration = this.ioDiskWriteDuration; diskWriteBytes = this.ioDiskWrittenBytes; - this.ioDiskWriteTime = TimeSpan.Zero; + this.ioDiskWriteDuration = TimeSpan.Zero; this.ioDiskWrittenBytes = 0; } } 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 { @@ -166,12 +166,12 @@ namespace BililiveRecorder.Core.Recording EndTime = endTime, NetworkMbps = netMbps, DiskBytesWritten = diskWriteBytes, - DiskWriteTime = diskWriteTime, + DiskWriteDuration = diskWriteDuration, DiskMBps = diskMBps, }); 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 可能不能满足录播 this.ioDiskWarningTimeout = now + TimeSpan.FromMinutes(2); // 最多每 2 分钟提醒一次 diff --git a/BililiveRecorder.Core/Recording/StandardRecordTask.cs b/BililiveRecorder.Core/Recording/StandardRecordTask.cs index d180700..8348efc 100644 --- a/BililiveRecorder.Core/Recording/StandardRecordTask.cs +++ b/BililiveRecorder.Core/Recording/StandardRecordTask.cs @@ -177,7 +177,7 @@ namespace BililiveRecorder.Core.Recording lock (this.ioDiskStatsLock) { - this.ioDiskWriteTime += this.ioDiskStopwatch.Elapsed; + this.ioDiskWriteDuration += this.ioDiskStopwatch.Elapsed; this.ioDiskWrittenBytes += bytesWritten; } this.ioDiskStopwatch.Reset(); @@ -257,7 +257,7 @@ namespace BililiveRecorder.Core.Recording this.splitFileRule.SetSplitBeforeFlag(); break; 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(); break; } diff --git a/BililiveRecorder.Core/RecordingStats.cs b/BililiveRecorder.Core/RecordingStats.cs deleted file mode 100644 index c238b49..0000000 --- a/BililiveRecorder.Core/RecordingStats.cs +++ /dev/null @@ -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(ref T location, T value, [CallerMemberName] string propertyName = "") - { - if (EqualityComparer.Default.Equals(location, value)) - return false; - location = value; - this.OnPropertyChanged(propertyName); - return true; - } - } -} diff --git a/BililiveRecorder.Core/Room.cs b/BililiveRecorder.Core/Room.cs index 9a9e2a2..6fd1e23 100644 --- a/BililiveRecorder.Core/Room.cs +++ b/BililiveRecorder.Core/Room.cs @@ -94,7 +94,7 @@ namespace BililiveRecorder.Core public bool Recording => this.recordTask != null; public RoomConfig RoomConfig { get; } - public RecordingStats Stats { get; } = new RecordingStats(); + public RoomStats Stats { get; } = new RoomStats(); public Guid ObjectId { get; } = Guid.NewGuid(); @@ -353,7 +353,14 @@ namespace BililiveRecorder.Core { 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.DiskWriteDuration = e.DiskWriteDuration; + this.Stats.DiskBytesWritten = e.DiskBytesWritten; + this.Stats.DiskMBps = e.DiskMBps; IOStats?.Invoke(this, e); } @@ -363,14 +370,31 @@ namespace BililiveRecorder.Core { this.logger.Verbose("Recording stats: {@stats}", e); - var diff = DateTimeOffset.UtcNow - this.recordTaskStartTime; - this.Stats.SessionDuration = TimeSpan.FromSeconds(Math.Round(diff.TotalSeconds)); - this.Stats.FileMaxTimestamp = TimeSpan.FromMilliseconds(e.FileMaxTimestamp); + this.Stats.SessionDuration = TimeSpan.FromMilliseconds(e.SessionDuration); + this.Stats.TotalInputBytes = e.TotalInputBytes; + this.Stats.TotalOutputBytes = e.TotalOutputBytes; + this.Stats.CurrentFileSize = e.CurrentFileSize; 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.TotalInputBytes = e.TotalInputVideoByteCount + e.TotalInputAudioByteCount; - this.Stats.TotalOutputBytes = e.TotalOutputVideoByteCount + e.TotalOutputAudioByteCount; + this.Stats.InputVideoBytes = e.InputVideoBytes; + 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); } diff --git a/BililiveRecorder.Core/RoomStats.cs b/BililiveRecorder.Core/RoomStats.cs new file mode 100644 index 0000000..0a79ad6 --- /dev/null +++ b/BililiveRecorder.Core/RoomStats.cs @@ -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 + + /// + /// 当前统计区间的开始时间 + /// + public DateTimeOffset StartTime { get => this.___StartTime; set => this.SetField(ref this.___StartTime, value); } + + /// + /// 当前统计区间的结束时间 + /// + public DateTimeOffset EndTime { get => this.___EndTime; set => this.SetField(ref this.___EndTime, value); } + + /// + /// 当前统计区间的时长 + /// + public TimeSpan Duration { get => this.___Duration; set => this.SetField(ref this.___Duration, value); } + + /// + /// 下载了的数据量 + /// + public int NetworkBytesDownloaded { get => this.___NetworkBytesDownloaded; set => this.SetField(ref this.___NetworkBytesDownloaded, value); } + + /// + /// 平均下载速度,mibi-bits per second + /// + public double NetworkMbps { get => this.___NetworkMbps; set => this.SetField(ref this.___NetworkMbps, value); } + + /// + /// 统计区间内的磁盘写入耗时 + /// + public TimeSpan DiskWriteDuration { get => this.___DiskWriteDuration; set => this.SetField(ref this.___DiskWriteDuration, value); } + + /// + /// 统计区间内写入磁盘的数据量 + /// + public int DiskBytesWritten { get => this.___DiskBytesWritten; set => this.SetField(ref this.___DiskBytesWritten, value); } + + /// + /// 平均写入速度,mibi-bytes per second + /// + public double DiskMBps { get => this.___DiskMBps; set => this.SetField(ref this.___DiskMBps, value); } + + #endregion + #region Recording Stats Properties + + /// + /// 从录制开始到现在一共经过的时间 + /// + public TimeSpan SessionDuration { get => this.___SessionDuration; set => this.SetField(ref this.___SessionDuration, 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 long CurrentFileSize { get => this.___CurrentFileSize; set => this.SetField(ref this.___CurrentFileSize, 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 AddedDuration { get => this.___AddedDuration; set => this.SetField(ref this.___AddedDuration, value); } + + /// + /// 当前这一个统计区间所经过的时间长度 + /// + public double PassedTime { get => this.___PassedTime; set => this.SetField(ref this.___PassedTime, value); } + + /// + /// 录制速度比例 + /// + public double DurationRatio { get => this.___DurationRatio; set => this.SetField(ref this.___DurationRatio, value); } + + //---------------------------------------- + + /// + /// 当前统计区间新收到的视频数据大小 + /// + public long InputVideoBytes { get => this.___InputVideoBytes; set => this.SetField(ref this.___InputVideoBytes, value); } + /// + /// 当前统计区间新收到的音频数据大小 + /// + public long InputAudioBytes { get => this.___InputAudioBytes; set => this.SetField(ref this.___InputAudioBytes, value); } + + /// + /// 当前统计区间新写入的视频帧数量 + /// + public int OutputVideoFrames { get => this.___OutputVideoFrames; set => this.SetField(ref this.___OutputVideoFrames, value); } + /// + /// 当前统计区间新写入的音频帧数量 + /// + public int OutputAudioFrames { get => this.___OutputAudioFrames; set => this.SetField(ref this.___OutputAudioFrames, value); } + /// + /// 当前统计区间新写入的视频数据大小 + /// + public long OutputVideoBytes { get => this.___OutputVideoBytes; set => this.SetField(ref this.___OutputVideoBytes, value); } + /// + /// 当前统计区间新写入的音频数据大小 + /// + public long OutputAudioBytes { get => this.___OutputAudioBytes; set => this.SetField(ref this.___OutputAudioBytes, value); } + + /// + /// 总共收到的视频数据大小 + /// + public long TotalInputVideoBytes { get => this.___TotalInputVideoBytes; set => this.SetField(ref this.___TotalInputVideoBytes, value); } + /// + /// 总共收到的音频数据大小 + /// + public long TotalInputAudioBytes { get => this.___TotalInputAudioBytes; set => this.SetField(ref this.___TotalInputAudioBytes, value); } + + /// + /// 总共写入的视频帧数量 + /// + public int TotalOutputVideoFrames { get => this.___TotalOutputVideoFrames; set => this.SetField(ref this.___TotalOutputVideoFrames, value); } + /// + /// 总共写入的音频帧数量 + /// + public int TotalOutputAudioFrames { get => this.___TotalOutputAudioFrames; set => this.SetField(ref this.___TotalOutputAudioFrames, value); } + /// + /// 总共写入的视频数据大小 + /// + public long TotalOutputVideoBytes { get => this.___TotalOutputVideoBytes; set => this.SetField(ref this.___TotalOutputVideoBytes, value); } + /// + /// 总共写入的音频数据大小 + /// + 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(ref T location, T value, [CallerMemberName] string propertyName = "") + { + if (EqualityComparer.Default.Equals(location, value)) + return false; + location = value; + this.OnPropertyChanged(propertyName); + return true; + } + } +} diff --git a/BililiveRecorder.Web/Api/RoomController.cs b/BililiveRecorder.Web/Api/RoomController.cs index b7ba1df..ffca155 100644 --- a/BililiveRecorder.Web/Api/RoomController.cs +++ b/BililiveRecorder.Web/Api/RoomController.cs @@ -152,12 +152,12 @@ namespace BililiveRecorder.Web.Api [HttpGet("{roomId:int}/stats")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] - public ActionResult GetRoomStats(int roomId) + public ActionResult GetRoomStats(int roomId) { var room = this.FetchRoom(roomId); if (room is null) return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); - return this.mapper.Map(room.Stats); + return this.mapper.Map(room.Stats); } /// @@ -168,12 +168,12 @@ namespace BililiveRecorder.Web.Api [HttpGet("{objectId:guid}/stats")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(RestApiError), StatusCodes.Status404NotFound)] - public ActionResult GetRoomStats(Guid objectId) + public ActionResult GetRoomStats(Guid objectId) { var room = this.FetchRoom(objectId); if (room is null) return this.NotFound(new RestApiError { Code = RestApiErrorCode.RoomNotFound, Message = "Room not found" }); - return this.mapper.Map(room.Stats); + return this.mapper.Map(room.Stats); } #endregion diff --git a/BililiveRecorder.Web/FakeRecorderForWeb.cs b/BililiveRecorder.Web/FakeRecorderForWeb.cs index 32edc8a..637ef88 100644 --- a/BililiveRecorder.Web/FakeRecorderForWeb.cs +++ b/BililiveRecorder.Web/FakeRecorderForWeb.cs @@ -20,7 +20,7 @@ namespace BililiveRecorder.Web public event EventHandler>? RecordSessionEnded; public event EventHandler>? RecordFileOpening; public event EventHandler>? RecordFileClosed; - public event EventHandler>? NetworkingStats; + public event EventHandler>? IOStats; public event EventHandler>? RecordingStats; public event PropertyChangedEventHandler? PropertyChanged; #pragma warning restore CS0067 diff --git a/BililiveRecorder.Web/Models/Graphql/IOStatsType.cs b/BililiveRecorder.Web/Models/Graphql/IOStatsType.cs new file mode 100644 index 0000000..7453883 --- /dev/null +++ b/BililiveRecorder.Web/Models/Graphql/IOStatsType.cs @@ -0,0 +1,20 @@ +using BililiveRecorder.Core; +using GraphQL.Types; + +namespace BililiveRecorder.Web.Models.Graphql +{ + public class IOStatsType : ObjectGraphType + { + 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); + } + } +} diff --git a/BililiveRecorder.Web/Models/Graphql/RecordingStatsType.cs b/BililiveRecorder.Web/Models/Graphql/RecordingStatsType.cs index 71409bb..22b7c00 100644 --- a/BililiveRecorder.Web/Models/Graphql/RecordingStatsType.cs +++ b/BililiveRecorder.Web/Models/Graphql/RecordingStatsType.cs @@ -3,17 +3,31 @@ using GraphQL.Types; namespace BililiveRecorder.Web.Models.Graphql { - public class RecordingStatsType : ObjectGraphType + public class RecordingStatsType : ObjectGraphType { public RecordingStatsType() { - this.Field(x => x.NetworkMbps); - this.Field(x => x.SessionDuration); - this.Field(x => x.FileMaxTimestamp); - this.Field(x => x.SessionMaxTimestamp); - this.Field(x => x.DurationRatio); + this.Field(x => x.SessionDuration, type: typeof(TimeSpanMillisecondsGraphType)); this.Field(x => x.TotalInputBytes); 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); } } } diff --git a/BililiveRecorder.Web/Models/Graphql/RoomType.cs b/BililiveRecorder.Web/Models/Graphql/RoomType.cs index e627793..812e848 100644 --- a/BililiveRecorder.Web/Models/Graphql/RoomType.cs +++ b/BililiveRecorder.Web/Models/Graphql/RoomType.cs @@ -18,7 +18,8 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.Streaming); this.Field(x => x.AutoRecordForThisSession); 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)); } } } diff --git a/BililiveRecorder.Web/Models/Rest/DataMappingProfile.cs b/BililiveRecorder.Web/Models/Rest/DataMappingProfile.cs index 77c8c3c..276de87 100644 --- a/BililiveRecorder.Web/Models/Rest/DataMappingProfile.cs +++ b/BililiveRecorder.Web/Models/Rest/DataMappingProfile.cs @@ -1,6 +1,7 @@ using AutoMapper; using BililiveRecorder.Core; using BililiveRecorder.Core.Config.V3; +using BililiveRecorder.Core.Event; namespace BililiveRecorder.Web.Models.Rest { @@ -11,14 +12,23 @@ namespace BililiveRecorder.Web.Models.Rest this.CreateMap() .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.IoStats, x => x.MapFrom(s => s.Stats)) + .ForMember(x => x.RecordingStats, x => x.MapFrom(s => s.Stats)) ; - this.CreateMap() + this.CreateMap() .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.FileMaxTimestamp, x => x.MapFrom(s => s.FileMaxTimestamp.TotalMilliseconds)) ; + this.CreateMap() + .ForMember(x => x.Duration, x => x.MapFrom(s => s.Duration.TotalMilliseconds)) + .ForMember(x => x.DiskWriteDuration, x => x.MapFrom(s => s.DiskWriteDuration.TotalMilliseconds)) + ; + + this.CreateMap(); + this.CreateMap(); this.CreateMap(); diff --git a/BililiveRecorder.Web/Models/Rest/RoomDto.cs b/BililiveRecorder.Web/Models/Rest/RoomDto.cs index cda3476..7df11c9 100644 --- a/BililiveRecorder.Web/Models/Rest/RoomDto.cs +++ b/BililiveRecorder.Web/Models/Rest/RoomDto.cs @@ -16,6 +16,7 @@ namespace BililiveRecorder.Web.Models.Rest public bool Streaming { get; set; } public bool DanmakuConnected { 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!; } } diff --git a/BililiveRecorder.Web/Models/Rest/RoomIOStatsDto.cs b/BililiveRecorder.Web/Models/Rest/RoomIOStatsDto.cs new file mode 100644 index 0000000..32f194a --- /dev/null +++ b/BililiveRecorder.Web/Models/Rest/RoomIOStatsDto.cs @@ -0,0 +1,47 @@ +using System; + +namespace BililiveRecorder.Web.Models.Rest +{ + public class RoomIOStatsDto + { + /// + /// 当前统计区间的开始时间 + /// + public DateTimeOffset StartTime { get; set; } + + /// + /// 当前统计区间的结束时间 + /// + public DateTimeOffset EndTime { get; set; } + + /// + /// 当前统计区间的时长,毫秒 + /// + public double Duration { get; set; } + + /// + /// 下载了的数据量 + /// + public int NetworkBytesDownloaded { get; set; } + + /// + /// 平均下载速度,mibi-bits per second + /// + public double NetworkMbps { get; set; } + + /// + /// 统计区间内的磁盘写入耗时,毫秒 + /// + public double DiskWriteDuration { get; set; } + + /// + /// 统计区间内写入磁盘的数据量 + /// + public int DiskBytesWritten { get; set; } + + /// + /// 平均写入速度,mibi-bytes per second + /// + public double DiskMBps { get; set; } + } +} diff --git a/BililiveRecorder.Web/Models/Rest/RoomRecordingStatsDto.cs b/BililiveRecorder.Web/Models/Rest/RoomRecordingStatsDto.cs new file mode 100644 index 0000000..5f39107 --- /dev/null +++ b/BililiveRecorder.Web/Models/Rest/RoomRecordingStatsDto.cs @@ -0,0 +1,104 @@ +namespace BililiveRecorder.Web.Models.Rest +{ + public class RoomRecordingStatsDto + { + /// + /// 从录制开始到现在一共经过的时间,毫秒 + /// + public double SessionDuration { get; set; } + + /// + /// 总接受字节数 + /// + public long TotalInputBytes { get; set; } + + /// + /// 总写入字节数 + /// + public long TotalOutputBytes { get; set; } + + /// + /// 当前文件的大小 + /// + public long CurrentFileSize { get; set; } + + /// + /// 本次直播流收到的最大时间戳(已修复过,相当于总时长,毫秒) + /// + public int SessionMaxTimestamp { get; set; } + + /// + /// 当前文件的最大时间戳(相当于总时长,毫秒) + /// + public int FileMaxTimestamp { get; set; } + + /// + /// 当前这一个统计区间的直播数据时长,毫秒 + /// + public double AddedDuration { get; set; } + + /// + /// 当前这一个统计区间所经过的时间长度,毫秒 + /// + public double PassedTime { get; set; } + + /// + /// 录制速度比例 + /// + public double DurationRatio { get; set; } + + //---------------------------------------- + + /// + /// 当前统计区间新收到的视频数据大小 + /// + public long InputVideoBytes { get; set; } + /// + /// 当前统计区间新收到的音频数据大小 + /// + public long InputAudioBytes { get; set; } + + /// + /// 当前统计区间新写入的视频帧数量 + /// + public int OutputVideoFrames { get; set; } + /// + /// 当前统计区间新写入的音频帧数量 + /// + public int OutputAudioFrames { get; set; } + /// + /// 当前统计区间新写入的视频数据大小 + /// + public long OutputVideoBytes { get; set; } + /// + /// 当前统计区间新写入的音频数据大小 + /// + public long OutputAudioBytes { get; set; } + + /// + /// 总共收到的视频数据大小 + /// + public long TotalInputVideoBytes { get; set; } + /// + /// 总共收到的音频数据大小 + /// + public long TotalInputAudioBytes { get; set; } + + /// + /// 总共写入的视频帧数量 + /// + public int TotalOutputVideoFrames { get; set; } + /// + /// 总共写入的音频帧数量 + /// + public int TotalOutputAudioFrames { get; set; } + /// + /// 总共写入的视频数据大小 + /// + public long TotalOutputVideoBytes { get; set; } + /// + /// 总共写入的音频数据大小 + /// + public long TotalOutputAudioBytes { get; set; } + } +} diff --git a/BililiveRecorder.Web/Models/Rest/RoomStatsDto.cs b/BililiveRecorder.Web/Models/Rest/RoomStatsDto.cs deleted file mode 100644 index bed1ec4..0000000 --- a/BililiveRecorder.Web/Models/Rest/RoomStatsDto.cs +++ /dev/null @@ -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; } - } -} diff --git a/BililiveRecorder.Web/Startup.cs b/BililiveRecorder.Web/Startup.cs index 7156bcc..27fb480 100644 --- a/BililiveRecorder.Web/Startup.cs +++ b/BililiveRecorder.Web/Startup.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.IO; using System.Threading.Tasks; +using AutoMapper; using BililiveRecorder.Core; using BililiveRecorder.Web.Graphql; using BililiveRecorder.Web.Models.Rest; @@ -53,6 +54,15 @@ namespace BililiveRecorder.Web // 实际运行时在 BililiveRecorder.Web.Program 里会加上真的 IRecorder services.TryAddSingleton(new FakeRecorderForWeb()); +#if DEBUG + // TODO 移动到一个单独的测试项目里 + if (Debugger.IsAttached) + { + var configuration = new MapperConfiguration(cfg => cfg.AddProfile()); + configuration.AssertConfigurationIsValid(); + } +#endif + services .AddAutoMapper(c => c.AddProfile()) .AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()))