Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions Desktop.Linux/Services/ScreenCapturerLinux.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Remotely.Desktop.Shared.Services;
using Microsoft.Extensions.Logging;
using Remotely.Shared.Primitives;
using Remotely.Shared.Models.Dtos;
using SkiaSharp;
using System.Drawing;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -46,6 +47,72 @@ public IEnumerable<string> GetDisplayNames()
return _x11Screens.Keys.Select(x => x.ToString());
}

public IEnumerable<DisplayLayoutDto> GetDisplayLayouts()
{
return _x11Screens.Select(kvp => new DisplayLayoutDto
{
DisplayName = kvp.Key,
X = kvp.Value.x,
Y = kvp.Value.y,
Width = kvp.Value.width,
Height = kvp.Value.height,
IsPrimary = kvp.Value.primary
});
}

public Result<SKBitmap> GetDisplayThumbnail(string displayName)
{
try
{
if (!_x11Screens.TryGetValue(displayName, out var screen))
{
return Result.Fail<SKBitmap>($"Display '{displayName}' not found.");
}

var bounds = new Rectangle(screen.x, screen.y, screen.width, screen.height);
var bitmap = new SKBitmap(bounds.Width, bounds.Height);
var window = LibX11.XDefaultRootWindow(Display);

var imagePointer = LibX11.XGetImage(Display,
window,
bounds.X,
bounds.Y,
bounds.Width,
bounds.Height,
~0,
2);

if (imagePointer == nint.Zero)
{
return Result.Ok(bitmap);
}

var image = Marshal.PtrToStructure<LibX11.XImage>(imagePointer);
var pixels = bitmap.GetPixels();
unsafe
{
var scan1 = (byte*)pixels.ToPointer();
var scan2 = (byte*)image.data.ToPointer();
var bytesPerPixel = bitmap.BytesPerPixel;
var totalSize = bitmap.Height * bitmap.Width * bytesPerPixel;
for (var counter = 0; counter < totalSize; counter++)
{
scan1[counter] = scan2[counter];
}
}

Marshal.DestroyStructure<LibX11.XImage>(imagePointer);
LibX11.XDestroyImage(imagePointer);

return Result.Ok(bitmap);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while capturing thumbnail for display {displayName}.", displayName);
return Result.Fail<SKBitmap>(ex);
}
}

public SKRect GetFrameDiffArea()
{
if (_currentFrame is null)
Expand Down
6 changes: 5 additions & 1 deletion Desktop.Shared/Abstractions/IScreenCapturer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Remotely.Shared.Primitives;
using Remotely.Shared.Models.Dtos;
using Remotely.Shared.Primitives;
using SkiaSharp;
using System.Drawing;

Expand All @@ -13,12 +14,15 @@ public interface IScreenCapturer : IDisposable
bool IsGpuAccelerated { get; }
string SelectedScreen { get; }
IEnumerable<string> GetDisplayNames();
IEnumerable<DisplayLayoutDto> GetDisplayLayouts();
SKRect GetFrameDiffArea();

Result<SKBitmap> GetImageDiff();

Result<SKBitmap> GetNextFrame();

Result<SKBitmap> GetDisplayThumbnail(string displayName);

int GetScreenCount();

Rectangle GetVirtualScreenBounds();
Expand Down
2 changes: 2 additions & 0 deletions Desktop.Shared/Services/ScreenCaster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ await viewer.SendScreenData(
screenBounds.Width,
screenBounds.Height);

await viewer.SendScreenThumbnails();

await viewer.SendCursorChange(_cursorIconWatcher.GetCurrentCursor());

await viewer.SendWindowsSessions();
Expand Down
42 changes: 41 additions & 1 deletion Desktop.Shared/Services/Viewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public interface IViewer : IDisposable
Task SendDesktopStream(IAsyncEnumerable<byte[]> asyncEnumerable, Guid streamId);
Task SendFile(FileUpload fileUpload, Action<double> progressUpdateCallback, CancellationToken cancelToken);
Task SendScreenData(string selectedDisplay, IEnumerable<string> displayNames, int screenWidth, int screenHeight);
Task SendScreenThumbnails();
Task SendScreenSize(int width, int height);
Task SendSessionMetrics(SessionMetricsDto metrics);
Task SendWindowsSessions();
Expand Down Expand Up @@ -273,11 +274,50 @@ public async Task SendScreenData(
DisplayNames = displayNames,
SelectedDisplay = selectedDisplay,
ScreenWidth = screenWidth,
ScreenHeight = screenHeight
ScreenHeight = screenHeight,
DisplayLayouts = Capturer.GetDisplayLayouts().ToArray()
};
await TrySendToViewer(dto, DtoType.ScreenData, ViewerConnectionId);
}

