mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-15 19:22:19 +08:00
Add DanmakuReceiver
This commit is contained in:
parent
3b33e9d65f
commit
98c4fc8da5
34
BililiveRecorder.Core/DanmakuEvents.cs
Normal file
34
BililiveRecorder.Core/DanmakuEvents.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public delegate void ConnectedEvt(object sender, ConnectedEvtArgs e);
|
||||
public class ConnectedEvtArgs
|
||||
{
|
||||
public int roomid;
|
||||
}
|
||||
|
||||
public delegate void DisconnectEvt(object sender, DisconnectEvtArgs e);
|
||||
public class DisconnectEvtArgs
|
||||
{
|
||||
public Exception Error;
|
||||
}
|
||||
|
||||
public delegate void ReceivedRoomCountEvt(object sender, ReceivedRoomCountArgs e);
|
||||
public class ReceivedRoomCountArgs
|
||||
{
|
||||
public uint UserCount;
|
||||
}
|
||||
|
||||
public delegate void ReceivedDanmakuEvt(object sender, ReceivedDanmakuArgs e);
|
||||
public class ReceivedDanmakuArgs
|
||||
{
|
||||
public DanmakuModel Danmaku;
|
||||
}
|
||||
|
||||
public delegate void LogMessageEvt(object sender, LogMessageArgs e);
|
||||
public class LogMessageArgs
|
||||
{
|
||||
public string message = string.Empty;
|
||||
}
|
||||
}
|
250
BililiveRecorder.Core/DanmakuModel.cs
Normal file
250
BililiveRecorder.Core/DanmakuModel.cs
Normal file
|
@ -0,0 +1,250 @@
|
|||
using System;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public enum MsgTypeEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 彈幕
|
||||
/// </summary>
|
||||
Comment,
|
||||
|
||||
/// <summary>
|
||||
/// 禮物
|
||||
/// </summary>
|
||||
GiftSend,
|
||||
|
||||
/// <summary>
|
||||
/// 歡迎老爷
|
||||
/// </summary>
|
||||
Welcome,
|
||||
|
||||
/// <summary>
|
||||
/// 直播開始
|
||||
/// </summary>
|
||||
LiveStart,
|
||||
|
||||
/// <summary>
|
||||
/// 直播結束
|
||||
/// </summary>
|
||||
LiveEnd,
|
||||
/// <summary>
|
||||
/// 其他
|
||||
/// </summary>
|
||||
Unknown,
|
||||
/// <summary>
|
||||
/// 欢迎船员
|
||||
/// </summary>
|
||||
WelcomeGuard,
|
||||
/// <summary>
|
||||
/// 购买船票(上船)
|
||||
/// </summary>
|
||||
GuardBuy
|
||||
|
||||
}
|
||||
|
||||
public class DanmakuModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息類型
|
||||
/// </summary>
|
||||
public MsgTypeEnum MsgType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 彈幕內容
|
||||
/// <para>此项有值的消息类型:<list type="bullet">
|
||||
/// <item><see cref="MsgTypeEnum.Comment"/></item>
|
||||
/// </list></para>
|
||||
/// </summary>
|
||||
public string CommentText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 彈幕用戶
|
||||
/// </summary>
|
||||
[Obsolete("请使用 UserName")]
|
||||
public string CommentUser
|
||||
{
|
||||
get { return UserName; }
|
||||
set { UserName = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 消息触发者用户名
|
||||
/// <para>此项有值的消息类型:<list type="bullet">
|
||||
/// <item><see cref="MsgTypeEnum.Comment"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.GiftSend"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.Welcome"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.WelcomeGuard"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.GuardBuy"/></item>
|
||||
/// </list></para>
|
||||
/// </summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息触发者用户ID
|
||||
/// <para>此项有值的消息类型:<list type="bullet">
|
||||
/// <item><see cref="MsgTypeEnum.Comment"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.GiftSend"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.Welcome"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.WelcomeGuard"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.GuardBuy"/></item>
|
||||
/// </list></para>
|
||||
/// </summary>
|
||||
public int UserID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户舰队等级
|
||||
/// <para>0 为非船员 1 为总督 2 为提督 3 为舰长</para>
|
||||
/// <para>此项有值的消息类型:<list type="bullet">
|
||||
/// <item><see cref="MsgTypeEnum.Comment"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.WelcomeGuard"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.GuardBuy"/></item>
|
||||
/// </list></para>
|
||||
/// </summary>
|
||||
public int UserGuardLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 禮物用戶
|
||||
/// </summary>
|
||||
[Obsolete("请使用 UserName")]
|
||||
public string GiftUser
|
||||
{
|
||||
get { return UserName; }
|
||||
set { UserName = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禮物名稱
|
||||
/// </summary>
|
||||
public string GiftName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 禮物數量
|
||||
/// </summary>
|
||||
[Obsolete("请使用 GiftCount")]
|
||||
public string GiftNum { get { return GiftCount.ToString(); } }
|
||||
|
||||
/// <summary>
|
||||
/// 礼物数量
|
||||
/// <para>此项有值的消息类型:<list type="bullet">
|
||||
/// <item><see cref="MsgTypeEnum.GiftSend"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.GuardBuy"/></item>
|
||||
/// </list></para>
|
||||
/// <para>此字段也用于标识上船 <see cref="MsgTypeEnum.GuardBuy"/> 的数量(月数)</para>
|
||||
/// </summary>
|
||||
public int GiftCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前房间的礼物积分(Room Cost)
|
||||
/// 因以前出现过不传递rcost的礼物,并且用处不大,所以弃用
|
||||
/// </summary>
|
||||
[Obsolete("如有需要请自行解析RawData", true)]
|
||||
public string Giftrcost { get { return "0"; } set { } }
|
||||
|
||||
/// <summary>
|
||||
/// 该用户是否为房管(包括主播)
|
||||
/// <para>此项有值的消息类型:<list type="bullet">
|
||||
/// <item><see cref="MsgTypeEnum.Comment"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.GiftSend"/></item>
|
||||
/// </list></para>
|
||||
/// </summary>
|
||||
public bool isAdmin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否VIP用戶(老爺)
|
||||
/// <para>此项有值的消息类型:<list type="bullet">
|
||||
/// <item><see cref="MsgTypeEnum.Comment"/></item>
|
||||
/// <item><see cref="MsgTypeEnum.Welcome"/></item>
|
||||
/// </list></para>
|
||||
/// </summary>
|
||||
public bool isVIP { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="MsgTypeEnum.LiveStart"/>,<see cref="MsgTypeEnum.LiveEnd"/> 事件对应的房间号
|
||||
/// </summary>
|
||||
public string roomID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始数据, 高级开发用
|
||||
/// </summary>
|
||||
public string RawData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内部用, JSON数据版本号 通常应该是2
|
||||
/// </summary>
|
||||
public int JSON_Version { get; set; }
|
||||
|
||||
public DanmakuModel()
|
||||
{ }
|
||||
|
||||
public DanmakuModel(string JSON)
|
||||
{
|
||||
RawData = JSON;
|
||||
JSON_Version = 2;
|
||||
var obj = new JSONObject(JSON);
|
||||
string cmd = obj["cmd"].str;
|
||||
switch (cmd)
|
||||
{
|
||||
case "LIVE":
|
||||
MsgType = MsgTypeEnum.LiveStart;
|
||||
roomID = obj["roomid"].str;
|
||||
break;
|
||||
case "PREPARING":
|
||||
MsgType = MsgTypeEnum.LiveEnd;
|
||||
roomID = obj["roomid"].str;
|
||||
break;
|
||||
case "DANMU_MSG":
|
||||
MsgType = MsgTypeEnum.Comment;
|
||||
CommentText = obj["info"][1].str;
|
||||
UserID = (int)obj["info"][2][0].i;
|
||||
UserName = obj["info"][2][1].str;
|
||||
isAdmin = obj["info"][2][2].str == "1";
|
||||
isVIP = obj["info"][2][3].str == "1";
|
||||
UserGuardLevel = (int)obj["info"][7].i;
|
||||
break;
|
||||
case "SEND_GIFT":
|
||||
MsgType = MsgTypeEnum.GiftSend;
|
||||
GiftName = obj["data"]["giftName"].str;
|
||||
UserName = obj["data"]["uname"].str;
|
||||
UserID = (int)obj["data"]["uid"].i;
|
||||
// Giftrcost = obj["data"]["rcost"].ToString();
|
||||
GiftCount = (int)obj["data"]["num"].i;
|
||||
break;
|
||||
case "WELCOME":
|
||||
{
|
||||
MsgType = MsgTypeEnum.Welcome;
|
||||
UserName = obj["data"]["uname"].str;
|
||||
UserID = (int)obj["data"]["uid"].i;
|
||||
isVIP = true;
|
||||
isAdmin = obj["data"]["isadmin"].str == "1";
|
||||
break;
|
||||
|
||||
}
|
||||
case "WELCOME_GUARD":
|
||||
{
|
||||
MsgType = MsgTypeEnum.WelcomeGuard;
|
||||
UserName = obj["data"]["username"].str;
|
||||
UserID = (int)obj["data"]["uid"].i;
|
||||
UserGuardLevel = (int)obj["data"]["guard_level"].i;
|
||||
break;
|
||||
}
|
||||
case "GUARD_BUY":
|
||||
{
|
||||
MsgType = MsgTypeEnum.GuardBuy;
|
||||
UserID = (int)obj["data"]["uid"].i;
|
||||
UserName = obj["data"]["username"].str;
|
||||
UserGuardLevel = (int)obj["data"]["guard_level"].i;
|
||||
GiftName = UserGuardLevel == 3 ? "舰长" : UserGuardLevel == 2 ? "提督" : UserGuardLevel == 1 ? "总督" : "";
|
||||
GiftCount = (int)obj["data"]["num"].i;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
MsgType = MsgTypeEnum.Unknown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
285
BililiveRecorder.Core/DanmakuReceiver.cs
Normal file
285
BililiveRecorder.Core/DanmakuReceiver.cs
Normal file
|
@ -0,0 +1,285 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public class DanmakuReceiver
|
||||
{
|
||||
private const string defaulthosts = "broadcastlv.chat.bilibili.com";
|
||||
private string CIDInfoUrl = "http://live.bilibili.com/api/player?id=cid:";
|
||||
private string ChatHost = defaulthosts;
|
||||
private int ChatPort = 2243;
|
||||
|
||||
private TcpClient Client;
|
||||
private NetworkStream NetStream;
|
||||
|
||||
private Thread ReceiveMessageLoopThread;
|
||||
private Thread HeartbeatLoopThread;
|
||||
private CancellationTokenSource HeartbeatLoopSource;
|
||||
|
||||
public bool isConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
private set
|
||||
{
|
||||
_isConnected = value; if (!value) HeartbeatLoopSource.Cancel();
|
||||
}
|
||||
}
|
||||
private bool _isConnected = false;
|
||||
public Exception Error { get; private set; }
|
||||
public uint ViewerCount { get; private set; }
|
||||
|
||||
public event ConnectedEvt Connected;
|
||||
public event DisconnectEvt Disconnected;
|
||||
public event ReceivedRoomCountEvt ReceivedRoomCount;
|
||||
public event ReceivedDanmakuEvt ReceivedDanmaku;
|
||||
public event LogMessageEvt LogMessage;
|
||||
|
||||
public bool Connect(int roomId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.isConnected) throw new InvalidOperationException();
|
||||
int channelId = roomId;
|
||||
|
||||
try
|
||||
{
|
||||
var request2 = WebRequest.Create(CIDInfoUrl + channelId);
|
||||
request2.Timeout = 2000;
|
||||
var response2 = request2.GetResponse();
|
||||
using (var stream = response2.GetResponseStream())
|
||||
{
|
||||
using (var sr = new StreamReader(stream))
|
||||
{
|
||||
var text = sr.ReadToEnd();
|
||||
var xml = "<root>" + text + "</root>";
|
||||
XmlDocument doc = new XmlDocument();
|
||||
doc.LoadXml(xml);
|
||||
ChatHost = doc["root"]["dm_server"].InnerText;
|
||||
ChatPort = int.Parse(doc["root"]["dm_port"].InnerText);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
HttpWebResponse errorResponse = ex.Response as HttpWebResponse;
|
||||
if (errorResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{ // 直播间不存在(HTTP 404)
|
||||
LogMessage?.Invoke(this, new LogMessageArgs() { message = "该直播间疑似不存在,弹幕姬只支持使用原房间号连接" });
|
||||
}
|
||||
else
|
||||
{ // B站服务器响应错误
|
||||
LogMessage?.Invoke(this, new LogMessageArgs() { message = "B站服务器响应弹幕服务器地址出错,尝试使用常见地址连接" });
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{ // 其他错误(XML解析错误?)
|
||||
LogMessage?.Invoke(this, new LogMessageArgs() { message = "获取弹幕服务器地址时出现未知错误,尝试使用常见地址连接" });
|
||||
}
|
||||
|
||||
Client = new TcpClient();
|
||||
Client.Connect(ChatHost, ChatPort);
|
||||
if (!Client.Connected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
NetStream = Client.GetStream();
|
||||
SendSocketData(7, "{\"roomid\":" + channelId + ",\"uid\":0}");
|
||||
isConnected = true;
|
||||
|
||||
ReceiveMessageLoopThread = new Thread(this.ReceiveMessageLoop);
|
||||
ReceiveMessageLoopThread.IsBackground = true;
|
||||
ReceiveMessageLoopThread.Start();
|
||||
|
||||
HeartbeatLoopSource = new CancellationTokenSource();
|
||||
Repeat.Interval(TimeSpan.FromSeconds(30), this.SendHeartbeat, HeartbeatLoopSource.Token);
|
||||
|
||||
// HeartbeatLoopThread = new Thread(this.HeartbeatLoop);
|
||||
// HeartbeatLoopThread.IsBackground = true;
|
||||
// HeartbeatLoopThread.Start();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Error = ex;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
isConnected = false;
|
||||
try
|
||||
{
|
||||
Client.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{ }
|
||||
|
||||
NetStream = null;
|
||||
}
|
||||
|
||||
private void ReceiveMessageLoop()
|
||||
{
|
||||
Debug.WriteLine("ReceiveMessageLoop Started!");
|
||||
try
|
||||
{
|
||||
var stableBuffer = new byte[Client.ReceiveBufferSize];
|
||||
while (this.isConnected)
|
||||
{
|
||||
|
||||
NetStream.ReadB(stableBuffer, 0, 4);
|
||||
var packetlength = BitConverter.ToInt32(stableBuffer, 0);
|
||||
packetlength = IPAddress.NetworkToHostOrder(packetlength);
|
||||
|
||||
if (packetlength < 16)
|
||||
throw new NotSupportedException("协议失败: (L:" + packetlength + ")");
|
||||
|
||||
NetStream.ReadB(stableBuffer, 0, 2);//magic
|
||||
NetStream.ReadB(stableBuffer, 0, 2);//protocol_version
|
||||
NetStream.ReadB(stableBuffer, 0, 4);
|
||||
var typeId = BitConverter.ToInt32(stableBuffer, 0);
|
||||
typeId = IPAddress.NetworkToHostOrder(typeId);
|
||||
|
||||
Console.WriteLine(typeId);
|
||||
NetStream.ReadB(stableBuffer, 0, 4);//magic, params?
|
||||
var playloadlength = packetlength - 16;
|
||||
if (playloadlength == 0)
|
||||
continue;//没有内容了
|
||||
typeId = typeId - 1;//和反编译的代码对应
|
||||
var buffer = new byte[playloadlength];
|
||||
NetStream.ReadB(buffer, 0, playloadlength);
|
||||
switch (typeId)
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
{
|
||||
var viewer = BitConverter.ToUInt32(buffer.Take(4).Reverse().ToArray(), 0); //观众人数
|
||||
ViewerCount = viewer;
|
||||
ReceivedRoomCount?.Invoke(this, new ReceivedRoomCountArgs() { UserCount = viewer });
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
case 4://playerCommand
|
||||
{
|
||||
var json = Encoding.UTF8.GetString(buffer, 0, playloadlength);
|
||||
try
|
||||
{
|
||||
ReceivedDanmaku?.Invoke(this, new ReceivedDanmakuArgs() { Danmaku = new DanmakuModel(json) });
|
||||
}
|
||||
catch (Exception)
|
||||
{ } // ignored
|
||||
break;
|
||||
}
|
||||
case 5://newScrollMessage
|
||||
case 7:
|
||||
case 16:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Error = ex;
|
||||
_disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void HeartbeatLoop()
|
||||
{
|
||||
Debug.WriteLine("HeartbeatLoop Started!");
|
||||
try
|
||||
{
|
||||
while (this.isConnected)
|
||||
{
|
||||
this.SendHeartbeat();
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
Thread.Sleep(1000);//1s
|
||||
if (!isConnected)
|
||||
{
|
||||
Debug.WriteLine("HeartbeatLoop Break");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Error = ex;
|
||||
_disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void _disconnect()
|
||||
{
|
||||
if (isConnected)
|
||||
{
|
||||
Debug.WriteLine("Disconnected");
|
||||
isConnected = false;
|
||||
Client.Close();
|
||||
NetStream = null;
|
||||
Disconnected?.Invoke(this, new DisconnectEvtArgs() { Error = Error });
|
||||
}
|
||||
}
|
||||
|
||||
private void SendHeartbeat()
|
||||
{
|
||||
SendSocketData(2);
|
||||
Debug.WriteLine("Message Sent: Heartbeat");
|
||||
}
|
||||
|
||||
private void SendSocketData(int action, string body = "")
|
||||
{
|
||||
SendSocketData(0, 16, /*protocolversion*/1, action, 1, body);
|
||||
}
|
||||
|
||||
private void SendSocketData(int packetlength, short magic, short ver, int action, int param = 1, string body = "")
|
||||
{
|
||||
var playload = Encoding.UTF8.GetBytes(body);
|
||||
if (packetlength == 0)
|
||||
{
|
||||
packetlength = playload.Length + 16;
|
||||
}
|
||||
var buffer = new byte[packetlength];
|
||||
using (var ms = new MemoryStream(buffer))
|
||||
{
|
||||
var b = BitConverter.GetBytes(buffer.Length).ToBE();
|
||||
ms.Write(b, 0, 4);
|
||||
b = BitConverter.GetBytes(magic).ToBE();
|
||||
ms.Write(b, 0, 2);
|
||||
b = BitConverter.GetBytes(ver).ToBE();
|
||||
ms.Write(b, 0, 2);
|
||||
b = BitConverter.GetBytes(action).ToBE();
|
||||
ms.Write(b, 0, 4);
|
||||
b = BitConverter.GetBytes(param).ToBE();
|
||||
ms.Write(b, 0, 4);
|
||||
if (playload.Length > 0)
|
||||
{
|
||||
ms.Write(playload, 0, playload.Length);
|
||||
}
|
||||
NetStream.Write(buffer, 0, buffer.Length);
|
||||
NetStream.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Use this for initialization
|
||||
void Awake()
|
||||
{
|
||||
isConnected = false;
|
||||
ViewerCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
31
BililiveRecorder.Core/DanmakuUtils.cs
Normal file
31
BililiveRecorder.Core/DanmakuUtils.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
internal static class DanmakuUtils
|
||||
{
|
||||
public static byte[] ToBE(this byte[] b)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian) return b.Reverse().ToArray(); else return b;
|
||||
}
|
||||
|
||||
public static void ReadB(this NetworkStream stream, byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (offset + count > buffer.Length)
|
||||
throw new ArgumentException();
|
||||
int read = 0;
|
||||
while (read < count)
|
||||
{
|
||||
var available = stream.Read(buffer, offset, count - read);
|
||||
if (available == 0)
|
||||
{
|
||||
throw new ObjectDisposedException(null);
|
||||
}
|
||||
read += available;
|
||||
offset += available;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1375
BililiveRecorder.Core/JSONObject.cs
Normal file
1375
BililiveRecorder.Core/JSONObject.cs
Normal file
File diff suppressed because it is too large
Load Diff
43
BililiveRecorder.Core/Repeat.cs
Normal file
43
BililiveRecorder.Core/Repeat.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Author: Roger Lipscombe
|
||||
* Source: https://stackoverflow.com/a/7472334
|
||||
* */
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
internal static class Repeat
|
||||
{
|
||||
public static Task Interval(
|
||||
TimeSpan pollInterval,
|
||||
Action action,
|
||||
CancellationToken token)
|
||||
{
|
||||
// We don't use Observable.Interval:
|
||||
// If we block, the values start bunching up behind each other.
|
||||
return Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
for (; ; )
|
||||
{
|
||||
if (token.WaitCancellationRequested(pollInterval))
|
||||
break;
|
||||
|
||||
action();
|
||||
}
|
||||
}, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
}
|
||||
}
|
||||
|
||||
static class CancellationTokenExtensions
|
||||
{
|
||||
public static bool WaitCancellationRequested(
|
||||
this CancellationToken token,
|
||||
TimeSpan timeout)
|
||||
{
|
||||
return token.WaitHandle.WaitOne(timeout);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user