BililiveRecorder/BililiveRecorder.Core/RecordedRoom.cs

592 lines
21 KiB
C#
Raw Normal View History

2020-11-27 18:51:02 +08:00
using BililiveRecorder.Core.Config;
2018-11-01 23:40:50 +08:00
using BililiveRecorder.FlvProcessor;
2018-03-21 20:56:56 +08:00
using NLog;
using System;
2019-01-17 00:28:09 +08:00
using System.Collections.Generic;
2018-03-12 18:57:20 +08:00
using System.ComponentModel;
2018-03-18 18:55:28 +08:00
using System.IO;
2018-03-13 13:21:01 +08:00
using System.Linq;
2018-03-13 14:23:53 +08:00
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
2018-10-31 06:22:38 +08:00
using System.Threading;
using System.Threading.Tasks;
2018-03-12 18:57:20 +08:00
namespace BililiveRecorder.Core
{
2018-10-24 14:33:05 +08:00
public class RecordedRoom : IRecordedRoom
2018-03-12 18:57:20 +08:00
{
2018-03-21 20:56:56 +08:00
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly Random random = new Random();
2020-08-14 17:01:04 +08:00
private static readonly Version VERSION_1_0 = new Version(1, 0);
2018-03-15 21:55:01 +08:00
2018-10-31 06:22:38 +08:00
private int _roomid;
private int _realRoomid;
private string _streamerName;
private string _title;
private bool _isStreaming;
2018-10-31 06:22:38 +08:00
2019-08-22 01:26:18 +08:00
public int ShortRoomId
2018-10-31 06:22:38 +08:00
{
get => _roomid;
private set
{
if (value == _roomid) { return; }
_roomid = value;
2019-08-22 01:26:18 +08:00
TriggerPropertyChanged(nameof(ShortRoomId));
2018-10-31 06:22:38 +08:00
}
}
2019-08-22 01:26:18 +08:00
public int RoomId
2018-10-31 06:22:38 +08:00
{
get => _realRoomid;
private set
{
if (value == _realRoomid) { return; }
_realRoomid = value;
2019-08-22 01:26:18 +08:00
TriggerPropertyChanged(nameof(RoomId));
2018-10-31 06:22:38 +08:00
}
}
public string StreamerName
{
get => _streamerName;
private set
{
if (value == _streamerName) { return; }
_streamerName = value;
TriggerPropertyChanged(nameof(StreamerName));
}
}
public string Title
{
get => _title;
private set
{
if (value == _title) { return; }
_title = value;
TriggerPropertyChanged(nameof(Title));
}
}
public bool IsMonitoring => StreamMonitor.IsMonitoring;
2018-10-31 06:22:38 +08:00
public bool IsRecording => !(StreamDownloadTask?.IsCompleted ?? true);
public bool IsStreaming
{
get => _isStreaming;
private set
{
if (value == _isStreaming) { return; }
_isStreaming = value;
TriggerPropertyChanged(nameof(IsStreaming));
}
}
2018-03-12 18:57:20 +08:00
2020-11-23 17:35:42 +08:00
private readonly IBasicDanmakuWriter basicDanmakuWriter;
2018-10-31 06:22:38 +08:00
private readonly Func<IFlvStreamProcessor> newIFlvStreamProcessor;
2018-11-03 07:45:56 +08:00
private IFlvStreamProcessor _processor;
2018-10-31 06:23:01 +08:00
public IFlvStreamProcessor Processor
{
get => _processor;
private set
{
if (value == _processor) { return; }
_processor = value;
TriggerPropertyChanged(nameof(Processor));
}
}
2018-03-24 09:48:06 +08:00
2018-11-01 23:40:50 +08:00
private ConfigV1 _config { get; }
2020-11-27 18:51:02 +08:00
private BililiveAPI BililiveAPI { get; }
2018-11-03 07:45:56 +08:00
public IStreamMonitor StreamMonitor { get; }
2018-10-31 06:22:38 +08:00
2018-12-17 21:24:52 +08:00
private bool _retry = true;
private HttpResponseMessage _response;
2018-11-03 07:45:56 +08:00
private Stream _stream;
2018-10-31 06:23:01 +08:00
private Task StartupTask = null;
private readonly object StartupTaskLock = new object();
2018-10-31 06:23:01 +08:00
public Task StreamDownloadTask = null;
public CancellationTokenSource cancellationTokenSource = null;
2018-03-13 13:21:01 +08:00
2018-11-03 07:45:56 +08:00
private double _DownloadSpeedPersentage = 0;
2019-11-24 09:08:29 +08:00
private double _DownloadSpeedMegaBitps = 0;
2018-11-03 07:45:56 +08:00
private long _lastUpdateSize = 0;
private int _lastUpdateTimestamp = 0;
public DateTime LastUpdateDateTime { get; private set; } = DateTime.Now;
public double DownloadSpeedPersentage
{
get { return _DownloadSpeedPersentage; }
private set { if (value != _DownloadSpeedPersentage) { _DownloadSpeedPersentage = value; TriggerPropertyChanged(nameof(DownloadSpeedPersentage)); } }
}
2019-11-24 09:08:29 +08:00
public double DownloadSpeedMegaBitps
2018-03-26 06:14:01 +08:00
{
2019-11-24 09:08:29 +08:00
get { return _DownloadSpeedMegaBitps; }
private set { if (value != _DownloadSpeedMegaBitps) { _DownloadSpeedMegaBitps = value; TriggerPropertyChanged(nameof(DownloadSpeedMegaBitps)); } }
2018-03-26 06:14:01 +08:00
}
2020-12-05 18:30:04 +08:00
public Guid Guid { get; } = Guid.NewGuid();
2018-11-01 23:40:50 +08:00
public RecordedRoom(ConfigV1 config,
2020-11-23 17:35:42 +08:00
IBasicDanmakuWriter basicDanmakuWriter,
2018-10-25 19:20:23 +08:00
Func<int, IStreamMonitor> newIStreamMonitor,
2018-10-31 06:22:38 +08:00
Func<IFlvStreamProcessor> newIFlvStreamProcessor,
2020-11-27 18:51:02 +08:00
BililiveAPI bililiveAPI,
2018-10-25 19:20:23 +08:00
int roomid)
2018-03-13 13:21:01 +08:00
{
2018-10-25 19:20:23 +08:00
this.newIFlvStreamProcessor = newIFlvStreamProcessor;
2018-11-01 23:40:50 +08:00
_config = config;
2020-11-27 18:51:02 +08:00
BililiveAPI = bililiveAPI;
2018-03-20 00:12:32 +08:00
2020-11-23 17:35:42 +08:00
this.basicDanmakuWriter = basicDanmakuWriter;
2019-08-22 01:26:18 +08:00
RoomId = roomid;
2020-11-23 17:35:42 +08:00
StreamerName = "获取中...";
2018-03-15 21:55:01 +08:00
2019-08-22 01:26:18 +08:00
StreamMonitor = newIStreamMonitor(RoomId);
StreamMonitor.RoomInfoUpdated += StreamMonitor_RoomInfoUpdated;
StreamMonitor.StreamStarted += StreamMonitor_StreamStarted;
2020-11-23 17:35:42 +08:00
StreamMonitor.ReceivedDanmaku += StreamMonitor_ReceivedDanmaku;
2019-08-22 01:26:18 +08:00
StreamMonitor.FetchRoomInfoAsync();
}
2018-03-21 20:56:56 +08:00
2020-11-23 17:35:42 +08:00
private void StreamMonitor_ReceivedDanmaku(object sender, ReceivedDanmakuArgs e)
{
switch (e.Danmaku.MsgType)
{
case MsgTypeEnum.LiveStart:
IsStreaming = true;
break;
case MsgTypeEnum.LiveEnd:
IsStreaming = false;
break;
default:
break;
}
2020-11-23 17:35:42 +08:00
basicDanmakuWriter.Write(e.Danmaku);
}
private void StreamMonitor_RoomInfoUpdated(object sender, RoomInfoUpdatedArgs e)
{
2019-08-22 01:26:18 +08:00
RoomId = e.RoomInfo.RoomId;
ShortRoomId = e.RoomInfo.ShortRoomId;
StreamerName = e.RoomInfo.UserName;
Title = e.RoomInfo.Title;
IsStreaming = e.RoomInfo.IsStreaming;
2018-03-21 20:56:56 +08:00
}
public bool Start()
{
if (disposedValue)
{
throw new ObjectDisposedException(nameof(RecordedRoom));
}
2018-03-27 15:53:03 +08:00
var r = StreamMonitor.Start();
TriggerPropertyChanged(nameof(IsMonitoring));
2018-03-21 20:56:56 +08:00
return r;
}
public void Stop()
{
if (disposedValue)
{
throw new ObjectDisposedException(nameof(RecordedRoom));
}
2018-03-27 15:53:03 +08:00
StreamMonitor.Stop();
TriggerPropertyChanged(nameof(IsMonitoring));
2018-03-13 14:23:53 +08:00
}
public void RefreshRoomInfo()
{
if (disposedValue)
{
throw new ObjectDisposedException(nameof(RecordedRoom));
}
2019-08-22 01:26:18 +08:00
StreamMonitor.FetchRoomInfoAsync();
}
private void StreamMonitor_StreamStarted(object sender, StreamStartedArgs e)
2018-03-20 00:12:32 +08:00
{
lock (StartupTaskLock)
2018-03-20 00:12:32 +08:00
{
if (!IsRecording && (StartupTask?.IsCompleted ?? true))
{
StartupTask = _StartRecordAsync();
}
2018-03-20 00:12:32 +08:00
}
2018-03-13 14:23:53 +08:00
}
2018-03-15 21:55:01 +08:00
public void StartRecord()
2018-03-13 14:23:53 +08:00
{
if (disposedValue)
{
throw new ObjectDisposedException(nameof(RecordedRoom));
}
2018-03-27 15:53:03 +08:00
StreamMonitor.Check(TriggerType.Manual);
2018-03-13 13:21:01 +08:00
}
2018-03-23 06:57:22 +08:00
public void StopRecord()
{
if (disposedValue)
{
throw new ObjectDisposedException(nameof(RecordedRoom));
}
2018-12-17 21:24:52 +08:00
_retry = false;
try
{
if (cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
if (!(StreamDownloadTask?.Wait(TimeSpan.FromSeconds(2)) ?? true))
{
logger.Log(RoomId, LogLevel.Warn, "停止录制超时,尝试强制关闭连接,请检查网络连接是否稳定");
_stream?.Close();
_stream?.Dispose();
_response?.Dispose();
StreamDownloadTask?.Wait();
}
}
}
catch (Exception ex)
2018-03-23 06:57:22 +08:00
{
2019-08-22 01:26:18 +08:00
logger.Log(RoomId, LogLevel.Error, "在尝试停止录制时发生错误,请检查网络连接是否稳定", ex);
2018-03-23 06:57:22 +08:00
}
2018-12-17 21:24:52 +08:00
finally
{
_retry = true;
}
2018-03-23 06:57:22 +08:00
}
2018-12-17 21:24:52 +08:00
private async Task _StartRecordAsync()
2018-03-15 21:55:01 +08:00
{
2018-10-31 06:22:38 +08:00
if (IsRecording)
2018-03-15 21:55:01 +08:00
{
2019-08-22 01:26:18 +08:00
logger.Log(RoomId, LogLevel.Debug, "已经在录制中了");
2018-03-21 20:56:56 +08:00
return;
2018-03-15 21:55:01 +08:00
}
// HttpWebRequest request = null;
2018-10-31 06:22:38 +08:00
cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
2018-03-21 20:56:56 +08:00
try
2018-03-15 21:55:01 +08:00
{
2019-10-31 21:04:08 +08:00
string flv_path = await BililiveAPI.GetPlayUrlAsync(RoomId);
unwrap_redir:
using (var client = new HttpClient(new HttpClientHandler
{
2019-10-31 21:04:08 +08:00
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
}))
{
2018-12-18 00:16:24 +08:00
client.Timeout = TimeSpan.FromMilliseconds(_config.TimingStreamConnect);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
client.DefaultRequestHeaders.UserAgent.Clear();
client.DefaultRequestHeaders.UserAgent.ParseAdd(Utils.UserAgent);
client.DefaultRequestHeaders.Referrer = new Uri("https://live.bilibili.com");
client.DefaultRequestHeaders.Add("Origin", "https://live.bilibili.com");
2019-10-31 21:04:08 +08:00
2019-08-22 01:26:18 +08:00
logger.Log(RoomId, LogLevel.Info, "连接直播服务器 " + new Uri(flv_path).Host);
logger.Log(RoomId, LogLevel.Debug, "直播流地址: " + flv_path);
_response = await client.GetAsync(flv_path, HttpCompletionOption.ResponseHeadersRead);
}
2018-03-18 18:55:28 +08:00
2019-10-31 21:04:08 +08:00
if (_response.StatusCode == HttpStatusCode.Redirect || _response.StatusCode == HttpStatusCode.Moved)
{
// workaround for missing Referrer
flv_path = _response.Headers.Location.OriginalString;
_response.Dispose();
goto unwrap_redir;
}
else if (_response.StatusCode != HttpStatusCode.OK)
2018-03-18 18:55:28 +08:00
{
2019-08-22 01:26:18 +08:00
logger.Log(RoomId, LogLevel.Info, string.Format("尝试下载直播流时服务器返回了 ({0}){1}", _response.StatusCode, _response.ReasonPhrase));
2018-12-18 00:16:24 +08:00
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
_CleanupFlvRequest();
2018-03-21 20:56:56 +08:00
return;
}
else
{
2018-11-07 06:07:49 +08:00
Processor = newIFlvStreamProcessor().Initialize(GetStreamFilePath, GetClipFilePath, _config.EnabledFeature, _config.CuttingMode);
2018-11-01 23:40:50 +08:00
Processor.ClipLengthFuture = _config.ClipLengthFuture;
Processor.ClipLengthPast = _config.ClipLengthPast;
2018-11-07 06:07:49 +08:00
Processor.CuttingNumber = _config.CuttingNumber;
2020-11-23 17:35:42 +08:00
Processor.StreamFinalized += (sender, e) => { basicDanmakuWriter.Disable(); };
2019-01-17 00:28:09 +08:00
Processor.OnMetaData += (sender, e) =>
{
e.Metadata["BililiveRecorder"] = new Dictionary<string, object>()
{
{
"starttime",
DateTime.UtcNow
},
{
"version",
BuildInfo.Version + " " + BuildInfo.HeadShaShort
2019-01-17 00:28:09 +08:00
},
{
"roomid",
2019-08-22 01:26:18 +08:00
RoomId.ToString()
2019-01-17 00:28:09 +08:00
},
{
"streamername",
StreamerName
},
};
};
2018-03-21 20:56:56 +08:00
_stream = await _response.Content.ReadAsStreamAsync();
2020-08-14 17:01:04 +08:00
try
{
if (_response.Headers.ConnectionClose == false || (_response.Headers.ConnectionClose is null && _response.Version != VERSION_1_0))
_stream.ReadTimeout = 3 * 1000;
}
catch (InvalidOperationException) { }
2018-03-21 20:56:56 +08:00
StreamDownloadTask = Task.Run(_ReadStreamLoop);
TriggerPropertyChanged(nameof(IsRecording));
2018-03-18 18:55:28 +08:00
}
2018-03-21 20:56:56 +08:00
}
catch (TaskCanceledException)
{
// client.GetAsync timed out
// useless exception message :/
_CleanupFlvRequest();
2019-08-22 01:26:18 +08:00
logger.Log(RoomId, LogLevel.Warn, "连接直播服务器超时。");
2018-12-18 00:16:24 +08:00
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
}
2018-03-21 20:56:56 +08:00
catch (Exception ex)
{
_CleanupFlvRequest();
2019-08-22 01:26:18 +08:00
logger.Log(RoomId, LogLevel.Warn, "启动直播流下载出错。" + (_retry ? "将重试启动。" : ""), ex);
2018-12-17 21:24:52 +08:00
if (_retry)
2018-10-24 14:33:05 +08:00
{
2018-12-18 00:16:24 +08:00
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
2018-10-24 14:33:05 +08:00
}
2018-03-21 20:56:56 +08:00
}
return;
async Task _ReadStreamLoop()
{
try
{
const int BUF_SIZE = 1024 * 8;
byte[] buffer = new byte[BUF_SIZE];
while (!token.IsCancellationRequested)
{
int bytesRead = await _stream.ReadAsync(buffer, 0, BUF_SIZE, token);
_UpdateDownloadSpeed(bytesRead);
if (bytesRead != 0)
{
if (bytesRead != BUF_SIZE)
{
Processor.AddBytes(buffer.Take(bytesRead).ToArray());
}
else
{
Processor.AddBytes(buffer);
}
}
else
{
break;
}
}
2018-12-17 21:24:52 +08:00
2019-08-22 01:26:18 +08:00
logger.Log(RoomId, LogLevel.Info,
(token.IsCancellationRequested ? "本地操作结束当前录制。" : "服务器关闭直播流,可能是直播已结束。")
+ (_retry ? "将重试启动。" : ""));
2018-12-17 21:24:52 +08:00
if (_retry)
{
2018-12-18 00:16:24 +08:00
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
2018-12-17 21:24:52 +08:00
}
}
catch (Exception e)
{
2018-12-17 21:24:52 +08:00
if (e is ObjectDisposedException && token.IsCancellationRequested) { return; }
2019-08-22 01:26:18 +08:00
logger.Log(RoomId, LogLevel.Warn, "录播发生错误", e);
}
finally
{
_CleanupFlvRequest();
}
}
2018-10-31 06:22:38 +08:00
void _CleanupFlvRequest()
2018-03-21 20:56:56 +08:00
{
2018-10-31 06:22:38 +08:00
if (Processor != null)
{
Processor.FinallizeFile();
Processor.Dispose();
Processor = null;
}
_stream?.Dispose();
_stream = null;
_response?.Dispose();
_response = null;
2018-10-31 06:22:38 +08:00
2018-12-17 21:24:52 +08:00
_lastUpdateTimestamp = 0;
2019-11-24 09:08:29 +08:00
DownloadSpeedMegaBitps = 0d;
2018-11-03 07:45:56 +08:00
DownloadSpeedPersentage = 0d;
2018-10-31 06:22:38 +08:00
TriggerPropertyChanged(nameof(IsRecording));
2018-03-15 21:55:01 +08:00
}
2018-10-31 06:22:38 +08:00
void _UpdateDownloadSpeed(int bytesRead)
2018-03-15 21:55:01 +08:00
{
2018-10-31 06:22:38 +08:00
DateTime now = DateTime.Now;
2018-11-03 07:45:56 +08:00
double passedSeconds = (now - LastUpdateDateTime).TotalSeconds;
_lastUpdateSize += bytesRead;
if (passedSeconds > 1.5)
2018-10-31 06:22:38 +08:00
{
2019-11-24 09:08:29 +08:00
DownloadSpeedMegaBitps = _lastUpdateSize / passedSeconds * 8d / 1_000_000d; // mega bit per second
2018-11-28 22:55:58 +08:00
DownloadSpeedPersentage = (DownloadSpeedPersentage / 2) + ((Processor.TotalMaxTimestamp - _lastUpdateTimestamp) / passedSeconds / 1000 / 2); // ((RecordedTime/1000) / RealTime)%
2018-11-07 06:07:49 +08:00
_lastUpdateTimestamp = Processor.TotalMaxTimestamp;
2018-11-03 07:45:56 +08:00
_lastUpdateSize = 0;
2018-10-31 06:22:38 +08:00
LastUpdateDateTime = now;
}
2018-03-15 21:55:01 +08:00
}
}
2018-03-13 13:21:01 +08:00
// Called by API or GUI
public void Clip()
{
2018-10-31 06:22:38 +08:00
Processor?.Clip();
2018-03-13 13:21:01 +08:00
}
public void Shutdown()
{
Dispose(true);
}
2020-11-23 17:35:42 +08:00
private string GetStreamFilePath()
{
string path = FormatFilename(_config.RecordFilenameFormat);
// 有点脏的写法,不过凑合吧
if (_config.RecordDanmaku)
{
var xmlpath = Path.ChangeExtension(path, "xml");
2020-12-15 18:53:52 +08:00
basicDanmakuWriter.EnableWithPath(xmlpath, this);
2020-11-23 17:35:42 +08:00
}
return path;
}
2020-04-25 01:27:39 +08:00
private string GetClipFilePath() => FormatFilename(_config.ClipFilenameFormat);
private string FormatFilename(string formatString)
{
DateTime now = DateTime.Now;
string date = now.ToString("yyyyMMdd");
string time = now.ToString("HHmmss");
string randomStr = random.Next(100, 999).ToString();
2020-04-25 01:27:39 +08:00
var filename = formatString
.Replace(@"{date}", date)
.Replace(@"{time}", time)
.Replace(@"{random}", randomStr)
2020-04-25 01:27:39 +08:00
.Replace(@"{roomid}", RoomId.ToString())
.Replace(@"{title}", Title.RemoveInvalidFileName())
.Replace(@"{name}", StreamerName.RemoveInvalidFileName());
if (!filename.EndsWith(".flv", StringComparison.OrdinalIgnoreCase))
filename += ".flv";
2020-04-26 13:00:10 +08:00
filename = filename.RemoveInvalidFileName(ignore_slash: true);
2020-04-25 01:27:39 +08:00
filename = Path.Combine(_config.WorkDirectory, filename);
filename = Path.GetFullPath(filename);
if (!CheckPath(_config.WorkDirectory, Path.GetDirectoryName(filename)))
{
logger.Log(RoomId, LogLevel.Warn, "录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
filename = Path.Combine(_config.WorkDirectory, RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv");
}
if (new FileInfo(filename).Exists)
{
logger.Log(RoomId, LogLevel.Warn, "录制文件名冲突,请检查设置。将写入到默认路径。");
filename = Path.Combine(_config.WorkDirectory, RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv");
}
return filename;
}
private static bool CheckPath(string parent, string child)
{
DirectoryInfo di_p = new DirectoryInfo(parent);
DirectoryInfo di_c = new DirectoryInfo(child);
2020-05-01 08:37:56 +08:00
if (di_c.FullName == di_p.FullName)
return true;
2020-04-25 01:27:39 +08:00
bool isParent = false;
while (di_c.Parent != null)
{
if (di_c.Parent.FullName == di_p.FullName)
{
isParent = true;
break;
}
else
di_c = di_c.Parent;
}
return isParent;
}
2018-03-12 18:57:20 +08:00
public event PropertyChangedEventHandler PropertyChanged;
2018-03-24 09:48:06 +08:00
protected void TriggerPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
#region IDisposable Support
private bool disposedValue = false; // 要检测冗余调用
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
Stop();
StopRecord();
2020-11-23 17:35:42 +08:00
Processor?.FinallizeFile();
Processor?.Dispose();
StreamMonitor?.Dispose();
_response?.Dispose();
_stream?.Dispose();
2018-11-28 22:55:58 +08:00
cancellationTokenSource?.Dispose();
2020-11-23 17:35:42 +08:00
basicDanmakuWriter?.Dispose();
}
Processor = null;
_response = null;
_stream = null;
cancellationTokenSource = null;
disposedValue = true;
}
}
public void Dispose()
{
// 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
Dispose(true);
}
#endregion
2018-03-12 18:57:20 +08:00
}
}