public async Task SendScreenThumbnails()
{
foreach (var displayName in Capturer.GetDisplayNames())
{
try
{
var thumbnailResult = Capturer.GetDisplayThumbnail(displayName);
if (!thumbnailResult.IsSuccess || thumbnailResult.Value is null)
{
continue;
}

using var bitmap = thumbnailResult.Value;
const int maxWidth = 320;
int thumbWidth = Math.Min(maxWidth, bitmap.Width);
int thumbHeight = (int)Math.Round((double)bitmap.Height / bitmap.Width * thumbWidth);

using var scaledBitmap = bitmap.Resize(new SkiaSharp.SKImageInfo(thumbWidth, thumbHeight), SkiaSharp.SKFilterQuality.Low);
using var image = SkiaSharp.SKImage.FromBitmap(scaledBitmap);
using var data = image.Encode(SkiaSharp.SKEncodedImageFormat.Jpeg, 50);
var jpegBytes = data.ToArray();

var thumbnailDto = new ScreenThumbnailDto
{
DisplayName = displayName,
ImageBytes = jpegBytes,
Width = thumbWidth,
Height = thumbHeight
};
await TrySendToViewer(thumbnailDto, DtoType.ScreenThumbnail, ViewerConnectionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while sending thumbnail for display {displayName}.", displayName);
}
}
}

public async Task SendScreenSize(int width, int height)
{
var dto = new ScreenSizeDto(width, height);
Expand Down
5 changes: 5 additions & 0 deletions Desktop.UI/ViewModels/Fakes/FakeMainViewViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ public Task SendScreenData(string selectedDisplay, IEnumerable<string> displayNa
return Task.CompletedTask;
}

public Task SendScreenThumbnails()
{
return Task.CompletedTask;
}

public Task SendScreenSize(int width, int height)
{
return Task.CompletedTask;
Expand Down
38 changes: 38 additions & 0 deletions Desktop.Win/Services/ScreenCapturerWin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Remotely.Desktop.Shared.Abstractions;
using Remotely.Desktop.Shared.Services;
using Remotely.Shared.Models;
using Remotely.Shared.Models.Dtos;
using Bitbound.SimpleMessenger;
using Remotely.Desktop.Win.Helpers;
using Remotely.Desktop.Win.Models;
Expand Down Expand Up @@ -112,6 +113,43 @@ public IEnumerable<string> GetDisplayNames()
.Select(x => x.DeviceName);
}

public IEnumerable<DisplayLayoutDto> GetDisplayLayouts()
{
return DisplaysEnumerationHelper.GetDisplays().Select(d => new DisplayLayoutDto
{
DisplayName = d.DeviceName,
X = d.MonitorArea.X,
Y = d.MonitorArea.Y,
Width = d.MonitorArea.Width,
Height = d.MonitorArea.Height,
IsPrimary = d.IsPrimary
});
}

public Result<SKBitmap> GetDisplayThumbnail(string displayName)
{
try
{
if (!_displays.TryGetValue(displayName, out var display))
{
return Result.Fail<SKBitmap>($"Display '{displayName}' not found.");
}

var area = display.MonitorArea;
using var bitmap = new Bitmap(area.Width, area.Height, PixelFormat.Format32bppArgb);
using (var graphic = Graphics.FromImage(bitmap))
{
graphic.CopyFromScreen(area.Left, area.Top, 0, 0, new Size(area.Width, area.Height));
}
return Result.Ok(bitmap.ToSKBitmap());
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while capturing thumbnail for display {displayName}.", displayName);
return Result.Fail<SKBitmap>(ex);
}
}

public SKRect GetFrameDiffArea()
{
if (CurrentFrame is null)
Expand Down
58 changes: 58 additions & 0 deletions Server/wwwroot/css/remote-control.css
Original file line number Diff line number Diff line change
Expand Up @@ -480,3 +480,61 @@ footer {
grid-template-columns: 0 2fr 1fr;
}
}

/* Monitor switch animation */
@keyframes switching-fade {
0% { opacity: 1; }
50% { opacity: 0.2; }
100% { opacity: 1; }
}

#screenViewer.switching {
animation: switching-fade 0.3s ease-in-out;
}

/* Monitor layout diagram */
.monitor-layout-diagram {
display: flex;
justify-content: center;
padding: 6px 4px 4px 4px;
cursor: default;
}

.monitor-layout-rect {
fill: rgba(80, 120, 200, 0.3);
stroke: rgba(100, 150, 230, 0.8);
stroke-width: 1.5;
cursor: pointer;
transition: fill 0.15s ease;
}

.monitor-layout-rect:hover {
fill: rgba(80, 120, 200, 0.6);
}

.monitor-layout-rect-selected {
fill: rgba(0, 160, 120, 0.45);
stroke: rgba(0, 200, 150, 0.9);
}

/* Monitor thumbnail buttons */
#screenSelectMenu button {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px;
gap: 4px;
width: 100%;
height: auto;
min-height: 40px;
}

