diff --git a/BililiveRecorder.Core/Event/IOStatsEventArgs.cs b/BililiveRecorder.Core/Event/IOStatsEventArgs.cs
new file mode 100644
index 0000000..428510a
--- /dev/null
+++ b/BililiveRecorder.Core/Event/IOStatsEventArgs.cs
@@ -0,0 +1,29 @@
+using System;
+
+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
+ ///
+ public double NetworkMbps { get; set; }
+
+ public TimeSpan DiskWriteTime { get; set; }
+
+ public int DiskBytesWritten { get; set; }
+
+ ///
+ /// mibi-bytes per seconds
+ ///
+ public double DiskMBps { get; set; }
+ }
+}
diff --git a/BililiveRecorder.Core/Event/NetworkingStatsEventArgs.cs b/BililiveRecorder.Core/Event/NetworkingStatsEventArgs.cs
deleted file mode 100644
index 3b93471..0000000
--- a/BililiveRecorder.Core/Event/NetworkingStatsEventArgs.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-
-namespace BililiveRecorder.Core.Event
-{
- public class NetworkingStatsEventArgs : EventArgs
- {
- public DateTimeOffset StartTime { get; set; }
-
- public DateTimeOffset EndTime { get; set; }
-
- public TimeSpan Duration { get; set; }
-
- public int BytesDownloaded { get; set; }
-
- public double Mbps { get; set; }
- }
-}
diff --git a/BililiveRecorder.Core/IRecorder.cs b/BililiveRecorder.Core/IRecorder.cs
index 59d156f..ffafe24 100644
--- a/BililiveRecorder.Core/IRecorder.cs
+++ b/BililiveRecorder.Core/IRecorder.cs
@@ -15,7 +15,7 @@ namespace BililiveRecorder.Core
event EventHandler>? RecordSessionEnded;
event EventHandler>? RecordFileOpening;
event EventHandler>? RecordFileClosed;
- event EventHandler>? NetworkingStats;
+ event EventHandler>? IOStats;
event EventHandler>? RecordingStats;
IRoom AddRoom(int roomid);
diff --git a/BililiveRecorder.Core/IRoom.cs b/BililiveRecorder.Core/IRoom.cs
index 04de18f..fb44ce0 100644
--- a/BililiveRecorder.Core/IRoom.cs
+++ b/BililiveRecorder.Core/IRoom.cs
@@ -29,7 +29,7 @@ namespace BililiveRecorder.Core
event EventHandler? RecordFileOpening;
event EventHandler? RecordFileClosed;
event EventHandler? RecordingStats;
- event EventHandler? NetworkingStats;
+ event EventHandler? IOStats;
void StartRecord();
void StopRecord();
diff --git a/BililiveRecorder.Core/Recorder.cs b/BililiveRecorder.Core/Recorder.cs
index 66e4652..1b280b9 100644
--- a/BililiveRecorder.Core/Recorder.cs
+++ b/BililiveRecorder.Core/Recorder.cs
@@ -50,7 +50,7 @@ namespace BililiveRecorder.Core
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;
@@ -81,7 +81,7 @@ namespace BililiveRecorder.Core
room.RecordSessionEnded += this.Room_RecordSessionEnded;
room.RecordFileOpening += this.Room_RecordFileOpening;
room.RecordFileClosed += this.Room_RecordFileClosed;
- room.NetworkingStats += this.Room_NetworkingStats;
+ room.IOStats += this.Room_IOStats;
room.RecordingStats += this.Room_RecordingStats;
room.PropertyChanged += this.Room_PropertyChanged;
@@ -122,10 +122,10 @@ namespace BililiveRecorder.Core
#region Events
- private void Room_NetworkingStats(object sender, NetworkingStatsEventArgs e)
+ private void Room_IOStats(object sender, IOStatsEventArgs e)
{
var room = (IRoom)sender;
- NetworkingStats?.Invoke(this, new AggregatedRoomEventArgs(room, e));
+ IOStats?.Invoke(this, new AggregatedRoomEventArgs(room, e));
}
private void Room_RecordingStats(object sender, RecordingStatsEventArgs e)
diff --git a/BililiveRecorder.Core/Recording/IRecordTask.cs b/BililiveRecorder.Core/Recording/IRecordTask.cs
index 5fce8c1..84577cc 100644
--- a/BililiveRecorder.Core/Recording/IRecordTask.cs
+++ b/BililiveRecorder.Core/Recording/IRecordTask.cs
@@ -8,7 +8,7 @@ namespace BililiveRecorder.Core.Recording
{
Guid SessionId { get; }
- event EventHandler? NetworkingStats;
+ event EventHandler? IOStats;
event EventHandler? RecordingStats;
event EventHandler? RecordFileOpening;
event EventHandler? RecordFileClosed;
diff --git a/BililiveRecorder.Core/Recording/RawDataRecordTask.cs b/BililiveRecorder.Core/Recording/RawDataRecordTask.cs
index 90d792d..4517b88 100644
--- a/BililiveRecorder.Core/Recording/RawDataRecordTask.cs
+++ b/BililiveRecorder.Core/Recording/RawDataRecordTask.cs
@@ -63,9 +63,18 @@ namespace BililiveRecorder.Core.Recording
if (bytesRead == 0)
break;
- Interlocked.Add(ref this.fillerDownloadedBytes, bytesRead);
+ Interlocked.Add(ref this.ioNetworkDownloadedBytes, bytesRead);
+ this.ioDiskStopwatch.Restart();
await file.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
+ this.ioDiskStopwatch.Stop();
+
+ lock (this.ioDiskStatsLock)
+ {
+ this.ioDiskWriteTime += this.ioDiskStopwatch.Elapsed;
+ this.ioDiskWrittenBytes += bytesRead;
+ }
+ this.ioDiskStopwatch.Reset();
}
}
catch (OperationCanceledException ex)
diff --git a/BililiveRecorder.Core/Recording/RecordTaskBase.cs b/BililiveRecorder.Core/Recording/RecordTaskBase.cs
index 8e11d45..a19f16f 100644
--- a/BililiveRecorder.Core/Recording/RecordTaskBase.cs
+++ b/BililiveRecorder.Core/Recording/RecordTaskBase.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -35,9 +36,17 @@ namespace BililiveRecorder.Core.Recording
protected bool started = false;
protected bool timeoutTriggered = false;
- private readonly object fillerStatsLock = new object();
- protected int fillerDownloadedBytes;
- private DateTimeOffset fillerStatsLastTrigger;
+
+ private readonly object ioStatsLock = new();
+ protected int ioNetworkDownloadedBytes;
+
+ protected Stopwatch ioDiskStopwatch = new();
+ protected object ioDiskStatsLock = new();
+ protected TimeSpan ioDiskWriteTime;
+ protected int ioDiskWrittenBytes;
+ private DateTimeOffset ioDiskWarningTimeout;
+
+ private DateTimeOffset ioStatsLastTrigger;
private TimeSpan durationSinceNoDataReceived;
protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, FileNameGenerator fileNameGenerator)
@@ -48,20 +57,20 @@ namespace BililiveRecorder.Core.Recording
this.fileNameGenerator = fileNameGenerator ?? throw new ArgumentNullException(nameof(fileNameGenerator));
this.ct = this.cts.Token;
- this.timer.Elapsed += this.Timer_Elapsed_TriggerNetworkStats;
+ this.timer.Elapsed += this.Timer_Elapsed_TriggerIOStats;
}
public Guid SessionId { get; } = Guid.NewGuid();
#region Events
- public event EventHandler? NetworkingStats;
+ public event EventHandler? IOStats;
public event EventHandler? RecordingStats;
public event EventHandler? RecordFileOpening;
public event EventHandler? RecordFileClosed;
public event EventHandler? RecordSessionEnded;
- protected void OnNetworkingStats(NetworkingStatsEventArgs e) => NetworkingStats?.Invoke(this, e);
+ protected void OnIOStats(IOStatsEventArgs e) => IOStats?.Invoke(this, e);
protected void OnRecordingStats(RecordingStatsEventArgs e) => RecordingStats?.Invoke(this, e);
protected void OnRecordFileOpening(RecordFileOpeningEventArgs e) => RecordFileOpening?.Invoke(this, e);
protected void OnRecordFileClosed(RecordFileClosedEventArgs e) => RecordFileClosed?.Invoke(this, e);
@@ -98,7 +107,7 @@ namespace BililiveRecorder.Core.Recording
var stream = await this.GetStreamAsync(fullUrl: fullUrl, timeout: (int)this.room.RoomConfig.TimingStreamConnect).ConfigureAwait(false);
- this.fillerStatsLastTrigger = DateTimeOffset.UtcNow;
+ this.ioStatsLastTrigger = DateTimeOffset.UtcNow;
this.durationSinceNoDataReceived = TimeSpan.Zero;
this.ct.Register(state => Task.Run(async () =>
@@ -118,38 +127,61 @@ namespace BililiveRecorder.Core.Recording
protected abstract void StartRecordingLoop(Stream stream);
- private void Timer_Elapsed_TriggerNetworkStats(object sender, ElapsedEventArgs e)
+ private void Timer_Elapsed_TriggerIOStats(object sender, ElapsedEventArgs e)
{
- int bytes;
- TimeSpan diff;
- DateTimeOffset start, end;
+ int networkDownloadBytes, diskWriteBytes;
+ TimeSpan durationDiff, diskWriteTime;
+ DateTimeOffset startTime, endTime;
- lock (this.fillerStatsLock)
+
+ lock (this.ioStatsLock) // 锁 timer elapsed 事件本身防止并行运行
{
- bytes = Interlocked.Exchange(ref this.fillerDownloadedBytes, 0);
- end = DateTimeOffset.UtcNow;
- start = this.fillerStatsLastTrigger;
- this.fillerStatsLastTrigger = end;
- diff = end - start;
+ // networks
+ networkDownloadBytes = Interlocked.Exchange(ref this.ioNetworkDownloadedBytes, 0); // 锁网络统计
+ endTime = DateTimeOffset.UtcNow;
+ startTime = this.ioStatsLastTrigger;
+ this.ioStatsLastTrigger = endTime;
+ durationDiff = endTime - startTime;
- this.durationSinceNoDataReceived = bytes > 0 ? TimeSpan.Zero : this.durationSinceNoDataReceived + diff;
+ this.durationSinceNoDataReceived = networkDownloadBytes > 0 ? TimeSpan.Zero : this.durationSinceNoDataReceived + durationDiff;
+
+ // disks
+ lock (this.ioDiskStatsLock) // 锁硬盘统计
+ {
+ diskWriteTime = this.ioDiskWriteTime;
+ diskWriteBytes = this.ioDiskWrittenBytes;
+ this.ioDiskWriteTime = TimeSpan.Zero;
+ this.ioDiskWrittenBytes = 0;
+ }
}
- var mbps = bytes * (8d / 1024d / 1024d) / diff.TotalSeconds;
+ var netMbps = networkDownloadBytes * (8d / 1024d / 1024d) / durationDiff.TotalSeconds;
+ var diskMBps = diskWriteBytes / (1024d * 1024d) / diskWriteTime.TotalSeconds;
- this.OnNetworkingStats(new NetworkingStatsEventArgs
+ this.OnIOStats(new IOStatsEventArgs
{
- BytesDownloaded = bytes,
- Duration = diff,
- StartTime = start,
- EndTime = end,
- Mbps = mbps
+ NetworkBytesDownloaded = networkDownloadBytes,
+ Duration = durationDiff,
+ StartTime = startTime,
+ EndTime = endTime,
+ NetworkMbps = netMbps,
+ DiskBytesWritten = diskWriteBytes,
+ DiskWriteTime = diskWriteTime,
+ DiskMBps = diskMBps,
});
+ var now = DateTimeOffset.Now;
+ if (diskWriteBytes > 0 && this.ioDiskWarningTimeout < now && (diskWriteTime.TotalSeconds > 1d || diskMBps < 2d))
+ {
+ // 硬盘 IO 可能不能满足录播
+ this.ioDiskWarningTimeout = now + TimeSpan.FromMinutes(2); // 最多每 2 分钟提醒一次
+ this.logger.Warning("检测到硬盘写入速度较慢可能影响录播,请检查是否有其他软件或游戏正在使用硬盘");
+ }
+
if ((!this.timeoutTriggered) && (this.durationSinceNoDataReceived.TotalMilliseconds > this.room.RoomConfig.TimingWatchdogTimeout))
{
this.timeoutTriggered = true;
- this.logger.Warning("直播服务器未断开连接但停止发送直播数据,将会主动断开连接");
+ this.logger.Warning("检测到录制卡住,可能是网络或硬盘原因,将会主动断开连接");
this.RequestStop();
}
}
diff --git a/BililiveRecorder.Core/Recording/StandardRecordTask.cs b/BililiveRecorder.Core/Recording/StandardRecordTask.cs
index 3e0a026..d180700 100644
--- a/BililiveRecorder.Core/Recording/StandardRecordTask.cs
+++ b/BililiveRecorder.Core/Recording/StandardRecordTask.cs
@@ -129,7 +129,7 @@ namespace BililiveRecorder.Core.Recording
if (bytesRead == 0)
break;
writer.Advance(bytesRead);
- Interlocked.Add(ref this.fillerDownloadedBytes, bytesRead);
+ Interlocked.Add(ref this.ioNetworkDownloadedBytes, bytesRead);
}
catch (Exception ex)
{
@@ -171,7 +171,16 @@ namespace BililiveRecorder.Core.Recording
if (this.context.Comments.Count > 0)
this.logger.Debug("修复逻辑输出 {@Comments}", this.context.Comments);
- await this.writer.WriteAsync(this.context).ConfigureAwait(false);
+ this.ioDiskStopwatch.Restart();
+ var bytesWritten = await this.writer.WriteAsync(this.context).ConfigureAwait(false);
+ this.ioDiskStopwatch.Stop();
+
+ lock (this.ioDiskStatsLock)
+ {
+ this.ioDiskWriteTime += this.ioDiskStopwatch.Elapsed;
+ this.ioDiskWrittenBytes += bytesWritten;
+ }
+ this.ioDiskStopwatch.Reset();
if (this.context.Actions.Any(x => x is PipelineDisconnectAction))
{
diff --git a/BililiveRecorder.Core/Room.cs b/BililiveRecorder.Core/Room.cs
index b382812..9a9e2a2 100644
--- a/BililiveRecorder.Core/Room.cs
+++ b/BililiveRecorder.Core/Room.cs
@@ -102,7 +102,7 @@ namespace BililiveRecorder.Core
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;
@@ -219,7 +219,7 @@ namespace BililiveRecorder.Core
return;
var task = this.recordTaskFactory.CreateRecordTask(this);
- task.NetworkingStats += this.RecordTask_NetworkingStats;
+ task.IOStats += this.RecordTask_IOStats;
task.RecordingStats += this.RecordTask_RecordingStats;
task.RecordFileOpening += this.RecordTask_RecordFileOpening;
task.RecordFileClosed += this.RecordTask_RecordFileClosed;
@@ -349,13 +349,13 @@ namespace BililiveRecorder.Core
#region Event Handlers
///
- private void RecordTask_NetworkingStats(object sender, NetworkingStatsEventArgs e)
+ private void RecordTask_IOStats(object sender, IOStatsEventArgs e)
{
- this.logger.Verbose("Networking stats: {@stats}", e);
+ this.logger.Verbose("IO stats: {@stats}", e);
- this.Stats.NetworkMbps = e.Mbps;
+ this.Stats.NetworkMbps = e.NetworkMbps;
- NetworkingStats?.Invoke(this, e);
+ IOStats?.Invoke(this, e);
}
///
diff --git a/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs b/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs
index 6675c0f..0f8b207 100644
--- a/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs
+++ b/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs
@@ -12,6 +12,6 @@ namespace BililiveRecorder.Flv
event EventHandler FileClosed;
- Task WriteAsync(FlvProcessingContext context);
+ Task WriteAsync(FlvProcessingContext context);
}
}
diff --git a/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs b/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs
index 4d55044..9e2eda0 100644
--- a/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs
+++ b/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs
@@ -25,6 +25,8 @@ namespace BililiveRecorder.Flv.Writer
private KeyframesScriptDataValue? keyframesScriptDataValue = null;
private double lastDuration;
+ private int bytesWrittenByCurrentWriteCall { get; set; }
+
public event EventHandler? FileClosed;
public Action? BeforeScriptTagWrite { get; set; }
@@ -37,7 +39,7 @@ namespace BililiveRecorder.Flv.Writer
this.disableKeyframes = disableKeyframes;
}
- public async Task WriteAsync(FlvProcessingContext context)
+ public async Task WriteAsync(FlvProcessingContext context)
{
if (this.state == WriterState.Invalid)
throw new InvalidOperationException("FlvProcessingContextWriter is in a invalid state.");
@@ -70,11 +72,16 @@ namespace BililiveRecorder.Flv.Writer
this.semaphoreSlim.Release();
}
+ var bytesWritten = this.bytesWrittenByCurrentWriteCall;
+ this.bytesWrittenByCurrentWriteCall = 0;
+
// Dispose tags
foreach (var action in context.Actions)
if (action is PipelineDataAction dataAction)
foreach (var tag in dataAction.Tags)
tag.BinaryData?.Dispose();
+
+ return bytesWritten;
}
#region Flv Writer Implementation
@@ -214,6 +221,8 @@ namespace BililiveRecorder.Flv.Writer
await this.tagWriter.WriteTag(this.nextAudioHeaderTag).ConfigureAwait(false);
}
+ this.bytesWrittenByCurrentWriteCall += (int)this.tagWriter.FileSize;
+
this.state = WriterState.Writing;
}
@@ -236,9 +245,13 @@ namespace BililiveRecorder.Flv.Writer
var duration = tags[tags.Count - 1].Timestamp / 1000d;
this.lastDuration = duration;
+ var beforeFileSize = this.tagWriter.FileSize;
+
foreach (var tag in tags)
await this.tagWriter.WriteTag(tag).ConfigureAwait(false);
+ this.bytesWrittenByCurrentWriteCall += (int)(this.tagWriter.FileSize - beforeFileSize);
+
await this.RewriteScriptTagImpl(duration, firstTag.IsKeyframeData(), firstTag.Timestamp, pos).ConfigureAwait(false);
}
@@ -255,7 +268,11 @@ namespace BililiveRecorder.Flv.Writer
throw new InvalidOperationException($"Can't write data tag with current state ({this.state})");
}
+ var beforeFileSize = this.tagWriter.FileSize;
+
await this.tagWriter.WriteTag(endAction.Tag).ConfigureAwait(false);
+
+ this.bytesWrittenByCurrentWriteCall += (int)(this.tagWriter.FileSize - beforeFileSize);
}
#endregion