mirror of
https://github.com/leiurayer/downkyi.git
synced 2025-04-27 05:30:47 +08:00
弹幕转字幕
This commit is contained in:
parent
8f7b9df8f9
commit
396326ab55
src/DownKyi.Core
BiliApi
BiliUtils
Danmaku
protobuf/bilibili/community/service/dm/v1
Danmaku2Ass
Bilibili.csCollision.csConfig.csCreater.csDanmaku.csDisplay.csFilter.csProducer.csStudio.csSubtitle.csUtils.cs
DownKyi.Core.csprojapp.configpackages.config@ -13,6 +13,7 @@ namespace DownKyi.Core.BiliApi.BiliUtils
|
||||
/// 番剧(电影、电视剧)md号:md28228367, MD28228367, https://www.bilibili.com/bangumi/media/md28228367 <para/>
|
||||
/// 课程ss号:https://www.bilibili.com/cheese/play/ss205 <para/>
|
||||
/// 课程ep号:https://www.bilibili.com/cheese/play/ep3489 <para/>
|
||||
/// 收藏夹:ml1329019876, ML1329019876, https://www.bilibili.com/medialist/detail/ml1329019876 <para/>
|
||||
/// 用户空间:uid928123, UID928123, uid:928123, UID:928123, https://space.bilibili.com/928123
|
||||
/// </summary>
|
||||
public static class ParseEntrance
|
||||
@ -25,6 +26,7 @@ namespace DownKyi.Core.BiliApi.BiliUtils
|
||||
public static readonly string BangumiUrl = $"{WwwUrl}/bangumi/play/";
|
||||
public static readonly string BangumiMediaUrl = $"{WwwUrl}/bangumi/media/";
|
||||
public static readonly string CheeseUrl = $"{WwwUrl}/cheese/play/";
|
||||
public static readonly string FavoritesUrl = $"{WwwUrl}/medialist/detail/";
|
||||
|
||||
#region 视频
|
||||
|
||||
@ -290,6 +292,52 @@ namespace DownKyi.Core.BiliApi.BiliUtils
|
||||
|
||||
#endregion
|
||||
|
||||
#region 收藏夹
|
||||
|
||||
/// <summary>
|
||||
/// 是否为收藏夹id
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsFavoritesId(string input)
|
||||
{
|
||||
return IsIntId(input, "ml");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为收藏夹url
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsFavoritesUrl(string input)
|
||||
{
|
||||
string favoritesId = GetId(input, FavoritesUrl);
|
||||
return IsFavoritesId(favoritesId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取收藏夹id
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static long GetFavoritesId(string input)
|
||||
{
|
||||
if (IsFavoritesId(input))
|
||||
{
|
||||
return Number.GetInt(input.Remove(0, 2));
|
||||
}
|
||||
else if (IsFavoritesUrl(input))
|
||||
{
|
||||
return Number.GetInt(GetId(input, FavoritesUrl).Remove(0, 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 用户空间
|
||||
|
||||
/// <summary>
|
||||
|
104
src/DownKyi.Core/BiliApi/Danmaku/DanmakuProtobuf.cs
Normal file
104
src/DownKyi.Core/BiliApi/Danmaku/DanmakuProtobuf.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using Bilibili.Community.Service.Dm.V1;
|
||||
using DownKyi.Core.BiliApi.Danmaku.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace DownKyi.Core.BiliApi.Danmaku
|
||||
{
|
||||
public static class DanmakuProtobuf
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 下载6分钟内的弹幕,返回弹幕列表
|
||||
/// </summary>
|
||||
/// <param name="avid">稿件avID</param>
|
||||
/// <param name="cid">视频CID</param>
|
||||
/// <param name="segmentIndex">分包,每6分钟一包</param>
|
||||
/// <returns></returns>
|
||||
public static List<BiliDanmaku> GetDanmakuProto(long avid, long cid, int segmentIndex)
|
||||
{
|
||||
string url = $"https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid={cid}&pid={avid}&segment_index={segmentIndex}";
|
||||
//string referer = "https://www.bilibili.com";
|
||||
|
||||
string directory = Path.Combine(Storage.StorageManager.GetDanmaku(), $"{cid}");
|
||||
string filePath = Path.Combine(directory, $"{segmentIndex}.proto");
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
System.Net.WebClient mywebclient = new System.Net.WebClient();
|
||||
mywebclient.DownloadFile(url, filePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Utils.Debug.Console.PrintLine("GetDanmakuProto()发生异常: {0}", e);
|
||||
//Logging.LogManager.Error(e);
|
||||
}
|
||||
|
||||
var danmakuList = new List<BiliDanmaku>();
|
||||
try
|
||||
{
|
||||
using (var input = File.OpenRead(filePath))
|
||||
{
|
||||
DmSegMobileReply danmakus = DmSegMobileReply.Parser.ParseFrom(input);
|
||||
if (danmakus == null || danmakus.Elems == null)
|
||||
{
|
||||
return danmakuList;
|
||||
}
|
||||
|
||||
foreach (var dm in danmakus.Elems)
|
||||
{
|
||||
var danmaku = new BiliDanmaku
|
||||
{
|
||||
Id = dm.Id,
|
||||
Progress = dm.Progress,
|
||||
Mode = dm.Mode,
|
||||
Fontsize = dm.Fontsize,
|
||||
Color = dm.Color,
|
||||
MidHash = dm.MidHash,
|
||||
Content = dm.Content,
|
||||
Ctime = dm.Ctime,
|
||||
Weight = dm.Weight,
|
||||
//Action = dm.Action,
|
||||
Pool = dm.Pool
|
||||
};
|
||||
danmakuList.Add(danmaku);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Utils.Debug.Console.PrintLine("GetDanmakuProto()发生异常: {0}", e);
|
||||
//Logging.LogManager.Error(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return danmakuList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载所有弹幕,返回弹幕列表
|
||||
/// </summary>
|
||||
/// <param name="avid">稿件avID</param>
|
||||
/// <param name="cid">视频CID</param>
|
||||
/// <returns></returns>
|
||||
public static List<BiliDanmaku> GetAllDanmakuProto(long avid, long cid)
|
||||
{
|
||||
var danmakuList = new List<BiliDanmaku>();
|
||||
|
||||
int segmentIndex = 0;
|
||||
while (true)
|
||||
{
|
||||
segmentIndex += 1;
|
||||
var danmakus = GetDanmakuProto(avid, cid, segmentIndex);
|
||||
if (danmakus == null) { break; }
|
||||
danmakuList.AddRange(danmakus);
|
||||
}
|
||||
return danmakuList;
|
||||
}
|
||||
}
|
||||
}
|
33
src/DownKyi.Core/BiliApi/Danmaku/Models/BiliDanmaku.cs
Normal file
33
src/DownKyi.Core/BiliApi/Danmaku/Models/BiliDanmaku.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace DownKyi.Core.BiliApi.Danmaku.Models
|
||||
{
|
||||
public class BiliDanmaku
|
||||
{
|
||||
public long Id { get; set; } //弹幕dmID
|
||||
public int Progress { get; set; } //出现时间
|
||||
public int Mode { get; set; } //弹幕类型
|
||||
public int Fontsize { get; set; } //文字大小
|
||||
public uint Color { get; set; } //弹幕颜色
|
||||
public string MidHash { get; set; } //发送者UID的HASH
|
||||
public string Content { get; set; } //弹幕内容
|
||||
public long Ctime { get; set; } //发送时间
|
||||
public int Weight { get; set; } //权重
|
||||
//public string Action { get; set; } //动作?
|
||||
public int Pool { get; set; } //弹幕池
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string separator = "\n";
|
||||
return $"id: {Id}{separator}" +
|
||||
$"progress: {Progress}{separator}" +
|
||||
$"mode: {Mode}{separator}" +
|
||||
$"fontsize: {Fontsize}{separator}" +
|
||||
$"color: {Color}{separator}" +
|
||||
$"midHash: {MidHash}{separator}" +
|
||||
$"content: {Content}{separator}" +
|
||||
$"ctime: {Ctime}{separator}" +
|
||||
$"weight: {Weight}{separator}" +
|
||||
//$"action: {Action}{separator}" +
|
||||
$"pool: {Pool}";
|
||||
}
|
||||
}
|
||||
}
|
14819
src/DownKyi.Core/BiliApi/protobuf/bilibili/community/service/dm/v1/Dm.cs
Normal file
14819
src/DownKyi.Core/BiliApi/protobuf/bilibili/community/service/dm/v1/Dm.cs
Normal file
File diff suppressed because it is too large
Load Diff
169
src/DownKyi.Core/Danmaku2Ass/Bilibili.cs
Normal file
169
src/DownKyi.Core/Danmaku2Ass/Bilibili.cs
Normal file
@ -0,0 +1,169 @@
|
||||
using DownKyi.Core.BiliApi.Danmaku;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
public class Bilibili
|
||||
{
|
||||
private static Bilibili instance;
|
||||
|
||||
private readonly Dictionary<string, bool> config = new Dictionary<string, bool>
|
||||
{
|
||||
{ "top_filter", false },
|
||||
{ "bottom_filter", false },
|
||||
{ "scroll_filter", false }
|
||||
};
|
||||
|
||||
private readonly Dictionary<int, string> mapping = new Dictionary<int, string>
|
||||
{
|
||||
{ 0, "none" }, // 保留项
|
||||
{ 1, "scroll" },
|
||||
{ 2, "scroll" },
|
||||
{ 3, "scroll" },
|
||||
{ 4, "bottom" },
|
||||
{ 5, "top" },
|
||||
{ 6, "scroll" }, // 逆向滚动弹幕,还是当滚动处理
|
||||
{ 7, "none" }, // 高级弹幕,暂时不要考虑
|
||||
{ 8, "none" }, // 代码弹幕,暂时不要考虑
|
||||
{ 9, "none" }, // BAS弹幕,暂时不要考虑
|
||||
{ 10, "none" }, // 未知,暂时不要考虑
|
||||
{ 11, "none" }, // 保留项
|
||||
{ 12, "none" }, // 保留项
|
||||
{ 13, "none" }, // 保留项
|
||||
{ 14, "none" }, // 保留项
|
||||
{ 15, "none" }, // 保留项
|
||||
};
|
||||
|
||||
// 弹幕标准字体大小
|
||||
private readonly int normalFontSize = 25;
|
||||
|
||||
/// <summary>
|
||||
/// 获取Bilibili实例
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static Bilibili GetInstance()
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = new Bilibili();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏Bilibili()方法,必须使用单例模式
|
||||
/// </summary>
|
||||
private Bilibili() { }
|
||||
|
||||
/// <summary>
|
||||
/// 是否屏蔽顶部弹幕
|
||||
/// </summary>
|
||||
/// <param name="isFilter"></param>
|
||||
/// <returns></returns>
|
||||
public Bilibili SetTopFilter(bool isFilter)
|
||||
{
|
||||
config["top_filter"] = isFilter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否屏蔽底部弹幕
|
||||
/// </summary>
|
||||
/// <param name="isFilter"></param>
|
||||
/// <returns></returns>
|
||||
public Bilibili SetBottomFilter(bool isFilter)
|
||||
{
|
||||
config["bottom_filter"] = isFilter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否屏蔽滚动弹幕
|
||||
/// </summary>
|
||||
/// <param name="isFilter"></param>
|
||||
/// <returns></returns>
|
||||
public Bilibili SetScrollFilter(bool isFilter)
|
||||
{
|
||||
config["scroll_filter"] = isFilter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Create(long avid, long cid, Config subtitleConfig, string assFile)
|
||||
{
|
||||
// 弹幕转换
|
||||
var biliDanmakus = DanmakuProtobuf.GetAllDanmakuProto(avid, cid);
|
||||
|
||||
// 按弹幕出现顺序排序
|
||||
biliDanmakus.Sort((x, y) => { return x.Progress.CompareTo(y.Progress); });
|
||||
|
||||
var danmakus = new List<Danmaku>();
|
||||
foreach (var biliDanmaku in biliDanmakus)
|
||||
{
|
||||
var danmaku = new Danmaku
|
||||
{
|
||||
// biliDanmaku.Progress单位是毫秒,所以除以1000,单位变为秒
|
||||
Start = biliDanmaku.Progress / 1000.0f,
|
||||
Style = mapping[biliDanmaku.Mode],
|
||||
Color = (int)biliDanmaku.Color,
|
||||
Commenter = biliDanmaku.MidHash,
|
||||
Content = biliDanmaku.Content,
|
||||
SizeRatio = 1.0f * biliDanmaku.Fontsize / normalFontSize
|
||||
};
|
||||
|
||||
danmakus.Add(danmaku);
|
||||
}
|
||||
|
||||
// 弹幕预处理
|
||||
Producer producer = new Producer(config, danmakus);
|
||||
producer.StartHandle();
|
||||
|
||||
// 字幕生成
|
||||
var keepedDanmakus = producer.KeepedDanmakus;
|
||||
var studio = new Studio(subtitleConfig, keepedDanmakus);
|
||||
studio.StartHandle();
|
||||
studio.CreateAssFile(assFile);
|
||||
}
|
||||
|
||||
public Dictionary<string, int> GetResolution(int quality)
|
||||
{
|
||||
var resolution = new Dictionary<string, int>
|
||||
{
|
||||
{ "width", 0 },
|
||||
{ "height", 0 }
|
||||
};
|
||||
|
||||
switch (quality)
|
||||
{
|
||||
// 240P 极速(仅mp4方式)
|
||||
case 6:
|
||||
break;
|
||||
// 360P 流畅
|
||||
case 16:
|
||||
break;
|
||||
// 480P 清晰
|
||||
case 32:
|
||||
break;
|
||||
// 720P 高清(登录)
|
||||
case 64:
|
||||
break;
|
||||
// 720P60 高清(大会员)
|
||||
case 74:
|
||||
break;
|
||||
// 1080P 高清(登录)
|
||||
case 80:
|
||||
break;
|
||||
// 1080P+ 高清(大会员)
|
||||
case 112:
|
||||
break;
|
||||
// 1080P60 高清(大会员)
|
||||
case 116:
|
||||
break;
|
||||
// 4K 超清(大会员)(需要fourk=1)
|
||||
case 120:
|
||||
break;
|
||||
}
|
||||
return resolution;
|
||||
}
|
||||
}
|
||||
}
|
60
src/DownKyi.Core/Danmaku2Ass/Collision.cs
Normal file
60
src/DownKyi.Core/Danmaku2Ass/Collision.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
/// <summary>
|
||||
/// 碰撞处理
|
||||
/// </summary>
|
||||
public class Collision
|
||||
{
|
||||
private readonly int lineCount;
|
||||
private readonly List<int> leaves;
|
||||
|
||||
public Collision(int lineCount)
|
||||
{
|
||||
this.lineCount = lineCount;
|
||||
leaves = Leaves();
|
||||
}
|
||||
|
||||
private List<int> Leaves()
|
||||
{
|
||||
var ret = new List<int>(lineCount);
|
||||
for (int i = 0; i < lineCount; i++) ret.Add(0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 碰撞检测
|
||||
/// 返回行号和时间偏移
|
||||
/// </summary>
|
||||
/// <param name="display"></param>
|
||||
/// <returns></returns>
|
||||
public Tuple<int, float> Detect(Display display)
|
||||
{
|
||||
List<float> beyonds = new List<float>();
|
||||
for (int i = 0; i < leaves.Count; i++)
|
||||
{
|
||||
float beyond = display.Danmaku.Start - leaves[i];
|
||||
// 某一行有足够空间,直接返回行号和 0 偏移
|
||||
if (beyond >= 0)
|
||||
{
|
||||
return Tuple.Create(i, 0f);
|
||||
}
|
||||
beyonds.Add(beyond);
|
||||
}
|
||||
|
||||
// 所有行都没有空间了,那么找出哪一行能在最短时间内让出空间
|
||||
float soon = beyonds.Max();
|
||||
int lineIndex = beyonds.IndexOf(soon);
|
||||
float offset = -soon;
|
||||
return Tuple.Create(lineIndex, offset);
|
||||
}
|
||||
|
||||
public void Update(float leave, int lineIndex, float offset)
|
||||
{
|
||||
leaves[lineIndex] = Utils.IntCeiling(leave + offset);
|
||||
}
|
||||
}
|
||||
}
|
57
src/DownKyi.Core/Danmaku2Ass/Config.cs
Normal file
57
src/DownKyi.Core/Danmaku2Ass/Config.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
public class Config
|
||||
{
|
||||
public string Title = "Downkyi";
|
||||
public int ScreenWidth = 1920;
|
||||
public int ScreenHeight = 1080;
|
||||
public string FontName = "黑体";
|
||||
public int BaseFontSize; // 字体大小,像素
|
||||
|
||||
// 限制行数
|
||||
private int lineCount;
|
||||
public int LineCount
|
||||
{
|
||||
get { return lineCount; }
|
||||
set
|
||||
{
|
||||
if (value == 0)
|
||||
{
|
||||
lineCount = (int)Math.Floor(ScreenHeight / BaseFontSize * 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
lineCount = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LayoutAlgorithm; // 布局算法,async/sync
|
||||
public int TuneDuration; // 微调时长
|
||||
public int DropOffset; // 丢弃偏移
|
||||
public int BottomMargin; // 底部边距
|
||||
public int CustomOffset; // 自定义偏移
|
||||
public string HeaderTemplate = @"[Script Info]
|
||||
; Script generated by Downkyi Danmaku Converter
|
||||
; https://github.com/FlySelfLog/downkyi
|
||||
Title: {title}
|
||||
ScriptType: v4.00+
|
||||
Collisions: Normal
|
||||
PlayResX: {width}
|
||||
PlayResY: {height}
|
||||
Timer: 10.0000
|
||||
WrapStyle: 2
|
||||
ScaledBorderAndShadow: no
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,{fontname},54,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0.00,0.00,1,2.00,0.00,2,30,30,120,0
|
||||
Style: Alternate,{fontname},36,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0.00,0.00,1,2.00,0.00,2,30,30,84,0
|
||||
Style: Danmaku,{fontname},{fontsize},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0.00,0.00,1,1.00,0.00,2,30,30,30,0
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text";
|
||||
}
|
||||
}
|
87
src/DownKyi.Core/Danmaku2Ass/Creater.cs
Normal file
87
src/DownKyi.Core/Danmaku2Ass/Creater.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建器
|
||||
/// </summary>
|
||||
public class Creater
|
||||
{
|
||||
public Config Config;
|
||||
public List<Danmaku> Danmakus;
|
||||
public List<Subtitle> Subtitles;
|
||||
public string Text;
|
||||
|
||||
public Creater(Config config, List<Danmaku> danmakus)
|
||||
{
|
||||
Config = config;
|
||||
Danmakus = danmakus;
|
||||
Subtitles = SetSubtitles();
|
||||
Text = SetText();
|
||||
}
|
||||
|
||||
protected List<Subtitle> SetSubtitles()
|
||||
{
|
||||
var scroll = new Collision(Config.LineCount);
|
||||
var stayed = new Collision(Config.LineCount);
|
||||
Dictionary<string, Collision> collisions = new Dictionary<string, Collision>
|
||||
{
|
||||
{ "scroll", scroll },
|
||||
{ "top", stayed },
|
||||
{ "bottom", stayed }
|
||||
};
|
||||
|
||||
List<Subtitle> subtitles = new List<Subtitle>();
|
||||
foreach (var danmaku in Danmakus)
|
||||
{
|
||||
// 丢弃不支持的
|
||||
if (danmaku.Style == "none")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建显示方式对象
|
||||
var display = Display.Factory(Config, danmaku);
|
||||
var collision = collisions[danmaku.Style];
|
||||
var detect = collision.Detect(display);
|
||||
int lineIndex = detect.Item1;
|
||||
float waitingOffset = detect.Item2;
|
||||
|
||||
// 超过容忍的偏移量,丢弃掉此条弹幕
|
||||
if (waitingOffset > Config.DropOffset)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 接受偏移,更新碰撞信息
|
||||
display.Relayout(lineIndex);
|
||||
collision.Update(display.Leave, lineIndex, waitingOffset);
|
||||
|
||||
// 再加上自定义偏移
|
||||
float offset = waitingOffset + Config.CustomOffset;
|
||||
Subtitle subtitle = new Subtitle(danmaku, display, offset);
|
||||
|
||||
subtitles.Add(subtitle);
|
||||
}
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
protected string SetText()
|
||||
{
|
||||
string header = Config.HeaderTemplate
|
||||
.Replace("{title}", Config.Title)
|
||||
.Replace("{width}", Config.ScreenWidth.ToString())
|
||||
.Replace("{height}", Config.ScreenHeight.ToString())
|
||||
.Replace("{fontname}", Config.FontName)
|
||||
.Replace("{fontsize}", Config.BaseFontSize.ToString());
|
||||
|
||||
string events = string.Empty;
|
||||
foreach (var subtitle in Subtitles)
|
||||
{
|
||||
events += "\n" + subtitle.Text;
|
||||
}
|
||||
|
||||
return header + events;
|
||||
}
|
||||
}
|
||||
}
|
12
src/DownKyi.Core/Danmaku2Ass/Danmaku.cs
Normal file
12
src/DownKyi.Core/Danmaku2Ass/Danmaku.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
public class Danmaku
|
||||
{
|
||||
public float Start { get; set; }
|
||||
public string Style { get; set; }
|
||||
public int Color { get; set; }
|
||||
public string Commenter { get; set; }
|
||||
public string Content { get; set; }
|
||||
public float SizeRatio { get; set; }
|
||||
}
|
||||
}
|
406
src/DownKyi.Core/Danmaku2Ass/Display.cs
Normal file
406
src/DownKyi.Core/Danmaku2Ass/Display.cs
Normal file
@ -0,0 +1,406 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示方式
|
||||
/// </summary>
|
||||
public class Display
|
||||
{
|
||||
public Config Config;
|
||||
public Danmaku Danmaku;
|
||||
public int LineIndex;
|
||||
|
||||
public int FontSize;
|
||||
public bool IsScaled;
|
||||
public int MaxLength;
|
||||
public int Width;
|
||||
public int Height;
|
||||
|
||||
public Tuple<int, int> Horizontal;
|
||||
public Tuple<int, int> Vertical;
|
||||
|
||||
public int Duration;
|
||||
public int Leave;
|
||||
|
||||
protected Display() { }
|
||||
|
||||
public Display(Config config, Danmaku danmaku)
|
||||
{
|
||||
Config = config;
|
||||
Danmaku = danmaku;
|
||||
LineIndex = 0;
|
||||
|
||||
IsScaled = SetIsScaled();
|
||||
FontSize = SetFontSize();
|
||||
MaxLength = SetMaxLength();
|
||||
Width = SetWidth();
|
||||
Height = SetHeight();
|
||||
|
||||
Horizontal = SetHorizontal();
|
||||
Vertical = SetVertical();
|
||||
|
||||
Duration = SetDuration();
|
||||
Leave = SetLeave();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据弹幕样式自动创建对应的 Display 类
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static Display Factory(Config config, Danmaku danmaku)
|
||||
{
|
||||
Dictionary<string, Display> dict = new Dictionary<string, Display>
|
||||
{
|
||||
{ "scroll", new ScrollDisplay(config, danmaku) },
|
||||
{ "top", new TopDisplay(config, danmaku) },
|
||||
{ "bottom", new BottomDisplay(config, danmaku) }
|
||||
};
|
||||
return dict[danmaku.Style];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字体大小
|
||||
/// 按用户自定义的字体大小来缩放
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int SetFontSize()
|
||||
{
|
||||
if (IsScaled)
|
||||
{
|
||||
Console.WriteLine($"{Danmaku.SizeRatio}");
|
||||
}
|
||||
return Utils.IntCeiling(Config.BaseFontSize * Danmaku.SizeRatio);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字体是否被缩放过
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected bool SetIsScaled()
|
||||
{
|
||||
return !Math.Round(Danmaku.SizeRatio, 2).Equals(1.0);
|
||||
//return Danmaku.SizeRatio.Equals(1.0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最长的行字符数
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int SetMaxLength()
|
||||
{
|
||||
string[] lines = Danmaku.Content.Split('\n');
|
||||
int maxLength = 0;
|
||||
foreach (string line in lines)
|
||||
{
|
||||
int length = Utils.DisplayLength(line);
|
||||
if (maxLength < length)
|
||||
{
|
||||
maxLength = length;
|
||||
}
|
||||
}
|
||||
return maxLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 整条字幕宽度
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int SetWidth()
|
||||
{
|
||||
float charCount = MaxLength;// / 2;
|
||||
return Utils.IntCeiling(FontSize * charCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 整条字幕高度
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int SetHeight()
|
||||
{
|
||||
int lineCount = Danmaku.Content.Split('\n').Length;
|
||||
return lineCount * FontSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 出现和消失的水平坐标位置
|
||||
/// 默认在屏幕中间
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Tuple<int, int> SetHorizontal()
|
||||
{
|
||||
int x = (int)Math.Floor(Config.ScreenWidth / 2.0);
|
||||
return Tuple.Create(x, x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 出现和消失的垂直坐标位置
|
||||
/// 默认在屏幕中间
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Tuple<int, int> SetVertical()
|
||||
{
|
||||
int y = (int)Math.Floor(Config.ScreenHeight / 2.0);
|
||||
return Tuple.Create(y, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 整条字幕的显示时间
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual int SetDuration()
|
||||
{
|
||||
int baseDuration = 3 + Config.TuneDuration;
|
||||
if (baseDuration <= 0)
|
||||
{
|
||||
baseDuration = 0;
|
||||
}
|
||||
float charCount = MaxLength / 2;
|
||||
|
||||
int value;
|
||||
if (charCount < 6)
|
||||
{
|
||||
value = baseDuration + 1;
|
||||
}
|
||||
else if (charCount < 12)
|
||||
{
|
||||
value = baseDuration + 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = baseDuration + 3;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 离开碰撞时间
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual int SetLeave()
|
||||
{
|
||||
return (int)(Danmaku.Start + Duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按照新的行号重新布局
|
||||
/// </summary>
|
||||
/// <param name="lineIndex"></param>
|
||||
public void Relayout(int lineIndex)
|
||||
{
|
||||
LineIndex = lineIndex;
|
||||
Horizontal = SetHorizontal();
|
||||
Vertical = SetVertical();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 顶部
|
||||
/// </summary>
|
||||
public class TopDisplay : Display
|
||||
{
|
||||
public TopDisplay(Config config, Danmaku danmaku) : base(config, danmaku)
|
||||
{
|
||||
Console.WriteLine("TopDisplay constructor.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override Tuple<int, int> SetVertical()
|
||||
{
|
||||
// 这里 y 坐标为 0 就是最顶行了
|
||||
int y = LineIndex * Config.BaseFontSize;
|
||||
return Tuple.Create(y, y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 底部
|
||||
/// </summary>
|
||||
public class BottomDisplay : Display
|
||||
{
|
||||
public BottomDisplay(Config config, Danmaku danmaku) : base(config, danmaku)
|
||||
{
|
||||
Console.WriteLine("BottomDisplay constructor.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override Tuple<int, int> SetVertical()
|
||||
{
|
||||
// 要让字幕不超出底部,减去高度
|
||||
int y = Config.ScreenHeight - (LineIndex * Config.BaseFontSize) - Height;
|
||||
// 再减去自定义的底部边距
|
||||
y -= Config.BottomMargin;
|
||||
return Tuple.Create(y, y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 滚动
|
||||
/// </summary>
|
||||
public class ScrollDisplay : Display
|
||||
{
|
||||
public int Distance;
|
||||
public int Speed;
|
||||
|
||||
public ScrollDisplay(Config config, Danmaku danmaku) : base()
|
||||
{
|
||||
Console.WriteLine("ScrollDisplay constructor.");
|
||||
|
||||
Config = config;
|
||||
Danmaku = danmaku;
|
||||
LineIndex = 0;
|
||||
|
||||
IsScaled = SetIsScaled();
|
||||
FontSize = SetFontSize();
|
||||
MaxLength = SetMaxLength();
|
||||
Width = SetWidth();
|
||||
Height = SetHeight();
|
||||
|
||||
Horizontal = SetHorizontal();
|
||||
Vertical = SetVertical();
|
||||
|
||||
Distance = SetDistance();
|
||||
Speed = SetSpeed();
|
||||
|
||||
Duration = SetDuration();
|
||||
Leave = SetLeave();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ASS 的水平位置参考点是整条字幕文本的中点
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override Tuple<int, int> SetHorizontal()
|
||||
{
|
||||
int x1 = Config.ScreenWidth + (int)Math.Floor(Width / 2.0);
|
||||
int x2 = 0 - (int)Math.Floor(Width / 2.0);
|
||||
return Tuple.Create(x1, x2);
|
||||
}
|
||||
|
||||
protected override Tuple<int, int> SetVertical()
|
||||
{
|
||||
int baseFontSize = Config.BaseFontSize;
|
||||
|
||||
// 垂直位置,按基准字体大小算每一行的高度
|
||||
int y = (LineIndex + 1) * baseFontSize;
|
||||
|
||||
// 个别弹幕可能字体比基准要大,所以最上的一行还要避免挤出顶部屏幕
|
||||
// 坐标不能小于字体大小
|
||||
if (y < FontSize)
|
||||
{
|
||||
y = FontSize;
|
||||
}
|
||||
return Tuple.Create(y, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字幕坐标点的移动距离
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int SetDistance()
|
||||
{
|
||||
Tuple<int, int> x = Horizontal;
|
||||
return x.Item1 - x.Item2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字幕每个字的移动的速度
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int SetSpeed()
|
||||
{
|
||||
// 基准时间,就是每个字的移动时间
|
||||
// 12 秒加上用户自定义的微调
|
||||
int baseDuration = 12 + Config.TuneDuration;
|
||||
if (baseDuration <= 0)
|
||||
{
|
||||
baseDuration = 1;
|
||||
}
|
||||
return Utils.IntCeiling(Config.ScreenWidth / baseDuration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算每条弹幕的显示时长,同步方式
|
||||
/// 每个弹幕的滚动速度都一样,辨认度好,适合观看剧集类视频。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public int SyncDuration()
|
||||
{
|
||||
return Distance / Speed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算每条弹幕的显示时长,异步方式
|
||||
/// 每个弹幕的滚动速度都不一样,动态调整,辨认度低,适合观看 MTV 类视频。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public int AsyncDuration()
|
||||
{
|
||||
int baseDuration = 6 + Config.TuneDuration;
|
||||
if (baseDuration <= 0)
|
||||
{
|
||||
baseDuration = 0;
|
||||
}
|
||||
float charCount = MaxLength / 2;
|
||||
|
||||
int value;
|
||||
if (charCount < 6)
|
||||
{
|
||||
value = (int)(baseDuration + charCount);
|
||||
}
|
||||
else if (charCount < 12)
|
||||
{
|
||||
value = baseDuration + (int)(charCount / 2);
|
||||
}
|
||||
else if (charCount < 24)
|
||||
{
|
||||
value = baseDuration + (int)(charCount / 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = baseDuration + 10;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 整条字幕的移动时间
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override int SetDuration()
|
||||
{
|
||||
string methodName = Config.LayoutAlgorithm.Substring(0, 1).ToUpper() + Config.LayoutAlgorithm.Substring(1);
|
||||
methodName += "Duration";
|
||||
MethodInfo method = typeof(ScrollDisplay).GetMethod(methodName);
|
||||
if (method != null)
|
||||
{
|
||||
return (int)method.Invoke(this, null);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 离开碰撞时间
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override int SetLeave()
|
||||
{
|
||||
// 对于滚动样式弹幕来说,就是最后一个字符离开最右边缘的时间
|
||||
// 坐标是字幕中点,在屏幕外和内各有半个字幕宽度
|
||||
// 也就是跑过一个字幕宽度的路程
|
||||
float duration = Width / Speed;
|
||||
return (int)(Danmaku.Start + duration);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
88
src/DownKyi.Core/Danmaku2Ass/Filter.cs
Normal file
88
src/DownKyi.Core/Danmaku2Ass/Filter.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
/// <summary>
|
||||
/// 过滤器基类
|
||||
/// </summary>
|
||||
public class Filter
|
||||
{
|
||||
public virtual List<Danmaku> DoFilter(List<Danmaku> danmakus)
|
||||
{
|
||||
throw new NotImplementedException("使用了过滤器的未实现的方法。");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 顶部样式过滤器
|
||||
/// </summary>
|
||||
public class TopFilter : Filter
|
||||
{
|
||||
public override List<Danmaku> DoFilter(List<Danmaku> danmakus)
|
||||
{
|
||||
List<Danmaku> keep = new List<Danmaku>();
|
||||
foreach (var danmaku in danmakus)
|
||||
{
|
||||
if (danmaku.Style == "top")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
keep.Add(danmaku);
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 底部样式过滤器
|
||||
/// </summary>
|
||||
public class BottomFilter : Filter
|
||||
{
|
||||
public override List<Danmaku> DoFilter(List<Danmaku> danmakus)
|
||||
{
|
||||
List<Danmaku> keep = new List<Danmaku>();
|
||||
foreach (var danmaku in danmakus)
|
||||
{
|
||||
if (danmaku.Style == "bottom")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
keep.Add(danmaku);
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 滚动样式过滤器
|
||||
/// </summary>
|
||||
public class ScrollFilter : Filter
|
||||
{
|
||||
public override List<Danmaku> DoFilter(List<Danmaku> danmakus)
|
||||
{
|
||||
List<Danmaku> keep = new List<Danmaku>();
|
||||
foreach (var danmaku in danmakus)
|
||||
{
|
||||
if (danmaku.Style == "scroll")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
keep.Add(danmaku);
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自定义过滤器
|
||||
/// </summary>
|
||||
public class CustomFilter : Filter
|
||||
{
|
||||
public override List<Danmaku> DoFilter(List<Danmaku> danmakus)
|
||||
{
|
||||
// TODO
|
||||
return base.DoFilter(danmakus);
|
||||
}
|
||||
}
|
||||
}
|
104
src/DownKyi.Core/Danmaku2Ass/Producer.cs
Normal file
104
src/DownKyi.Core/Danmaku2Ass/Producer.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
public class Producer
|
||||
{
|
||||
public Dictionary<string, bool> Config;
|
||||
public Dictionary<string, Filter> Filters;
|
||||
public List<Danmaku> Danmakus;
|
||||
public List<Danmaku> KeepedDanmakus;
|
||||
public Dictionary<string, int> FilterDetail;
|
||||
|
||||
public Producer(Dictionary<string, bool> config, List<Danmaku> danmakus)
|
||||
{
|
||||
Config = config;
|
||||
Danmakus = danmakus;
|
||||
}
|
||||
|
||||
public void StartHandle()
|
||||
{
|
||||
LoadFilter();
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
public void LoadFilter()
|
||||
{
|
||||
Filters = new Dictionary<string, Filter>();
|
||||
if (Config["top_filter"])
|
||||
{
|
||||
Filters.Add("top_filter", new TopFilter());
|
||||
}
|
||||
if (Config["bottom_filter"])
|
||||
{
|
||||
Filters.Add("bottom_filter", new BottomFilter());
|
||||
}
|
||||
if (Config["scroll_filter"])
|
||||
{
|
||||
Filters.Add("scroll_filter", new ScrollFilter());
|
||||
}
|
||||
//if (Config["custom_filter"])
|
||||
//{
|
||||
// Filters.Add("custom_filter", new CustomFilter());
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
public void ApplyFilter()
|
||||
{
|
||||
Dictionary<string, int> filterDetail = new Dictionary<string, int>() {
|
||||
{ "top_filter",0},
|
||||
{ "bottom_filter",0},
|
||||
{ "scroll_filter",0},
|
||||
//{ "custom_filter",0}
|
||||
};
|
||||
|
||||
List<Danmaku> danmakus = Danmakus;
|
||||
//string[] orders = { "top_filter", "bottom_filter", "scroll_filter", "custom_filter" };
|
||||
string[] orders = { "top_filter", "bottom_filter", "scroll_filter" };
|
||||
foreach (var name in orders)
|
||||
{
|
||||
Filter filter;
|
||||
try
|
||||
{
|
||||
filter = Filters[name];
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("ApplyFilter()发生异常: {0}", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
int count = danmakus.Count;
|
||||
danmakus = filter.DoFilter(danmakus);
|
||||
filterDetail[name] = count - danmakus.Count;
|
||||
}
|
||||
|
||||
KeepedDanmakus = danmakus;
|
||||
FilterDetail = filterDetail;
|
||||
}
|
||||
|
||||
public Dictionary<string, int> Report()
|
||||
{
|
||||
int blockedCount = 0;
|
||||
foreach (int count in FilterDetail.Values)
|
||||
{
|
||||
blockedCount += count;
|
||||
}
|
||||
|
||||
int passedCount = KeepedDanmakus.Count;
|
||||
int totalCount = blockedCount + passedCount;
|
||||
|
||||
Dictionary<string, int> ret = new Dictionary<string, int>
|
||||
{
|
||||
{ "blocked", blockedCount },
|
||||
{ "passed", passedCount },
|
||||
{ "total", totalCount }
|
||||
};
|
||||
|
||||
return (Dictionary<string, int>)ret.Concat(FilterDetail);
|
||||
}
|
||||
}
|
||||
}
|
83
src/DownKyi.Core/Danmaku2Ass/Studio.cs
Normal file
83
src/DownKyi.Core/Danmaku2Ass/Studio.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
/// <summary>
|
||||
/// 字幕工程类
|
||||
/// </summary>
|
||||
public class Studio
|
||||
{
|
||||
public Config Config;
|
||||
public List<Danmaku> Danmakus;
|
||||
|
||||
public Creater Creater;
|
||||
public int KeepedCount;
|
||||
public int DropedCount;
|
||||
|
||||
public Studio(Config config, List<Danmaku> danmakus)
|
||||
{
|
||||
Config = config;
|
||||
Danmakus = danmakus;
|
||||
}
|
||||
|
||||
public void StartHandle()
|
||||
{
|
||||
Creater = SetCreater();
|
||||
KeepedCount = SetKeepedCount();
|
||||
DropedCount = SetDropedCount();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ass 创建器
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected Creater SetCreater()
|
||||
{
|
||||
return new Creater(Config, Danmakus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保留条数
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int SetKeepedCount()
|
||||
{
|
||||
return Creater.Subtitles.Count();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 丢弃条数
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int SetDropedCount()
|
||||
{
|
||||
return Danmakus.Count - KeepedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 ass 字幕
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
public void CreateAssFile(string fileName)
|
||||
{
|
||||
CreateFile(fileName, Creater.Text);
|
||||
}
|
||||
|
||||
public void CreateFile(string fileName, string text)
|
||||
{
|
||||
File.WriteAllText(fileName, text);
|
||||
}
|
||||
|
||||
public Dictionary<string, int> Report()
|
||||
{
|
||||
return new Dictionary<string, int>()
|
||||
{
|
||||
{"total", Danmakus.Count},
|
||||
{"droped", DropedCount},
|
||||
{"keeped", KeepedCount},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
154
src/DownKyi.Core/Danmaku2Ass/Subtitle.cs
Normal file
154
src/DownKyi.Core/Danmaku2Ass/Subtitle.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
/// <summary>
|
||||
/// 字幕
|
||||
/// </summary>
|
||||
public class Subtitle
|
||||
{
|
||||
public Danmaku Danmaku;
|
||||
public Display Display;
|
||||
public float Offset;
|
||||
|
||||
public float Start;
|
||||
public float End;
|
||||
public string Color;
|
||||
public Dictionary<string, int> Position;
|
||||
public string StartMarkup;
|
||||
public string EndMarkup;
|
||||
public string ColorMarkup;
|
||||
public string BorderMarkup;
|
||||
public string FontSizeMarkup;
|
||||
public string StyleMarkup;
|
||||
public string LayerMarkup;
|
||||
public string ContentMarkup;
|
||||
public string Text;
|
||||
|
||||
public Subtitle(Danmaku danmaku, Display display, float offset = 0)
|
||||
{
|
||||
Danmaku = danmaku;
|
||||
Display = display;
|
||||
Offset = offset;
|
||||
|
||||
Start = SetStart();
|
||||
End = SetEnd();
|
||||
Color = SetColor();
|
||||
Position = SetPosition();
|
||||
StartMarkup = SetStartMarkup();
|
||||
EndMarkup = SetEndMarkup();
|
||||
ColorMarkup = SetColorMarkup();
|
||||
BorderMarkup = SetBorderMarkup();
|
||||
FontSizeMarkup = SetFontSizeMarkup();
|
||||
StyleMarkup = SetStyleMarkup();
|
||||
LayerMarkup = SetLayerMarkup();
|
||||
ContentMarkup = SetContentMarkup();
|
||||
Text = SetText();
|
||||
}
|
||||
|
||||
protected float SetStart()
|
||||
{
|
||||
return Danmaku.Start + Offset;
|
||||
}
|
||||
|
||||
protected float SetEnd()
|
||||
{
|
||||
return Start + Display.Duration;
|
||||
}
|
||||
|
||||
protected string SetColor()
|
||||
{
|
||||
return Utils.Int2bgr(Danmaku.Color);
|
||||
}
|
||||
|
||||
protected Dictionary<string, int> SetPosition()
|
||||
{
|
||||
Tuple<int, int> x = Display.Horizontal;
|
||||
Tuple<int, int> y = Display.Vertical;
|
||||
|
||||
Dictionary<string, int> value = new Dictionary<string, int>
|
||||
{
|
||||
{ "x1", x.Item1 },
|
||||
{ "x2", x.Item2 },
|
||||
{ "y1", y.Item1 },
|
||||
{ "y2", y.Item2 }
|
||||
};
|
||||
return value;
|
||||
}
|
||||
|
||||
protected string SetStartMarkup()
|
||||
{
|
||||
return Utils.Second2hms(Start);
|
||||
}
|
||||
|
||||
protected string SetEndMarkup()
|
||||
{
|
||||
return Utils.Second2hms(End);
|
||||
}
|
||||
|
||||
protected string SetColorMarkup()
|
||||
{
|
||||
// 白色不需要加特别标记
|
||||
if (Color == "FFFFFF")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return "\\c&H" + Color;
|
||||
}
|
||||
|
||||
protected string SetBorderMarkup()
|
||||
{
|
||||
// 暗色加个亮色边框,方便阅读
|
||||
if (Utils.IsDark(Danmaku.Color))
|
||||
{
|
||||
//return "\\3c&HFFFFFF";
|
||||
return "\\3c&H000000";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "\\3c&H000000";
|
||||
}
|
||||
//return "";
|
||||
}
|
||||
|
||||
protected string SetFontSizeMarkup()
|
||||
{
|
||||
if (Display.IsScaled)
|
||||
{
|
||||
return $"\\fs{Display.FontSize}";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
protected string SetStyleMarkup()
|
||||
{
|
||||
if (Danmaku.Style == "scroll")
|
||||
{
|
||||
return $"\\move({Position["x1"]}, {Position["y1"]}, {Position["x2"]}, {Position["y2"]})";
|
||||
}
|
||||
return $"\\a6\\pos({Position["x1"]}, {Position["y1"]})";
|
||||
}
|
||||
|
||||
protected string SetLayerMarkup()
|
||||
{
|
||||
if (Danmaku.Style != "scroll")
|
||||
{
|
||||
return "-2";
|
||||
}
|
||||
return "-1";
|
||||
}
|
||||
|
||||
protected string SetContentMarkup()
|
||||
{
|
||||
string markup = StyleMarkup + ColorMarkup + BorderMarkup + FontSizeMarkup;
|
||||
string content = Utils.CorrectTypos(Danmaku.Content);
|
||||
return $"{{{markup}}}{content}";
|
||||
}
|
||||
|
||||
protected string SetText()
|
||||
{
|
||||
return $"Dialogue: {LayerMarkup},{StartMarkup},{EndMarkup},Danmaku,,0000,0000,0000,,{ContentMarkup}";
|
||||
}
|
||||
}
|
||||
}
|
227
src/DownKyi.Core/Danmaku2Ass/Utils.cs
Normal file
227
src/DownKyi.Core/Danmaku2Ass/Utils.cs
Normal file
@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace DownKyi.Core.Danmaku2Ass
|
||||
{
|
||||
internal static class Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 向上取整,返回int类型
|
||||
/// </summary>
|
||||
/// <param name="number"></param>
|
||||
/// <returns></returns>
|
||||
public static int IntCeiling(float number)
|
||||
{
|
||||
return (int)Math.Ceiling(number);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字符长度,1个汉字当2个英文
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
public static int DisplayLength(string text)
|
||||
{
|
||||
return Encoding.Default.GetBytes(text).Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修正一些评论者的拼写错误
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
public static string CorrectTypos(string text)
|
||||
{
|
||||
text = text.Replace("/n", "\\N");
|
||||
text = text.Replace(">", ">");
|
||||
text = text.Replace("<", "<");
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 秒数转 时:分:秒 格式
|
||||
/// </summary>
|
||||
/// <param name="seconds"></param>
|
||||
/// <returns></returns>
|
||||
public static string Second2hms(float seconds)
|
||||
{
|
||||
if (seconds < 0)
|
||||
{
|
||||
return "0:00:00.00";
|
||||
}
|
||||
|
||||
int i = (int)Math.Floor(seconds / 1.0);
|
||||
int dec = (int)(Math.Round(seconds % 1.0f, 2) * 100);
|
||||
if (dec >= 100)
|
||||
{
|
||||
dec = 99;
|
||||
}
|
||||
|
||||
int min = (int)Math.Floor(i / 60.0);
|
||||
int second = (int)(i % 60.0f);
|
||||
|
||||
int hour = (int)Math.Floor(min / 60.0);
|
||||
|
||||
return $"{hour:D}:{min:D2}:{second:D2}.{dec:D2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 时:分:秒 格式转 秒数
|
||||
/// </summary>
|
||||
/// <param name="hms"></param>
|
||||
/// <returns></returns>
|
||||
public static float Hms2second(string hms)
|
||||
{
|
||||
string[] numbers = hms.Split(':');
|
||||
float seconds = 0;
|
||||
|
||||
for (int i = 0; i < numbers.Length; i++)
|
||||
{
|
||||
seconds += (float)(float.Parse(numbers[numbers.Length - i - 1]) * Math.Pow(60, i));
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同Hms2second(string hms),不过可以用 +/- 符号来连接多个
|
||||
/// 即 3:00-2:30 相当于 30 秒
|
||||
/// </summary>
|
||||
/// <param name="xhms"></param>
|
||||
/// <returns></returns>
|
||||
public static float Xhms2second(string xhms)
|
||||
{
|
||||
string[] args = xhms.Replace("+", " +").Replace("-", " -").Split(' ');
|
||||
float result = 0;
|
||||
foreach (string hms in args)
|
||||
{
|
||||
result += Hms2second(hms);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 颜色值,整型转 RGB
|
||||
/// </summary>
|
||||
/// <param name="integer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Int2rgb(int integer)
|
||||
{
|
||||
return integer.ToString("X").PadLeft(6, '0'); ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 颜色值,整型转 BGR
|
||||
/// </summary>
|
||||
/// <param name="integer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Int2bgr(int integer)
|
||||
{
|
||||
string rgb = Int2rgb(integer);
|
||||
string bgr = rgb.Substring(4, 2) + rgb.Substring(2, 2) + rgb.Substring(0, 2);
|
||||
return bgr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 颜色值,整型转 HLS
|
||||
/// </summary>
|
||||
/// <param name="integer"></param>
|
||||
/// <returns></returns>
|
||||
public static float[] Int2hls(int integer)
|
||||
{
|
||||
string rgb = Int2rgb(integer);
|
||||
int[] rgb_decimals = { 0, 0, 0 };
|
||||
rgb_decimals[0] = int.Parse(rgb.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
|
||||
rgb_decimals[1] = int.Parse(rgb.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
|
||||
rgb_decimals[2] = int.Parse(rgb.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
|
||||
|
||||
int[] rgb_coordinates = { 0, 0, 0 };
|
||||
rgb_coordinates[0] = (int)Math.Floor(rgb_decimals[0] / 255.0);
|
||||
rgb_coordinates[1] = (int)Math.Floor(rgb_decimals[1] / 255.0);
|
||||
rgb_coordinates[2] = (int)Math.Floor(rgb_decimals[2] / 255.0);
|
||||
float[] hls_corrdinates = Rgb2hls(rgb_coordinates);
|
||||
|
||||
float[] hls = { 0, 0, 0 };
|
||||
hls[0] = hls_corrdinates[0] * 360;
|
||||
hls[1] = hls_corrdinates[1] * 100;
|
||||
hls[2] = hls_corrdinates[2] * 100;
|
||||
return hls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HLS: Hue, Luminance, Saturation
|
||||
/// H: position in the spectrum
|
||||
/// L: color lightness
|
||||
/// S: color saturation
|
||||
/// </summary>
|
||||
/// <param name="rgb"></param>
|
||||
/// <returns></returns>
|
||||
private static float[] Rgb2hls(int[] rgb)
|
||||
{
|
||||
float[] hls = { 0, 0, 0 };
|
||||
int maxc = rgb.Max();
|
||||
int minc = rgb.Min();
|
||||
hls[1] = (minc + maxc) / 2.0f;
|
||||
if (minc == maxc)
|
||||
{
|
||||
return hls;
|
||||
}
|
||||
|
||||
if (hls[1] <= 0.5)
|
||||
{
|
||||
hls[2] = (maxc - minc) / (maxc + minc);
|
||||
}
|
||||
else
|
||||
{
|
||||
hls[2] = (maxc - minc) / (2.0f - maxc - minc);
|
||||
}
|
||||
float rc = (maxc - rgb[0]) / (maxc - minc);
|
||||
float gc = (maxc - rgb[1]) / (maxc - minc);
|
||||
float bc = (maxc - rgb[2]) / (maxc - minc);
|
||||
if (rgb[0] == maxc)
|
||||
{
|
||||
hls[0] = bc - gc;
|
||||
}
|
||||
else if (rgb[1] == maxc)
|
||||
{
|
||||
hls[0] = 2.0f + rc - bc;
|
||||
}
|
||||
else
|
||||
{
|
||||
hls[0] = 4.0f + gc - rc;
|
||||
}
|
||||
hls[0] = (hls[0] / 6.0f) % 1.0f;
|
||||
return hls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否属于暗色
|
||||
/// </summary>
|
||||
/// <param name="integer"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsDark(int integer)
|
||||
{
|
||||
if (integer == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
float[] hls = Int2hls(integer);
|
||||
float hue = hls[0];
|
||||
float lightness = hls[1];
|
||||
|
||||
// HSL 色轮见
|
||||
// http://zh.wikipedia.org/zh-cn/HSL和HSV色彩空间
|
||||
// 以下的数值都是我的主观判断认为是暗色
|
||||
if ((hue > 30 && hue < 210) && lightness < 33)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if ((hue < 30 || hue > 210) && lightness < 66)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -36,6 +36,9 @@
|
||||
<Reference Include="Brotli.Core, Version=2.1.1.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Brotli.NET.2.1.1\lib\net45\Brotli.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Google.Protobuf, Version=3.18.1.0, Culture=neutral, PublicKeyToken=a7d26565bac4d604, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Google.Protobuf.3.18.1\lib\net45\Google.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
@ -45,13 +48,25 @@
|
||||
<HintPath>..\packages\QRCoder.1.4.1\lib\net40\QRCoder.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data.SQLite, Version=1.0.112.1, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Data.SQLite.Core.1.0.112.2\lib\net40\System.Data.SQLite.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Management" />
|
||||
<Reference Include="System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Numerics" />
|
||||
<Reference Include="System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.CompilerServices.Unsafe, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
@ -123,6 +138,8 @@
|
||||
<Compile Include="BiliApi\Cheese\Models\CheeseStat.cs" />
|
||||
<Compile Include="BiliApi\Cheese\Models\CheeseUpInfo.cs" />
|
||||
<Compile Include="BiliApi\Cheese\Models\CheeseView.cs" />
|
||||
<Compile Include="BiliApi\Danmaku\DanmakuProtobuf.cs" />
|
||||
<Compile Include="BiliApi\Danmaku\Models\BiliDanmaku.cs" />
|
||||
<Compile Include="BiliApi\Login\LoginHelper.cs" />
|
||||
<Compile Include="BiliApi\Models\BaseModel.cs" />
|
||||
<Compile Include="BiliApi\Login\LoginInfo.cs" />
|
||||
@ -134,6 +151,7 @@
|
||||
<Compile Include="BiliApi\Login\Models\UserInfoForNavigation.cs" />
|
||||
<Compile Include="BiliApi\Models\Dimension.cs" />
|
||||
<Compile Include="BiliApi\Models\VideoOwner.cs" />
|
||||
<Compile Include="BiliApi\protobuf\bilibili\community\service\dm\v1\Dm.cs" />
|
||||
<Compile Include="BiliApi\VideoStream\Models\PlayUrl.cs" />
|
||||
<Compile Include="BiliApi\VideoStream\Models\PlayUrlDash.cs" />
|
||||
<Compile Include="BiliApi\VideoStream\Models\PlayUrlDashVideo.cs" />
|
||||
@ -161,6 +179,20 @@
|
||||
<Compile Include="BiliApi\WebClient.cs" />
|
||||
<Compile Include="BiliApi\Zone\VideoZone.cs" />
|
||||
<Compile Include="BiliApi\Zone\ZoneAttr.cs" />
|
||||
<Compile Include="Danmaku2Ass\Bilibili.cs" />
|
||||
<Compile Include="Danmaku2Ass\Collision.cs" />
|
||||
<Compile Include="Danmaku2Ass\Config.cs" />
|
||||
<Compile Include="Danmaku2Ass\Creater.cs" />
|
||||
<Compile Include="Danmaku2Ass\Danmaku.cs" />
|
||||
<Compile Include="Danmaku2Ass\Display.cs" />
|
||||
<Compile Include="Danmaku2Ass\Filter.cs" />
|
||||
<Compile Include="Danmaku2Ass\Producer.cs" />
|
||||
<Compile Include="Danmaku2Ass\Studio.cs" />
|
||||
<Compile Include="Danmaku2Ass\Subtitle.cs" />
|
||||
<Compile Include="Danmaku2Ass\Utils.cs" />
|
||||
<Compile Include="Downloader\MultiThreadDownloader.cs" />
|
||||
<Compile Include="Downloader\PartialDownloader.cs" />
|
||||
<Compile Include="FFmpeg\FFmpegHelper.cs" />
|
||||
<Compile Include="Logging\LogInfo.cs" />
|
||||
<Compile Include="Logging\LogLevel.cs" />
|
||||
<Compile Include="Logging\LogManager.cs" />
|
||||
@ -210,6 +242,7 @@
|
||||
<Compile Include="Utils\Web.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
@ -221,6 +254,8 @@
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\packages\Brotli.NET.2.1.1\build\Brotli.NET.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Brotli.NET.2.1.1\build\Brotli.NET.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\System.Data.SQLite.Core.1.0.112.2\build\net40\System.Data.SQLite.Core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\System.Data.SQLite.Core.1.0.112.2\build\net40\System.Data.SQLite.Core.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Google.Protobuf.Tools.3.18.1\build\Google.Protobuf.Tools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Google.Protobuf.Tools.3.18.1\build\Google.Protobuf.Tools.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\packages\System.Data.SQLite.Core.1.0.112.2\build\net40\System.Data.SQLite.Core.targets" Condition="Exists('..\packages\System.Data.SQLite.Core.1.0.112.2\build\net40\System.Data.SQLite.Core.targets')" />
|
||||
<Import Project="..\packages\Google.Protobuf.Tools.3.18.1\build\Google.Protobuf.Tools.targets" Condition="Exists('..\packages\Google.Protobuf.Tools.3.18.1\build\Google.Protobuf.Tools.targets')" />
|
||||
</Project>
|
11
src/DownKyi.Core/app.config
Normal file
11
src/DownKyi.Core/app.config
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
@ -1,8 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Brotli.NET" version="2.1.1" targetFramework="net472" />
|
||||
<package id="Google.Protobuf" version="3.18.1" targetFramework="net472" />
|
||||
<package id="Google.Protobuf.Tools" version="3.18.1" targetFramework="net472" />
|
||||
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net472" />
|
||||
<package id="QRCoder" version="1.4.1" targetFramework="net472" />
|
||||
<package id="System.Buffers" version="4.5.1" targetFramework="net472" />
|
||||
<package id="System.Data.SQLite.Core" version="1.0.112.2" targetFramework="net472" />
|
||||
<package id="System.Memory" version="4.5.4" targetFramework="net472" />
|
||||
<package id="System.Numerics.Vectors" version="4.5.0" targetFramework="net472" />
|
||||
<package id="System.Runtime.CompilerServices.Unsafe" version="5.0.0" targetFramework="net472" />
|
||||
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net472" />
|
||||
</packages>
|
Loading…
Reference in New Issue
Block a user