mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
Merged PR 6: Linux capture fixes.
This commit is contained in:
parent
dcbc2e8930
commit
d2bb8b926b
@ -23,11 +23,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.1.3" />
|
||||
<PackageReference Include="Microsoft.WSMan.Management" Version="7.1.3" />
|
||||
<PackageReference Include="Microsoft.WSMan.Runtime" Version="7.1.3" />
|
||||
|
||||
@ -40,15 +40,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.MixedReality.WebRTC" Version="2.0.2" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.2" />
|
||||
<PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="2.80.2" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.Core.ViewModels;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.Core.Interfaces
|
||||
@ -11,6 +12,6 @@ namespace Remotely.Desktop.Core.Interfaces
|
||||
|
||||
Task ReceiveFile(byte[] buffer, string fileName, string messageId, bool endOfFile, bool startOfFile);
|
||||
void OpenFileTransferWindow(Viewer viewer);
|
||||
Task UploadFile(FileUpload file, Viewer viewer, Action<double> progressUpdateCallback);
|
||||
Task UploadFile(FileUpload file, Viewer viewer, CancellationToken cancelToken, Action<double> progressUpdateCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
public interface IDtoMessageHandler
|
||||
{
|
||||
Task ParseMessage(Services.Viewer viewer, byte[] message);
|
||||
Task ParseMessage(Viewer viewer, byte[] message);
|
||||
}
|
||||
public class DtoMessageHandler : IDtoMessageHandler
|
||||
{
|
||||
@ -31,7 +31,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
private IClipboardService ClipboardService { get; }
|
||||
private IFileTransferService FileTransferService { get; }
|
||||
private IKeyboardMouseInput KeyboardMouseInput { get; }
|
||||
public async Task ParseMessage(Services.Viewer viewer, byte[] message)
|
||||
public async Task ParseMessage(Viewer viewer, byte[] message)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -154,25 +154,19 @@ namespace Remotely.Desktop.Core.Services
|
||||
dto.StartOfFile);
|
||||
}
|
||||
|
||||
private async Task GetWindowsSessions(Services.Viewer viewer)
|
||||
private async Task GetWindowsSessions(Viewer viewer)
|
||||
{
|
||||
await viewer.SendWindowsSessions();
|
||||
}
|
||||
|
||||
private void HandleFrameReceived(Services.Viewer viewer)
|
||||
private void HandleFrameReceived(Viewer viewer)
|
||||
{
|
||||
for (int i = 0; i < 5; i++)
|
||||
while (viewer.PendingSentFrames.Count > 0 &&
|
||||
!viewer.IsStalled &&
|
||||
viewer.IsConnected)
|
||||
{
|
||||
if (viewer.PendingSentFrames.TryDequeue(out var frame))
|
||||
if (viewer.PendingSentFrames.TryDequeue(out _))
|
||||
{
|
||||
var roundtrip = (DateTimeOffset.Now - frame.Timestamp).TotalSeconds;
|
||||
var bps = frame.FrameSize / (roundtrip / 2);
|
||||
|
||||
if (bps > viewer.PeakBytesPerSecond)
|
||||
{
|
||||
viewer.PeakBytesPerSecond = bps;
|
||||
Debug.WriteLine($"Peak Mbps: {bps / 1024 / 1024 * 8}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -198,19 +192,19 @@ namespace Remotely.Desktop.Core.Services
|
||||
KeyboardMouseInput.SendKeyUp(dto.Key);
|
||||
}
|
||||
|
||||
private void MouseDown(byte[] message, Services.Viewer viewer)
|
||||
private void MouseDown(byte[] message, Viewer viewer)
|
||||
{
|
||||
var dto = MessagePackSerializer.Deserialize<MouseDownDto>(message);
|
||||
KeyboardMouseInput.SendMouseButtonAction(dto.Button, ButtonAction.Down, dto.PercentX, dto.PercentY, viewer);
|
||||
}
|
||||
|
||||
private void MouseMove(byte[] message, Services.Viewer viewer)
|
||||
private void MouseMove(byte[] message, Viewer viewer)
|
||||
{
|
||||
var dto = MessagePackSerializer.Deserialize<MouseMoveDto>(message);
|
||||
KeyboardMouseInput.SendMouseMove(dto.PercentX, dto.PercentY, viewer);
|
||||
}
|
||||
|
||||
private void MouseUp(byte[] message, Services.Viewer viewer)
|
||||
private void MouseUp(byte[] message, Viewer viewer)
|
||||
{
|
||||
var dto = MessagePackSerializer.Deserialize<MouseUpDto>(message);
|
||||
KeyboardMouseInput.SendMouseButtonAction(dto.Button, ButtonAction.Up, dto.PercentX, dto.PercentY, viewer);
|
||||
@ -222,12 +216,12 @@ namespace Remotely.Desktop.Core.Services
|
||||
KeyboardMouseInput.SendMouseWheel(-(int)dto.DeltaY);
|
||||
}
|
||||
|
||||
private void OpenFileTransferWindow(Services.Viewer viewer)
|
||||
private void OpenFileTransferWindow(Viewer viewer)
|
||||
{
|
||||
FileTransferService.OpenFileTransferWindow(viewer);
|
||||
}
|
||||
|
||||
private void SelectScreen(byte[] message, Services.Viewer viewer)
|
||||
private void SelectScreen(byte[] message, Viewer viewer)
|
||||
{
|
||||
var dto = MessagePackSerializer.Deserialize<SelectScreenDto>(message);
|
||||
viewer.Capturer.SetSelectedScreen(dto.DisplayName);
|
||||
@ -238,7 +232,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
KeyboardMouseInput.SetKeyStatesUp();
|
||||
}
|
||||
|
||||
private void Tap(byte[] message, Services.Viewer viewer)
|
||||
private void Tap(byte[] message, Viewer viewer)
|
||||
{
|
||||
var dto = MessagePackSerializer.Deserialize<TapDto>(message);
|
||||
KeyboardMouseInput.SendMouseButtonAction(0, ButtonAction.Down, dto.PercentX, dto.PercentY, viewer);
|
||||
@ -257,7 +251,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
KeyboardMouseInput.ToggleBlockInput(dto.ToggleOn);
|
||||
}
|
||||
|
||||
private void ToggleWebRtcVideo(byte[] message, Services.Viewer viewer)
|
||||
private void ToggleWebRtcVideo(byte[] message, Viewer viewer)
|
||||
{
|
||||
var dto = MessagePackSerializer.Deserialize<ToggleWebRtcVideoDto>(message);
|
||||
viewer.ToggleWebRtcVideo(dto.ToggleOn);
|
||||
|
||||
@ -14,7 +14,6 @@ using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
@ -55,6 +54,8 @@ namespace Remotely.Desktop.Core.Services
|
||||
viewer.Name = screenCastRequest.RequesterName;
|
||||
viewer.ViewerConnectionID = screenCastRequest.ViewerID;
|
||||
|
||||
var screenBounds = viewer.Capturer.CurrentScreenBounds;
|
||||
|
||||
Logger.Write($"Starting screen cast. Requester: {viewer.Name}. " +
|
||||
$"Viewer ID: {viewer.ViewerConnectionID}. App Mode: {_conductor.Mode}");
|
||||
|
||||
@ -78,8 +79,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
viewer.Capturer.SelectedScreen,
|
||||
viewer.Capturer.GetDisplayNames().ToArray());
|
||||
|
||||
await viewer.SendScreenSize(viewer.Capturer.CurrentScreenBounds.Width,
|
||||
viewer.Capturer.CurrentScreenBounds.Height);
|
||||
await viewer.SendScreenSize(screenBounds.Width, screenBounds.Height);
|
||||
|
||||
await viewer.SendCursorChange(_cursorIconWatcher.GetCurrentCursor());
|
||||
|
||||
@ -96,11 +96,11 @@ namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
await viewer.SendScreenCapture(new CaptureFrame()
|
||||
{
|
||||
EncodedImageBytes = ImageUtils.EncodeWithSkia(initialFrame, SKEncodedImageFormat.Webp, _maxQuality),
|
||||
Left = viewer.Capturer.CurrentScreenBounds.Left,
|
||||
Top = viewer.Capturer.CurrentScreenBounds.Top,
|
||||
Width = viewer.Capturer.CurrentScreenBounds.Width,
|
||||
Height = viewer.Capturer.CurrentScreenBounds.Height
|
||||
EncodedImageBytes = ImageUtils.EncodeJpeg(initialFrame, _maxQuality),
|
||||
Left = screenBounds.Left,
|
||||
Top = screenBounds.Top,
|
||||
Width = screenBounds.Width,
|
||||
Height = screenBounds.Height
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -172,23 +172,23 @@ namespace Remotely.Desktop.Core.Services
|
||||
if (viewer.Capturer.CaptureFullscreen)
|
||||
{
|
||||
// Recalculate Bps.
|
||||
viewer.PeakBytesPerSecond = 0;
|
||||
encodedImageBytes = ImageUtils.EncodeWithSkia(clone, SKEncodedImageFormat.Jpeg, _maxQuality);
|
||||
viewer.AverageBytesPerSecond = 0;
|
||||
encodedImageBytes = ImageUtils.EncodeJpeg(clone, _maxQuality);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (viewer.PeakBytesPerSecond > 0)
|
||||
if (viewer.AverageBytesPerSecond > 0)
|
||||
{
|
||||
var expectedSize = clone.Height * clone.Width * 4 * .1;
|
||||
var timeToSend = expectedSize / viewer.PeakBytesPerSecond;
|
||||
var timeToSend = expectedSize / viewer.AverageBytesPerSecond;
|
||||
currentQuality = Math.Max(_minQuality, Math.Min(_maxQuality, (int)(.1 / timeToSend * _maxQuality)));
|
||||
if (currentQuality < _maxQuality - 10)
|
||||
{
|
||||
refreshNeeded = true;
|
||||
Debug.WriteLine($"Quality Reduced: {currentQuality}");
|
||||
}
|
||||
Debug.WriteLine($"Current Quality: {currentQuality}");
|
||||
}
|
||||
encodedImageBytes = ImageUtils.EncodeWithSkia(clone, SKEncodedImageFormat.Jpeg, currentQuality);
|
||||
encodedImageBytes = ImageUtils.EncodeJpeg(clone, currentQuality);
|
||||
}
|
||||
|
||||
viewer.Capturer.CaptureFullscreen = false;
|
||||
|
||||
@ -11,11 +11,16 @@ using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
public class Viewer : IDisposable
|
||||
{
|
||||
private long _bytesSent;
|
||||
private TimeSpan _timeSpentSending = TimeSpan.Zero;
|
||||
|
||||
public Viewer(ICasterSocket casterSocket,
|
||||
IScreenCapturer screenCapturer,
|
||||
IClipboardService clipboardService,
|
||||
@ -64,7 +69,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public double PeakBytesPerSecond { get; set; }
|
||||
public double AverageBytesPerSecond { get; set; }
|
||||
public ConcurrentQueue<SentFrame> PendingSentFrames { get; } = new();
|
||||
|
||||
public WebRtcSession RtcSession { get; set; }
|
||||
@ -150,7 +155,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
|
||||
public async Task SendFile(FileUpload fileUpload, Action<double> progressUpdateCallback)
|
||||
public async Task SendFile(FileUpload fileUpload, CancellationToken cancelToken, Action<double> progressUpdateCallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -163,13 +168,18 @@ namespace Remotely.Desktop.Core.Services
|
||||
StartOfFile = true
|
||||
};
|
||||
|
||||
await SendToViewer(() => RtcSession.SendDto(fileDto),
|
||||
() => CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
await SendToViewer(async () => await RtcSession.SendDto(fileDto),
|
||||
async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
|
||||
using var fs = File.OpenRead(fileUpload.FilePath);
|
||||
using var br = new BinaryReader(fs);
|
||||
while (fs.Position < fs.Length)
|
||||
{
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
fileDto = new FileDto()
|
||||
{
|
||||
Buffer = br.ReadBytes(50_000),
|
||||
@ -178,8 +188,8 @@ namespace Remotely.Desktop.Core.Services
|
||||
};
|
||||
|
||||
await SendToViewer(
|
||||
() => RtcSession.SendDto(fileDto),
|
||||
() => CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
async () => await RtcSession.SendDto(fileDto),
|
||||
async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
|
||||
progressUpdateCallback((double)fs.Position / fs.Length);
|
||||
}
|
||||
@ -192,8 +202,8 @@ namespace Remotely.Desktop.Core.Services
|
||||
StartOfFile = false
|
||||
};
|
||||
|
||||
await SendToViewer(() => RtcSession.SendDto(fileDto),
|
||||
() => CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
await SendToViewer(async () => await RtcSession.SendDto(fileDto),
|
||||
async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
@ -219,6 +229,8 @@ namespace Remotely.Desktop.Core.Services
|
||||
var width = screenFrame.Width;
|
||||
var height = screenFrame.Height;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < screenFrame.EncodedImageBytes.Length; i += 50_000)
|
||||
{
|
||||
var dto = new CaptureFrameDto()
|
||||
@ -231,8 +243,8 @@ namespace Remotely.Desktop.Core.Services
|
||||
ImageBytes = screenFrame.EncodedImageBytes.Skip(i).Take(50_000).ToArray()
|
||||
};
|
||||
|
||||
await SendToViewer(() => RtcSession.SendDto(dto),
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
await SendToViewer(async () => await RtcSession.SendDto(dto),
|
||||
async () => await CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
|
||||
var endOfFrameDto = new CaptureFrameDto()
|
||||
@ -244,8 +256,19 @@ namespace Remotely.Desktop.Core.Services
|
||||
EndOfFrame = true
|
||||
};
|
||||
|
||||
await SendToViewer(() => RtcSession.SendDto(endOfFrameDto),
|
||||
() => CasterSocket.SendDtoToViewer(endOfFrameDto, ViewerConnectionID));
|
||||
await SendToViewer(
|
||||
async () => await RtcSession.SendDto(endOfFrameDto),
|
||||
async () => await CasterSocket.SendDtoToViewer(endOfFrameDto, ViewerConnectionID));
|
||||
|
||||
sw.Stop();
|
||||
|
||||
_bytesSent += screenFrame.EncodedImageBytes.Length;
|
||||
_timeSpentSending += sw.Elapsed;
|
||||
|
||||
|
||||
AverageBytesPerSecond = _bytesSent / _timeSpentSending.TotalSeconds;
|
||||
|
||||
Debug.WriteLine($"Mbps: {AverageBytesPerSecond / 1024 / 1024 * 8}");
|
||||
}
|
||||
|
||||
public async Task SendScreenData(string selectedScreen, string[] displayNames)
|
||||
@ -280,7 +303,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
TaskHelper.DelayUntil(() =>
|
||||
!PendingSentFrames.TryPeek(out var result) || DateTimeOffset.Now - result.Timestamp < TimeSpan.FromSeconds(1),
|
||||
TimeSpan.MaxValue);
|
||||
TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
||||
public void ToggleWebRtcVideo(bool toggleOn)
|
||||
|
||||
@ -7,6 +7,7 @@ using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
@ -109,7 +110,14 @@ namespace Remotely.Desktop.Core.Services
|
||||
|
||||
public Task SendDto<T>(T dto) where T : BaseDto
|
||||
{
|
||||
return Task.Run(() => CaptureChannel.SendMessage(MessagePackSerializer.Serialize(dto)));
|
||||
return Task.Run(() =>
|
||||
{
|
||||
CaptureChannel.SendMessage(MessagePackSerializer.Serialize(dto));
|
||||
while (CurrentBuffer > 64_000)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SetRemoteDescription(string type, string sdp)
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.Desktop;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
@ -14,18 +12,31 @@ namespace Remotely.Desktop.Core.Utilities
|
||||
{
|
||||
public class ImageUtils
|
||||
{
|
||||
public static byte[] EncodeWithSkia(Bitmap bitmap, SKEncodedImageFormat format, int quality)
|
||||
private static ImageCodecInfo _jpegEncoder = ImageCodecInfo.GetImageEncoders().FirstOrDefault(x => x.FormatID == ImageFormat.Jpeg.Guid);
|
||||
|
||||
//public static byte[] EncodeWithSkia(Bitmap bitmap, SKEncodedImageFormat format, int quality)
|
||||
//{
|
||||
// using var ms = new MemoryStream();
|
||||
// var info = new SKImageInfo(bitmap.Width, bitmap.Height);
|
||||
// var skBitmap = new SKBitmap(info);
|
||||
// using (var pixmap = skBitmap.PeekPixels())
|
||||
// {
|
||||
// bitmap.ToSKPixmap(pixmap);
|
||||
// }
|
||||
|
||||
// skBitmap.Encode(ms, format, quality);
|
||||
|
||||
// return ms.ToArray();
|
||||
//}
|
||||
|
||||
public static byte[] EncodeJpeg(Bitmap bitmap, int quality)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
var info = new SKImageInfo(bitmap.Width, bitmap.Height);
|
||||
var skBitmap = new SKBitmap(info);
|
||||
using (var pixmap = skBitmap.PeekPixels())
|
||||
using var encoderParams = new EncoderParameters(1)
|
||||
{
|
||||
bitmap.ToSKPixmap(pixmap);
|
||||
}
|
||||
|
||||
skBitmap.Encode(ms, format, quality);
|
||||
|
||||
Param = new[] { new EncoderParameter(Encoder.Quality, quality) }
|
||||
};
|
||||
bitmap.Save(ms, _jpegEncoder, encoderParams);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
@ -152,11 +163,7 @@ namespace Remotely.Desktop.Core.Utilities
|
||||
{
|
||||
throw new Exception("Bitmaps are not of equal dimensions.");
|
||||
}
|
||||
if (!Bitmap.IsAlphaPixelFormat(currentFrame.PixelFormat) || !Bitmap.IsAlphaPixelFormat(previousFrame.PixelFormat) ||
|
||||
!Bitmap.IsCanonicalPixelFormat(currentFrame.PixelFormat) || !Bitmap.IsCanonicalPixelFormat(previousFrame.PixelFormat))
|
||||
{
|
||||
throw new Exception("Bitmaps must be 32 bits per pixel and contain alpha channel.");
|
||||
}
|
||||
|
||||
var width = currentFrame.Width;
|
||||
var height = currentFrame.Height;
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace Remotely.Desktop.Core.ViewModels
|
||||
{
|
||||
@ -7,6 +8,8 @@ namespace Remotely.Desktop.Core.ViewModels
|
||||
private string _filePath;
|
||||
private double _percentProgress;
|
||||
|
||||
public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();
|
||||
|
||||
public string DisplayName => Path.GetFileName(FilePath);
|
||||
|
||||
public string FilePath
|
||||
@ -19,7 +22,6 @@ namespace Remotely.Desktop.Core.ViewModels
|
||||
{
|
||||
_filePath = value;
|
||||
FirePropertyChanged();
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
public double PercentProgress
|
||||
|
||||
@ -120,7 +120,9 @@ namespace Remotely.Desktop.Linux
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RunWithMainWindow<MainWindow>();
|
||||
await Dispatcher.UIThread.InvokeAsync(() => {
|
||||
this.RunWithMainWindow<MainWindow>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:vm="clr-namespace:Remotely.Desktop.Linux.ViewModels;assembly=Remotely_Desktop"
|
||||
x:Class="Remotely.Desktop.Linux.Controls.MessageBox"
|
||||
Icon="{Binding Icon}"
|
||||
Icon="{Binding WindowIcon}"
|
||||
Title="{Binding Caption}"
|
||||
SizeToContent="WidthAndHeight" MinWidth="300" MinHeight="100"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
@ -55,8 +55,6 @@
|
||||
<PackageReference Include="Avalonia" Version="0.10.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.0" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.2" />
|
||||
<PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="2.80.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Desktop.Core\Desktop.Core.csproj" />
|
||||
|
||||
@ -82,6 +82,14 @@ namespace Remotely.Desktop.Linux.Native
|
||||
[DllImport("libX11")]
|
||||
public static extern void XDestroyImage(IntPtr ximage);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern void XNoOp(IntPtr display);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern void XFree(IntPtr data);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern int XGetWindowAttributes(IntPtr display, IntPtr window, out XWindowAttributes windowAttributes);
|
||||
|
||||
public struct XImage
|
||||
{
|
||||
@ -103,5 +111,32 @@ namespace Remotely.Desktop.Linux.Native
|
||||
public ulong blue_mask;
|
||||
public IntPtr obdata; /* hook for the object routines to hang on */
|
||||
}
|
||||
|
||||
public struct XWindowAttributes
|
||||
{
|
||||
public int x;
|
||||
public int y;
|
||||
public int width;
|
||||
public int height;
|
||||
public int border_width;
|
||||
public int depth;
|
||||
public IntPtr visual;
|
||||
public IntPtr root;
|
||||
public int @class;
|
||||
public int bit_gravity;
|
||||
public int win_gravity;
|
||||
public int backing_store;
|
||||
public ulong backing_planes;
|
||||
public ulong backing_pixel;
|
||||
public bool save_under;
|
||||
public IntPtr colormap;
|
||||
public bool map_installed;
|
||||
public int map_state;
|
||||
public long all_event_masks;
|
||||
public long your_event_mask;
|
||||
public long do_not_propagate_mask;
|
||||
public bool override_redirect;
|
||||
public IntPtr screen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
Desktop.Linux/Native/libXrandr.cs
Normal file
68
Desktop.Linux/Native/libXrandr.cs
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright © 2000 Compaq Computer Corporation, Inc.
|
||||
* Copyright © 2002 Hewlett-Packard Company, Inc.
|
||||
* Copyright © 2006 Intel Corporation
|
||||
* Copyright © 2008 Red Hat, Inc.
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that copyright
|
||||
* notice and this permission notice appear in supporting documentation, and
|
||||
* that the name of the copyright holders not be used in advertising or
|
||||
* publicity pertaining to distribution of the software without specific,
|
||||
* written prior permission. The copyright holders make no representations
|
||||
* about the suitability of this software for any purpose. It is provided "as
|
||||
* is" without express or implied warranty.
|
||||
*
|
||||
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
||||
* INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
|
||||
* EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
* DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
* OF THIS SOFTWARE.
|
||||
*
|
||||
* Author: Jim Gettys, HP Labs, Hewlett-Packard, Inc.
|
||||
* Keith Packard, Intel Corporation
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.Linux.Native
|
||||
{
|
||||
public static class LibXrandr
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct XRRMonitorInfo
|
||||
{
|
||||
// Atom
|
||||
public IntPtr name;
|
||||
public bool primary;
|
||||
public bool automatic;
|
||||
public int noutput;
|
||||
public int x;
|
||||
public int y;
|
||||
public int width;
|
||||
public int height;
|
||||
public int mwidth;
|
||||
public int mheight;
|
||||
// RROutput*
|
||||
public IntPtr outputs;
|
||||
}
|
||||
|
||||
[DllImport("libXrandr")]
|
||||
public static extern IntPtr XRRGetMonitors(IntPtr display, IntPtr window, bool get_active, out int monitors);
|
||||
|
||||
[DllImport("libXrandr")]
|
||||
public static extern void XRRFreeMonitors(IntPtr monitors);
|
||||
|
||||
[DllImport("libXrandr")]
|
||||
public static extern IntPtr XRRAllocateMonitor(IntPtr display, int output);
|
||||
}
|
||||
}
|
||||
@ -117,11 +117,11 @@ namespace Remotely.Desktop.Linux.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFile(FileUpload fileUpload, Viewer viewer, Action<double> progressUpdateCallback)
|
||||
public async Task UploadFile(FileUpload fileUpload, Viewer viewer, CancellationToken cancelToken, Action<double> progressUpdateCallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
await viewer.SendFile(fileUpload, progressUpdateCallback);
|
||||
await viewer.SendFile(fileUpload, cancelToken, progressUpdateCallback);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -90,10 +90,12 @@ namespace Remotely.Desktop.Linux.Services
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
|
||||
var screenBounds = viewer.Capturer.CurrentScreenBounds;
|
||||
LibXtst.XTestFakeMotionEvent(Display,
|
||||
viewer.Capturer.GetSelectedScreenIndex(),
|
||||
(int)(viewer.Capturer.CurrentScreenBounds.Width * percentX),
|
||||
(int)(viewer.Capturer.CurrentScreenBounds.Height * percentY),
|
||||
LibX11.XDefaultScreen(Display),
|
||||
screenBounds.X + (int)(screenBounds.Width * percentX),
|
||||
screenBounds.Y + (int)(screenBounds.Height * percentY),
|
||||
0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
|
||||
@ -7,14 +7,15 @@ using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace Remotely.Desktop.Linux.Services
|
||||
{
|
||||
public class ScreenCapturerLinux : IScreenCapturer
|
||||
{
|
||||
private readonly SemaphoreSlim _screenCaptureLock = new(1,1);
|
||||
private readonly Dictionary<string, int> _x11Screens = new();
|
||||
private readonly object _screenBoundsLock = new();
|
||||
private readonly Dictionary<string, LibXrandr.XRRMonitorInfo> _x11Screens = new();
|
||||
public ScreenCapturerLinux()
|
||||
{
|
||||
Display = LibX11.XOpenDisplay(null);
|
||||
@ -33,34 +34,27 @@ namespace Remotely.Desktop.Linux.Services
|
||||
LibX11.XCloseDisplay(Display);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetDisplayNames() => _x11Screens.Keys;
|
||||
public IEnumerable<string> GetDisplayNames() => _x11Screens.Keys.Select(x => x.ToString());
|
||||
|
||||
public Bitmap GetNextFrame()
|
||||
{
|
||||
try
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
_screenCaptureLock.Wait();
|
||||
|
||||
return GetX11Screen();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
Init();
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_screenCaptureLock.Release();
|
||||
try
|
||||
{
|
||||
return GetX11Capture();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
Init();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
public int GetScreenCount()
|
||||
{
|
||||
return LibX11.XScreenCount(Display);
|
||||
}
|
||||
public int GetScreenCount() => _x11Screens.Count;
|
||||
|
||||
public int GetSelectedScreenIndex() => _x11Screens[SelectedScreen];
|
||||
public int GetSelectedScreenIndex() => int.Parse(SelectedScreen ?? "0");
|
||||
|
||||
public Rectangle GetVirtualScreenBounds()
|
||||
{
|
||||
@ -83,12 +77,33 @@ namespace Remotely.Desktop.Linux.Services
|
||||
{
|
||||
CaptureFullscreen = true;
|
||||
_x11Screens.Clear();
|
||||
|
||||
var monitorsPtr = LibXrandr.XRRGetMonitors(Display, LibX11.XDefaultRootWindow(Display), true, out var monitorCount);
|
||||
|
||||
for (var i = 0; i < GetScreenCount(); i++)
|
||||
var monitorInfoSize = Marshal.SizeOf<LibXrandr.XRRMonitorInfo>();
|
||||
|
||||
for (var i = 0; i < monitorCount; i++)
|
||||
{
|
||||
_x11Screens.Add(i.ToString(), i);
|
||||
var monitorPtr = new IntPtr(monitorsPtr.ToInt64() + i * monitorInfoSize);
|
||||
var monitorInfo = Marshal.PtrToStructure<LibXrandr.XRRMonitorInfo>(monitorPtr);
|
||||
|
||||
Logger.Write($"Found monitor: " +
|
||||
$"{monitorInfo.width}," +
|
||||
$"{monitorInfo.height}," +
|
||||
$"{monitorInfo.x}, " +
|
||||
$"{monitorInfo.y}");
|
||||
|
||||
_x11Screens.Add(i.ToString(), monitorInfo);
|
||||
}
|
||||
|
||||
LibXrandr.XRRFreeMonitors(monitorsPtr);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SelectedScreen) ||
|
||||
!_x11Screens.ContainsKey(SelectedScreen))
|
||||
{
|
||||
SelectedScreen = _x11Screens.Keys.First();
|
||||
RefreshCurrentScreenBounds();
|
||||
}
|
||||
SetSelectedScreen(_x11Screens.Keys.First());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -97,40 +112,49 @@ namespace Remotely.Desktop.Linux.Services
|
||||
}
|
||||
public void SetSelectedScreen(string displayName)
|
||||
{
|
||||
if (displayName == SelectedScreen)
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (_x11Screens.ContainsKey(displayName))
|
||||
try
|
||||
{
|
||||
SelectedScreen = displayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedScreen = _x11Screens.Keys.First();
|
||||
}
|
||||
var width = LibX11.XDisplayWidth(Display, _x11Screens[SelectedScreen]);
|
||||
var height = LibX11.XDisplayHeight(Display, _x11Screens[SelectedScreen]);
|
||||
CurrentScreenBounds = new Rectangle(0, 0, width, height);
|
||||
CaptureFullscreen = true;
|
||||
ScreenChanged?.Invoke(this, CurrentScreenBounds);
|
||||
Logger.Write($"Setting display to {displayName}.");
|
||||
if (displayName == SelectedScreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_x11Screens.ContainsKey(displayName))
|
||||
{
|
||||
SelectedScreen = displayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedScreen = _x11Screens.Keys.First();
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
RefreshCurrentScreenBounds();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap GetX11Screen()
|
||||
private Bitmap GetX11Capture()
|
||||
{
|
||||
var currentFrame = new Bitmap(CurrentScreenBounds.Width, CurrentScreenBounds.Height, PixelFormat.Format32bppArgb);
|
||||
|
||||
var window = LibX11.XRootWindow(Display, _x11Screens[SelectedScreen]);
|
||||
var window = LibX11.XDefaultRootWindow(Display);
|
||||
|
||||
var imagePointer = LibX11.XGetImage(Display,
|
||||
window,
|
||||
CurrentScreenBounds.X,
|
||||
CurrentScreenBounds.Y,
|
||||
CurrentScreenBounds.Width,
|
||||
CurrentScreenBounds.Height,
|
||||
~0,
|
||||
2);
|
||||
|
||||
var imagePointer = LibX11.XGetImage(Display, window, 0, 0, CurrentScreenBounds.Width, CurrentScreenBounds.Height, ~0, 2);
|
||||
var image = Marshal.PtrToStructure<LibX11.XImage>(imagePointer);
|
||||
|
||||
var bd = currentFrame.LockBits(new Rectangle(0, 0, CurrentScreenBounds.Width, CurrentScreenBounds.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
|
||||
@ -147,9 +171,25 @@ namespace Remotely.Desktop.Linux.Services
|
||||
}
|
||||
|
||||
currentFrame.UnlockBits(bd);
|
||||
Marshal.DestroyStructure<LibX11.XImage>(imagePointer);
|
||||
LibX11.XDestroyImage(imagePointer);
|
||||
|
||||
return currentFrame;
|
||||
}
|
||||
|
||||
private void RefreshCurrentScreenBounds()
|
||||
{
|
||||
var screen = _x11Screens[SelectedScreen];
|
||||
|
||||
Logger.Write($"Setting new screen bounds: " +
|
||||
$"{screen.width}," +
|
||||
$"{screen.height}," +
|
||||
$"{screen.x}, " +
|
||||
$"{screen.y}");
|
||||
|
||||
CurrentScreenBounds = new Rectangle(screen.x, screen.y, screen.width, screen.height);
|
||||
CaptureFullscreen = true;
|
||||
ScreenChanged?.Invoke(this, CurrentScreenBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ namespace Remotely.Desktop.Linux.ViewModels
|
||||
FileUploads.Add(fileUpload);
|
||||
});
|
||||
|
||||
await _fileTransferService.UploadFile(fileUpload, _viewer, async progress =>
|
||||
await _fileTransferService.UploadFile(fileUpload, _viewer, fileUpload.CancellationTokenSource.Token, async progress =>
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
@ -103,6 +103,7 @@ namespace Remotely.Desktop.Linux.ViewModels
|
||||
if (param is FileUpload fileUpload)
|
||||
{
|
||||
FileUploads.Remove(fileUpload);
|
||||
fileUpload.CancellationTokenSource.Cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -243,6 +243,7 @@ namespace Remotely.Desktop.Linux.ViewModels
|
||||
{
|
||||
FileName = "sudo",
|
||||
Arguments = "bash -c \"apt-get -y install libx11-dev ; " +
|
||||
"apt-get -y install libxrandr-dev ; " +
|
||||
"apt-get -y install libc6-dev ; " +
|
||||
"apt-get -y install libgdiplus ; " +
|
||||
"apt-get -y install libxtst-dev ; " +
|
||||
|
||||
@ -39,8 +39,6 @@
|
||||
<PackageReference Include="SharpDX" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.2" />
|
||||
<PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="2.80.2" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -118,11 +118,11 @@ namespace Remotely.Desktop.Win.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFile(FileUpload fileUpload, Viewer viewer, Action<double> progressUpdateCallback)
|
||||
public async Task UploadFile(FileUpload fileUpload, Viewer viewer, CancellationToken cancelToken, Action<double> progressUpdateCallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
await viewer.SendFile(fileUpload, progressUpdateCallback);
|
||||
await viewer.SendFile(fileUpload, cancelToken, progressUpdateCallback);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -14,7 +14,8 @@ namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class KeyboardMouseInputWin : IKeyboardMouseInput
|
||||
{
|
||||
private volatile bool inputBlocked;
|
||||
private volatile bool _inputBlocked;
|
||||
private Thread _inputProcessingThread;
|
||||
|
||||
private CancellationTokenSource CancelTokenSource { get; set; }
|
||||
|
||||
@ -22,15 +23,19 @@ namespace Remotely.Desktop.Win.Services
|
||||
|
||||
public Tuple<double, double> GetAbsolutePercentFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer)
|
||||
{
|
||||
var absoluteX = (capturer.CurrentScreenBounds.Width * percentX) + capturer.CurrentScreenBounds.Left - capturer.GetVirtualScreenBounds().Left;
|
||||
var absoluteY = (capturer.CurrentScreenBounds.Height * percentY) + capturer.CurrentScreenBounds.Top - capturer.GetVirtualScreenBounds().Top;
|
||||
var screenBounds = capturer.CurrentScreenBounds;
|
||||
|
||||
var absoluteX = (screenBounds.Width * percentX) + screenBounds.Left - capturer.GetVirtualScreenBounds().Left;
|
||||
var absoluteY = (screenBounds.Height * percentY) + screenBounds.Top - capturer.GetVirtualScreenBounds().Top;
|
||||
return new Tuple<double, double>(absoluteX / capturer.GetVirtualScreenBounds().Width, absoluteY / capturer.GetVirtualScreenBounds().Height);
|
||||
}
|
||||
|
||||
public Tuple<double, double> GetAbsolutePointFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer)
|
||||
{
|
||||
var absoluteX = (capturer.CurrentScreenBounds.Width * percentX) + capturer.CurrentScreenBounds.Left;
|
||||
var absoluteY = (capturer.CurrentScreenBounds.Height * percentY) + capturer.CurrentScreenBounds.Top;
|
||||
var screenBounds = capturer.CurrentScreenBounds;
|
||||
|
||||
var absoluteX = (screenBounds.Width * percentX) + screenBounds.Left;
|
||||
var absoluteY = (screenBounds.Height * percentY) + screenBounds.Top;
|
||||
return new Tuple<double, double>(absoluteX, absoluteY);
|
||||
}
|
||||
|
||||
@ -41,8 +46,6 @@ namespace Remotely.Desktop.Win.Services
|
||||
App.Current.Exit -= App_Exit;
|
||||
App.Current.Exit += App_Exit;
|
||||
});
|
||||
|
||||
StartInputProcessingThread();
|
||||
}
|
||||
|
||||
public void SendKeyDown(string key)
|
||||
@ -225,10 +228,20 @@ namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
InputActions.Enqueue(() =>
|
||||
{
|
||||
inputBlocked = toggleOn;
|
||||
_inputBlocked = toggleOn;
|
||||
var result = BlockInput(toggleOn);
|
||||
Logger.Write($"Result of ToggleBlockInput set to {toggleOn}: {result}");
|
||||
|
||||
if (!toggleOn)
|
||||
{
|
||||
CancelTokenSource.Cancel();
|
||||
}
|
||||
});
|
||||
|
||||
if (toggleOn)
|
||||
{
|
||||
StartInputProcessingThread();
|
||||
}
|
||||
}
|
||||
|
||||
private void App_Exit(object sender, System.Windows.ExitEventArgs e)
|
||||
@ -301,51 +314,65 @@ namespace Remotely.Desktop.Win.Services
|
||||
}
|
||||
private void StartInputProcessingThread()
|
||||
{
|
||||
CancelTokenSource?.Cancel();
|
||||
CancelTokenSource?.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
CancelTokenSource?.Cancel();
|
||||
CancelTokenSource?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
|
||||
// After BlockInput is enabled, only simulated input coming from the same thread
|
||||
// will work. So we have to start a new thread that runs continuously and
|
||||
// processes a queue of input events.
|
||||
var newThread = new Thread(() =>
|
||||
_inputProcessingThread = new Thread(() =>
|
||||
{
|
||||
Logger.Write($"New input processing thread started on thread {Thread.CurrentThread.ManagedThreadId}.");
|
||||
CancelTokenSource = new CancellationTokenSource();
|
||||
|
||||
if (inputBlocked)
|
||||
if (_inputBlocked)
|
||||
{
|
||||
ToggleBlockInput(true);
|
||||
}
|
||||
CheckQueue(CancelTokenSource.Token);
|
||||
});
|
||||
|
||||
newThread.SetApartmentState(ApartmentState.STA);
|
||||
newThread.Start();
|
||||
_inputProcessingThread.SetApartmentState(ApartmentState.STA);
|
||||
_inputProcessingThread.Start();
|
||||
}
|
||||
|
||||
private void TryOnInputDesktop(Action inputAction)
|
||||
{
|
||||
InputActions.Enqueue(() =>
|
||||
if (!_inputBlocked)
|
||||
{
|
||||
try
|
||||
if (!Win32Interop.SwitchToInputDesktop())
|
||||
{
|
||||
if (!Win32Interop.SwitchToInputDesktop())
|
||||
Logger.Write("Desktop switch failed while sending input.");
|
||||
}
|
||||
inputAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
InputActions.Enqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Write("Desktop switch failed during input processing.");
|
||||
if (!Win32Interop.SwitchToInputDesktop())
|
||||
{
|
||||
Logger.Write("Desktop switch failed during input processing.");
|
||||
|
||||
// Thread likely has hooks in current desktop. SendKeys will create one with no way to unhook it.
|
||||
// Start a new thread for processing input.
|
||||
StartInputProcessingThread();
|
||||
return;
|
||||
// Thread likely has hooks in current desktop. SendKeys will create one with no way to unhook it.
|
||||
// Start a new thread for processing input.
|
||||
StartInputProcessingThread();
|
||||
return;
|
||||
}
|
||||
inputAction();
|
||||
}
|
||||
inputAction();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
private readonly Dictionary<string, int> _bitBltScreens = new();
|
||||
private readonly Dictionary<string, DirectXOutput> _directxScreens = new();
|
||||
private readonly SemaphoreSlim _screenCaptureLock = new(1,1);
|
||||
private readonly object _screenBoundsLock = new();
|
||||
|
||||
public ScreenCapturerWin()
|
||||
{
|
||||
@ -53,8 +53,14 @@ namespace Remotely.Desktop.Win.Services
|
||||
|
||||
public event EventHandler<Rectangle> ScreenChanged;
|
||||
|
||||
private enum GetDirectXFrameResult
|
||||
{
|
||||
Success,
|
||||
Failure,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
public bool CaptureFullscreen { get; set; } = true;
|
||||
public Rectangle CurrentScreenBounds { get; private set; } = Screen.PrimaryScreen.Bounds;
|
||||
public bool NeedsInit { get; set; } = true;
|
||||
public string SelectedScreen { get; private set; } = Screen.PrimaryScreen.DeviceName;
|
||||
public void Dispose()
|
||||
@ -67,56 +73,62 @@ namespace Remotely.Desktop.Win.Services
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
public Rectangle CurrentScreenBounds { get; private set; } = Screen.PrimaryScreen.Bounds;
|
||||
|
||||
public IEnumerable<string> GetDisplayNames() => Screen.AllScreens.Select(x => x.DeviceName);
|
||||
|
||||
public Bitmap GetNextFrame()
|
||||
{
|
||||
try
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
_screenCaptureLock.Wait();
|
||||
|
||||
if (NeedsInit)
|
||||
try
|
||||
{
|
||||
Logger.Write("Init needed in GetNextFrame.");
|
||||
Init();
|
||||
}
|
||||
|
||||
// Sometimes DX will result in a timeout, even when there are changes
|
||||
// on the screen. I've observed this when a laptop lid is closed, or
|
||||
// on some machines that aren't connected to a monitor. This will
|
||||
// have it fall back to BitBlt in those cases.
|
||||
// TODO: Make DX capture work with changed screen orientation.
|
||||
if (_directxScreens.ContainsKey(SelectedScreen) &&
|
||||
SystemInformation.ScreenOrientation != ScreenOrientation.Angle270 &&
|
||||
SystemInformation.ScreenOrientation != ScreenOrientation.Angle90)
|
||||
{
|
||||
var (result, frame) = GetDirectXFrame();
|
||||
|
||||
if (result == GetDirectXFrameResult.Success ||
|
||||
result == GetDirectXFrameResult.Timeout)
|
||||
if (NeedsInit)
|
||||
{
|
||||
return frame;
|
||||
Logger.Write("Init needed in GetNextFrame.");
|
||||
Init();
|
||||
}
|
||||
|
||||
// Sometimes DX will result in a timeout, even when there are changes
|
||||
// on the screen. I've observed this when a laptop lid is closed, or
|
||||
// on some machines that aren't connected to a monitor. This will
|
||||
// have it fall back to BitBlt in those cases.
|
||||
// TODO: Make DX capture work with changed screen orientation.
|
||||
if (_directxScreens.ContainsKey(SelectedScreen) &&
|
||||
SystemInformation.ScreenOrientation != ScreenOrientation.Angle270 &&
|
||||
SystemInformation.ScreenOrientation != ScreenOrientation.Angle90)
|
||||
{
|
||||
var (result, frame) = GetDirectXFrame();
|
||||
|
||||
if (result == GetDirectXFrameResult.Success)
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
|
||||
return GetBitBltFrame();
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Write(e);
|
||||
NeedsInit = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetBitBltFrame();
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Write(e);
|
||||
NeedsInit = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_screenCaptureLock.Release();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int GetScreenCount() => Screen.AllScreens.Length;
|
||||
|
||||
public int GetSelectedScreenIndex() => _bitBltScreens[SelectedScreen];
|
||||
public int GetSelectedScreenIndex()
|
||||
{
|
||||
if (_bitBltScreens.TryGetValue(SelectedScreen, out var index))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Rectangle GetVirtualScreenBounds() => SystemInformation.VirtualScreen;
|
||||
|
||||
@ -133,20 +145,23 @@ namespace Remotely.Desktop.Win.Services
|
||||
|
||||
public void SetSelectedScreen(string displayName)
|
||||
{
|
||||
if (displayName == SelectedScreen)
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (displayName == SelectedScreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_bitBltScreens.ContainsKey(displayName))
|
||||
{
|
||||
SelectedScreen = displayName;
|
||||
if (_bitBltScreens.ContainsKey(displayName))
|
||||
{
|
||||
SelectedScreen = displayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedScreen = _bitBltScreens.Keys.First();
|
||||
}
|
||||
RefreshCurrentScreenBounds();
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedScreen = _bitBltScreens.Keys.First();
|
||||
}
|
||||
RefreshCurrentScreenBounds();
|
||||
}
|
||||
|
||||
private void ClearDirectXOutputs()
|
||||
@ -182,14 +197,6 @@ namespace Remotely.Desktop.Win.Services
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private enum GetDirectXFrameResult
|
||||
{
|
||||
Success,
|
||||
Failure,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
private (GetDirectXFrameResult result, Bitmap frame) GetDirectXFrame()
|
||||
{
|
||||
try
|
||||
|
||||
@ -111,7 +111,7 @@ namespace Remotely.Desktop.Win.ViewModels
|
||||
FileUploads.Add(fileUpload);
|
||||
});
|
||||
|
||||
await _fileTransferService.UploadFile(fileUpload, _viewer, (double progress) =>
|
||||
await _fileTransferService.UploadFile(fileUpload, _viewer, fileUpload.CancellationTokenSource.Token, (double progress) =>
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() => fileUpload.PercentProgress = progress);
|
||||
});
|
||||
@ -122,6 +122,7 @@ namespace Remotely.Desktop.Win.ViewModels
|
||||
if (param is FileUpload fileUpload)
|
||||
{
|
||||
FileUploads.Remove(fileUpload);
|
||||
fileUpload.CancellationTokenSource.Cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
38
README.md
38
README.md
@ -33,7 +33,7 @@ It's *highly* encouraged that you get comfortable building and deploying from so
|
||||
## Build Instructions (GitHub)
|
||||
GitHub Actions allows you to build and deploy Remotely for free from their cloud servers. The definitions for the build processes are located in `/.github/workflows/` folder.
|
||||
|
||||
After forking the repo, follow the instructions in the workflow YML file. The easiest workflow to use is the Build.yml worfklow, and I'd recommend starting with that one. It will produce a build artifact (ZIP package) identical to what is on the Releases page, only the clients will have your server URL hard-coded.
|
||||
After forking the repo, follow the instructions in the workflow YML file. The easiest workflow to use is the Build.yml worfklow, and I'd recommend starting with that one. It will produce a build artifact (ZIP package) identical to what was on the Releases page, only the clients will have your server URL hard-coded.
|
||||
|
||||
### Instructions for using the Build workflow:
|
||||
- Fork the repo if you haven't already.
|
||||
@ -49,18 +49,17 @@ After forking the repo, follow the instructions in the workflow YML file. The e
|
||||
- If you're going to host on Windows, change the Server Runtime Identifier to `win-x64`.
|
||||
- Click "Run workflow".
|
||||
- When it's finished, there will be a build artifact for download that contains the server and clients.
|
||||
- Download the ZIP file and extract the files to the location where your site will be hosted (e.g. `/var/www/remotely`).
|
||||
- Run the install script located in the folder (e.g. `Ubuntu_Server_Install.sh`).
|
||||
|
||||
|
||||
## Hosting a Server (Windows)
|
||||
* Create a site in IIS that will run Remotely.
|
||||
* Run Install-RemotelyServer.ps1 (as an administrator), which is on the [Releases page](https://github.com/lucent-sea/Remotely/releases/latest) and in the [Utilities folder in source control](https://raw.githubusercontent.com/lucent-sea/Remotely/master/Utilities/Install-RemotelyServer.ps1).
|
||||
* Alternatively, you can build from source and copy the server files to the site folder.
|
||||
- Download the ZIP file and extract the files to the location where your site will be hosted (e.g. `/var/www/remotely`).
|
||||
- Run the install script located in the folder (e.g. `Ubuntu_Server_Install.sh`).
|
||||
- In the site's content directory, make a copy of the `appsettings.json` file and name it `appsettings.Production.json`.
|
||||
- The server will use this new file for reading/writing its settings, and it won't be overwritten by future ugprades.
|
||||
* Download and install the latest .NET Runtime (not the SDK) with the Hosting Bundle.
|
||||
* Link: https://dotnet.microsoft.com/download/dotnet-core/current/runtime
|
||||
* This includes the Hosting Bundle for Windows, which allows you to run ASP.NET Core in IIS.
|
||||
* Important: If you installed .NET Runtime before installing all the required IIS features, you may need to run a repair on the .NET Runtime installation.
|
||||
* Change values in appsettings.json for your environment. Make a copy named `appsettings.Production.json` (see Configuration section below).
|
||||
* By default, SQLite is used for the database.
|
||||
* The "Remotely.db" database file is automatically created in the root folder of your site.
|
||||
* You can browse and modify the contents using [DB Browser for SQLite](https://sqlitebrowser.org/).
|
||||
@ -73,18 +72,19 @@ After forking the repo, follow the instructions in the workflow YML file. The e
|
||||
## Hosting a Server (Ubuntu)
|
||||
* **IMPORTANT**: Recently, the default web server was switched from Nginx to Caddy Server. They cannot both be used on the same box. If you want to continue using Nginx, you'll need to set up the configuration manually. See the `Example_Nginx_Config.txt` file in the `Utilities` folder for an example.
|
||||
* Ubuntu 20.04, 19.04, and 18.04 have been tested.
|
||||
* Run Ubuntu_Server_Install.sh (with sudo), which is on the [Releases page](https://github.com/lucent-sea/Remotely/releases/latest) and in the [Utilities folder in source control](https://raw.githubusercontent.com/lucent-sea/Remotely/master/Utilities/Ubuntu_Server_Install.sh).
|
||||
* The script is designed to install Remotely and Caddy on the same server, running Ubuntu 20.04. You'll need to manually set up other configurations.
|
||||
* A helpful user supplied an example Apache configuration, which can be found in the Utilities folder.
|
||||
* The script will prompt for the "App root" location, which is the above directory where the server files are located.
|
||||
* The script installs the .NET runtime, as well as other dependencies.
|
||||
* Certbot is used in this script and will install an SSL certificate for your site. Your server needs to have a public domain name that is accessible from the internet for this to work.
|
||||
* More information: https://letsencrypt.org/, https://certbot.eff.org/
|
||||
* Alternatively, you can build from source (using RuntimeIdentifier "linux-x64" for the server) and copy the server files to the site folder.
|
||||
* Change values in appsettings.json for your environment. Make a copy named `appsettings.Production.json` (see Configuration section below).
|
||||
* Documentation for hosting behind Nginx can be found here: https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx
|
||||
* There is no default account. You must create the first one via the Register page, which will create an account that is both a server and organization admin.
|
||||
|
||||
## Upgrading
|
||||
* To upgrade a server, do any of the below to copy the new Server application files.
|
||||
* Run one of the GitHub Actions workflows, then copy the ZIP contents to the site's content folder.
|
||||
* Build from source as described above and `rsync`/`robocopy` the output files to the server directory.
|
||||
* Build from source and deploy to IIS (e.g. `dotnet publish /p:PublishProfile=MyProfile`)
|
||||
* For Linux, you'll need to restart the Remotely service in systemd after overwriting the files.
|
||||
* For Windows, you'll need to shut down the site's Application Pool in IIS before copying the files.
|
||||
* Windows won't let you overwrite files that are in use.
|
||||
* The only things that shouldn't be overwritten are the database DB file (if using SQLite) and the `appsettings.Production.json`. These files should never exist in the publish output.
|
||||
|
||||
## Hosting Scenarios
|
||||
There are countless ways to host an ASP.NET Core app, and I can't document or automate all of them. For hosting scenarios aside from the above two, please refer to Microsoft's documentation.
|
||||
@ -113,13 +113,6 @@ The first account created will be an admin for both the server and the organizat
|
||||
|
||||
An organization admin has access to the Organization page and server log entries specific to his/her organization. A server admin has access to the Server Config page and can see server log entries that don't belong to an organization.
|
||||
|
||||
## Upgrading
|
||||
* To upgrade a server, do any of the below to copy the new Server application files.
|
||||
* Run one of the GitHub Actions workflows.
|
||||
* Build from source as described above and `rsync`/`robocopy` the output files to the server directory.
|
||||
* Build from source and deploy to IIS (e.g. `dotnet publish /p:PublishProfile=MyProfile`)
|
||||
* For Linux, you'll also need to restart the Remotely service in systemd after overwriting the files.
|
||||
* The only things that can't be overwritten are the database DB file (if using SQLite) and the `appsettings.Production.json`. These files should never exist in the publish output.
|
||||
|
||||
## Branding
|
||||
Within the Account section, there is a tab for branding, which will apply to the quick support clients and Windows installer.
|
||||
@ -182,6 +175,7 @@ You can change database by changing `DBProvider` in `ApplicationOptions` to `SQL
|
||||
* Linux: Only Ubuntu 18.04+ is tested.
|
||||
* For the Ubuntu's "quick support" client, you must first install the following dependencies:
|
||||
* libx11-dev
|
||||
* libxrandr-dev
|
||||
* libc6-dev
|
||||
* libgdiplus
|
||||
* libxtst-dev
|
||||
|
||||
@ -34,6 +34,11 @@ namespace Remotely.Server.Components.Devices
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
JsInterop.ScrollToEnd(_chatMessagesWindow);
|
||||
base.OnAfterRender(firstRender);
|
||||
}
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
AppState.PropertyChanged += AppState_PropertyChanged;
|
||||
@ -85,9 +90,9 @@ namespace Remotely.Server.Components.Devices
|
||||
session.MissedChats++;
|
||||
}
|
||||
|
||||
JsInterop.ScrollToEnd(_chatMessagesWindow);
|
||||
|
||||
InvokeAsync(StateHasChanged);
|
||||
|
||||
JsInterop.ScrollToEnd(_chatMessagesWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,19 +18,26 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.2.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.13" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -32,6 +32,7 @@ apt-get -y install dotnet-runtime-5.0
|
||||
rm packages-microsoft-prod.deb
|
||||
|
||||
apt-get -y install libx11-dev
|
||||
apt-get -y install libxrandr-dev
|
||||
apt-get -y install unzip
|
||||
apt-get -y install libc6-dev
|
||||
apt-get -y install libgdiplus
|
||||
|
||||
@ -10,9 +10,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ConcurrentList" Version="1.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="5.0.5" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="5.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -25,7 +25,7 @@ namespace Remotely.Shared.Utilities
|
||||
{
|
||||
CheckLogFileExists();
|
||||
|
||||
File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Debug]\t-[{callerName}]\t{message}{Environment.NewLine}");
|
||||
File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Debug]\t[{callerName}]\t{message}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(message);
|
||||
@ -62,7 +62,7 @@ namespace Remotely.Shared.Utilities
|
||||
WriteLock.Wait();
|
||||
|
||||
CheckLogFileExists();
|
||||
File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t-[{callerName}]\t{message}{Environment.NewLine}");
|
||||
File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t[{callerName}]\t{message}{Environment.NewLine}");
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
catch { }
|
||||
@ -84,7 +84,7 @@ namespace Remotely.Shared.Utilities
|
||||
|
||||
while (exception != null)
|
||||
{
|
||||
File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t-[{callerName}]\t{exception?.Message}\t{exception?.StackTrace}\t{exception?.Source}{Environment.NewLine}");
|
||||
File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t[{callerName}]\t{exception?.Message}\t{exception?.StackTrace}\t{exception?.Source}{Environment.NewLine}");
|
||||
Console.WriteLine(exception.Message);
|
||||
exception = exception.InnerException;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -74,10 +74,10 @@ namespace Remotely.Tests
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
var screen = _capturer.GetNextFrame();
|
||||
var a = ImageUtils.EncodeWithSkia(screen, SkiaSharp.SKEncodedImageFormat.Jpeg, 70);
|
||||
Debug.WriteLine("JPEG Size: " + a.Length.ToString("N0"));
|
||||
var b = ImageUtils.EncodeWithSkia(screen, SkiaSharp.SKEncodedImageFormat.Webp, 70);
|
||||
Debug.WriteLine("WEBP Size: " + b.Length.ToString("N0"));
|
||||
//var a = ImageUtils.EncodeWithSkia(screen, SkiaSharp.SKEncodedImageFormat.Jpeg, 70);
|
||||
//Debug.WriteLine("JPEG Size: " + a.Length.ToString("N0"));
|
||||
//var b = ImageUtils.EncodeWithSkia(screen, SkiaSharp.SKEncodedImageFormat.Webp, 70);
|
||||
//Debug.WriteLine("WEBP Size: " + b.Length.ToString("N0"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@ -23,9 +23,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user