.monitor-thumbnail {
max-width: 160px;
width: 100%;
height: auto;
border-radius: 3px;
display: block;
background-color: rgba(40, 40, 40, 0.5);
min-height: 20px;
}
12 changes: 11 additions & 1 deletion Server/wwwroot/src/DtoMessageHandler.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Server/wwwroot/src/DtoMessageHandler.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion Server/wwwroot/src/DtoMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CursorChangeDto,
ScreenDataDto,
ScreenSizeDto,
ScreenThumbnailDto,
FileDto,
WindowsSessionsDto,
DtoWrapper,
Expand Down Expand Up @@ -47,6 +48,8 @@ export class DtoMessageHandler {
this.HandleFile(wrapper);
case DtoType.SessionMetrics:
await this.HandleSessionMetrics(wrapper);
case DtoType.ScreenThumbnail:
this.HandleScreenThumbnail(wrapper);
default:
break;
}
Expand Down Expand Up @@ -99,7 +102,7 @@ export class DtoMessageHandler {
document.title = `${screenDataDto.MachineName} - Remote Control Session`;
UI.ToggleConnectUI(false);
UI.SetScreenSize(screenDataDto.ScreenWidth, screenDataDto.ScreenHeight);
UI.UpdateDisplays(screenDataDto.SelectedDisplay, screenDataDto.DisplayNames);
UI.UpdateDisplays(screenDataDto.SelectedDisplay, screenDataDto.DisplayNames, screenDataDto.DisplayLayouts);
}

HandleScreenSize(wrapper: DtoWrapper) {
Expand All @@ -109,9 +112,20 @@ export class DtoMessageHandler {
return;
}

UI.ScreenViewer.classList.remove("switching");
UI.SetScreenSize(screenSizeDto.Width, screenSizeDto.Height);
}

HandleScreenThumbnail(wrapper: DtoWrapper) {
let thumbnailDto = TryComplete<ScreenThumbnailDto>(wrapper);

if (!thumbnailDto) {
return;
}

UI.UpdateThumbnail(thumbnailDto.DisplayName, thumbnailDto.ImageBytes);
}

async HandleSessionMetrics(wrapper: DtoWrapper) {
let metricsDto = TryComplete<SessionMetricsDto>(wrapper);

Expand Down
1 change: 1 addition & 0 deletions Server/wwwroot/src/Enums/DtoType.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading