BililiveRecorder/BililiveRecorder.Core/Room.cs

601 lines
22 KiB
C#
Raw Normal View History

2021-02-23 18:03:37 +08:00
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using BililiveRecorder.Core.Api;
using BililiveRecorder.Core.Config.V2;
using BililiveRecorder.Core.Danmaku;
using BililiveRecorder.Core.Event;
using BililiveRecorder.Core.Recording;
using Microsoft.Extensions.DependencyInjection;
using Polly;
2021-02-23 18:03:37 +08:00
using Serilog;
using Serilog.Events;
2021-02-23 18:03:37 +08:00
using Timer = System.Timers.Timer;
namespace BililiveRecorder.Core
{
public class Room : IRoom
{
private readonly object recordStartLock = new object();
private readonly SemaphoreSlim recordRetryDelaySemaphoreSlim = new SemaphoreSlim(1, 1);
private readonly Timer timer;
private readonly IServiceScope scope;
private readonly ILogger loggerWithoutContext;
private readonly IDanmakuClient danmakuClient;
private readonly IApiClient apiClient;
private readonly IBasicDanmakuWriter basicDanmakuWriter;
private readonly IRecordTaskFactory recordTaskFactory;
private readonly CancellationTokenSource cts;
private readonly CancellationToken ct;
private ILogger logger;
private bool disposedValue;
private int shortId;
private string name = string.Empty;
private string title = string.Empty;
private string areaNameParent = string.Empty;
private string areaNameChild = string.Empty;
private bool danmakuConnected;
2021-04-21 23:18:23 +08:00
private bool streaming;
private bool autoRecordForThisSession = true;
2021-02-23 18:03:37 +08:00
private IRecordTask? recordTask;
private DateTimeOffset recordTaskStartTime;
2021-04-30 19:35:15 +08:00
public Room(IServiceScope scope, RoomConfig roomConfig, int initDelayFactor, ILogger logger, IDanmakuClient danmakuClient, IApiClient apiClient, IBasicDanmakuWriter basicDanmakuWriter, IRecordTaskFactory recordTaskFactory)
2021-02-23 18:03:37 +08:00
{
this.scope = scope ?? throw new ArgumentNullException(nameof(scope));
this.RoomConfig = roomConfig ?? throw new ArgumentNullException(nameof(roomConfig));
this.loggerWithoutContext = logger?.ForContext<Room>() ?? throw new ArgumentNullException(nameof(logger));
this.logger = this.loggerWithoutContext.ForContext(LoggingContext.RoomId, this.RoomConfig.RoomId);
this.danmakuClient = danmakuClient ?? throw new ArgumentNullException(nameof(danmakuClient));
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.basicDanmakuWriter = basicDanmakuWriter ?? throw new ArgumentNullException(nameof(basicDanmakuWriter));
this.recordTaskFactory = recordTaskFactory ?? throw new ArgumentNullException(nameof(recordTaskFactory));
this.timer = new Timer(this.RoomConfig.TimingCheckInterval * 1000);
this.cts = new CancellationTokenSource();
this.ct = this.cts.Token;
2021-04-21 23:18:23 +08:00
this.PropertyChanged += this.Room_PropertyChanged;
2021-02-23 18:03:37 +08:00
this.RoomConfig.PropertyChanged += this.RoomConfig_PropertyChanged;
2021-04-21 23:18:23 +08:00
2021-02-23 18:03:37 +08:00
this.timer.Elapsed += this.Timer_Elapsed;
2021-04-21 23:18:23 +08:00
2021-02-23 18:03:37 +08:00
this.danmakuClient.StatusChanged += this.DanmakuClient_StatusChanged;
this.danmakuClient.DanmakuReceived += this.DanmakuClient_DanmakuReceived;
_ = Task.Run(async () =>
{
2021-04-30 19:35:15 +08:00
await Task.Delay(1500 + (initDelayFactor * 500));
this.timer.Start();
2021-02-23 18:03:37 +08:00
await this.RefreshRoomInfoAsync();
});
}
public int ShortId { get => this.shortId; private set => this.SetField(ref this.shortId, value); }
public string Name { get => this.name; private set => this.SetField(ref this.name, value); }
public string Title { get => this.title; private set => this.SetField(ref this.title, value); }
public string AreaNameParent { get => this.areaNameParent; private set => this.SetField(ref this.areaNameParent, value); }
public string AreaNameChild { get => this.areaNameChild; private set => this.SetField(ref this.areaNameChild, value); }
2021-04-21 23:18:23 +08:00
public bool Streaming { get => this.streaming; private set => this.SetField(ref this.streaming, value); }
public bool AutoRecordForThisSession { get => this.autoRecordForThisSession; private set => this.SetField(ref this.autoRecordForThisSession, value); }
2021-02-23 18:03:37 +08:00
public bool DanmakuConnected { get => this.danmakuConnected; private set => this.SetField(ref this.danmakuConnected, value); }
2021-04-21 23:18:23 +08:00
public bool Recording => this.recordTask != null;
2021-02-23 18:03:37 +08:00
public RoomConfig RoomConfig { get; }
public RecordingStats Stats { get; } = new RecordingStats();
public Guid ObjectId { get; } = Guid.NewGuid();
public event EventHandler<RecordSessionStartedEventArgs>? RecordSessionStarted;
public event EventHandler<RecordSessionEndedEventArgs>? RecordSessionEnded;
public event EventHandler<RecordFileOpeningEventArgs>? RecordFileOpening;
public event EventHandler<RecordFileClosedEventArgs>? RecordFileClosed;
public event EventHandler<IOStatsEventArgs>? IOStats;
2021-02-23 18:03:37 +08:00
public event EventHandler<RecordingStatsEventArgs>? RecordingStats;
public event PropertyChangedEventHandler? PropertyChanged;
public void SplitOutput()
{
2021-04-30 19:35:15 +08:00
if (this.disposedValue)
return;
2021-02-23 18:03:37 +08:00
lock (this.recordStartLock)
{
this.recordTask?.SplitOutput();
}
}
public void StartRecord()
{
2021-04-30 19:35:15 +08:00
if (this.disposedValue)
return;
2021-02-23 18:03:37 +08:00
lock (this.recordStartLock)
{
2021-04-21 23:18:23 +08:00
this.AutoRecordForThisSession = true;
2021-02-23 18:03:37 +08:00
2021-04-21 23:18:23 +08:00
_ = Task.Run(() =>
2021-02-23 18:03:37 +08:00
{
try
{
2021-04-21 23:18:23 +08:00
this.CreateAndStartNewRecordTask();
2021-02-23 18:03:37 +08:00
}
catch (Exception ex)
{
2021-04-23 19:15:20 +08:00
this.logger.Write(ex is ExecutionRejectedException ? LogEventLevel.Verbose : LogEventLevel.Warning, ex, "尝试开始录制时出错");
2021-02-23 18:03:37 +08:00
}
});
}
}
public void StopRecord()
{
2021-04-30 19:35:15 +08:00
if (this.disposedValue)
return;
2021-02-23 18:03:37 +08:00
lock (this.recordStartLock)
{
2021-04-21 23:18:23 +08:00
this.AutoRecordForThisSession = false;
2021-02-23 18:03:37 +08:00
if (this.recordTask == null)
return;
this.recordTask.RequestStop();
}
}
public async Task RefreshRoomInfoAsync()
{
2021-04-30 19:35:15 +08:00
if (this.disposedValue)
return;
2021-02-23 18:03:37 +08:00
try
{
await this.FetchUserInfoAsync().ConfigureAwait(false);
await this.FetchRoomInfoAsync().ConfigureAwait(false);
this.StartDamakuConnection(delay: false);
2021-04-21 23:18:23 +08:00
if (this.Streaming && this.AutoRecordForThisSession && this.RoomConfig.AutoRecord)
this.CreateAndStartNewRecordTask();
2021-02-23 18:03:37 +08:00
}
catch (Exception ex)
{
2021-04-23 19:15:20 +08:00
this.logger.Write(ex is ExecutionRejectedException ? LogEventLevel.Verbose : LogEventLevel.Warning, ex, "刷新房间信息时出错");
2021-02-23 18:03:37 +08:00
}
}
#region Recording
/// <exception cref="Exception"/>
private async Task FetchRoomInfoAsync()
{
2021-04-30 19:35:15 +08:00
if (this.disposedValue)
return;
2021-02-23 18:03:37 +08:00
var room = (await this.apiClient.GetRoomInfoAsync(this.RoomConfig.RoomId).ConfigureAwait(false)).Data;
if (room != null)
{
this.RoomConfig.RoomId = room.RoomId;
this.ShortId = room.ShortId;
this.Title = room.Title;
this.AreaNameParent = room.ParentAreaName;
this.AreaNameChild = room.AreaName;
this.Streaming = room.LiveStatus == 1;
}
}
/// <exception cref="Exception"/>
private async Task FetchUserInfoAsync()
{
2021-04-30 19:35:15 +08:00
if (this.disposedValue)
return;
2021-02-23 18:03:37 +08:00
var user = await this.apiClient.GetUserInfoAsync(this.RoomConfig.RoomId).ConfigureAwait(false);
this.Name = user.Data?.Info?.Name ?? this.Name;
}
///
private void CreateAndStartNewRecordTask()
{
lock (this.recordStartLock)
{
if (this.disposedValue)
return;
if (!this.Streaming)
return;
if (this.recordTask != null)
return;
var task = this.recordTaskFactory.CreateRecordTask(this);
task.IOStats += this.RecordTask_IOStats;
2021-02-23 18:03:37 +08:00
task.RecordingStats += this.RecordTask_RecordingStats;
task.RecordFileOpening += this.RecordTask_RecordFileOpening;
task.RecordFileClosed += this.RecordTask_RecordFileClosed;
task.RecordSessionEnded += this.RecordTask_RecordSessionEnded;
this.recordTask = task;
this.recordTaskStartTime = DateTimeOffset.UtcNow;
2021-04-21 19:48:28 +08:00
this.Stats.Reset();
2021-02-23 18:03:37 +08:00
this.OnPropertyChanged(nameof(this.Recording));
_ = Task.Run(async () =>
{
try
{
await this.recordTask.StartAsync();
}
2021-11-20 14:34:35 +08:00
catch (NoMatchingQnValueException)
{
this.recordTask = null;
this.OnPropertyChanged(nameof(this.Recording));
// 无匹配的画质,重试录制之前等待更长时间
_ = Task.Run(() => this.RestartAfterRecordTaskFailedAsync(RestartRecordingReason.NoMatchingQnValue));
return;
}
2021-02-23 18:03:37 +08:00
catch (Exception ex)
{
2021-04-23 19:15:20 +08:00
this.logger.Write(ex is ExecutionRejectedException ? LogEventLevel.Verbose : LogEventLevel.Warning, ex, "启动录制出错");
2021-02-23 18:03:37 +08:00
this.recordTask = null;
this.OnPropertyChanged(nameof(this.Recording));
2021-04-21 23:18:23 +08:00
// 请求直播流出错时的重试逻辑
2021-11-20 14:34:35 +08:00
_ = Task.Run(() => this.RestartAfterRecordTaskFailedAsync(RestartRecordingReason.GenericRetry));
2021-04-21 23:18:23 +08:00
2021-02-23 18:03:37 +08:00
return;
}
2021-11-20 14:34:35 +08:00
2021-02-23 18:03:37 +08:00
RecordSessionStarted?.Invoke(this, new RecordSessionStartedEventArgs(this)
{
SessionId = this.recordTask.SessionId
});
});
}
}
///
2021-11-20 14:34:35 +08:00
private async Task RestartAfterRecordTaskFailedAsync(RestartRecordingReason restartRecordingReason)
2021-02-23 18:03:37 +08:00
{
2021-04-30 19:35:15 +08:00
if (this.disposedValue)
return;
2021-04-21 23:18:23 +08:00
if (!this.Streaming || !this.AutoRecordForThisSession)
return;
try
2021-02-23 18:03:37 +08:00
{
2021-04-21 23:18:23 +08:00
if (!await this.recordRetryDelaySemaphoreSlim.WaitAsync(0).ConfigureAwait(false))
return;
2021-02-23 18:03:37 +08:00
try
{
2021-11-20 14:34:35 +08:00
var delay = restartRecordingReason switch
{
RestartRecordingReason.GenericRetry => this.RoomConfig.TimingStreamRetry,
RestartRecordingReason.NoMatchingQnValue => this.RoomConfig.TimingStreamRetryNoQn * 1000,
_ => throw new InvalidOperationException()
};
await Task.Delay((int)delay, this.ct).ConfigureAwait(false);
2021-02-23 18:03:37 +08:00
}
2021-04-23 19:15:20 +08:00
catch (TaskCanceledException)
{
// 房间已经被删除
return;
}
2021-04-21 23:18:23 +08:00
finally
2021-02-23 18:03:37 +08:00
{
2021-04-21 23:18:23 +08:00
this.recordRetryDelaySemaphoreSlim.Release();
2021-02-23 18:03:37 +08:00
}
2021-04-21 23:18:23 +08:00
if (!this.Streaming || !this.AutoRecordForThisSession)
return;
await this.FetchRoomInfoAsync().ConfigureAwait(false);
if (this.Streaming && this.AutoRecordForThisSession)
this.CreateAndStartNewRecordTask();
}
catch (Exception ex)
{
2021-04-23 19:15:20 +08:00
this.logger.Write(ex is ExecutionRejectedException ? LogEventLevel.Verbose : LogEventLevel.Warning, ex, "重试开始录制时出错");
2021-11-20 14:34:35 +08:00
_ = Task.Run(() => this.RestartAfterRecordTaskFailedAsync(restartRecordingReason));
2021-02-23 18:03:37 +08:00
}
}
///
private void StartDamakuConnection(bool delay = true) =>
Task.Run(async () =>
{
2021-04-30 19:35:15 +08:00
if (this.disposedValue)
return;
2021-02-23 18:03:37 +08:00
try
{
if (delay)
2021-04-23 19:15:20 +08:00
try
{
await Task.Delay((int)this.RoomConfig.TimingDanmakuRetry, this.ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// 房间已被删除
return;
}
2021-02-23 18:03:37 +08:00
await this.danmakuClient.ConnectAsync(this.RoomConfig.RoomId, this.ct).ConfigureAwait(false);
}
catch (Exception ex)
{
2021-04-23 19:15:20 +08:00
this.logger.Write(ex is ExecutionRejectedException ? LogEventLevel.Verbose : LogEventLevel.Warning, ex, "连接弹幕服务器时出错");
2021-02-23 18:03:37 +08:00
if (!this.ct.IsCancellationRequested)
this.StartDamakuConnection();
}
});
#endregion
#region Event Handlers
///
private void RecordTask_IOStats(object sender, IOStatsEventArgs e)
2021-02-23 18:03:37 +08:00
{
this.logger.Verbose("IO stats: {@stats}", e);
2021-02-23 18:03:37 +08:00
this.Stats.NetworkMbps = e.NetworkMbps;
2021-02-23 18:03:37 +08:00
IOStats?.Invoke(this, e);
2021-02-23 18:03:37 +08:00
}
///
private void RecordTask_RecordingStats(object sender, RecordingStatsEventArgs e)
{
this.logger.Verbose("Recording stats: {@stats}", e);
var diff = DateTimeOffset.UtcNow - this.recordTaskStartTime;
2021-02-26 21:57:10 +08:00
this.Stats.SessionDuration = TimeSpan.FromSeconds(Math.Round(diff.TotalSeconds));
2021-02-23 18:03:37 +08:00
this.Stats.FileMaxTimestamp = TimeSpan.FromMilliseconds(e.FileMaxTimestamp);
this.Stats.SessionMaxTimestamp = TimeSpan.FromMilliseconds(e.SessionMaxTimestamp);
2021-08-04 21:58:35 +08:00
this.Stats.DurationRatio = e.DurationRatio;
2021-02-23 18:03:37 +08:00
this.Stats.TotalInputBytes = e.TotalInputVideoByteCount + e.TotalInputAudioByteCount;
this.Stats.TotalOutputBytes = e.TotalOutputVideoByteCount + e.TotalOutputAudioByteCount;
RecordingStats?.Invoke(this, e);
}
///
private void RecordTask_RecordFileClosed(object sender, RecordFileClosedEventArgs e)
{
this.basicDanmakuWriter.Disable();
2021-02-23 18:03:37 +08:00
RecordFileClosed?.Invoke(this, e);
}
///
private void RecordTask_RecordFileOpening(object sender, RecordFileOpeningEventArgs e)
{
if (this.RoomConfig.RecordDanmaku)
this.basicDanmakuWriter.EnableWithPath(Path.ChangeExtension(e.FullPath, "xml"), this);
2021-03-18 22:28:10 +08:00
else
this.basicDanmakuWriter.Disable();
2021-02-23 18:03:37 +08:00
RecordFileOpening?.Invoke(this, e);
}
///
private void RecordTask_RecordSessionEnded(object sender, EventArgs e)
{
Guid id;
lock (this.recordStartLock)
{
id = this.recordTask?.SessionId ?? default;
this.recordTask = null;
2021-04-21 23:18:23 +08:00
_ = Task.Run(async () =>
{
// 录制结束退出后的重试逻辑
2021-04-23 19:15:20 +08:00
// 比 RestartAfterRecordTaskFailedAsync 少了等待时间
2021-04-21 23:18:23 +08:00
if (!this.Streaming || !this.AutoRecordForThisSession)
return;
2021-04-23 19:15:20 +08:00
try
{
await this.FetchRoomInfoAsync().ConfigureAwait(false);
2021-04-21 23:18:23 +08:00
2021-04-23 19:15:20 +08:00
if (this.Streaming && this.AutoRecordForThisSession)
this.CreateAndStartNewRecordTask();
}
catch (Exception ex)
{
this.logger.Write(LogEventLevel.Warning, ex, "重试开始录制时出错");
2021-11-20 14:34:35 +08:00
_ = Task.Run(() => this.RestartAfterRecordTaskFailedAsync(RestartRecordingReason.GenericRetry));
2021-04-23 19:15:20 +08:00
}
2021-04-21 23:18:23 +08:00
});
2021-02-23 18:03:37 +08:00
}
2021-03-18 22:28:10 +08:00
this.basicDanmakuWriter.Disable();
2021-02-23 18:03:37 +08:00
this.OnPropertyChanged(nameof(this.Recording));
2021-04-21 19:48:28 +08:00
this.Stats.Reset();
2021-02-23 18:03:37 +08:00
RecordSessionEnded?.Invoke(this, new RecordSessionEndedEventArgs(this)
{
SessionId = id
});
}
private void DanmakuClient_DanmakuReceived(object sender, Api.Danmaku.DanmakuReceivedEventArgs e)
{
var d = e.Danmaku;
switch (d.MsgType)
{
case Api.Danmaku.DanmakuMsgType.LiveStart:
this.Streaming = true;
break;
case Api.Danmaku.DanmakuMsgType.LiveEnd:
this.Streaming = false;
break;
case Api.Danmaku.DanmakuMsgType.RoomChange:
this.Title = d.Title ?? this.Title;
this.AreaNameParent = d.ParentAreaName ?? this.AreaNameParent;
this.AreaNameChild = d.AreaName ?? this.AreaNameChild;
break;
default:
break;
}
2021-04-29 18:08:55 +08:00
_ = this.basicDanmakuWriter.WriteAsync(d);
2021-02-23 18:03:37 +08:00
}
private void DanmakuClient_StatusChanged(object sender, Api.Danmaku.StatusChangedEventArgs e)
{
if (e.Connected)
{
this.DanmakuConnected = true;
2021-04-30 19:35:15 +08:00
this.logger.Information("弹幕服务器已连接");
2021-02-23 18:03:37 +08:00
}
else
{
this.DanmakuConnected = false;
2021-04-30 19:35:15 +08:00
this.logger.Information("与弹幕服务器的连接被断开");
2021-04-21 23:18:23 +08:00
this.StartDamakuConnection(delay: true);
2021-02-23 18:03:37 +08:00
}
}
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
2021-04-21 23:18:23 +08:00
this.StartDamakuConnection(delay: false);
2021-02-23 18:03:37 +08:00
if (this.RoomConfig.AutoRecord)
2021-04-21 23:18:23 +08:00
{
_ = Task.Run(async () =>
{
2021-05-02 21:02:33 +08:00
try
{
// 定时主动检查不需要错误重试
await this.FetchRoomInfoAsync().ConfigureAwait(false);
2021-04-21 23:18:23 +08:00
2021-05-02 21:02:33 +08:00
if (this.Streaming && this.AutoRecordForThisSession && this.RoomConfig.AutoRecord)
this.CreateAndStartNewRecordTask();
}
catch (Exception) { }
2021-04-21 23:18:23 +08:00
});
}
}
private void Room_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(this.Streaming):
if (this.Streaming)
{
if (this.AutoRecordForThisSession && this.RoomConfig.AutoRecord)
this.CreateAndStartNewRecordTask();
}
else
{
this.AutoRecordForThisSession = true;
}
break;
default:
break;
}
2021-02-23 18:03:37 +08:00
}
private void RoomConfig_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(this.RoomConfig.RoomId):
this.logger = this.loggerWithoutContext.ForContext(LoggingContext.RoomId, this.RoomConfig.RoomId);
break;
case nameof(this.RoomConfig.TimingCheckInterval):
this.timer.Interval = this.RoomConfig.TimingCheckInterval * 1000;
break;
case nameof(this.RoomConfig.AutoRecord):
if (this.RoomConfig.AutoRecord)
2021-04-21 23:18:23 +08:00
{
this.AutoRecordForThisSession = true;
if (this.Streaming && this.AutoRecordForThisSession)
this.CreateAndStartNewRecordTask();
}
break;
2021-02-23 18:03:37 +08:00
default:
break;
}
}
#endregion
#region PropertyChanged
protected void SetField<T>(ref T location, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(location, value))
return;
location = value;
if (propertyName != null)
this.OnPropertyChanged(propertyName);
}
protected void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName: propertyName));
#endregion
#region Dispose
protected virtual void Dispose(bool disposing)
{
if (!this.disposedValue)
{
this.disposedValue = true;
if (disposing)
{
// dispose managed state (managed objects)
this.cts.Cancel();
this.cts.Dispose();
this.recordTask?.RequestStop();
this.basicDanmakuWriter.Disable();
this.scope.Dispose();
}
// free unmanaged resources (unmanaged objects) and override finalizer
// set large fields to null
}
}
// override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~Room()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
}