mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-15 19:22:19 +08:00
commit
f6ad6b8593
45
BililiveRecorder.Cli/BililiveRecorder.Cli.csproj
Normal file
45
BililiveRecorder.Cli/BililiveRecorder.Cli.csproj
Normal file
|
@ -0,0 +1,45 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<StartupObject>BililiveRecorder.Cli.Program</StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="config.json" />
|
||||
<None Remove="NLog.config" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\TempBuildInfo\BuildInfo.Cli.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="config.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="NLog.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="4.9.4" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.4.3" />
|
||||
<PackageReference Include="NLog" Version="4.5.10" />
|
||||
<PackageReference Include="NLog.Config" Version="4.5.10" />
|
||||
<PackageReference Include="NLog.Schema" Version="4.5.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" />
|
||||
<ProjectReference Include="..\BililiveRecorder.FlvProcessor\BililiveRecorder.FlvProcessor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
<Exec Command="cd $(SolutionDir)
powershell -ExecutionPolicy Bypass -File .\CI\patch_buildinfo.ps1 Cli" />
|
||||
</Target>
|
||||
|
||||
|
||||
</Project>
|
17
BililiveRecorder.Cli/NLog.config
Normal file
17
BililiveRecorder.Cli/NLog.config
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
|
||||
autoReload="true"
|
||||
throwExceptions="false"
|
||||
internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log"
|
||||
>
|
||||
<targets>
|
||||
<target name="console" xsi:type="Console" encoding="utf-8"
|
||||
layout="${longdate} ${level:upperCase=true} ${processid} ${logger} ${event-properties:item=roomid} ${message} ${exception:format=Type} ${exception:format=Message} ${exception:format=ToString}"
|
||||
/>
|
||||
</targets>
|
||||
<rules>
|
||||
<logger name="*" minlevel="Info" writeTo="console"/>
|
||||
</rules>
|
||||
</nlog>
|
3168
BililiveRecorder.Cli/NLog.xsd
Normal file
3168
BililiveRecorder.Cli/NLog.xsd
Normal file
File diff suppressed because it is too large
Load Diff
112
BililiveRecorder.Cli/Program.cs
Normal file
112
BililiveRecorder.Cli/Program.cs
Normal file
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Autofac;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using CommandLine;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.Cli
|
||||
{
|
||||
class Program
|
||||
{
|
||||
private static IContainer Container { get; set; }
|
||||
private static ILifetimeScope RootScope { get; set; }
|
||||
private static IRecorder Recorder { get; set; }
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static void Main(string[] _)
|
||||
{
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterModule<FlvProcessorModule>();
|
||||
builder.RegisterModule<CoreModule>();
|
||||
builder.RegisterType<CommandConfigV1>().As<ConfigV1>().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
Container = builder.Build();
|
||||
|
||||
RootScope = Container.BeginLifetimeScope("recorder_root");
|
||||
Recorder = RootScope.Resolve<IRecorder>();
|
||||
if (!Recorder.Initialize(System.IO.Directory.GetCurrentDirectory()))
|
||||
{
|
||||
Console.WriteLine("Initialize Error");
|
||||
return;
|
||||
}
|
||||
|
||||
Parser.Default
|
||||
.ParseArguments<CommandConfigV1>(() => (CommandConfigV1)Recorder.Config, Environment.GetCommandLineArgs())
|
||||
.WithParsed(Run);
|
||||
}
|
||||
|
||||
private static void Run(ConfigV1 option)
|
||||
{
|
||||
foreach (var room in option.RoomList)
|
||||
{
|
||||
if (Recorder.Where(r => r.RoomId == room.Roomid).Count() == 0)
|
||||
{
|
||||
Recorder.AddRoom(room.Roomid);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("开始录播");
|
||||
Task.WhenAll(Recorder.Select(x => Task.Run(() => x.Start()))).Wait();
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
Task.WhenAll(Recorder.Select(x => Task.Run(() => x.StopRecord()))).Wait();
|
||||
logger.Info("停止录播");
|
||||
};
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigV1Metadata
|
||||
{
|
||||
[Option('o', "dir", Default = ".", HelpText = "Output directory", Required = false)]
|
||||
[Utils.DoNotCopyProperty]
|
||||
public object WorkDirectory { get; set; }
|
||||
|
||||
[Option("cookie", HelpText = "Provide custom cookies", Required = false)]
|
||||
public object Cookie { get; set; }
|
||||
|
||||
[Option("avoidtxy", HelpText = "Avoid Tencent Cloud server", Required = false)]
|
||||
public object AvoidTxy { get; set; }
|
||||
|
||||
[Option("live_api_host", HelpText = "Use custom api host", Required = false)]
|
||||
public object LiveApiHost { get; set; }
|
||||
|
||||
[Option("record_filename_format", HelpText = "Recording name format", Required = false)]
|
||||
public object RecordFilenameFormat { get; set; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
[MetadataType(typeof(ConfigV1Metadata))]
|
||||
class CommandConfigV1 : ConfigV1
|
||||
{
|
||||
[Option('i', "id", HelpText = "room id", Required = true)]
|
||||
[Utils.DoNotCopyProperty]
|
||||
public string _RoomList
|
||||
{
|
||||
set
|
||||
{
|
||||
var roomids = value.Split(',');
|
||||
RoomList.Clear();
|
||||
|
||||
foreach (var roomid in roomids)
|
||||
{
|
||||
var room = new RoomV1();
|
||||
room.Roomid = Int32.Parse(roomid);
|
||||
room.Enabled = false;
|
||||
RoomList.Add(room);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
1
BililiveRecorder.Cli/config.json
Normal file
1
BililiveRecorder.Cli/config.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":1,"data":"{\"roomlist\":[],\"feature\":0,\"clip_length_future\":10,\"clip_length_past\":20,\"cutting_mode\":0,\"cutting_number\":10,\"timing_stream_retry\":6000,\"timing_stream_connect\":3000,\"timing_danmaku_retry\":2000,\"timing_check_interval\":300,\"timing_watchdog_timeout\":10000,\"cookie\":\"\",\"avoidtxy\":true}"}
|
5
BililiveRecorder.Cli/runtimeconfig.template.json
Normal file
5
BililiveRecorder.Cli/runtimeconfig.template.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"configProperties": {
|
||||
"System.Globalization.Invariant": true
|
||||
}
|
||||
}
|
195
BililiveRecorder.Core/BasicDanmakuWriter.cs
Normal file
195
BililiveRecorder.Core/BasicDanmakuWriter.cs
Normal file
|
@ -0,0 +1,195 @@
|
|||
using BililiveRecorder.Core.Config;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public class BasicDanmakuWriter : IBasicDanmakuWriter
|
||||
{
|
||||
private static readonly XmlWriterSettings xmlWriterSettings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
Encoding = Encoding.UTF8,
|
||||
CloseOutput = true,
|
||||
WriteEndDocumentOnClose = true
|
||||
};
|
||||
|
||||
private XmlWriter xmlWriter = null;
|
||||
private DateTimeOffset offset = DateTimeOffset.UtcNow;
|
||||
private readonly ConfigV1 config;
|
||||
|
||||
public BasicDanmakuWriter(ConfigV1 config)
|
||||
{
|
||||
this.config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
|
||||
public void EnableWithPath(string path)
|
||||
{
|
||||
if (disposedValue) return;
|
||||
|
||||
semaphoreSlim.Wait();
|
||||
try
|
||||
{
|
||||
if (xmlWriter != null)
|
||||
{
|
||||
xmlWriter.Close();
|
||||
xmlWriter.Dispose();
|
||||
xmlWriter = null;
|
||||
}
|
||||
|
||||
try { Directory.CreateDirectory(Path.GetDirectoryName(path)); } catch (Exception) { }
|
||||
var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
|
||||
xmlWriter = XmlWriter.Create(stream, xmlWriterSettings);
|
||||
WriteStartDocument(xmlWriter);
|
||||
offset = DateTimeOffset.UtcNow;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
if (disposedValue) return;
|
||||
|
||||
semaphoreSlim.Wait();
|
||||
try
|
||||
{
|
||||
if (xmlWriter != null)
|
||||
{
|
||||
xmlWriter.Close();
|
||||
xmlWriter.Dispose();
|
||||
xmlWriter = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(DanmakuModel danmakuModel)
|
||||
{
|
||||
if (disposedValue) return;
|
||||
|
||||
semaphoreSlim.Wait();
|
||||
try
|
||||
{
|
||||
if (xmlWriter != null)
|
||||
{
|
||||
// TimeSpan diff = DateTimeOffset.UtcNow - offset;
|
||||
|
||||
switch (danmakuModel.MsgType)
|
||||
{
|
||||
case MsgTypeEnum.Comment:
|
||||
{
|
||||
var type = danmakuModel.RawObj?["info"]?[0]?[1]?.ToObject<int>() ?? 1;
|
||||
var size = danmakuModel.RawObj?["info"]?[0]?[2]?.ToObject<int>() ?? 25;
|
||||
var color = danmakuModel.RawObj?["info"]?[0]?[3]?.ToObject<int>() ?? 0XFFFFFF;
|
||||
long st = danmakuModel.RawObj?["info"]?[0]?[4]?.ToObject<long>() ?? 0L;
|
||||
var ts = Math.Max((DateTimeOffset.FromUnixTimeMilliseconds(st) - offset).TotalSeconds, 0d);
|
||||
|
||||
xmlWriter.WriteStartElement("d");
|
||||
xmlWriter.WriteAttributeString("p", $"{ts},{type},{size},{color},{st},0,{danmakuModel.UserID},0");
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(danmakuModel.CommentText);
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
case MsgTypeEnum.SuperChat:
|
||||
if (config.RecordDanmakuSuperChat)
|
||||
{
|
||||
xmlWriter.WriteStartElement("sc");
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("price", danmakuModel.Price.ToString());
|
||||
xmlWriter.WriteAttributeString("time", danmakuModel.SCKeepTime.ToString());
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(danmakuModel.CommentText);
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
case MsgTypeEnum.GiftSend:
|
||||
if (config.RecordDanmakuGift)
|
||||
{
|
||||
xmlWriter.WriteStartElement("gift");
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("giftname", danmakuModel.GiftName);
|
||||
xmlWriter.WriteAttributeString("giftcount", danmakuModel.GiftCount.ToString());
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
case MsgTypeEnum.GuardBuy:
|
||||
if (config.RecordDanmakuGuard)
|
||||
{
|
||||
xmlWriter.WriteStartElement("guard");
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("level", danmakuModel.UserGuardLevel.ToString()); ;
|
||||
xmlWriter.WriteAttributeString("count", danmakuModel.GiftCount.ToString());
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteStartDocument(XmlWriter writer)
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("i");
|
||||
writer.WriteAttributeString("BililiveRecorder", "B站录播姬拓展版弹幕文件");
|
||||
writer.WriteComment("\nB站录播姬 " + BuildInfo.Version + " " + BuildInfo.HeadSha1 + "\n本文件在B站主站视频弹幕XML格式的基础上进行了拓展\n\nsc为SuperChat\ngift为礼物\nguard为上船\n\nattribute \"raw\" 为原始数据\n");
|
||||
writer.WriteElementString("chatserver", "chat.bilibili.com");
|
||||
writer.WriteElementString("chatid", "0");
|
||||
writer.WriteElementString("mission", "0");
|
||||
writer.WriteElementString("maxlimit", "1000");
|
||||
writer.WriteElementString("state", "0");
|
||||
writer.WriteElementString("real_name", "0");
|
||||
writer.WriteElementString("source", "0");
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
semaphoreSlim.Dispose();
|
||||
xmlWriter?.Close();
|
||||
xmlWriter?.Dispose();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -111,7 +111,7 @@ namespace BililiveRecorder.Core
|
|||
/// <exception cref="Exception"/>
|
||||
public static async Task<string> GetPlayUrlAsync(int roomid)
|
||||
{
|
||||
string url = $@"https://api.live.bilibili.com/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web";
|
||||
string url = $@"{Config.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web";
|
||||
if (Config.AvoidTxy)
|
||||
{
|
||||
// 尽量避开腾讯云
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.Core.Config
|
||||
{
|
||||
public static class ConfigParser
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public static bool Load(string directory, ConfigV1 config = null)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
|
@ -42,9 +45,9 @@ namespace BililiveRecorder.Core.Config
|
|||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: Log Exception
|
||||
logger.Error(ex, "Failed to parse config!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,12 +92,42 @@ namespace BililiveRecorder.Core.Config
|
|||
[JsonProperty("cookie")]
|
||||
public string Cookie { get => _cookie; set => SetField(ref _cookie, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制弹幕
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku")]
|
||||
public bool RecordDanmaku { get => _recordDanmaku; set => SetField(ref _recordDanmaku, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 SuperChat
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_sc")]
|
||||
public bool RecordDanmakuSuperChat { get => _recordDanmakuSuperChat; set => SetField(ref _recordDanmakuSuperChat, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 礼物
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_gift")]
|
||||
public bool RecordDanmakuGift { get => _recordDanmakuGift; set => SetField(ref _recordDanmakuGift, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 上船
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_guard")]
|
||||
public bool RecordDanmakuGuard { get => _recordDanmakuGuard; set => SetField(ref _recordDanmakuGuard, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 尽量避开腾讯云服务器,可有效提升录制文件能正常播放的概率。(垃圾腾讯云直播服务)
|
||||
/// </summary>
|
||||
[JsonProperty("avoidtxy")]
|
||||
public bool AvoidTxy { get => _avoidTxy; set => SetField(ref _avoidTxy, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 替换api.live.bilibili.com服务器为其他反代,可以支持在云服务器上录制
|
||||
/// </summary>
|
||||
[JsonProperty("live_api_host")]
|
||||
public string LiveApiHost { get => _liveApiHost; set => SetField(ref _liveApiHost, value); }
|
||||
|
||||
[JsonProperty("record_filename_format")]
|
||||
public string RecordFilenameFormat
|
||||
{
|
||||
|
@ -118,7 +148,7 @@ namespace BililiveRecorder.Core.Config
|
|||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
logger.Debug("设置 [{0}] 的值已从 [{1}] 修改到 [{2}]", propertyName, field, value);
|
||||
logger.Trace("设置 [{0}] 的值已从 [{1}] 修改到 [{2}]", propertyName, field, value);
|
||||
field = value; OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
#endregion
|
||||
|
@ -126,7 +156,7 @@ namespace BililiveRecorder.Core.Config
|
|||
private uint _clipLengthPast = 20;
|
||||
private uint _clipLengthFuture = 10;
|
||||
private uint _cuttingNumber = 10;
|
||||
private EnabledFeature _enabledFeature = EnabledFeature.Both;
|
||||
private EnabledFeature _enabledFeature = EnabledFeature.RecordOnly;
|
||||
private AutoCuttingMode _cuttingMode = AutoCuttingMode.Disabled;
|
||||
private string _workDirectory;
|
||||
|
||||
|
@ -141,6 +171,13 @@ namespace BililiveRecorder.Core.Config
|
|||
private string _record_filename_format = @"{roomid}-{name}/录制-{roomid}-{date}-{time}-{title}.flv";
|
||||
private string _clip_filename_format = @"{roomid}-{name}/剪辑片段-{roomid}-{date}-{time}-{title}.flv";
|
||||
|
||||
private bool _recordDanmaku = false;
|
||||
private bool _recordDanmakuSuperChat = false;
|
||||
private bool _recordDanmakuGift = false;
|
||||
private bool _recordDanmakuGuard = false;
|
||||
|
||||
private bool _avoidTxy = false;
|
||||
|
||||
private string _liveApiHost = "https://api.live.bilibili.com";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace BililiveRecorder.Core
|
|||
builder.RegisterType<TcpClient>().AsSelf().ExternallyOwned();
|
||||
builder.RegisterType<StreamMonitor>().As<IStreamMonitor>().ExternallyOwned();
|
||||
builder.RegisterType<RecordedRoom>().As<IRecordedRoom>().ExternallyOwned();
|
||||
builder.RegisterType<BasicDanmakuWriter>().As<IBasicDanmakuWriter>().ExternallyOwned();
|
||||
builder.RegisterType<Recorder>().As<IRecorder>().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,8 +40,11 @@ namespace BililiveRecorder.Core
|
|||
/// <summary>
|
||||
/// 购买船票(上船)
|
||||
/// </summary>
|
||||
GuardBuy
|
||||
|
||||
GuardBuy,
|
||||
/// <summary>
|
||||
/// SuperChat
|
||||
/// </summary>
|
||||
SuperChat
|
||||
}
|
||||
|
||||
public class DanmakuModel
|
||||
|
@ -81,6 +84,16 @@ namespace BililiveRecorder.Core
|
|||
/// </summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SC 价格
|
||||
/// </summary>
|
||||
public double Price { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SC 保持时间
|
||||
/// </summary>
|
||||
public int SCKeepTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息触发者用户ID
|
||||
/// <para>此项有值的消息类型:<list type="bullet">
|
||||
|
@ -170,6 +183,11 @@ namespace BililiveRecorder.Core
|
|||
/// </summary>
|
||||
public string RawData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始数据, 高级开发用
|
||||
/// </summary>
|
||||
public JObject RawObj { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内部用, JSON数据版本号 通常应该是2
|
||||
/// </summary>
|
||||
|
@ -184,6 +202,7 @@ namespace BililiveRecorder.Core
|
|||
JSON_Version = 2;
|
||||
|
||||
var obj = JObject.Parse(JSON);
|
||||
RawObj = obj;
|
||||
string cmd = obj["cmd"]?.ToObject<string>();
|
||||
switch (cmd)
|
||||
{
|
||||
|
@ -211,24 +230,6 @@ namespace BililiveRecorder.Core
|
|||
UserID = obj["data"]["uid"].ToObject<int>();
|
||||
GiftCount = obj["data"]["num"].ToObject<int>();
|
||||
break;
|
||||
case "WELCOME":
|
||||
{
|
||||
MsgType = MsgTypeEnum.Welcome;
|
||||
UserName = obj["data"]["uname"].ToObject<string>();
|
||||
UserID = obj["data"]["uid"].ToObject<int>();
|
||||
IsVIP = true;
|
||||
IsAdmin = obj["data"]?["is_admin"]?.ToObject<bool>() ?? obj["data"]?["isadmin"]?.ToObject<string>() == "1";
|
||||
break;
|
||||
|
||||
}
|
||||
case "WELCOME_GUARD":
|
||||
{
|
||||
MsgType = MsgTypeEnum.WelcomeGuard;
|
||||
UserName = obj["data"]["username"].ToObject<string>();
|
||||
UserID = obj["data"]["uid"].ToObject<int>();
|
||||
UserGuardLevel = obj["data"]["guard_level"].ToObject<int>();
|
||||
break;
|
||||
}
|
||||
case "GUARD_BUY":
|
||||
{
|
||||
MsgType = MsgTypeEnum.GuardBuy;
|
||||
|
@ -239,6 +240,35 @@ namespace BililiveRecorder.Core
|
|||
GiftCount = obj["data"]["num"].ToObject<int>();
|
||||
break;
|
||||
}
|
||||
case "SUPER_CHAT_MESSAGE":
|
||||
{
|
||||
MsgType = MsgTypeEnum.SuperChat;
|
||||
CommentText = obj["data"]["message"]?.ToString();
|
||||
UserID = obj["data"]["uid"].ToObject<int>();
|
||||
UserName = obj["data"]["user_info"]["uname"].ToString();
|
||||
Price = obj["data"]["price"].ToObject<double>();
|
||||
SCKeepTime = obj["data"]["time"].ToObject<int>();
|
||||
break;
|
||||
}
|
||||
/*
|
||||
case "WELCOME":
|
||||
{
|
||||
MsgType = MsgTypeEnum.Welcome;
|
||||
UserName = obj["data"]["uname"].ToObject<string>();
|
||||
UserID = obj["data"]["uid"].ToObject<int>();
|
||||
IsVIP = true;
|
||||
IsAdmin = obj["data"]?["is_admin"]?.ToObject<bool>() ?? obj["data"]?["isadmin"]?.ToObject<string>() == "1";
|
||||
break;
|
||||
}
|
||||
case "WELCOME_GUARD":
|
||||
{
|
||||
MsgType = MsgTypeEnum.WelcomeGuard;
|
||||
UserName = obj["data"]["username"].ToObject<string>();
|
||||
UserID = obj["data"]["uid"].ToObject<int>();
|
||||
UserGuardLevel = obj["data"]["guard_level"].ToObject<int>();
|
||||
break;
|
||||
}
|
||||
*/
|
||||
default:
|
||||
{
|
||||
MsgType = MsgTypeEnum.Unknown;
|
||||
|
|
11
BililiveRecorder.Core/IBasicDanmakuWriter.cs
Normal file
11
BililiveRecorder.Core/IBasicDanmakuWriter.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public interface IBasicDanmakuWriter : IDisposable
|
||||
{
|
||||
void Disable();
|
||||
void EnableWithPath(string path);
|
||||
void Write(DanmakuModel danmakuModel);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Random random = new Random();
|
||||
private static readonly Version VERSION_1_0 = new Version(1, 0);
|
||||
|
||||
private int _roomid;
|
||||
private int _realRoomid;
|
||||
|
@ -69,6 +70,7 @@ namespace BililiveRecorder.Core
|
|||
public bool IsMonitoring => StreamMonitor.IsMonitoring;
|
||||
public bool IsRecording => !(StreamDownloadTask?.IsCompleted ?? true);
|
||||
|
||||
private readonly IBasicDanmakuWriter basicDanmakuWriter;
|
||||
private readonly Func<IFlvStreamProcessor> newIFlvStreamProcessor;
|
||||
private IFlvStreamProcessor _processor;
|
||||
public IFlvStreamProcessor Processor
|
||||
|
@ -110,6 +112,7 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
|
||||
public RecordedRoom(ConfigV1 config,
|
||||
IBasicDanmakuWriter basicDanmakuWriter,
|
||||
Func<int, IStreamMonitor> newIStreamMonitor,
|
||||
Func<IFlvStreamProcessor> newIFlvStreamProcessor,
|
||||
int roomid)
|
||||
|
@ -118,16 +121,24 @@ namespace BililiveRecorder.Core
|
|||
|
||||
_config = config;
|
||||
|
||||
this.basicDanmakuWriter = basicDanmakuWriter;
|
||||
|
||||
RoomId = roomid;
|
||||
StreamerName = "...";
|
||||
StreamerName = "获取中...";
|
||||
|
||||
StreamMonitor = newIStreamMonitor(RoomId);
|
||||
StreamMonitor.RoomInfoUpdated += StreamMonitor_RoomInfoUpdated;
|
||||
StreamMonitor.StreamStarted += StreamMonitor_StreamStarted;
|
||||
StreamMonitor.ReceivedDanmaku += StreamMonitor_ReceivedDanmaku;
|
||||
|
||||
StreamMonitor.FetchRoomInfoAsync();
|
||||
}
|
||||
|
||||
private void StreamMonitor_ReceivedDanmaku(object sender, ReceivedDanmakuArgs e)
|
||||
{
|
||||
basicDanmakuWriter.Write(e.Danmaku);
|
||||
}
|
||||
|
||||
private void StreamMonitor_RoomInfoUpdated(object sender, RoomInfoUpdatedArgs e)
|
||||
{
|
||||
RoomId = e.RoomInfo.RoomId;
|
||||
|
@ -286,6 +297,7 @@ namespace BililiveRecorder.Core
|
|||
Processor.ClipLengthFuture = _config.ClipLengthFuture;
|
||||
Processor.ClipLengthPast = _config.ClipLengthPast;
|
||||
Processor.CuttingNumber = _config.CuttingNumber;
|
||||
Processor.StreamFinalized += (sender, e) => { basicDanmakuWriter.Disable(); };
|
||||
Processor.OnMetaData += (sender, e) =>
|
||||
{
|
||||
e.Metadata["BililiveRecorder"] = new Dictionary<string, object>()
|
||||
|
@ -310,7 +322,13 @@ namespace BililiveRecorder.Core
|
|||
};
|
||||
|
||||
_stream = await _response.Content.ReadAsStreamAsync();
|
||||
_stream.ReadTimeout = 3 * 1000;
|
||||
|
||||
try
|
||||
{
|
||||
if (_response.Headers.ConnectionClose == false || (_response.Headers.ConnectionClose is null && _response.Version != VERSION_1_0))
|
||||
_stream.ReadTimeout = 3 * 1000;
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
|
||||
StreamDownloadTask = Task.Run(_ReadStreamLoop);
|
||||
TriggerPropertyChanged(nameof(IsRecording));
|
||||
|
@ -427,7 +445,19 @@ namespace BililiveRecorder.Core
|
|||
Dispose(true);
|
||||
}
|
||||
|
||||
private string GetStreamFilePath() => FormatFilename(_config.RecordFilenameFormat);
|
||||
private string GetStreamFilePath()
|
||||
{
|
||||
string path = FormatFilename(_config.RecordFilenameFormat);
|
||||
|
||||
// 有点脏的写法,不过凑合吧
|
||||
if (_config.RecordDanmaku)
|
||||
{
|
||||
var xmlpath = Path.ChangeExtension(path, "xml");
|
||||
basicDanmakuWriter.EnableWithPath(xmlpath);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private string GetClipFilePath() => FormatFilename(_config.ClipFilenameFormat);
|
||||
|
||||
|
@ -505,11 +535,13 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
Stop();
|
||||
StopRecord();
|
||||
Processor?.FinallizeFile();
|
||||
Processor?.Dispose();
|
||||
StreamMonitor?.Dispose();
|
||||
_response?.Dispose();
|
||||
_stream?.Dispose();
|
||||
cancellationTokenSource?.Dispose();
|
||||
basicDanmakuWriter?.Dispose();
|
||||
}
|
||||
|
||||
Processor = null;
|
||||
|
|
|
@ -48,7 +48,7 @@ namespace BililiveRecorder.Core
|
|||
|
||||
Rooms.CollectionChanged += (sender, e) =>
|
||||
{
|
||||
logger.Debug($"Rooms.CollectionChanged;{e.Action};" +
|
||||
logger.Trace($"Rooms.CollectionChanged;{e.Action};" +
|
||||
$"O:{e.OldItems?.Cast<IRecordedRoom>()?.Select(rr => rr.RoomId.ToString())?.Aggregate((current, next) => current + "," + next)};" +
|
||||
$"N:{e.NewItems?.Cast<IRecordedRoom>()?.Select(rr => rr.RoomId.ToString())?.Aggregate((current, next) => current + "," + next)}");
|
||||
};
|
||||
|
@ -62,11 +62,11 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
try
|
||||
{
|
||||
logger.Debug("设置 Cookie 等待...");
|
||||
logger.Trace("设置 Cookie 等待...");
|
||||
await Task.Delay(100);
|
||||
logger.Debug("设置 Cookie 信息...");
|
||||
logger.Trace("设置 Cookie 信息...");
|
||||
await BililiveAPI.ApplyCookieSettings(Config.Cookie);
|
||||
logger.Debug("设置成功");
|
||||
logger.Debug("设置 Cookie 成功");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
@ -41,6 +41,7 @@ namespace BililiveRecorder.Core
|
|||
private NetworkStream dmNetStream;
|
||||
private Thread dmReceiveMessageLoopThread;
|
||||
private CancellationTokenSource dmTokenSource = null;
|
||||
private bool dmConnectionTriggered = false;
|
||||
private readonly Timer httpTimer;
|
||||
|
||||
public int Roomid { get; private set; } = 0;
|
||||
|
@ -57,6 +58,7 @@ namespace BililiveRecorder.Core
|
|||
Roomid = roomid;
|
||||
|
||||
ReceivedDanmaku += Receiver_ReceivedDanmaku;
|
||||
RoomInfoUpdated += StreamMonitor_RoomInfoUpdated;
|
||||
|
||||
dmTokenSource = new CancellationTokenSource();
|
||||
Repeat.Interval(TimeSpan.FromSeconds(30), () =>
|
||||
|
@ -97,8 +99,16 @@ namespace BililiveRecorder.Core
|
|||
httpTimer.Interval = config.TimingCheckInterval * 1000;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Task.Run(() => ConnectWithRetryAsync());
|
||||
private void StreamMonitor_RoomInfoUpdated(object sender, RoomInfoUpdatedArgs e)
|
||||
{
|
||||
Roomid = e.RoomInfo.RoomId;
|
||||
if (!dmConnectionTriggered)
|
||||
{
|
||||
dmConnectionTriggered = true;
|
||||
Task.Run(() => ConnectWithRetryAsync());
|
||||
}
|
||||
}
|
||||
|
||||
private void Receiver_ReceivedDanmaku(object sender, ReceivedDanmakuArgs e)
|
||||
|
|
|
@ -204,7 +204,8 @@ namespace BililiveRecorder.FlvProcessor
|
|||
_readHead += text_size;
|
||||
}
|
||||
object value = DecodeScriptDataValue(buff, ref _readHead);
|
||||
d.Add(key, value);
|
||||
// d.Add(key, value);
|
||||
d[key] = value; // fix duplicates
|
||||
}
|
||||
_readHead += 3;
|
||||
return d;
|
||||
|
|
|
@ -121,7 +121,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
lock (_writelock)
|
||||
{
|
||||
if (_finalized) { throw new InvalidOperationException("Processor Already Closed"); }
|
||||
if (_finalized) { return; /*throw new InvalidOperationException("Processor Already Closed");*/ }
|
||||
if (_leftover != null)
|
||||
{
|
||||
byte[] c = new byte[_leftover.Length + data.Length];
|
||||
|
@ -302,7 +302,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
_tagVideoCount++;
|
||||
if (_tagVideoCount < 2)
|
||||
{
|
||||
logger.Trace("第一个 Video Tag 时间戳 {0} ms", tag.TimeStamp);
|
||||
logger.Debug("第一个 Video Tag 时间戳 {0} ms", tag.TimeStamp);
|
||||
_headerTags.Add(tag);
|
||||
}
|
||||
else
|
||||
|
@ -310,7 +310,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
_baseTimeStamp = tag.TimeStamp;
|
||||
_hasOffset = true;
|
||||
StartDateTime = DateTime.Now;
|
||||
logger.Trace("重设时间戳 {0} 毫秒", _baseTimeStamp);
|
||||
logger.Debug("重设时间戳 {0} 毫秒", _baseTimeStamp);
|
||||
}
|
||||
}
|
||||
else if (tag.TagType == TagType.AUDIO)
|
||||
|
@ -318,7 +318,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
_tagAudioCount++;
|
||||
if (_tagAudioCount < 2)
|
||||
{
|
||||
logger.Trace("第一个 Audio Tag 时间戳 {0} ms", tag.TimeStamp);
|
||||
logger.Debug("第一个 Audio Tag 时间戳 {0} ms", tag.TimeStamp);
|
||||
_headerTags.Add(tag);
|
||||
}
|
||||
else
|
||||
|
@ -326,7 +326,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
_baseTimeStamp = tag.TimeStamp;
|
||||
_hasOffset = true;
|
||||
StartDateTime = DateTime.Now;
|
||||
logger.Trace("重设时间戳 {0} 毫秒", _baseTimeStamp);
|
||||
logger.Debug("重设时间戳 {0} 毫秒", _baseTimeStamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +356,11 @@ namespace BililiveRecorder.FlvProcessor
|
|||
if (!EnabledFeature.IsClipEnabled()) { return null; }
|
||||
lock (_writelock)
|
||||
{
|
||||
if (_finalized) { throw new InvalidOperationException("Processor Already Closed"); }
|
||||
if (_finalized)
|
||||
{
|
||||
return null;
|
||||
// throw new InvalidOperationException("Processor Already Closed");
|
||||
}
|
||||
|
||||
logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (_tags[_tags.Count - 1].TimeStamp - _tags[0].TimeStamp) / 1000d, ClipLengthFuture);
|
||||
IFlvClipProcessor clip = funcFlvClipProcessor().Initialize(GetClipFileName(), Metadata, _headerTags, new List<IFlvTag>(_tags.ToArray()), ClipLengthFuture);
|
||||
|
|
|
@ -74,6 +74,8 @@ namespace BililiveRecorder.WPF
|
|||
{
|
||||
logger.Error(ex, "检查更新时出错,如持续出错请联系开发者 rec@danmuji.org");
|
||||
}
|
||||
|
||||
_ = Task.Run(async () => { await Task.Delay(TimeSpan.FromDays(1)); await RunCheckUpdate(); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tb="http://www.hardcodet.net/taskbar"
|
||||
xmlns:core="clr-namespace:BililiveRecorder.Core;assembly=BililiveRecorder.Core"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
d:DataContext="{d:DesignInstance Type=core:Recorder}"
|
||||
mc:Ignorable="d"
|
||||
MinHeight="400" MinWidth="650"
|
||||
Title="录播姬" Height="450" Width="850"
|
||||
|
@ -16,6 +18,7 @@
|
|||
<TextBlock Text="{Binding}" TextWrapping="Wrap" MouseRightButtonUp="TextBlock_MouseRightButtonUp"/>
|
||||
</DataTemplate>
|
||||
<local:RecordStatusConverter x:Key="RSC"/>
|
||||
<local:ClipButtonVisibilityConverter x:Key="ClipButtonVisibilityConverter"/>
|
||||
<local:BoolToStringConverter x:Key="RecordStatusConverter" TrueValue="录制中" FalseValue="闲置"/>
|
||||
<local:BoolToStringConverter x:Key="MonitorStatusConverter" TrueValue="自动录制" FalseValue="非自动"/>
|
||||
</ResourceDictionary>
|
||||
|
@ -44,7 +47,7 @@
|
|||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding DownloadSpeedPersentage,StringFormat=0.##%,Mode=OneWay}" Margin="5,0,0,0"/>
|
||||
<TextBlock Margin="5,0" Text="{Binding RealRoomid,Mode=OneWay}"/>
|
||||
<TextBlock Margin="5,0" Text="{Binding RoomId,Mode=OneWay}"/>
|
||||
<TextBlock Text="{Binding StreamerName,Mode=OneWay}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
@ -56,16 +59,14 @@
|
|||
<tb:TaskbarIcon.ContextMenu>
|
||||
<ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
|
||||
<!--<MenuItem IsCheckable="True">不弹出提醒</MenuItem>-->
|
||||
<MenuItem Header="退出">
|
||||
<MenuItem Header="确认退出" Click="Taskbar_Quit_Click"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="退出" Click="Taskbar_Quit_Click"/>
|
||||
</ContextMenu>
|
||||
</tb:TaskbarIcon.ContextMenu>
|
||||
</tb:TaskbarIcon>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="7*"/>
|
||||
<RowDefinition Height="4*"/>
|
||||
<RowDefinition Height="8*"/>
|
||||
<RowDefinition Height="2*"/>
|
||||
<RowDefinition Height="4*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
|
@ -82,11 +83,14 @@
|
|||
<MenuItem Header="删除" Click="RemoveRecRoom"/>
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
<DataGrid.Resources>
|
||||
<local:BindingProxy x:Key="proxy" Data="{Binding Recorder}" />
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Header="回放剪辑">
|
||||
<DataGridTemplateColumn Visibility="{Binding Data.Config.EnabledFeature,Converter={StaticResource ClipButtonVisibilityConverter},Source={StaticResource proxy}}" Header="回放剪辑">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button Click="Clip_Click">剪辑</Button>
|
||||
<Button Click="Clip_Click" Content="{Binding Processor.Clips.Count,Mode=OneWay,FallbackValue=0}" ContentStringFormat="剪辑 ({0})"/>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
@ -98,7 +102,6 @@
|
|||
<DataGridTextColumn Binding="{Binding IsMonitoring,Converter={StaticResource MonitorStatusConverter},Mode=OneWay}" Header="是否自动录制"/>
|
||||
<DataGridTextColumn Binding="{Binding DownloadSpeedMegaBitps,StringFormat=0.## Mbps,Mode=OneWay}" Header="实时下载速度"/>
|
||||
<DataGridTextColumn Binding="{Binding DownloadSpeedPersentage,StringFormat=0.## %,Mode=OneWay}" Header="录制速度比"/>
|
||||
<DataGridTextColumn Binding="{Binding Processor.Clips.Count,Mode=OneWay}" Header="剪辑数量"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<ItemsControl Grid.Row="1" Grid.RowSpan="2" x:Name="Log" ItemsSource="{Binding Logs}" ItemTemplate="{StaticResource LogTemplate}" ToolTip="右键点击可以复制单行日志">
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace BililiveRecorder.WPF
|
|||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Regex UrlToRoomidRegex = new Regex(@"^https?:\/\/live\.bilibili\.com\/(?<roomid>\d+)(?:[#\?].*)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
|
||||
private const int MAX_LOG_ROW = 25;
|
||||
private const int MAX_LOG_ROW = 30;
|
||||
private const string LAST_WORK_DIR_FILE = "lastworkdir";
|
||||
|
||||
private IContainer Container { get; set; }
|
||||
|
@ -42,6 +42,7 @@ namespace BililiveRecorder.WPF
|
|||
"QQ群: 689636812",
|
||||
"",
|
||||
"删除直播间按钮在列表右键菜单里",
|
||||
"新增了弹幕录制功能,默认关闭,设置里可开启",
|
||||
"",
|
||||
"录制速度比 在 100% 左右说明跟上了主播直播的速度",
|
||||
"小于 100% 说明录播电脑的下载带宽不够,跟不上录制直播"
|
||||
|
|
|
@ -37,6 +37,6 @@
|
|||
</targets>
|
||||
<rules>
|
||||
<logger name="*" minlevel="Info" writeTo="WPFLogger"/>
|
||||
<logger name="*" minlevel="Trace" writeTo="file"/>
|
||||
<logger name="*" minlevel="Debug" writeTo="file"/>
|
||||
</rules>
|
||||
</nlog>
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF"
|
||||
xmlns:core="clr-namespace:BililiveRecorder.Core;assembly=BililiveRecorder.Core"
|
||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
|
||||
xmlns:flv="clr-namespace:BililiveRecorder.FlvProcessor;assembly=BililiveRecorder.FlvProcessor"
|
||||
d:DataContext="{d:DesignInstance Type=config:ConfigV1}"
|
||||
mc:Ignorable="d"
|
||||
ShowInTaskbar="False" ResizeMode="NoResize"
|
||||
Title="设置 - 录播姬" Height="550" Width="300">
|
||||
Title="设置 - 录播姬" Height="630" Width="300">
|
||||
<Window.Resources>
|
||||
<local:ValueConverterGroup x:Key="EnumToInvertBooleanConverter">
|
||||
<local:EnumToBooleanConverter/>
|
||||
|
@ -36,6 +38,10 @@
|
|||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="1*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.Resources>
|
||||
|
@ -48,7 +54,159 @@
|
|||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Grid.Resources>
|
||||
<Grid Grid.Row="0">
|
||||
<StackPanel Grid.Row="0">
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Margin" Value="10,1"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<CheckBox IsChecked="{Binding RecordDanmaku}">
|
||||
录制弹幕
|
||||
</CheckBox>
|
||||
<CheckBox IsEnabled="{Binding RecordDanmaku}" IsChecked="{Binding RecordDanmakuSuperChat}">
|
||||
同时保存 SuperChat
|
||||
</CheckBox>
|
||||
<CheckBox IsEnabled="{Binding RecordDanmaku}" IsChecked="{Binding RecordDanmakuGift}">
|
||||
同时保存 送礼信息
|
||||
</CheckBox>
|
||||
<CheckBox IsEnabled="{Binding RecordDanmaku}" IsChecked="{Binding RecordDanmakuGuard}">
|
||||
同时保存 舰长购买
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1"/>
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Row="0" Grid.ColumnSpan="2" Margin="10,0">
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}" ToolTipService.InitialShowDelay="0"
|
||||
ToolTip="占内存更少,但不能剪辑回放">只使用录制功能</RadioButton>
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.ClipOnly}}" ToolTipService.InitialShowDelay="0"
|
||||
ToolTip="不保存所有直播数据到硬盘">只使用即时回放剪辑功能</RadioButton>
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.Both}}">同时启用两个功能</RadioButton>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0">剪辑过去时长:</TextBlock>
|
||||
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding ClipLengthPast,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding Path=EnabledFeature, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}"/>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">秒</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0">剪辑将来时长:</TextBlock>
|
||||
<Grid Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding ClipLengthFuture,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding Path=EnabledFeature, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}"/>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">秒</TextBlock>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Separator Grid.Row="3"/>
|
||||
<Grid Grid.Row="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0">自动切割单位:</TextBlock>
|
||||
<Grid Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding CuttingNumber,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTipService.InitialShowDelay="0" ToolTipService.ShowDuration="20000"
|
||||
IsEnabled="{Binding Path=CuttingMode, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.Disabled}}">
|
||||
<local:ClickSelectTextBox.ToolTip>
|
||||
<ToolTip>
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock FontWeight="Bold">注:</TextBlock>
|
||||
<TextBlock>实际切割出来的视频文件会比设定的要大一点</TextBlock>
|
||||
<TextBlock>根据实际情况不同,可能会 大不到1MiB 或 长几秒钟</TextBlock>
|
||||
<TextBlock>请根据实际需求调整</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</local:ClickSelectTextBox.ToolTip>
|
||||
</local:ClickSelectTextBox>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">分 或 MiB</TextBlock>
|
||||
</Grid>
|
||||
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Margin="10,0">
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.Disabled}}">不切割录制的视频文件</RadioButton>
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.ByTime}}">根据视频时间(分)切割</RadioButton>
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.BySize}}">根据文件大小(MiB)切割</RadioButton>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Separator Grid.Row="5"/>
|
||||
<Grid Grid.Row="6" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">录制保存文件名格式: (只支持FLV)</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding RecordFilenameFormat,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="7" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">剪辑保存文件名格式: (只支持FLV)</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding ClipFilenameFormat,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="8" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">文件名变量说明:</TextBlock>
|
||||
<TextBlock Grid.Row="1" HorizontalAlignment="Left">日期: {date} 时间: {time} 房间号: {roomid}</TextBlock>
|
||||
<TextBlock Grid.Row="2" HorizontalAlignment="Left">标题: {title} 主播名: {name} 随机数: {random}</TextBlock>
|
||||
</Grid>
|
||||
<Separator Grid.Row="9"/>
|
||||
<TextBlock Grid.Row="10" FontSize="16" FontWeight="Bold" TextAlignment="Center" HorizontalAlignment="Center">
|
||||
以下设置项谨慎修改,建议保留默认
|
||||
</TextBlock>
|
||||
<Separator Grid.Row="11"/>
|
||||
<Grid Grid.Row="12" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">请求 API 时使用 Cookie: (可选)</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding Cookie,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
<Separator Grid.Row="13"/>
|
||||
<Grid Grid.Row="14">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
|
@ -186,145 +344,7 @@
|
|||
<TextBlock Grid.Column="1" Margin="5,0,10,0">秒</TextBlock>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Separator Grid.Row="1"/>
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0">剪辑过去时长:</TextBlock>
|
||||
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding ClipLengthPast,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding Path=EnabledFeature, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}"/>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">秒</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0">剪辑将来时长:</TextBlock>
|
||||
<Grid Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding ClipLengthFuture,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding Path=EnabledFeature, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}"/>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">秒</TextBlock>
|
||||
</Grid>
|
||||
<StackPanel Grid.Row="3" Grid.ColumnSpan="2" Margin="10,0">
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.ClipOnly}}" ToolTipService.InitialShowDelay="0"
|
||||
ToolTip="不保存所有直播数据到硬盘">只使用即时回放剪辑功能</RadioButton>
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.RecordOnly}}" ToolTipService.InitialShowDelay="0"
|
||||
ToolTip="占内存更少,但不能剪辑回放">只使用录制功能</RadioButton>
|
||||
<RadioButton GroupName="EnabledFeature" IsChecked="{Binding Path=EnabledFeature, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:EnabledFeature.Both}}">同时启用两个功能</RadioButton>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Separator Grid.Row="3"/>
|
||||
<Grid Grid.Row="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0">自动切割单位:</TextBlock>
|
||||
<Grid Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:ClickSelectTextBox Grid.Column="0" Text="{Binding CuttingNumber,Delay=500,UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTipService.InitialShowDelay="0" ToolTipService.ShowDuration="20000"
|
||||
IsEnabled="{Binding Path=CuttingMode, Converter={StaticResource EnumToInvertBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.Disabled}}">
|
||||
<local:ClickSelectTextBox.ToolTip>
|
||||
<ToolTip>
|
||||
<StackPanel>
|
||||
<StackPanel.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</StackPanel.Resources>
|
||||
<TextBlock FontWeight="Bold">注:</TextBlock>
|
||||
<TextBlock>实际切割出来的视频文件会比设定的要大一点</TextBlock>
|
||||
<TextBlock>根据实际情况不同,可能会 大不到1MiB 或 长几秒钟</TextBlock>
|
||||
<TextBlock>请根据实际需求调整</TextBlock>
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</local:ClickSelectTextBox.ToolTip>
|
||||
</local:ClickSelectTextBox>
|
||||
<TextBlock Grid.Column="1" Margin="5,0,10,0">分 或 MiB</TextBlock>
|
||||
</Grid>
|
||||
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Margin="10,0">
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.Disabled}}">不切割录制的视频文件</RadioButton>
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.ByTime}}">根据视频时间(分)切割</RadioButton>
|
||||
<RadioButton GroupName="CuttingMode" IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.BySize}}">根据文件大小(MiB)切割</RadioButton>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Separator Grid.Row="5"/>
|
||||
<Grid Grid.Row="6" Margin="10,0">
|
||||
<CheckBox IsChecked="{Binding AvoidTxy}">
|
||||
<TextBlock>
|
||||
尽量不使用腾讯云服务器
|
||||
<LineBreak/> 注:目前这个选项勾选与否,没有什么区别。
|
||||
</TextBlock>
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
<Separator Grid.Row="7"/>
|
||||
<Grid Grid.Row="8" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">请求 API 时使用 Cookie: (可选)</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding Cookie,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
<Separator Grid.Row="9"/>
|
||||
<Grid Grid.Row="10" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">录制保存文件名格式:</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding RecordFilenameFormat,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="11" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">剪辑保存文件名格式:</TextBlock>
|
||||
<local:ClickSelectTextBox Grid.Row="1" Text="{Binding ClipFilenameFormat,Delay=500,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="12" Margin="10,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Left">文件名变量说明:</TextBlock>
|
||||
<TextBlock Grid.Row="1" HorizontalAlignment="Left">日期: {date} 时间: {time} 房间号: {roomid}</TextBlock>
|
||||
<TextBlock Grid.Row="2" HorizontalAlignment="Left">标题: {title} 主播名: {name} 随机数: {random}</TextBlock>
|
||||
</Grid>
|
||||
<!--
|
||||
<TextBlock Grid.Row="4" Grid.Column="0"></TextBlock>
|
||||
<StackPanel Grid.Row="4" Grid.Column="1">
|
||||
|
|
|
@ -1,11 +1,44 @@
|
|||
using System;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace BililiveRecorder.WPF
|
||||
{
|
||||
internal class ClipButtonVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is null) return Visibility.Visible;
|
||||
return EnabledFeature.RecordOnly == ((EnabledFeature)value) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class BindingProxy : Freezable
|
||||
{
|
||||
protected override Freezable CreateInstanceCore()
|
||||
{
|
||||
return new BindingProxy();
|
||||
}
|
||||
|
||||
public object Data
|
||||
{
|
||||
get { return (object)GetValue(DataProperty); }
|
||||
set { SetValue(DataProperty, value); }
|
||||
}
|
||||
|
||||
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
|
||||
public static readonly DependencyProperty DataProperty = DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
|
||||
}
|
||||
|
||||
internal class ValueConverterGroup : List<IValueConverter>, IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27428.1
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29924.181
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.WPF", "BililiveRecorder.WPF\BililiveRecorder.WPF.csproj", "{0C7D4236-BF43-4944-81FE-E07E05A3F31D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.Core", "BililiveRecorder.Core\BililiveRecorder.Core.csproj", "{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Core", "BililiveRecorder.Core\BililiveRecorder.Core.csproj", "{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{51748048-1949-4218-8DED-94014ABE7633} = {51748048-1949-4218-8DED-94014ABE7633}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.FlvProcessor", "BililiveRecorder.FlvProcessor\BililiveRecorder.FlvProcessor.csproj", "{51748048-1949-4218-8DED-94014ABE7633}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.FlvProcessor", "BililiveRecorder.FlvProcessor\BililiveRecorder.FlvProcessor.csproj", "{51748048-1949-4218-8DED-94014ABE7633}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Cli", "BililiveRecorder.Cli\BililiveRecorder.Cli.csproj", "{1B626335-283F-4313-9045-B5B96FAAB2DF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
@ -18,10 +20,6 @@ Global
|
|||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{51748048-1949-4218-8DED-94014ABE7633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{51748048-1949-4218-8DED-94014ABE7633}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{51748048-1949-4218-8DED-94014ABE7633}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{51748048-1949-4218-8DED-94014ABE7633}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0C7D4236-BF43-4944-81FE-E07E05A3F31D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0C7D4236-BF43-4944-81FE-E07E05A3F31D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0C7D4236-BF43-4944-81FE-E07E05A3F31D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
@ -30,6 +28,14 @@ Global
|
|||
{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{51748048-1949-4218-8DED-94014ABE7633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{51748048-1949-4218-8DED-94014ABE7633}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{51748048-1949-4218-8DED-94014ABE7633}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{51748048-1949-4218-8DED-94014ABE7633}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
image: Visual Studio 2017
|
||||
image: Visual Studio 2019
|
||||
version: 0.0.0.{build}
|
||||
platform: Any CPU
|
||||
skip_tags: true
|
||||
|
|
6
global.json
Normal file
6
global.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "3.1.102",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user