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
17 changes: 17 additions & 0 deletions Desktop.Shared/Services/DtoMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using MessagePack;
using Microsoft.Extensions.Logging;
using Remotely.Desktop.Native.Windows;
using Remotely.Shared.Enums;

namespace Remotely.Desktop.Shared.Services;

Expand Down Expand Up @@ -126,6 +127,9 @@ public async Task ParseMessage(IViewer viewer, byte[] message)
case DtoType.OpenFileTransferWindow:
OpenFileTransferWindow(viewer);
break;
case DtoType.QualityMode:
SetQualityMode(wrapper, viewer);
break;
default:
break;
}
Expand Down Expand Up @@ -284,6 +288,19 @@ private void OpenFileTransferWindow(IViewer viewer)
_fileTransferService.OpenFileTransferWindow(viewer);
}

private void SetQualityMode(DtoWrapper wrapper, IViewer viewer)
{
if (!DtoChunker.TryComplete<SetQualityModeDto>(wrapper, out var dto))
{
return;
}

viewer.QualityMode = dto!.QualityMode;
_logger.LogInformation("Quality mode changed to {qualityMode} for viewer {viewerConnectionId}.",
dto.QualityMode,
viewer.ViewerConnectionId);
}

private void SelectScreen(DtoWrapper wrapper, IViewer viewer)
{
if (!DtoChunker.TryComplete<SelectScreenDto>(wrapper, out var dto))
Expand Down
38 changes: 36 additions & 2 deletions Desktop.Shared/Services/Viewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.SignalR.Client;
using Remotely.Shared.Services;
using Remotely.Desktop.Native.Windows;
using Remotely.Shared.Enums;

namespace Remotely.Desktop.Shared.Services;

Expand All @@ -21,6 +22,7 @@ public interface IViewer : IDisposable
int ImageQuality { get; }
bool IsResponsive { get; }
string Name { get; set; }
RemoteControlQualityMode QualityMode { get; set; }
TimeSpan RoundTripLatency { get; }
string ViewerConnectionId { get; set; }

Expand All @@ -44,6 +46,14 @@ public interface IViewer : IDisposable
public class Viewer : IViewer
{
public const int DefaultQuality = 80;
public const int PerformanceQuality = 40;
public const int HighQuality = 100;

// Auto quality thresholds
private const int AutoQualityDecreaseStep = 10;
private const int AutoQualityIncreaseStep = 2;
private const double HighLatencyThresholdMs = 200;
private const double LowLatencyThresholdMs = 100;

private readonly IAudioCapturer _audioCapturer;
private readonly IClipboardService _clipboardService;
Expand Down Expand Up @@ -96,6 +106,7 @@ public bool DisconnectRequested
public int ImageQuality { get; private set; } = DefaultQuality;
public bool IsResponsive { get; private set; } = true;
public string Name { get; set; } = string.Empty;
public RemoteControlQualityMode QualityMode { get; set; } = RemoteControlQualityMode.Auto;
public TimeSpan RoundTripLatency { get; private set; }

public string ViewerConnectionId { get; set; } = string.Empty;
Expand All @@ -108,10 +119,33 @@ public void AppendSentFrame(SentFrame sentFrame)

public Task ApplyAutoQuality()
{
if (ImageQuality < DefaultQuality)
switch (QualityMode)
{
ImageQuality = Math.Min(DefaultQuality, ImageQuality + 2);
case RemoteControlQualityMode.Performance:
ImageQuality = PerformanceQuality;
break;

case RemoteControlQualityMode.Quality:
ImageQuality = HighQuality;
break;

case RemoteControlQualityMode.Auto:
default:
// Decrease quality when latency is high or viewer is falling behind.
if (RoundTripLatency.TotalMilliseconds > HighLatencyThresholdMs ||
_lastFrameSent - _lastFrameReceived > TimeSpan.FromMilliseconds(500))
{
ImageQuality = Math.Max(PerformanceQuality, ImageQuality - AutoQualityDecreaseStep);
}
else if (RoundTripLatency.TotalMilliseconds < LowLatencyThresholdMs &&
_lastFrameSent - _lastFrameReceived < TimeSpan.FromMilliseconds(200))
{
// Gradually recover quality when conditions improve.
ImageQuality = Math.Min(DefaultQuality, ImageQuality + AutoQualityIncreaseStep);
}
break;
}

return Task.CompletedTask;
}

Expand Down
3 changes: 3 additions & 0 deletions Desktop.UI/ViewModels/Fakes/FakeMainViewViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Remotely.Desktop.Shared.ViewModels;
using Remotely.Shared.Models;
using Remotely.Shared.Models.Dtos;
using Remotely.Shared.Enums;
using System.Collections.ObjectModel;
using System.Windows.Input;

Expand Down Expand Up @@ -84,6 +85,8 @@ private class FakeViewer : IViewer

public string Name { get; set; } = "Rick James";

public RemoteControlQualityMode QualityMode { get; set; } = RemoteControlQualityMode.Auto;

public TimeSpan RoundTripLatency => default;

public string ViewerConnectionId { get; set; } = string.Empty;
Expand Down
27 changes: 27 additions & 0 deletions Server/Pages/Viewer.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,27 @@
</select>
</div>

<div id="qualityModeMenu" class="popup-menu">
<div class="text-center" style="margin-bottom: 5px;">
Streaming Quality
</div>
<button id="qualityAutoButton" class="quality-mode-option toggled"
title="Automatically adapts quality based on network conditions.">
<i class="fas fa-magic"></i>
Auto (Adaptive)
</button>
<button id="qualityPerformanceButton" class="quality-mode-option"
title="Lower image quality, reduces bandwidth and improves performance on slow networks.">
<i class="fas fa-tachometer-alt"></i>
Performance (Lower Quality)
</button>
<button id="qualityHighButton" class="quality-mode-option"
title="Highest image quality, uses more bandwidth. Best for fast local networks.">
<i class="fas fa-star"></i>
Quality (Higher Bandwidth)
</button>
</div>

<div id="extrasMenu" class="popup-menu">

<button id="audioButton"
Expand All @@ -92,6 +113,12 @@
<i class="fas fa-gauge"></i>
Metrics
</button>

<button id="qualityModeButton"
title="Change the streaming quality mode.">
<i class="fas fa-sliders"></i>
Quality: <span id="qualityModeLabel">Auto</span>
</button>
</div>

<div id="metricsFrame" class="d-none">
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.

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

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

3 changes: 2 additions & 1 deletion Server/wwwroot/src/Enums/DtoType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
SetKeyStatesUp = 24,
FrameReceived = 25,
OpenFileTransferWindow = 27,
SessionMetrics = 28
SessionMetrics = 28,
QualityMode = 29
}
13 changes: 13 additions & 0 deletions Server/wwwroot/src/Enums/RemoteControlQualityMode.js

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

1 change: 1 addition & 0 deletions Server/wwwroot/src/Enums/RemoteControlQualityMode.js.map

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

13 changes: 13 additions & 0 deletions Server/wwwroot/src/Enums/RemoteControlQualityMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export enum RemoteControlQualityMode {
/** Auto-adapts quality based on network conditions (latency and frame delivery).
* This is the default mode. */
Auto = 0,

/** Uses lower image quality to reduce bandwidth usage and improve performance
* on slow or congested networks. */
Performance = 1,

/** Uses the highest image quality at the cost of increased bandwidth.
* Best for high-speed local networks where quality is the priority. */
Quality = 2
}
46 changes: 45 additions & 1 deletion Server/wwwroot/src/InputEventHandlers.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/InputEventHandlers.js.map

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions Server/wwwroot/src/InputEventHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,19 @@ import {
MetricsFrame,
SetStatusMessage,
BetaPillPullDown,
QualityModeButton,
QualityModeMenu,
QualityModeLabel,
QualityAutoButton,
QualityPerformanceButton,
QualityHighButton,
} from "./UI.js";
import { Sound } from "./Sound.js";
import { ViewerApp } from "./App.js";
import { Point } from "./Models/Point.js";
import { UploadFiles } from "./FileTransferService.js";
import { RemoteControlMode } from "./Enums/RemoteControlMode.js";
import { RemoteControlQualityMode } from "./Enums/RemoteControlQualityMode.js";
import { GetDistanceBetween } from "./Utilities.js";
import { ShowToast } from "./UI.js";

Expand Down Expand Up @@ -349,6 +356,65 @@ export function ApplyInputHandlers() {
MetricsFrame.classList.toggle("d-none");
});

