BililiveRecorder/BililiveRecorder.Core/RecordedRoom.cs

387 lines
14 KiB
C#
Raw Normal View History

2018-11-01 23:40:50 +08:00
using BililiveRecorder.Core.Config;
using BililiveRecorder.FlvProcessor;
2018-03-21 20:56:56 +08:00
using NLog;
using System;
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;
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();
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;
public int Roomid
{
get => _roomid;
private set
{
if (value == _roomid) { return; }
_roomid = value;
TriggerPropertyChanged(nameof(Roomid));
}
}
public int RealRoomid
{
get => _realRoomid;
private set
{
if (value == _realRoomid) { return; }
_realRoomid = value;
TriggerPropertyChanged(nameof(RealRoomid));
}
}
public string StreamerName
{
get => _streamerName;
private set
{
if (value == _streamerName) { return; }
_streamerName = value;
TriggerPropertyChanged(nameof(StreamerName));
}
}
public bool IsMonitoring => StreamMonitor.IsMonitoring;
2018-10-31 06:22:38 +08:00
public bool IsRecording => !(StreamDownloadTask?.IsCompleted ?? true);
2018-03-12 18:57:20 +08:00
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; }
2018-11-03 07:45:56 +08:00
public IStreamMonitor StreamMonitor { get; }
2018-10-31 06:22:38 +08:00
2018-11-03 07:45:56 +08:00
private HttpWebResponse _response;
private Stream _stream;
2018-10-31 06:23:01 +08:00
private Task StartupTask = null;
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;
private double _DownloadSpeedKiBps = 0;
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)); } }
}
2018-03-26 06:14:01 +08:00
public double DownloadSpeedKiBps
{
get { return _DownloadSpeedKiBps; }
private set { if (value != _DownloadSpeedKiBps) { _DownloadSpeedKiBps = value; TriggerPropertyChanged(nameof(DownloadSpeedKiBps)); } }
2018-03-26 06:14:01 +08:00
}
2018-11-01 23:40:50 +08:00
public RecordedRoom(ConfigV1 config,
2018-10-25 19:20:23 +08:00
Func<int, IStreamMonitor> newIStreamMonitor,
2018-10-31 06:22:38 +08:00
Func<IFlvStreamProcessor> newIFlvStreamProcessor,
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;
2018-03-20 00:12:32 +08:00
2018-03-21 20:56:56 +08:00
Roomid = roomid;
2018-03-15 21:55:01 +08:00
2018-10-31 06:22:38 +08:00
{
var roomInfo = BililiveAPI.GetRoomInfo(Roomid);
RealRoomid = roomInfo.RealRoomid;
StreamerName = roomInfo.Username;
}
2018-03-21 20:56:56 +08:00
2018-10-25 19:20:23 +08:00
StreamMonitor = newIStreamMonitor(RealRoomid);
2018-03-27 15:53:03 +08:00
StreamMonitor.StreamStatusChanged += StreamMonitor_StreamStatusChanged;
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
}
2018-10-31 06:23:01 +08:00
private void StreamMonitor_StreamStatusChanged(object sender, StreamStatusChangedArgs e)
2018-03-20 00:12:32 +08:00
{
2018-10-31 06:23:01 +08:00
if (StartupTask?.IsCompleted ?? true)
2018-03-20 00:12:32 +08:00
{
2018-10-31 06:23:01 +08:00
StartupTask = _StartRecordAsync(e.type);
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));
}
try
{
if (cancellationTokenSource != null)
{
cancellationTokenSource.Cancel();
if (!(StreamDownloadTask?.Wait(TimeSpan.FromSeconds(2)) ?? true))
{
logger.Log(RealRoomid, LogLevel.Warn, "尝试强制关闭连接,请检查网络连接是否稳定");
_stream?.Close();
_stream?.Dispose();
_response?.Close();
_response?.Dispose();
StreamDownloadTask?.Wait();
}
}
}
catch (Exception ex)
2018-03-23 06:57:22 +08:00
{
logger.Log(RealRoomid, LogLevel.Error, "在尝试停止录制时发生错误,请检查网络连接是否稳定", ex);
2018-03-23 06:57:22 +08:00
}
}
2018-10-31 06:22:38 +08:00
private async Task _StartRecordAsync(TriggerType triggerType)
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
{
2018-03-21 20:56:56 +08:00
logger.Log(RealRoomid, LogLevel.Debug, "已经在录制中了");
return;
2018-03-15 21:55:01 +08:00
}
2018-10-31 06:22:38 +08:00
HttpWebRequest request = null;
cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
2018-03-21 20:56:56 +08:00
try
2018-03-15 21:55:01 +08:00
{
2018-10-31 06:22:38 +08:00
string flv_path = BililiveAPI.GetPlayUrl(RealRoomid);
2018-11-10 12:17:41 +08:00
logger.Log(RealRoomid, LogLevel.Debug, "直播流地址: " + flv_path);
2018-10-31 06:22:38 +08:00
request = WebRequest.CreateHttp(flv_path);
request.Accept = "*/*";
request.AllowAutoRedirect = true;
request.Referer = "https://live.bilibili.com";
request.Headers["Origin"] = "https://live.bilibili.com";
request.UserAgent = Utils.UserAgent;
_response = await request.GetResponseAsync() as HttpWebResponse;
2018-03-18 18:55:28 +08:00
if (_response.StatusCode != HttpStatusCode.OK)
2018-03-18 18:55:28 +08:00
{
logger.Log(RealRoomid, LogLevel.Info, string.Format("尝试下载直播流时服务器返回了 ({0}){1}", _response.StatusCode, _response.StatusDescription));
_response.Close();
2018-10-31 06:22:38 +08:00
request = null;
if (_response.StatusCode == HttpStatusCode.NotFound)
2018-03-18 18:55:28 +08:00
{
2018-10-31 06:23:01 +08:00
logger.Log(RealRoomid, LogLevel.Info, "将在15秒后重试");
StreamMonitor.Check(TriggerType.HttpApiRecheck, 15); // TODO: 重试次数和时间
2018-03-18 18:55:28 +08:00
}
2018-03-21 20:56:56 +08:00
return;
}
else
{
2018-03-29 13:02:43 +08:00
if (triggerType == TriggerType.HttpApiRecheck)
{
triggerType = TriggerType.HttpApi;
}
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;
2018-03-21 20:56:56 +08:00
_stream = _response.GetResponseStream();
_stream.ReadTimeout = 3 * 1000;
2018-03-21 20:56:56 +08:00
2018-10-31 06:22:38 +08:00
StreamDownloadTask = Task.Run(async () =>
2018-03-18 18:55:28 +08:00
{
try
2018-03-18 18:55:28 +08:00
{
const int BUF_SIZE = 1024 * 8;
byte[] buffer = new byte[BUF_SIZE];
while (!token.IsCancellationRequested)
2018-03-21 20:56:56 +08:00
{
int bytesRead = await _stream.ReadAsync(buffer, 0, BUF_SIZE, token);
_UpdateDownloadSpeed(bytesRead);
if (bytesRead == 0)
2018-10-24 14:33:05 +08:00
{
// 录制已结束
// TODO: 重试次数和时间
// TODO: 用户操作停止时不重新继续
logger.Log(RealRoomid, LogLevel.Info,
(token.IsCancellationRequested ? "用户操作" : "直播已结束") + ",停止录制。"
+ (triggerType != TriggerType.HttpApiRecheck ? "将在15秒后重试启动。" : ""));
if (triggerType != TriggerType.HttpApiRecheck)
{
StreamMonitor.Check(TriggerType.HttpApiRecheck, 15);
}
break;
2018-10-24 14:33:05 +08:00
}
2018-03-21 20:56:56 +08:00
else
2018-10-24 14:33:05 +08:00
{
if (bytesRead != BUF_SIZE)
{
Processor.AddBytes(buffer.Take(bytesRead).ToArray());
}
else
{
Processor.AddBytes(buffer);
}
2018-10-24 14:33:05 +08:00
}
} // while(true)
// outside of while
}
finally
{
_CleanupFlvRequest();
}
2018-10-31 06:22:38 +08:00
});
2018-03-21 20:56:56 +08:00
TriggerPropertyChanged(nameof(IsRecording));
2018-03-18 18:55:28 +08:00
}
2018-03-21 20:56:56 +08:00
}
catch (Exception ex)
{
_CleanupFlvRequest();
2018-10-31 06:23:01 +08:00
logger.Log(RealRoomid, LogLevel.Warn, "启动直播流下载出错。" + (triggerType != TriggerType.HttpApiRecheck ? "将在15秒后重试启动。" : ""), ex);
2018-03-21 20:56:56 +08:00
if (triggerType != TriggerType.HttpApiRecheck)
2018-10-24 14:33:05 +08:00
{
StreamMonitor.Check(TriggerType.HttpApiRecheck, 15);
2018-10-24 14:33:05 +08:00
}
2018-03-21 20:56:56 +08:00
}
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;
}
request = null;
_stream?.Dispose();
_stream = null;
_response?.Dispose();
_response = null;
2018-10-31 06:22:38 +08:00
DownloadSpeedKiBps = 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
{
2018-11-03 07:45:56 +08:00
DownloadSpeedKiBps = _lastUpdateSize / passedSeconds / 1024; // KiB per sec
2018-11-07 06:07:49 +08:00
DownloadSpeedPersentage = (Processor.TotalMaxTimestamp - _lastUpdateTimestamp) / passedSeconds / 1000; // ((RecordedTime/1000) / RealTime)%
_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);
}
private string GetStreamFilePath() => Path.Combine(_config.WorkDirectory, RealRoomid.ToString(), "record",
$@"record-{RealRoomid}-{DateTime.Now.ToString("yyyyMMdd-HHmmss")}-{random.Next(100, 999)}.flv".RemoveInvalidFileName());
private string GetClipFilePath() => Path.Combine(_config.WorkDirectory, RealRoomid.ToString(), "clip",
$@"clip-{RealRoomid}-{DateTime.Now.ToString("yyyyMMdd-HHmmss")}-{random.Next(100, 999)}.flv".RemoveInvalidFileName());
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();
Processor?.Dispose();
StreamMonitor?.Dispose();
_response?.Dispose();
_stream?.Dispose();
cancellationTokenSource.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
}
}