diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e0fac7..6d90b97 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -36,6 +39,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Setup .NET on Windows uses: actions/setup-dotnet@v4 with: @@ -63,6 +69,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init --recursive + - name: Setup .NET uses: actions/setup-dotnet@v4 with: diff --git a/Application/BocchiTracker.WPF.sln b/Application/BocchiTracker.WPF.sln index a7b5ef3..d16bf52 100644 --- a/Application/BocchiTracker.WPF.sln +++ b/Application/BocchiTracker.WPF.sln @@ -50,6 +50,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameCaptureRTC", "Models\Ga EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BocchiTracker.WebRTCTest", "Tests\BocchiTracker.WebRTCTest\BocchiTracker.WebRTCTest.csproj", "{B3675F7D-DF3F-4F0E-A9E0-9575381BC964}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExternalTools", "ExternalTools", "{75B25179-1B35-461D-80AA-80EF52E7E101}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "websocket-sharp", "..\ExternalTools\websocket-sharp\websocket-sharp\websocket-sharp.csproj", "{0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -194,14 +198,21 @@ Global {ADEDB772-6FFB-4198-9107-1AD791899679}.Release|Any CPU.Build.0 = Release|Any CPU {ADEDB772-6FFB-4198-9107-1AD791899679}.Release|x64.ActiveCfg = Release|Any CPU {ADEDB772-6FFB-4198-9107-1AD791899679}.Release|x64.Build.0 = Release|Any CPU - {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Debug|Any CPU.ActiveCfg = Debug|x64 + {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Debug|Any CPU.Build.0 = Debug|x64 {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Debug|x64.ActiveCfg = Debug|x64 {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Debug|x64.Build.0 = Debug|x64 - {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Release|Any CPU.Build.0 = Release|Any CPU + {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Release|Any CPU.ActiveCfg = Release|x64 {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Release|x64.ActiveCfg = Release|x64 {B3675F7D-DF3F-4F0E-A9E0-9575381BC964}.Release|x64.Build.0 = Release|x64 + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}.Debug|x64.ActiveCfg = Debug|Any CPU + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}.Debug|x64.Build.0 = Debug|Any CPU + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}.Release|Any CPU.Build.0 = Release|Any CPU + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}.Release|x64.ActiveCfg = Release|Any CPU + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -226,6 +237,7 @@ Global {D214D1D6-7DBA-4F53-B070-9672BA3F7CBA} = {12AAC0C3-970D-43F6-BD47-0B662B7157F6} {ADEDB772-6FFB-4198-9107-1AD791899679} = {677ECDC0-9125-4E30-8B8C-E0CC5F98DFF5} {B3675F7D-DF3F-4F0E-A9E0-9575381BC964} = {E876F453-952B-4D58-AA0E-1D95DFF58FB5} + {0D0785D7-4A07-4FA0-919F-FD3AB6AED2F4} = {75B25179-1B35-461D-80AA-80EF52E7E101} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F04B3FFB-34D2-4BBE-85BA-F28DEE1BCAB6} diff --git a/Application/Models/Config/Configs/ProjectConfig.cs b/Application/Models/Config/Configs/ProjectConfig.cs index e1ec303..b2b8a89 100644 --- a/Application/Models/Config/Configs/ProjectConfig.cs +++ b/Application/Models/Config/Configs/ProjectConfig.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using BocchiTracker.Config.Parts; using BocchiTracker.ServiceClientData; namespace BocchiTracker.Config.Configs @@ -49,14 +50,11 @@ public class ServiceConfig public List DefaultValue { get; set;} = new List(); } - public class ExternalToolsPath - { - public string? ProcDumpPath { get; set; } - } - public class ProjectConfig { - public int Port { get; set; } = 8888; + public int Port { get; set; } = 8888; + + public int WebSocketPort { get; set; } = 8822; public List TicketTypes { get; set; } = new List { "Bug", "Task", "Question" }; diff --git a/Application/Models/Config/Configs/UserConfig.cs b/Application/Models/Config/Configs/UserConfig.cs index f6c613c..a0cdc11 100644 --- a/Application/Models/Config/Configs/UserConfig.cs +++ b/Application/Models/Config/Configs/UserConfig.cs @@ -1,4 +1,5 @@ -using BocchiTracker.ServiceClientData; +using BocchiTracker.Config.Parts; +using BocchiTracker.ServiceClientData; using System; using System.Collections.Generic; using System.Linq; @@ -7,20 +8,9 @@ namespace BocchiTracker.Config.Configs { - public class UserCaptureSetting - { - public GameCaptureType GameCaptureType { get; set; } = GameCaptureType.NotUse; - - public bool IncludeAudio = false; - - public int RecordingFrameRate { get; set; } = 30; - - public int RecordingMintes { get; set; } = 3; - } - public class UserConfig { - public UserCaptureSetting UserCaptureSetting { get; set; } = new UserCaptureSetting(); + public CaptureSetting CaptureSetting { get; set; } = new CaptureSetting(); public string? ProjectConfigFilename { get; set; } diff --git a/Application/Models/Config/Parts/CaptureSetting.cs b/Application/Models/Config/Parts/CaptureSetting.cs new file mode 100644 index 0000000..f380dcd --- /dev/null +++ b/Application/Models/Config/Parts/CaptureSetting.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BocchiTracker.Config.Parts +{ + public class CaptureSetting + { + public GameCaptureType GameCaptureType { get; set; } = GameCaptureType.NotUse; + + public SIPSorceryMedia.Abstractions.VideoCodecsEnum VideoCodecs { get; set; } = SIPSorceryMedia.Abstractions.VideoCodecsEnum.VP8; + + public bool IncludeAudio = false; + + public int RecordingFrameRate { get; set; } = 30; + + public int RecordingMintes { get; set; } = 3; + } +} diff --git a/Application/Models/Config/Parts/ExternalToolsPath.cs b/Application/Models/Config/Parts/ExternalToolsPath.cs new file mode 100644 index 0000000..af1815c --- /dev/null +++ b/Application/Models/Config/Parts/ExternalToolsPath.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BocchiTracker.Config.Parts +{ + public class ExternalToolsPath + { + public string? ProcDumpPath { get; set; } + + public string? FFmpegPath { get; set; } + } +} diff --git a/Application/Models/GameCaptureRTC/CaptureFrameStorage.cs b/Application/Models/GameCaptureRTC/CaptureFrameStorage.cs index fb0dbdb..5dc2883 100644 --- a/Application/Models/GameCaptureRTC/CaptureFrameStorage.cs +++ b/Application/Models/GameCaptureRTC/CaptureFrameStorage.cs @@ -1,44 +1,155 @@  +using BocchiTracker.ModelEvent; +using FFMpegCore; +using OpenCvSharp; +using Prism.Events; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using System; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; namespace BocchiTracker.GameCaptureRTC { - public class CaptureFrameStorage + public class CaptureFrameStorage : IDisposable { - private int _frameRate = 30; - private int _recordingMaxFrames = 0; - public ModelEvent.CaptureStreamParameter CaptureStreamParameter { get; set; } = new ModelEvent.CaptureStreamParameter(); + private object _mutext = new object(); - public CaptureFrameStorage(int inFrameRate, int inRecordingMintes) + private int _maxRecordingFrameCount = 0; + private int _maxSplitFrameCount = 0; + private int _curFrameCount = 0; + private int _curSpliteFrameCount = 0; + + private string _tempCancatMovieDirectory = Path.Combine(Path.GetTempPath(), "BocchiTracker", "temp", "concat_movies"); + private string _tempMovieDirectory = Path.Combine(Path.GetTempPath(), "BocchiTracker", "temp", "movies"); + private string _tempPicsDirectory = Path.Combine(Path.GetTempPath(), "BocchiTracker", "temp", "pics"); + + private int _movieID = 0; + private int _adjustedwidth = 640; + private int _adjustedHeight = 480; + + private VideoWriter _videoWriter = default!; + + public CaptureFrameStorage(string inFFmpegPath, int inMaxRecordingFrameCount, int inMaxSplitFrameCount) { - _frameRate = inFrameRate; - _recordingMaxFrames = (inRecordingMintes * 60) * _frameRate; + GlobalFFOptions.Configure(options => options.BinaryFolder = inFFmpegPath); + + _maxSplitFrameCount = inMaxSplitFrameCount; + _maxRecordingFrameCount = inMaxRecordingFrameCount; + + if (!Directory.Exists(_tempMovieDirectory)) + Directory.CreateDirectory(_tempMovieDirectory); + if (!Directory.Exists(_tempCancatMovieDirectory)) + Directory.CreateDirectory(_tempCancatMovieDirectory); + if (!Directory.Exists(_tempPicsDirectory)) + Directory.CreateDirectory(_tempPicsDirectory); + + Cleanup(); } public void AddFrame(int inWidth, int inHeight, int inStride, nint inData) { - if (CaptureStreamParameter.Frames.Count > _recordingMaxFrames) - CaptureStreamParameter.Frames.RemoveAt(0); - - // メモリ領域のコピーを作成して渡す - byte[] dataCopy = new byte[inHeight * inStride]; - unsafe + lock(_mutext) { - byte* src = (byte*)inData; - for (int i = 0; i < dataCopy.Length; i++) + if(_adjustedwidth > inWidth || _adjustedwidth == 0) + _adjustedwidth = inWidth % 2 == 0 ? inWidth : inWidth - 1; + if (_adjustedHeight > inHeight || _adjustedHeight == 0) + _adjustedHeight = inHeight % 2 == 0 ? inHeight : inHeight - 1; + + if (_curSpliteFrameCount == 0) + { + System.Console.WriteLine("start video capture"); + if (_videoWriter == null || _videoWriter.IsDisposed) + _videoWriter = new VideoWriter(); + _videoWriter.Open(Path.Combine(_tempMovieDirectory, $"movie.{_movieID}.mp4"), FourCC.MPG4, 30, new OpenCvSharp.Size(inWidth, inHeight)); + ++_movieID; + } + + unsafe + { + byte[] dataCopy = new byte[inHeight * inStride]; + unsafe + { + byte* src = (byte*)inData; + for (int i = 0; i < dataCopy.Length; i++) + { + dataCopy[i] = *(src + i); + } + } + + using (var mat = new Mat(inHeight, inWidth, MatType.CV_8UC3, dataCopy, inStride)) + { + _videoWriter.Write(mat); + + _curFrameCount++; + _curSpliteFrameCount++; + } + } + + if (_curSpliteFrameCount > _maxSplitFrameCount) { - dataCopy[i] = *(src + i); + System.Console.WriteLine("_videoWriter wrote maximum frame, so next video..."); + _curSpliteFrameCount = 0; + _videoWriter.Dispose(); + } + + if (_curFrameCount > _maxRecordingFrameCount) + { + var bochi_files = Directory.GetFiles(_tempMovieDirectory, "*.mp4") + .OrderBy(file => int.Parse(Regex.Match(file, @"(?<=movie\.)(\d+)(?=\.mp4)").Value)) + .ToList(); + if(bochi_files.Any()) + { + System.Console.WriteLine("the maximum recording time has been exceeded, so remove old video file"); + File.Delete(bochi_files[0]); + } + _curFrameCount -= _maxSplitFrameCount; } } + } - CaptureStreamParameter.Frames.Add(new ModelEvent.CaptureStreamParameter.Frame + public string ConcatMovie() + { + lock (_mutext) { - Data = dataCopy, - Width = inWidth, - Height = inHeight, - Stride = inStride, - }); + Dispose(); + _curFrameCount = 0; + _curSpliteFrameCount = 0; + + var output = Path.Combine(_tempCancatMovieDirectory, "bocchi_movie.mp4"); + var bochi_files = Directory.GetFiles(_tempMovieDirectory, "*.mp4"); + if (!bochi_files.Any()) + return string.Empty; + + var command = FFMpegArguments + .FromDemuxConcatInput(bochi_files) + .OutputToFile(output, overwrite: true, op => op.Resize(_adjustedwidth, _adjustedHeight)); + bool ret = command.ProcessSynchronously(); + if (ret) + Cleanup(); + return output; + } + } + + public void Cleanup() + { + var bochi_files = Directory.GetFiles(_tempMovieDirectory, "*.mp4"); + foreach (var file in bochi_files) + { + File.Delete(file); + } + } + + public void Dispose() + { + if (_videoWriter != null && !_videoWriter.IsDisposed) + { + _videoWriter.Dispose(); + } } } } diff --git a/Application/Models/GameCaptureRTC/GameCaptureRTC.csproj b/Application/Models/GameCaptureRTC/GameCaptureRTC.csproj index da6bc2b..090d987 100644 --- a/Application/Models/GameCaptureRTC/GameCaptureRTC.csproj +++ b/Application/Models/GameCaptureRTC/GameCaptureRTC.csproj @@ -8,17 +8,28 @@ + + + + + + + + + + - + + diff --git a/Application/Models/GameCaptureRTC/Module.cs b/Application/Models/GameCaptureRTC/Module.cs index b23c08a..6775285 100644 --- a/Application/Models/GameCaptureRTC/Module.cs +++ b/Application/Models/GameCaptureRTC/Module.cs @@ -3,7 +3,7 @@ namespace BocchiTracker.GameCaptureRTC { - public class Module : IModule + public class GameCaptureRTCModule : IModule { public void OnInitialized(IContainerProvider containerProvider) {} diff --git a/Application/Models/GameCaptureRTC/Protocol/ICaptureProtocol.cs b/Application/Models/GameCaptureRTC/Protocol/ICaptureProtocol.cs index 48731fe..35a1554 100644 --- a/Application/Models/GameCaptureRTC/Protocol/ICaptureProtocol.cs +++ b/Application/Models/GameCaptureRTC/Protocol/ICaptureProtocol.cs @@ -1,4 +1,5 @@ -using System; +using BocchiTracker.Config.Configs; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -8,7 +9,7 @@ namespace BocchiTracker.GameCaptureRTC.Protocol { public interface ICaptureProtocol { - void Start(); + void Start(int inPort, string inFFmpegPath, Config.Parts.CaptureSetting inCaptureSetting); void Stop(); diff --git a/Application/Models/GameCaptureRTC/Protocol/OBSCapture.cs b/Application/Models/GameCaptureRTC/Protocol/OBSCapture.cs index 448d8e8..86119d3 100644 --- a/Application/Models/GameCaptureRTC/Protocol/OBSCapture.cs +++ b/Application/Models/GameCaptureRTC/Protocol/OBSCapture.cs @@ -3,26 +3,23 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using BocchiTracker.Config.Configs; using OBSWebsocketDotNet; namespace BocchiTracker.GameCaptureRTC.Protocol { public class OBSCapture : ICaptureProtocol { - private OBSWebsocket _obsWebSocket; + private OBSWebsocket _obsWebSocket = default!; - public OBSCapture(int inPort, string inCaptureSource) - { - _obsWebSocket = new OBSWebsocket(); - var scenes = _obsWebSocket.ListScenes(); - } + public OBSCapture() {} public bool IsConnect() { throw new NotImplementedException(); } - - public void Start() + + public void Start(int inPort, string inFFmpegPath, Config.Parts.CaptureSetting inCaptureSetting) { throw new NotImplementedException(); } diff --git a/Application/Models/GameCaptureRTC/Protocol/WebRTC.cs b/Application/Models/GameCaptureRTC/Protocol/WebRTC.cs index 0a0dd02..977f927 100644 --- a/Application/Models/GameCaptureRTC/Protocol/WebRTC.cs +++ b/Application/Models/GameCaptureRTC/Protocol/WebRTC.cs @@ -10,28 +10,120 @@ using SIPSorceryMedia.Abstractions; using SIPSorceryMedia.FFmpeg; using BocchiTracker.Config.Configs; +using WebSocketSharp; +using Newtonsoft; +using Newtonsoft.Json.Linq; +using System.Net.Sockets; +using System.Threading; +using System.Reflection.Metadata; +using System.IO; namespace BocchiTracker.GameCaptureRTC.Protocol { + public class WebRTCWebSocketPeer : WebSocketBehavior + { + private RTCPeerConnection _pc = default!; + + public Func> CreatePeerConnection = default!; + + public RTCPeerConnection RTCPeerConnection => _pc; + + public RTCOfferOptions OfferOptions { get; set; } = default!; + + public RTCAnswerOptions AnswerOptions { get; set; } = default!; + + protected override async void OnMessage(MessageEventArgs e) + { + RTCSessionDescriptionInit init2; + if (RTCIceCandidateInit.TryParse(e.Data, out var init)) + { + Console.WriteLine("Got remote ICE candidate."); + _pc.addIceCandidate(init); + } + else if (RTCSessionDescriptionInit.TryParse(e.Data, out init2)) + { + Console.WriteLine($"Got remote SDP, type {init2.type}."); + SetDescriptionResultEnum setDescriptionResultEnum = _pc.setRemoteDescription(init2); + if (setDescriptionResultEnum != 0) + { + Console.WriteLine($"Failed to set remote description, {setDescriptionResultEnum}."); + _pc.Close("failed to set remote description"); + base.Close(); + } + else if (_pc.signalingState == RTCSignalingState.have_remote_offer) + { + RTCSessionDescriptionInit answerSdp = _pc.createAnswer(AnswerOptions); + await _pc.setLocalDescription(answerSdp).ConfigureAwait(continueOnCapturedContext: false); + Console.WriteLine($"Sending SDP answer to client {Context.UserEndPoint}."); + Context.WebSocket.Send(AddPlayerIdJson(answerSdp.toJSON())); + } + } + } + + protected override async void OnOpen() + { + base.OnOpen(); + Console.WriteLine($"Web socket client connection from {Context.UserEndPoint}."); + _pc = await CreatePeerConnection().ConfigureAwait(continueOnCapturedContext: false); + + var rtc_config = _pc.getConfiguration(); + var data = Newtonsoft.Json.JsonConvert.SerializeObject( + new Dictionary + { + { "type", "config" }, + { "peerConnectionOptions", new Dictionary { + { "bundlePolicy", rtc_config.bundlePolicy.ToString() }, + { "certificates", rtc_config.certificates }, + { "iceCandidatePoolSize", rtc_config.iceCandidatePoolSize }, + { "iceServers", rtc_config.iceServers }, + { "iceTransportPolicy", rtc_config.iceTransportPolicy.ToString() }, + { "rtcpMuxPolicy", rtc_config.rtcpMuxPolicy.ToString() }, + } + }, + } + ); + Context.WebSocket.Send(data); + + _pc.onicecandidate += delegate (RTCIceCandidate iceCandidate) + { + if (_pc.signalingState == RTCSignalingState.have_remote_offer || _pc.signalingState == RTCSignalingState.stable) + { + Context.WebSocket.Send(AddPlayerIdJson(iceCandidate.toJSON())); + } + }; + + RTCSessionDescriptionInit offerSdp = _pc.createOffer(OfferOptions); + await _pc.setLocalDescription(offerSdp).ConfigureAwait(continueOnCapturedContext: false); + Console.WriteLine($"Sending SDP offer to client {Context.UserEndPoint}."); + Context.WebSocket.Send(AddPlayerIdJson(offerSdp.toJSON())); + } + + private string AddPlayerIdJson(string inJson) + { + string custom_json = inJson; + custom_json = custom_json.Insert(inJson.LastIndexOf('}'), ", \"playerid\": \"BocchiTrackerPlayer\""); + return custom_json; + } + } + public class WebRTC : WebSocketBehavior, ICaptureProtocol, IDisposable { private readonly IEventAggregator _eventAggregator; - private WebSocketServer _web_socket; - private CaptureFrameStorage _captureFrameStorage; + private WebSocketServer _web_socket = default!; + private CaptureFrameStorage _captureFrameStorage = default!; private static bool _isConnecting; + private SubscriptionToken _subscriptionToken; - public WebRTC(IEventAggregator inEventAggregator, int inPort, bool inSecure, CaptureSetting inCaptureSetting, UserCaptureSetting inUserCaptureSetting) + public WebRTC(IEventAggregator inEventAggregator) { _eventAggregator = inEventAggregator; - _captureFrameStorage = new CaptureFrameStorage(inUserCaptureSetting.RecordingFrameRate, inUserCaptureSetting.RecordingMintes); - _web_socket = new WebSocketServer(IPAddress.Any, inPort, inSecure); - _web_socket.AddWebSocketService("/", (peer) => peer.CreatePeerConnection = () => CreatePeerConnection(inCaptureSetting, inUserCaptureSetting, _captureFrameStorage)); - _web_socket.Start(); + _subscriptionToken = _eventAggregator.GetEvent().Subscribe(Stop, ThreadOption.BackgroundThread); } public void Dispose() { _web_socket.Stop(); + _eventAggregator.GetEvent().Unsubscribe(_subscriptionToken); } public bool IsConnect() @@ -39,22 +131,32 @@ public bool IsConnect() return _isConnecting; } - public void Start() + public void Start(int inPort, string inFFmpegPath, Config.Parts.CaptureSetting inCaptureSetting) { + string? ffmpeg = inFFmpegPath; + if (ffmpeg == null || !Path.Exists(ffmpeg)) + throw new Exception("FFmpeg path is not set."); + int maxFrameCount = (60 * inCaptureSetting.RecordingFrameRate) * inCaptureSetting.RecordingMintes; + _captureFrameStorage = new CaptureFrameStorage(ffmpeg, maxFrameCount, maxFrameCount / 10); + _web_socket = new WebSocketServer(IPAddress.Any, inPort, false); + _web_socket.Log.Level = WebSocketSharp.LogLevel.Trace; + _web_socket.AllowForwardedRequest = true; + _web_socket.AddWebSocketService("/", (peer) => peer.CreatePeerConnection = () => CreatePeerConnection(ffmpeg, inCaptureSetting, _captureFrameStorage)); + _web_socket.Start(); } public void Stop() { - _eventAggregator - .GetEvent() - .Publish(new GameCaptureFinishEventParameter { CaptureStreamParameter = _captureFrameStorage.CaptureStreamParameter }); - _captureFrameStorage.CaptureStreamParameter.Frames.Clear(); + string movie = _captureFrameStorage.ConcatMovie(); + if(movie.IsNullOrEmpty()) + return; + _eventAggregator.GetEvent().Publish(movie); } - private static Task CreatePeerConnection(CaptureSetting inCaptureSetting, UserCaptureSetting inUserCaptureSetting, CaptureFrameStorage inFrameStorage) + private static Task CreatePeerConnection(string inFFmpegPath, Config.Parts.CaptureSetting inCaptureSetting, CaptureFrameStorage inFrameStorage) { - FFmpegInit.Initialise(FfmpegLogLevelEnum.AV_LOG_VERBOSE, inCaptureSetting.FFmpegPath); + FFmpegInit.Initialise(FfmpegLogLevelEnum.AV_LOG_VERBOSE, inFFmpegPath); var videoEP = new FFmpegVideoEndPoint(); videoEP.RestrictFormats(format => format.Codec == inCaptureSetting.VideoCodecs); @@ -73,7 +175,7 @@ private static Task CreatePeerConnection(CaptureSetting inCap }; var pc = new RTCPeerConnection(config); - if (inUserCaptureSetting.IncludeAudio) + if (inCaptureSetting.IncludeAudio) { MediaStreamTrack audioTrack = new MediaStreamTrack(SDPMediaTypesEnum.audio, false, new List { new SDPAudioVideoMediaFormat(SDPWellKnownMediaFormatsEnum.PCMU) }, MediaStreamStatusEnum.RecvOnly); diff --git a/Application/Models/GameCaptureRTC/RecordingController.cs b/Application/Models/GameCaptureRTC/RecordingController.cs index f586a1a..90fa955 100644 --- a/Application/Models/GameCaptureRTC/RecordingController.cs +++ b/Application/Models/GameCaptureRTC/RecordingController.cs @@ -1,6 +1,8 @@ -using BocchiTracker.GameCaptureRTC.Protocol; +using BocchiTracker.Config.Configs; +using BocchiTracker.GameCaptureRTC.Protocol; using BocchiTracker.ModelEvent; using Prism.Events; +using System.Threading.Tasks; namespace BocchiTracker.GameCaptureRTC { @@ -13,9 +15,6 @@ public class RecordingController : ICaptureProtocol public RecordingController(IEventAggregator inEventAggregator) { _eventAggregator = inEventAggregator; - _eventAggregator - .GetEvent() - .Subscribe(OnConfigReload); } public bool IsConnect() @@ -25,36 +24,31 @@ public bool IsConnect() return _captureProtocol.IsConnect(); } - public void Start() + public void Start(int inPort, string inFFmpegPath, Config.Parts.CaptureSetting inCaptureSetting) { - _captureProtocol?.Start(); - } - - public void Stop() - { - _captureProtocol?.Stop(); - } - - private void OnConfigReload(ConfigReloadEventParameter inParam) - { - if (inParam.UserConfig == null || inParam.ProjectConfig == null) - return; - - switch (inParam.UserConfig.UserCaptureSetting.GameCaptureType) + switch (inCaptureSetting.GameCaptureType) { case Config.GameCaptureType.OBSStudio: { - _captureProtocol = new Protocol.OBSCapture(inParam.ProjectConfig.Port, ""); + _captureProtocol = new Protocol.OBSCapture(); } break; case Config.GameCaptureType.WebRTC: { - _captureProtocol = new Protocol.WebRTC(_eventAggregator, inParam.ProjectConfig.Port, false, inParam.ProjectConfig.CaptureSetting, inParam.UserConfig.UserCaptureSetting); + _captureProtocol = new Protocol.WebRTC(_eventAggregator); } break; default: break; } + + if(_captureProtocol != null) + _captureProtocol.Start(inPort, inFFmpegPath, inCaptureSetting); + } + + public void Stop() + { + _captureProtocol?.Stop(); } } } diff --git a/Application/Models/IssueAssetCollector/Handlers/CreateActionHandler.cs b/Application/Models/IssueAssetCollector/Handlers/CreateActionHandler.cs index bda6986..c956392 100644 --- a/Application/Models/IssueAssetCollector/Handlers/CreateActionHandler.cs +++ b/Application/Models/IssueAssetCollector/Handlers/CreateActionHandler.cs @@ -2,6 +2,7 @@ using BocchiTracker.Config.Configs; using BocchiTracker.IssueAssetCollector.Handlers.Coredump; using BocchiTracker.IssueAssetCollector.Handlers.Log; +using BocchiTracker.IssueAssetCollector.Handlers.Movie; using BocchiTracker.IssueAssetCollector.Handlers.Screenshot; using BocchiTracker.ModelEvent; using BocchiTracker.ProcessLinkQuery.Queries; @@ -26,13 +27,15 @@ public class CreateActionHandler : ICreateActionHandler private readonly IEventAggregator _eventAggregator; private readonly IFilenameGeneratorFactory _filenameGeneratorFactory; private readonly ProjectConfig _projectConfig; + private readonly UserConfig _userConfig; private readonly AppStatusBundles _appStatusBundles; - public CreateActionHandler(IEventAggregator inEventAggregator, IFilenameGeneratorFactory inFilenameGenFac, AppStatusBundles inAppStatusBundles, ProjectConfig inConfig) + public CreateActionHandler(IEventAggregator inEventAggregator, IFilenameGeneratorFactory inFilenameGenFac, AppStatusBundles inAppStatusBundles, ProjectConfig inProjectConfig, UserConfig inUserConfig) { _eventAggregator = inEventAggregator; _filenameGeneratorFactory = inFilenameGenFac; - _projectConfig = inConfig; + _projectConfig = inProjectConfig; + _userConfig = inUserConfig; _appStatusBundles = inAppStatusBundles; } @@ -71,6 +74,12 @@ public IHandle Create(Type inType) _cacheHandles.Add(inType, handler); } + if(inType == typeof(MovieHandler)) + { + var handler = new MovieHandler(_eventAggregator, _filenameGeneratorFactory.GetFilenameGenerator(typeof(TimestampedFilenameGenerator))); + _cacheHandles.Add(inType, handler); + } + return _cacheHandles[inType]; } } diff --git a/Application/Models/IssueAssetCollector/Handlers/Movie/MovieHandler.cs b/Application/Models/IssueAssetCollector/Handlers/Movie/MovieHandler.cs index 3c1a221..7a41cff 100644 --- a/Application/Models/IssueAssetCollector/Handlers/Movie/MovieHandler.cs +++ b/Application/Models/IssueAssetCollector/Handlers/Movie/MovieHandler.cs @@ -1,21 +1,52 @@ using BocchiTracker.ApplicationInfoCollector; +using BocchiTracker.ModelEvent; +using Prism.Events; using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BocchiTracker.IssueAssetCollector.Handlers.Movie { + public class GameCaptureFrameConvertMovieProcess + { + public string Output { get; set; } = string.Empty; + private IEventAggregator _eventAggregator; + + public GameCaptureFrameConvertMovieProcess(IEventAggregator inEventAggregator) + { + _eventAggregator = inEventAggregator; + _eventAggregator + .GetEvent() + .Subscribe(OnGameCaptureFinishEvent, ThreadOption.BackgroundThread); + } + + public void OnGameCaptureFinishEvent(string inMoviePath) + { + File.Copy(inMoviePath, Output, true); + } + } + public class MovieHandler : IHandle { public IFilenameGenerator _filenameGenerator { private set; get; } + private IEventAggregator _eventAggregator; + private GameCaptureFrameConvertMovieProcess _convert_movie_process; - public MovieHandler(IFilenameGenerator inFilenameGenerator) + public MovieHandler(IEventAggregator inEventAggregator, IFilenameGenerator inFilenameGenerator) { _filenameGenerator = inFilenameGenerator; + _eventAggregator = inEventAggregator; + _convert_movie_process = new GameCaptureFrameConvertMovieProcess(inEventAggregator); } - public virtual void Handle(AppStatusBundle inAppStatusBundle, int inPID, string inOutput) { } + public void Handle(AppStatusBundle inAppStatusBundle, int inPID, string inOutput) + { + this._convert_movie_process.Output = Path.Combine(inOutput, _filenameGenerator.Generate(inAppStatusBundle) + ".mp4"); + _eventAggregator.GetEvent().Publish(); + } } } diff --git a/Application/Models/IssueAssetCollector/IssueAssetCollector.csproj b/Application/Models/IssueAssetCollector/IssueAssetCollector.csproj index b413a49..fb9b6fc 100644 --- a/Application/Models/IssueAssetCollector/IssueAssetCollector.csproj +++ b/Application/Models/IssueAssetCollector/IssueAssetCollector.csproj @@ -16,9 +16,8 @@ - - + diff --git a/Application/Models/ModelEvent/GameCaptureEvent.cs b/Application/Models/ModelEvent/GameCaptureEvent.cs index 638e4e8..791990e 100644 --- a/Application/Models/ModelEvent/GameCaptureEvent.cs +++ b/Application/Models/ModelEvent/GameCaptureEvent.cs @@ -1,36 +1,15 @@ using Prism.Events; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Threading.Tasks; namespace BocchiTracker.ModelEvent { - public class OBSStudioParameter - { - public string MoviePath { get; set; } = string.Empty; - } + public class GameCaptureStopRequest : PubSubEvent { } - public class CaptureStreamParameter - { - public class Frame - { - public int Width { get; set; } - public int Height { get; set; } - public int Stride { get; set; } - public byte[]? Data { get; set; } - } - public List Frames { get; set; } = new List(); - } - - public class GameCaptureFinishEventParameter - { - public CaptureStreamParameter? CaptureStreamParameter { get; set; } - public OBSStudioParameter? OBSStudioParameter { get; set; } - } - - public class GameCaptureStartEvent : PubSubEvent { } - - public class GameCaptureFinishEvent : PubSubEvent { } + public class GameCaptureFinishEvent : PubSubEvent { } } diff --git a/Application/Tests/BocchiTracker.WebRTCTest/BocchiTracker.WebRTCTest.csproj b/Application/Tests/BocchiTracker.WebRTCTest/BocchiTracker.WebRTCTest.csproj index b37329e..4a1f520 100644 --- a/Application/Tests/BocchiTracker.WebRTCTest/BocchiTracker.WebRTCTest.csproj +++ b/Application/Tests/BocchiTracker.WebRTCTest/BocchiTracker.WebRTCTest.csproj @@ -9,11 +9,12 @@ YutoArita ..\Artifact true - AnyCPU;x64 + x64 7.0 + diff --git a/Application/Tests/BocchiTracker.WebRTCTest/Program.cs b/Application/Tests/BocchiTracker.WebRTCTest/Program.cs index c67c8a1..adf0416 100644 --- a/Application/Tests/BocchiTracker.WebRTCTest/Program.cs +++ b/Application/Tests/BocchiTracker.WebRTCTest/Program.cs @@ -11,28 +11,20 @@ class Program { static void Main() { + string ffmpeg = "put your ffmpeg path"; + var eventAggregator = new EventAggregator(); var recordingController = new RecordingController(eventAggregator); var movieSaveProcess = new GameCaptureFrameConvertMovieProcess(eventAggregator); - eventAggregator.GetEvent().Publish(new ConfigReloadEventParameter - ( - new Config.Configs.ProjectConfig { - Port = 8888, - CaptureSetting = new Config.Configs.CaptureSetting - { - FFmpegPath = @"C:\Users\maris\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg.Shared_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-6.1.1-full_build-shared\bin", - VideoCodecs = SIPSorceryMedia.Abstractions.VideoCodecsEnum.VP8 - } - }, - new Config.Configs.UserConfig { - UserCaptureSetting = new Config.Configs.UserCaptureSetting - { - RecordingMintes = 1, - GameCaptureType = Config.GameCaptureType.WebRTC - } + var p_config = new Config.Configs.ProjectConfig(); + var u_config = new Config.Configs.UserConfig + { + CaptureSetting = new Config.Parts.CaptureSetting + { + VideoCodecs = SIPSorceryMedia.Abstractions.VideoCodecsEnum.VP8 } - )); + }; Console.WriteLine("サーバー接続中..."); while (!recordingController.IsConnect()) @@ -40,7 +32,7 @@ static void Main() Console.WriteLine("キャプチャーを開始しました。"); { - recordingController.Start(); + recordingController.Start(p_config.WebSocketPort, ffmpeg, u_config.CaptureSetting); Thread.Sleep(5000); recordingController.Stop(); } diff --git a/Application/WPF/BocchiTracker.Client.Config/ViewModels/DirectoryViewModel.cs b/Application/WPF/BocchiTracker.Client.Config/ViewModels/DirectoryViewModel.cs index b793a54..f9f0e16 100644 --- a/Application/WPF/BocchiTracker.Client.Config/ViewModels/DirectoryViewModel.cs +++ b/Application/WPF/BocchiTracker.Client.Config/ViewModels/DirectoryViewModel.cs @@ -72,10 +72,15 @@ public class ExternalToolPathes { public ReactiveProperty ProcdumpPath { get; set; } + public ReactiveProperty FFmpegPath { get; set; } + public ExternalToolPathes(ProjectConfig inProjectConfig) { ProcdumpPath = new ReactiveProperty(); ProcdumpPath.Subscribe(value => inProjectConfig.ExternalToolsPath.ProcDumpPath = value); + + FFmpegPath = new ReactiveProperty(); + FFmpegPath.Subscribe(value => inProjectConfig.ExternalToolsPath.FFmpegPath = value); } } @@ -124,6 +129,7 @@ private void OnConfigReload(ConfigReloadEventParameter inParam) MonitoredDirectories.OnAddItem(new Tuple(dir.Directory, dir.Filter)); } ExternalToolPathes.ProcdumpPath.Value = config.ExternalToolsPath.ProcDumpPath; + ExternalToolPathes.FFmpegPath.Value = config.ExternalToolsPath.FFmpegPath; FileSaveDirectory.WorkingDirectory.Value = config.FileSaveDirectory; FileSaveDirectory.CacheDirectory.Value = config.CacheDirectory; } @@ -144,6 +150,7 @@ private void OnSaveConfig() projectConfig.MonitoredDirectoryConfigs.Add(moniteredDirectory); } projectConfig.ExternalToolsPath.ProcDumpPath = ExternalToolPathes.ProcdumpPath.Value; + projectConfig.ExternalToolsPath.FFmpegPath = ExternalToolPathes.FFmpegPath.Value; projectConfig.CacheDirectory = FileSaveDirectory.CacheDirectory.Value; projectConfig.FileSaveDirectory = FileSaveDirectory.WorkingDirectory.Value; } diff --git a/Application/WPF/BocchiTracker.Client.Config/ViewModels/GeneralViewModel.cs b/Application/WPF/BocchiTracker.Client.Config/ViewModels/GeneralViewModel.cs index 2c97a3d..4912a54 100644 --- a/Application/WPF/BocchiTracker.Client.Config/ViewModels/GeneralViewModel.cs +++ b/Application/WPF/BocchiTracker.Client.Config/ViewModels/GeneralViewModel.cs @@ -62,11 +62,17 @@ public AuthenticationURL Slack [Range(1024, 65535, ErrorMessage = "Please enter value in 1024~65535")] public ReactiveProperty TcpPort { get; set; } + [Range(1024, 65535, ErrorMessage = "Please enter value in 1024~65535")] + public ReactiveProperty WebSocketPort { get; set; } + public GeneralViewModel(IEventAggregator inEventAggregator, ProjectConfig inProjectConfig) { TcpPort = new ReactiveProperty("8888").SetValidateAttribute(() => this.TcpPort); TcpPort.Subscribe(value => inProjectConfig.Port = int.Parse(value)); + WebSocketPort = new ReactiveProperty("8822").SetValidateAttribute(() => this.WebSocketPort); + WebSocketPort.Subscribe(value => inProjectConfig.WebSocketPort = int.Parse(value)); + inEventAggregator .GetEvent() .Subscribe(OnConfigReload, ThreadOption.UIThread); diff --git a/Application/WPF/BocchiTracker.Client.Config/Views/DirectoryView.xaml b/Application/WPF/BocchiTracker.Client.Config/Views/DirectoryView.xaml index f883f2d..a3cc051 100644 --- a/Application/WPF/BocchiTracker.Client.Config/Views/DirectoryView.xaml +++ b/Application/WPF/BocchiTracker.Client.Config/Views/DirectoryView.xaml @@ -30,9 +30,16 @@ - + + + + + diff --git a/Application/WPF/BocchiTracker.Client.Config/Views/GeneralView.xaml b/Application/WPF/BocchiTracker.Client.Config/Views/GeneralView.xaml index 59f5dca..85dc492 100644 --- a/Application/WPF/BocchiTracker.Client.Config/Views/GeneralView.xaml +++ b/Application/WPF/BocchiTracker.Client.Config/Views/GeneralView.xaml @@ -41,20 +41,36 @@ - - - - - + + + + + + - - - + + + + + + + + + + + diff --git a/Application/WPF/BocchiTracker.Client/App.xaml.cs b/Application/WPF/BocchiTracker.Client/App.xaml.cs index dd70e9b..09fcd98 100644 --- a/Application/WPF/BocchiTracker.Client/App.xaml.cs +++ b/Application/WPF/BocchiTracker.Client/App.xaml.cs @@ -36,6 +36,7 @@ using BocchiTracker.Config; using BocchiTracker.CrossServiceUploader; using BocchiTracker.ModelEvent; +using BocchiTracker.GameCaptureRTC; namespace BocchiTracker.Client { @@ -56,6 +57,9 @@ protected override void OnExit(ExitEventArgs e) var connection = Container.Resolve(); connection.Stop(); + var recording = Container.Resolve(); + recording.Stop(); + base.OnExit(e); } @@ -128,6 +132,7 @@ protected override void OnInitialized() var cacheProvider = Container.Resolve(); var connection = Container.Resolve(); + var recording = Container.Resolve(); var dataRepository = Container.Resolve(); var issueInfoBundle = Container.Resolve(); var serviceClientFactory = Container.Resolve(); @@ -136,6 +141,7 @@ protected override void OnInitialized() authConfigRepositoryFactory.Initialize(Path.Combine("Configs", nameof(AuthConfig) + "s")); _ = connection.StartAsync(projectConfig.Port); + recording.Start(projectConfig.WebSocketPort, projectConfig.ExternalToolsPath.FFmpegPath, userConfig.CaptureSetting); cacheProvider.SetCacheDirectory(string.IsNullOrEmpty(projectConfig.CacheDirectory) ? Path.GetTempPath() : projectConfig.CacheDirectory); @@ -180,6 +186,11 @@ protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) moduleCatalog.AddModule(); moduleCatalog.AddModule(); moduleCatalog.AddModule(); + moduleCatalog.AddModule( + dependsOn: new string[] + { + typeof(ConfigModule).Name + }); moduleCatalog.AddModule( dependsOn: new string[] { diff --git a/Application/WPF/BocchiTracker.Client/BocchiTracker.Client.csproj b/Application/WPF/BocchiTracker.Client/BocchiTracker.Client.csproj index 2b7312e..b46bb7a 100644 --- a/Application/WPF/BocchiTracker.Client/BocchiTracker.Client.csproj +++ b/Application/WPF/BocchiTracker.Client/BocchiTracker.Client.csproj @@ -8,6 +8,8 @@ False YutoArita ..\Artifact + + x64 @@ -19,6 +21,7 @@ + diff --git a/Application/WPF/BocchiTracker.Client/ViewModels/ReportParts/UtilityViewModel.cs b/Application/WPF/BocchiTracker.Client/ViewModels/ReportParts/UtilityViewModel.cs index ca95b61..397b127 100644 --- a/Application/WPF/BocchiTracker.Client/ViewModels/ReportParts/UtilityViewModel.cs +++ b/Application/WPF/BocchiTracker.Client/ViewModels/ReportParts/UtilityViewModel.cs @@ -30,6 +30,7 @@ using BocchiTracker.Client.Share.Commands; using BocchiTracker.ModelEvent; using BocchiTracker.IssueAssetCollector.Handlers.Log; +using BocchiTracker.IssueAssetCollector.Handlers.Movie; namespace BocchiTracker.Client.ViewModels.ReportParts { @@ -39,6 +40,7 @@ public class UtilityViewModel : BindableBase public ICommand TakeScreenshotCommand { get; private set; } public ICommand CaptureCoredumpCommand { get; private set; } + public ICommand TakeMovieCommand { get; private set; } public ICommand PostIssueCommand { get; private set; } [Required(ErrorMessage = "Required")] @@ -66,6 +68,7 @@ public UtilityViewModel( { TakeScreenshotCommand = new DelegateCommand(OnTakeScreenshot); CaptureCoredumpCommand = new DelegateCommand(OnCaptureCoredump); + TakeMovieCommand = new AsyncCommand(OnTakeMovie); PostIssueCommand = new AsyncCommand(OnPostIssue); PostServices = new ReactiveCollection(); @@ -189,6 +192,22 @@ public void OnTakeScreenshot() handler.Handle(_appStatusBundles.TrackerApplication, 0, _projectConfig.FileSaveDirectory); } + public async Task OnTakeMovie() + { + if (_projectConfig == null) + return; + + _eventAggregator.GetEvent().Publish(new ProgressEventParameter { Message = "Take movie" }); + { + await Task.Run(() => + { + var handler = _createActionHandler.Create(typeof(MovieHandler)); + handler.Handle(_appStatusBundles.TrackerApplication, 0, _projectConfig.FileSaveDirectory); + }); + } + _eventAggregator.GetEvent().Publish(); + } + public void OnConnectedCreateHandle(AppStatusBundle inAppStatusBundle) { { diff --git a/Application/WPF/BocchiTracker.Client/ViewModels/UserConfigParts/MovieCaptureParts.cs b/Application/WPF/BocchiTracker.Client/ViewModels/UserConfigParts/MovieCaptureParts.cs new file mode 100644 index 0000000..978a39b --- /dev/null +++ b/Application/WPF/BocchiTracker.Client/ViewModels/UserConfigParts/MovieCaptureParts.cs @@ -0,0 +1,57 @@ +using BocchiTracker.Config.Configs; +using BocchiTracker.Config; +using BocchiTracker.ServiceClientAdapters; +using Reactive.Bindings; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Prism.Mvvm; + +namespace BocchiTracker.Client.ViewModels.UserConfigParts +{ + public class MovieCaptureParts : BindableBase, IConfig + { + public ReactiveProperty NotUse { get; set; } = new ReactiveProperty(false); + public ReactiveProperty UseWebRTC { get; set; } = new ReactiveProperty(false); + public ReactiveProperty UseOBS { get; set; } = new ReactiveProperty(false); + + private UserConfig _userConfig; + + public void Initialize(CachedConfigRepository inUserConfigRepository, IAuthConfigRepositoryFactory inAuthConfigRepositoryFactory, ProjectConfig inProjectConfig) + { + Debug.Assert(inUserConfigRepository.Load() != null); + + _userConfig = inUserConfigRepository.Load(); + switch(_userConfig.CaptureSetting.GameCaptureType) + { + case GameCaptureType.NotUse: NotUse.Value = true; break; + case GameCaptureType.WebRTC: UseWebRTC.Value = true; break; + case GameCaptureType.OBSStudio: UseOBS.Value = true; break; + } + } + + public void Save(ref bool outIsNeedRestart) + { + outIsNeedRestart |= true; + + if (NotUse.Value) + { + _userConfig.CaptureSetting.GameCaptureType = GameCaptureType.NotUse; + return; + } + if (UseWebRTC.Value) + { + _userConfig.CaptureSetting.GameCaptureType = GameCaptureType.WebRTC; + return; + } + if (UseOBS.Value) + { + _userConfig.CaptureSetting.GameCaptureType = GameCaptureType.OBSStudio; + return; + } + } + } +} diff --git a/Application/WPF/BocchiTracker.Client/ViewModels/UserConfigViewModel.cs b/Application/WPF/BocchiTracker.Client/ViewModels/UserConfigViewModel.cs index 4eaaac0..a4012be 100644 --- a/Application/WPF/BocchiTracker.Client/ViewModels/UserConfigViewModel.cs +++ b/Application/WPF/BocchiTracker.Client/ViewModels/UserConfigViewModel.cs @@ -27,6 +27,7 @@ public class UserConfigViewModel : BindableBase public UserConfigParts.AuthenticationParts AuthenticationParts { get; set; } public UserConfigParts.ChoiceProjectConfigParts ChoiceProjectConfigParts { get; set; } public UserConfigParts.MiscParts MiscParts { get; set; } + public UserConfigParts.MovieCaptureParts MovieCaptureParts { get; set; } private readonly IEventAggregator _eventAggregator; private IAuthConfigRepositoryFactory _authConfigRepository; @@ -53,6 +54,7 @@ public UserConfigViewModel( AuthenticationParts = new UserConfigParts.AuthenticationParts(); MiscParts = new UserConfigParts.MiscParts(); ChoiceProjectConfigParts = new UserConfigParts.ChoiceProjectConfigParts(); + MovieCaptureParts = new UserConfigParts.MovieCaptureParts(); } private void OnConfigReload(ConfigReloadEventParameter inParam) @@ -61,14 +63,14 @@ private void OnConfigReload(ConfigReloadEventParameter inParam) if (userConfig == null) _userConfigRepository.Save(new UserConfig()); - foreach (var ui in new List { AuthenticationParts, ChoiceProjectConfigParts, MiscParts }) + foreach (var ui in new List { AuthenticationParts, ChoiceProjectConfigParts, MiscParts, MovieCaptureParts }) ui.Initialize(_userConfigRepository, _authConfigRepository, _projectConfigRepository.Load()); } public void OnSave() { bool isNeedRestart = false; - foreach (var ui in new List { AuthenticationParts, ChoiceProjectConfigParts, MiscParts }) + foreach (var ui in new List { AuthenticationParts, ChoiceProjectConfigParts, MiscParts, MovieCaptureParts }) ui.Save(ref isNeedRestart); var config = _userConfigRepository.Load(); diff --git a/Application/WPF/BocchiTracker.Client/Views/ReportParts/UtilityView.xaml b/Application/WPF/BocchiTracker.Client/Views/ReportParts/UtilityView.xaml index e0919bd..899f39f 100644 --- a/Application/WPF/BocchiTracker.Client/Views/ReportParts/UtilityView.xaml +++ b/Application/WPF/BocchiTracker.Client/Views/ReportParts/UtilityView.xaml @@ -36,6 +36,18 @@ +