QualityModeButton.addEventListener("click", (ev) => {
ev.stopPropagation();

CloseAllPopupMenus(QualityModeMenu.id);

const x =
document.body.clientWidth -
QualityModeButton.getBoundingClientRect().right;
const right = `${x.toFixed(0)}px`;
const y = QualityModeButton.getBoundingClientRect().bottom;
const top = `${y.toFixed(0)}px`;

QualityModeMenu.style.right = right;
QualityModeMenu.style.top = top;
QualityModeMenu.classList.toggle("open");

window.addEventListener(
"click",
() => {
CloseAllPopupMenus(null);
},
{ once: true }
);
});

const setQualityMode = async (mode: RemoteControlQualityMode, label: string) => {
[QualityAutoButton, QualityPerformanceButton, QualityHighButton].forEach(btn => {
btn.classList.remove("toggled");
});

switch (mode) {
case RemoteControlQualityMode.Auto:
QualityAutoButton.classList.add("toggled");
break;
case RemoteControlQualityMode.Performance:
QualityPerformanceButton.classList.add("toggled");
break;
case RemoteControlQualityMode.Quality:
QualityHighButton.classList.add("toggled");
break;
}

QualityModeLabel.innerText = label;
await ViewerApp.MessageSender.SendQualityMode(mode);
ShowToast(`Quality mode set to: ${label}`);
};

QualityAutoButton.addEventListener("click", async () => {
await setQualityMode(RemoteControlQualityMode.Auto, "Auto");
});

QualityPerformanceButton.addEventListener("click", async () => {
await setQualityMode(RemoteControlQualityMode.Performance, "Performance");
});

QualityHighButton.addEventListener("click", async () => {
await setQualityMode(RemoteControlQualityMode.Quality, "Quality");
});

ScreenViewer.addEventListener("pointerup", async ev => {
if (ViewerApp.ViewOnlyMode) {
return;
Expand Down
5 changes: 5 additions & 0 deletions Server/wwwroot/src/Interfaces/Dtos.js

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

Loading
Loading