Merge pull request #2981 from Kingtous/feat/dual_audio_transmission
feat: dual audio transmission support
This commit is contained in:
commit
563225e9d7
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -1334,8 +1334,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e"
|
||||
|
||||
[[package]]
|
||||
name = "default-net"
|
||||
version = "0.11.0"
|
||||
source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memalloc",
|
||||
|
@ -59,7 +59,7 @@ base64 = "0.13"
|
||||
sysinfo = "0.24"
|
||||
num_cpus = "1.13"
|
||||
bytes = { version = "1.2", features = ["serde"] }
|
||||
default-net = { git = "https://github.com/Kingtous/default-net" }
|
||||
default-net = "0.12.0"
|
||||
wol-rs = "0.9.1"
|
||||
flutter_rust_bridge = { version = "1.61.1", optional = true }
|
||||
errno = "0.2.8"
|
||||
|
1
flutter/assets/chat.svg
Normal file
1
flutter/assets/chat.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675159173189" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1697" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512.7 797H292.9c-24 0-47.3 4.7-70 12.5-57.3 19.5-108 50.7-155.5 87.4-13 10-28.3 10.9-40.2 1.7-8.2-6.3-12.6-14.7-12.6-25.1 0-133.2-0.2-266.5 0.1-399.7 0.1-36.9 6.7-73.1 17.3-108.6 10.8-36.1 26.1-70.1 47.4-101.2 32.7-47.8 76.2-81.7 131.7-99.5 18-5.8 36.6-9 55.4-10.5 12.2-1 24.4-1.1 36.6-1.1 26.5 0 52.9-0.3 79.4 0.1 72.8 0.9 145.6 0.6 218.5 0.3 41.6-0.2 83.2-0.3 124.9-0.3 28.3 0 56.1 3.9 83 13.1 34.5 11.7 65.3 29.6 92.2 54.3 16 14.8 30.2 31.1 42.6 49 32.4 46.9 52 98.7 61.1 154.8 3.2 19.8 5.1 39.7 4.7 59.8-0.9 50.3-11.7 98.3-33 144-13 27.9-29.5 53.5-49.7 76.6-30.5 34.8-67.3 60.7-110.9 76.7-23.6 8.7-48 13.6-73 15.2-6.2 0.4-12.4 0.5-18.6 0.5H512.7z m-4.6-580.6c0-0.1 0-0.1 0 0-70.5-0.1-141-0.1-211.5-0.1-10.2 0-20.4 0.2-30.6 1.3-26.9 2.8-52.1 10.8-75.3 24.9-26.8 16.2-47.3 38.8-64.1 64.9-15 23.2-25.7 48.3-33.7 74.7-9.3 30.9-15.1 62.6-15.2 94.8-0.3 110.5-0.1 220.9-0.1 331.4 0 1-0.5 2.4 0.5 3 1 0.6 1.9-0.6 2.7-1.1 28.5-18.3 58.1-34.6 89.3-47.9 41.6-17.8 84.6-28.4 130.3-28.3 136.6 0.4 273.2 0.1 409.8 0.2 13.8 0 27.6-0.1 41.3-1.8 20.1-2.5 39.5-7.6 57.9-16.3 36.9-17.4 66.3-43.5 88.8-77.3 40-60.4 55.1-126.7 45.3-198.5-5.3-38.9-17.3-75.7-36.2-110.2-14.1-25.7-31.8-48.7-54.2-67.8-34.8-29.9-75.5-45.2-121.1-45.6-74.6-0.8-149.3-0.3-223.9-0.3z" p-id="1698"></path><path d="M548.2 673.6c-17.5 0.4-34.7-2.3-51.7-6.4-6.4-1.5-11.5-5-16.1-9.6-24.6-24.3-48.9-48.8-72.3-74.3-21.6-23.5-42.6-47.5-61.8-73.1-13.4-17.9-26.4-36.1-35.1-56.9-8.1-19.4-10.5-39.5-7.4-60.4 4.1-27.4 16.7-50.8 33.5-72.3 6.3-8 13.2-15.3 20.8-22 9.3-8.2 20.2-10.3 31.9-5.9 11.8 4.5 18.7 13.4 20.2 26.1 1.2 10.3-2.1 19.1-9.7 26.2-11.8 11.2-21.8 23.7-28.6 38.6-6.7 14.7-8.8 29.7-2.7 45.1 4 10.2 10.3 19.3 16.5 28.4 17.1 24.9 36.8 47.7 56.8 70.2 22.1 24.9 45.6 48.5 68.9 72.3 2.4 2.5 5.1 4.8 7.5 7.3 2.2 2.2 5.1 1.8 7.7 2.1 16.1 2.2 32.1 2.8 48-1.3 13.2-3.4 23.6-10.4 30.9-22.3 12-19.3 38-20.4 51.9-2.7 7.7 9.9 8.5 24.1 1.8 35.4-16 27.1-40.1 43.2-70.2 50.9-4.2 1.1-8.4 1.9-12.7 2.6-9.2 1.5-18.5 2.4-28.1 2zM532.5 315.7c0.1-10.5 10.4-18.2 20.3-15.1 22.8 7.2 43.9 17.5 63.6 31 21.2 14.6 38.1 33.1 51.9 54.6 16.2 25.1 27.7 52.3 34.8 81.2 1 4 1.8 8 2.5 12.1 1.5 8.1-3.6 16.1-11.5 18.2-7.8 2.1-16.1-2-19-9.7-0.8-2.2-1.2-4.7-1.8-7-8-35.7-22.7-68.2-46-96.7-14.3-17.4-32.4-30-52.2-40.2-10.1-5.2-20.6-9.5-31.5-13-7.1-2.2-11.1-8-11.1-15.4zM615.6 513.1c-8.1-0.1-14.1-5.1-15.8-13.6-3.2-15.8-9.1-30.5-17.6-44.1-14.4-23.1-34.1-39.9-59.3-50.2-1.5-0.6-3-0.9-4.5-1.4-8.7-2.9-13.3-11.7-10.6-20.1 2.9-8.8 11.6-13.1 20.5-10.1 38.1 12.8 65.8 38 85.4 72.5 8.5 15 14.3 31.1 17.5 48.2 1.9 9.8-5.5 18.8-15.6 18.8z" p-id="1699"></path></svg>
|
After Width: | Height: | Size: 2.8 KiB |
24
flutter/assets/record_screen.svg
Normal file
24
flutter/assets/record_screen.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 415 415" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M174.848,188.711c2.407-2.068,3.577-5.109,3.577-9.297c0-4.133-1.193-7.307-3.647-9.701
|
||||
c-2.431-2.369-6.148-3.571-11.048-3.571h-15.915v25.73h15.576C168.552,191.872,172.407,190.809,174.848,188.711z"/>
|
||||
<path d="M0,65.241v284.518h415V65.241H0z M70.675,238.253c-18.293,0-33.123-14.83-33.123-33.123
|
||||
c0-18.293,14.83-33.123,33.123-33.123s33.123,14.83,33.123,33.123C103.798,223.423,88.968,238.253,70.675,238.253z
|
||||
M206.768,249.556h-22.422l-0.411-0.328c-2.099-1.679-3.467-4.433-4.067-8.185c-0.552-3.451-0.832-6.769-0.832-9.859v-6.979
|
||||
c0-4.492-1.212-8.003-3.602-10.435c-2.417-2.456-5.779-3.65-10.28-3.65h-17.337v39.437h-22.787v-101.66h38.701
|
||||
c11.546,0,20.743,2.699,27.335,8.023c6.688,5.403,10.078,13.012,10.078,22.614c0,5.404-1.442,10.124-4.285,14.028
|
||||
c-2.217,3.044-5.267,5.647-9.089,7.767c4.433,1.864,7.785,4.556,9.99,8.029c2.696,4.247,4.063,9.533,4.063,15.711v7.25
|
||||
c0,2.622,0.361,5.407,1.074,8.278c0.66,2.664,1.774,4.637,3.309,5.864l0.563,0.451V249.556z M287.335,249.556h-70.558v-101.66
|
||||
h70.422v18.246h-47.636v21.801h40.859v18.246h-40.859v25.121h47.771V249.556z M374.201,183.193l-0.552,1.65h-21.824v-1.5
|
||||
c0-6.092-1.443-10.781-4.29-13.937c-2.805-3.111-7.331-4.688-13.454-4.688c-5.458,0-9.671,2.155-12.879,6.589
|
||||
c-3.273,4.522-4.933,10.41-4.933,17.501v19.699c0,7.167,1.743,13.091,5.182,17.607c3.39,4.452,7.854,6.617,13.646,6.617
|
||||
c5.714,0,9.953-1.507,12.602-4.479c2.693-3.022,4.059-7.668,4.059-13.808v-1.5h21.756l0.552,1.65l0.004,0.23
|
||||
c0.187,11.009-3.245,19.89-10.199,26.396c-6.92,6.473-16.601,9.755-28.772,9.755c-12.247,0-22.344-4.006-30.01-11.906
|
||||
c-7.655-7.887-11.537-18.155-11.537-30.521v-19.583c0-12.311,3.785-22.573,11.25-30.504c7.488-7.957,17.34-11.991,29.28-11.991
|
||||
c12.524,0,22.485,3.289,29.605,9.776c7.166,6.531,10.705,15.519,10.519,26.714L374.201,183.193z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
1
flutter/assets/voice_call.svg
Normal file
1
flutter/assets/voice_call.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675772071409" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5514" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zM584 328c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z" p-id="5515"></path><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-0.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7 0.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c0.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-0.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66 0.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 0.05 0.01 0.1 0.01 0.15 1.07 12.15-0.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-0.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5 0.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l0.36 0.03c7.97 0.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-0.16 0.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l0.18-0.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z" p-id="5516"></path></svg>
|
After Width: | Height: | Size: 2.4 KiB |
1
flutter/assets/voice_call_waiting.svg
Normal file
1
flutter/assets/voice_call_waiting.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675683991720" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4457" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M561 362.67h-98V463h98V362.67z m200.67 0H661.33V463h100.33V362.67zM911 687c-62.22 0-121.33-9.33-177.33-28-20.22-6.22-37.33-2.34-51.33 11.67L572.67 780.34c-70-35.78-133.39-82.06-190.17-138.84S279.45 521.33 243.67 451.33l109.67-109.67c14-14 17.89-31.11 11.67-51.34-18.67-56-28-115.11-28-177.33 0-14-4.67-25.67-14-35-9.33-9.33-21-14-35-14H113c-14 0-25.67 4.67-35 14-9.33 9.33-14 21-14 35 0 112 21.39 220.11 64.17 324.33 42.78 104.22 103.83 196 183.17 275.34 79.33 79.34 171.1 140.4 275.33 183.17C690.89 938.61 799 960 911 960c14 0 25.67-4.67 35-14 9.33-9.33 14-21 14-35V736c0-14-4.67-25.67-14-35-9.33-9.33-21-14-35-14z m-51.33-224H960V362.67H859.67V463z" p-id="4458"></path></svg>
|
After Width: | Height: | Size: 1010 B |
@ -1723,3 +1723,30 @@ Future<void> updateSystemWindowTheme() async {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// macOS only
|
||||
///
|
||||
/// Note: not found a general solution for rust based AVFoundation bingding.
|
||||
/// [AVFoundation] crate has compile error.
|
||||
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
|
||||
|
||||
enum PermissionAuthorizeType {
|
||||
undetermined,
|
||||
authorized,
|
||||
denied, // and restricted
|
||||
}
|
||||
|
||||
Future<PermissionAuthorizeType> osxCanRecordAudio() async {
|
||||
int res = await kMacOSPermChannel.invokeMethod("canRecordAudio");
|
||||
print(res);
|
||||
if (res > 0) {
|
||||
return PermissionAuthorizeType.authorized;
|
||||
} else if (res == 0) {
|
||||
return PermissionAuthorizeType.undetermined;
|
||||
} else {
|
||||
return PermissionAuthorizeType.denied;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> osxRequestAudio() async {
|
||||
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
|
||||
}
|
||||
|
@ -106,6 +106,12 @@ const kRemoteImageQualityLow = 'low';
|
||||
/// [kRemoteImageQualityCustom] Custom image quality.
|
||||
const kRemoteImageQualityCustom = 'custom';
|
||||
|
||||
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
|
||||
const kRemoteAudioGuestToHost = 'guest-to-host';
|
||||
|
||||
/// [kRemoteAudioDualWay] dual-way audio mode(default).
|
||||
const kRemoteAudioDualWay = 'dual-way';
|
||||
|
||||
const kIgnoreDpi = true;
|
||||
|
||||
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
||||
|
@ -44,6 +44,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
var watchIsCanScreenRecording = false;
|
||||
var watchIsProcessTrust = false;
|
||||
var watchIsInputMonitoring = false;
|
||||
var watchIsCanRecordAudio = false;
|
||||
Timer? _updateTimer;
|
||||
|
||||
@override
|
||||
@ -79,7 +80,16 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
buildTip(context),
|
||||
buildIDBoard(context),
|
||||
buildPasswordBoard(context),
|
||||
buildHelpCards(),
|
||||
FutureBuilder<Widget>(
|
||||
future: buildHelpCards(),
|
||||
builder: (_, data) {
|
||||
if (data.hasData) {
|
||||
return data.data!;
|
||||
} else {
|
||||
return const Offstage();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -302,7 +312,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHelpCards() {
|
||||
Future<Widget> buildHelpCards() async {
|
||||
if (updateUrl.isNotEmpty) {
|
||||
return buildInstallCard(
|
||||
"Status",
|
||||
@ -349,6 +359,15 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
bind.mainIsInstalledDaemon(prompt: true);
|
||||
});
|
||||
}
|
||||
//// Disable microphone configuration for macOS. We will request the permission when needed.
|
||||
// else if ((await osxCanRecordAudio() !=
|
||||
// PermissionAuthorizeType.authorized)) {
|
||||
// return buildInstallCard("Permissions", "config_microphone", "Configure",
|
||||
// () async {
|
||||
// osxRequestAudio();
|
||||
// watchIsCanRecordAudio = true;
|
||||
// });
|
||||
// }
|
||||
} else if (Platform.isLinux) {
|
||||
if (bind.mainCurrentIsWayland()) {
|
||||
return buildInstallCard(
|
||||
@ -481,6 +500,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
if (watchIsCanRecordAudio) {
|
||||
if (Platform.isMacOS) {
|
||||
Future.microtask(() async {
|
||||
if ((await osxCanRecordAudio() ==
|
||||
PermissionAuthorizeType.authorized)) {
|
||||
watchIsCanRecordAudio = false;
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
watchIsCanRecordAudio = false;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
Get.put<RxBool>(svcStopped, tag: 'stop-service');
|
||||
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
|
||||
|
@ -521,6 +521,39 @@ class _CmControlPanel extends StatelessWidget {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !client.inVoiceCall,
|
||||
child: buildButton(context,
|
||||
color: Colors.red,
|
||||
onClick: () => closeVoiceCall(),
|
||||
icon: Icon(Icons.phone_disabled_rounded, color: Colors.white),
|
||||
text: "Stop voice call",
|
||||
textColor: Colors.white),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !client.incomingVoiceCall,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: buildButton(context,
|
||||
color: MyTheme.accent,
|
||||
onClick: () => handleVoiceCall(true),
|
||||
icon: Icon(Icons.phone_enabled, color: Colors.white),
|
||||
text: "Accept",
|
||||
textColor: Colors.white),
|
||||
),
|
||||
Expanded(
|
||||
child: buildButton(context,
|
||||
color: Colors.red,
|
||||
onClick: () => handleVoiceCall(false),
|
||||
icon:
|
||||
Icon(Icons.phone_disabled_rounded, color: Colors.white),
|
||||
text: "Dismiss",
|
||||
textColor: Colors.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !client.fromSwitch,
|
||||
child: buildButton(context,
|
||||
@ -626,7 +659,7 @@ class _CmControlPanel extends StatelessWidget {
|
||||
.marginSymmetric(horizontal: showElevation ? 0 : bigMargin);
|
||||
}
|
||||
|
||||
buildButton(
|
||||
Widget buildButton(
|
||||
BuildContext context, {
|
||||
required Color? color,
|
||||
required Function() onClick,
|
||||
@ -692,6 +725,14 @@ class _CmControlPanel extends StatelessWidget {
|
||||
void handleSwitchBack(BuildContext context) {
|
||||
bind.cmSwitchBack(connId: client.id);
|
||||
}
|
||||
|
||||
void handleVoiceCall(bool accept) {
|
||||
bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
|
||||
}
|
||||
|
||||
void closeVoiceCall() {
|
||||
bind.cmCloseVoiceCall(id: client.id);
|
||||
}
|
||||
}
|
||||
|
||||
void checkClickTime(int id, Function() callback) async {
|
||||
|
@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/chat_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:debounce_throttle/debounce_throttle.dart';
|
||||
@ -425,6 +426,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
menubarItems.add(_buildKeyboard(context));
|
||||
if (!isWeb) {
|
||||
menubarItems.add(_buildChat(context));
|
||||
menubarItems.add(_buildVoiceCall(context));
|
||||
}
|
||||
menubarItems.add(_buildRecording(context));
|
||||
menubarItems.add(_buildClose(context));
|
||||
@ -478,20 +480,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChat(BuildContext context) {
|
||||
return IconButton(
|
||||
tooltip: translate('Chat'),
|
||||
onPressed: () {
|
||||
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
|
||||
widget.ffi.chatModel.toggleChatOverlay();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.message,
|
||||
color: _MenubarTheme.commonColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonitor(BuildContext context) {
|
||||
final pi = widget.ffi.ffiModel.pi;
|
||||
return mod_menu.PopupMenuButton(
|
||||
@ -669,12 +657,17 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
? translate('Stop session recording')
|
||||
: translate('Start session recording'),
|
||||
onPressed: () => value.toggle(),
|
||||
icon: Icon(
|
||||
value.start
|
||||
? Icons.pause_circle_filled
|
||||
: Icons.videocam_outlined,
|
||||
color: _MenubarTheme.commonColor,
|
||||
),
|
||||
icon: value.start
|
||||
? Icon(
|
||||
Icons.pause_circle_filled,
|
||||
color: _MenubarTheme.commonColor,
|
||||
)
|
||||
: SvgPicture.asset(
|
||||
"assets/record_screen.svg",
|
||||
color: _MenubarTheme.commonColor,
|
||||
width: Theme.of(context).iconTheme.size ?? 22.0,
|
||||
height: Theme.of(context).iconTheme.size ?? 22.0,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
return Offstage();
|
||||
@ -695,6 +688,119 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChat(BuildContext context) {
|
||||
FfiModel ffiModel = Provider.of<FfiModel>(context);
|
||||
return mod_menu.PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: SvgPicture.asset(
|
||||
"assets/chat.svg",
|
||||
color: _MenubarTheme.commonColor,
|
||||
width: Theme.of(context).iconTheme.size ?? 24.0,
|
||||
height: Theme.of(context).iconTheme.size ?? 24.0,
|
||||
),
|
||||
tooltip: translate('Chat'),
|
||||
position: mod_menu.PopupMenuPosition.under,
|
||||
itemBuilder: (BuildContext context) => _getChatMenu(context)
|
||||
.map((entry) => entry.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
commonColor: _MenubarTheme.commonColor,
|
||||
height: _MenubarTheme.height,
|
||||
dividerHeight: _MenubarTheme.dividerHeight,
|
||||
)))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getVoiceCallIcon() {
|
||||
switch (widget.ffi.chatModel.voiceCallStatus.value) {
|
||||
case VoiceCallStatus.waitingForResponse:
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
widget.ffi.chatModel.closeVoiceCall(widget.id);
|
||||
},
|
||||
icon: SvgPicture.asset(
|
||||
"assets/voice_call_waiting.svg",
|
||||
color: Colors.red,
|
||||
width: Theme.of(context).iconTheme.size ?? 20.0,
|
||||
height: Theme.of(context).iconTheme.size ?? 20.0,
|
||||
));
|
||||
case VoiceCallStatus.connected:
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
widget.ffi.chatModel.closeVoiceCall(widget.id);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.phone_disabled_rounded,
|
||||
color: Colors.red,
|
||||
size: Theme.of(context).iconTheme.size ?? 22.0,
|
||||
),
|
||||
);
|
||||
default:
|
||||
return const Offstage();
|
||||
}
|
||||
}
|
||||
|
||||
String? _getVoiceCallTooltip() {
|
||||
switch (widget.ffi.chatModel.voiceCallStatus.value) {
|
||||
case VoiceCallStatus.waitingForResponse:
|
||||
return "Waiting";
|
||||
case VoiceCallStatus.connected:
|
||||
return "Disconnect";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildVoiceCall(BuildContext context) {
|
||||
return Obx(
|
||||
() {
|
||||
final tooltipText = _getVoiceCallTooltip();
|
||||
return tooltipText == null
|
||||
? const Offstage()
|
||||
: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: _getVoiceCallIcon(),
|
||||
tooltip: translate(tooltipText),
|
||||
onPressed: () => bind.sessionRequestVoiceCall(id: widget.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<MenuEntryBase<String>> _getChatMenu(BuildContext context) {
|
||||
final List<MenuEntryBase<String>> chatMenu = [];
|
||||
const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0);
|
||||
chatMenu.addAll([
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Text chat'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
|
||||
widget.ffi.chatModel.toggleChatOverlay();
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
translate('Voice call'),
|
||||
style: style,
|
||||
),
|
||||
proc: () {
|
||||
// Request a voice call.
|
||||
bind.sessionRequestVoiceCall(id: widget.id);
|
||||
},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
]);
|
||||
return chatMenu;
|
||||
}
|
||||
|
||||
List<MenuEntryBase<String>> _getControlMenu(BuildContext context) {
|
||||
final pi = widget.ffi.ffiModel.pi;
|
||||
final perms = widget.ffi.ffiModel.permissions;
|
||||
@ -884,7 +990,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
// ));
|
||||
// }
|
||||
}
|
||||
|
||||
return displayMenu;
|
||||
}
|
||||
|
||||
@ -1337,6 +1442,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
|
||||
if (perms['audio'] != false) {
|
||||
displayMenu
|
||||
.add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
|
||||
displayMenu
|
||||
.add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true));
|
||||
}
|
||||
|
||||
if (Platform.isWindows &&
|
||||
|
@ -216,6 +216,7 @@ void runMultiWindow(
|
||||
|
||||
void runConnectionManagerScreen(bool hide) async {
|
||||
await initEnv(kAppTypeConnectionManager);
|
||||
await bind.cmStartListenIpcThread();
|
||||
_runApp(
|
||||
'',
|
||||
const DesktopServerPage(),
|
||||
|
@ -2,6 +2,7 @@ import 'package:dash_chat_2/dash_chat_2.dart';
|
||||
import 'package:draggable_float_widget/draggable_float_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../consts.dart';
|
||||
@ -33,8 +34,13 @@ class ChatModel with ChangeNotifier {
|
||||
OverlayState? _overlayState;
|
||||
OverlayEntry? chatIconOverlayEntry;
|
||||
OverlayEntry? chatWindowOverlayEntry;
|
||||
|
||||
bool isConnManager = false;
|
||||
|
||||
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
|
||||
|
||||
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
|
||||
|
||||
final ChatUser me = ChatUser(
|
||||
id: "",
|
||||
firstName: "Me",
|
||||
@ -292,4 +298,34 @@ class ChatModel with ChangeNotifier {
|
||||
resetClientMode() {
|
||||
_messages[clientModeID]?.clear();
|
||||
}
|
||||
|
||||
void onVoiceCallWaiting() {
|
||||
_voiceCallStatus.value = VoiceCallStatus.waitingForResponse;
|
||||
}
|
||||
|
||||
void onVoiceCallStarted() {
|
||||
_voiceCallStatus.value = VoiceCallStatus.connected;
|
||||
}
|
||||
|
||||
void onVoiceCallClosed(String reason) {
|
||||
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||
}
|
||||
|
||||
void onVoiceCallIncoming() {
|
||||
if (isConnManager) {
|
||||
_voiceCallStatus.value = VoiceCallStatus.incoming;
|
||||
}
|
||||
}
|
||||
|
||||
void closeVoiceCall(String id) {
|
||||
bind.sessionCloseVoiceCall(id: id);
|
||||
}
|
||||
}
|
||||
|
||||
enum VoiceCallStatus {
|
||||
notStarted,
|
||||
waitingForResponse,
|
||||
connected,
|
||||
// Connection manager only.
|
||||
incoming
|
||||
}
|
||||
|
@ -203,6 +203,23 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (name == "on_url_scheme_received") {
|
||||
final url = evt['url'].toString();
|
||||
parseRustdeskUri(url);
|
||||
} else if (name == "on_voice_call_waiting") {
|
||||
// Waiting for the response from the peer.
|
||||
parent.target?.chatModel.onVoiceCallWaiting();
|
||||
} else if (name == "on_voice_call_started") {
|
||||
// Voice call is connected.
|
||||
parent.target?.chatModel.onVoiceCallStarted();
|
||||
} else if (name == "on_voice_call_closed") {
|
||||
// Voice call is closed with reason.
|
||||
final reason = evt['reason'].toString();
|
||||
parent.target?.chatModel.onVoiceCallClosed(reason);
|
||||
} else if (name == "on_voice_call_incoming") {
|
||||
// Voice call is requested by the peer.
|
||||
parent.target?.chatModel.onVoiceCallIncoming();
|
||||
} else if (name == "update_voice_call_state") {
|
||||
parent.target?.serverModel.updateVoiceCallState(evt);
|
||||
} else {
|
||||
debugPrint("Unknown event name: $name");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -118,8 +118,12 @@ class PlatformFFI {
|
||||
// Start a dbus service, no need to await
|
||||
_ffiBind.mainStartDbusServer();
|
||||
} else if (Platform.isMacOS && isMain) {
|
||||
// Start an ipc server for handling url schemes.
|
||||
_ffiBind.mainStartIpcUrlServer();
|
||||
Future.wait([
|
||||
// Start dbus service.
|
||||
_ffiBind.mainStartDbusServer(),
|
||||
// Start local audio pulseaudio server.
|
||||
_ffiBind.mainStartPa()
|
||||
]);
|
||||
}
|
||||
_startListenEvent(_ffiBind); // global event
|
||||
try {
|
||||
|
@ -579,6 +579,26 @@ class ServerModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void updateVoiceCallState(Map<String, dynamic> evt) {
|
||||
try {
|
||||
final client = Client.fromJson(jsonDecode(evt["client"]));
|
||||
final index = _clients.indexWhere((element) => element.id == client.id);
|
||||
if (index != -1) {
|
||||
_clients[index].inVoiceCall = client.inVoiceCall;
|
||||
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
|
||||
if (client.incomingVoiceCall) {
|
||||
// Has incoming phone call, let's set the window on top.
|
||||
Future.delayed(Duration.zero, () {
|
||||
window_on_top(null);
|
||||
});
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("updateVoiceCallState failed: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ClientType {
|
||||
@ -602,6 +622,8 @@ class Client {
|
||||
bool recording = false;
|
||||
bool disconnected = false;
|
||||
bool fromSwitch = false;
|
||||
bool inVoiceCall = false;
|
||||
bool incomingVoiceCall = false;
|
||||
|
||||
RxBool hasUnreadChatMessage = false.obs;
|
||||
|
||||
@ -623,6 +645,8 @@ class Client {
|
||||
recording = json['recording'];
|
||||
disconnected = json['disconnected'];
|
||||
fromSwitch = json['from_switch'];
|
||||
inVoiceCall = json['in_voice_call'];
|
||||
incomingVoiceCall = json['incoming_voice_call'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
@ -43,6 +43,8 @@
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Record the sound from microphone for the purpose of the remote desktop.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Cocoa
|
||||
import AVFoundation
|
||||
import FlutterMacOS
|
||||
import desktop_multi_window
|
||||
// import bitsdojo_window_macos
|
||||
@ -81,6 +82,23 @@ class MainFlutterWindow: NSWindow {
|
||||
case "terminate":
|
||||
NSApplication.shared.terminate(self)
|
||||
result(nil)
|
||||
case "canRecordAudio":
|
||||
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
||||
case .authorized:
|
||||
result(1)
|
||||
break
|
||||
case .notDetermined:
|
||||
result(0)
|
||||
break
|
||||
default:
|
||||
result(-1)
|
||||
break
|
||||
}
|
||||
case "requestRecordAudio":
|
||||
AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in
|
||||
result(granted)
|
||||
})
|
||||
break
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
@ -598,6 +598,18 @@ message Misc {
|
||||
}
|
||||
}
|
||||
|
||||
message VoiceCallRequest {
|
||||
int64 req_timestamp = 1;
|
||||
// Indicates whether the request is a connect action or a disconnect action.
|
||||
bool is_connect = 2;
|
||||
}
|
||||
|
||||
message VoiceCallResponse {
|
||||
bool accepted = 1;
|
||||
int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp].
|
||||
int64 ack_timestamp = 3;
|
||||
}
|
||||
|
||||
message Message {
|
||||
oneof union {
|
||||
SignedId signed_id = 3;
|
||||
@ -620,5 +632,7 @@ message Message {
|
||||
Cliprdr cliprdr = 20;
|
||||
MessageBox message_box = 21;
|
||||
SwitchSidesResponse switch_sides_response = 22;
|
||||
VoiceCallRequest voice_call_request = 23;
|
||||
VoiceCallResponse voice_call_response = 24;
|
||||
}
|
||||
}
|
||||
|
@ -1,58 +1,61 @@
|
||||
pub use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
Device, Host, StreamConfig,
|
||||
};
|
||||
use magnum_opus::{Channels::*, Decoder as AudioDecoder};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
ops::{Deref, Not},
|
||||
str::FromStr,
|
||||
sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock},
|
||||
sync::{Arc, atomic::AtomicBool, mpsc, Mutex, RwLock},
|
||||
};
|
||||
|
||||
pub use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
#[cfg(not(any(target_os = "android", target_os = "linux")))]
|
||||
use cpal::{
|
||||
Device,
|
||||
Host, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
};
|
||||
use magnum_opus::{Channels::*, Decoder as AudioDecoder};
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use file_trait::FileManager;
|
||||
use hbb_common::{
|
||||
AddrMangle,
|
||||
allow_err,
|
||||
anyhow::{anyhow, Context},
|
||||
bail,
|
||||
config::{
|
||||
Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT,
|
||||
Config, CONNECT_TIMEOUT, PeerConfig, PeerInfoSerde, READ_TIMEOUT, RELAY_PORT,
|
||||
RENDEZVOUS_TIMEOUT,
|
||||
},
|
||||
get_version_number, log,
|
||||
message_proto::{option_message::BoolOption, *},
|
||||
}, get_version_number,
|
||||
log,
|
||||
message_proto::{*, option_message::BoolOption},
|
||||
protobuf::Message as _,
|
||||
rand,
|
||||
rendezvous_proto::*,
|
||||
ResultType,
|
||||
socket_client,
|
||||
sodiumoxide::crypto::{box_, secretbox, sign},
|
||||
timeout,
|
||||
tokio::time::Duration,
|
||||
AddrMangle, ResultType, Stream,
|
||||
Stream, timeout, tokio::time::Duration,
|
||||
};
|
||||
pub use helper::LatencyController;
|
||||
pub use helper::*;
|
||||
pub use helper::LatencyController;
|
||||
use scrap::{
|
||||
codec::{Decoder, DecoderCfg},
|
||||
record::{Recorder, RecorderContext},
|
||||
VpxDecoderConfig, VpxVideoCodecId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{self, is_keyboard_mode_supported},
|
||||
server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED},
|
||||
};
|
||||
|
||||
pub use super::lang::*;
|
||||
|
||||
pub mod file_trait;
|
||||
pub mod helper;
|
||||
pub mod io_loop;
|
||||
use crate::{
|
||||
common::{self, is_keyboard_mode_supported},
|
||||
server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED},
|
||||
};
|
||||
|
||||
pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
@ -714,6 +717,7 @@ impl AudioHandler {
|
||||
.check_audio(frame.timestamp)
|
||||
.not()
|
||||
{
|
||||
log::debug!("audio frame {} is ignored", frame.timestamp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -724,6 +728,7 @@ impl AudioHandler {
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if self.simple.is_none() {
|
||||
log::debug!("PulseAudio simple binding does not exists");
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
@ -1543,7 +1548,6 @@ where
|
||||
F: 'static + FnMut(&[u8]) + Send,
|
||||
{
|
||||
let (video_sender, video_receiver) = mpsc::channel::<MediaData>();
|
||||
let (audio_sender, audio_receiver) = mpsc::channel::<MediaData>();
|
||||
let mut video_callback = video_callback;
|
||||
|
||||
let latency_controller = LatencyController::new();
|
||||
@ -1573,8 +1577,19 @@ where
|
||||
}
|
||||
log::info!("Video decoder loop exits");
|
||||
});
|
||||
let audio_sender = start_audio_thread(Some(latency_controller_cl));
|
||||
return (video_sender, audio_sender);
|
||||
}
|
||||
|
||||
/// Start an audio thread
|
||||
/// Return a audio [`MediaSender`]
|
||||
pub fn start_audio_thread(
|
||||
latency_controller: Option<Arc<Mutex<LatencyController>>>,
|
||||
) -> MediaSender {
|
||||
let latency_controller = latency_controller.unwrap_or(LatencyController::new());
|
||||
let (audio_sender, audio_receiver) = mpsc::channel::<MediaData>();
|
||||
std::thread::spawn(move || {
|
||||
let mut audio_handler = AudioHandler::new(latency_controller_cl);
|
||||
let mut audio_handler = AudioHandler::new(latency_controller);
|
||||
loop {
|
||||
if let Ok(data) = audio_receiver.recv() {
|
||||
match data {
|
||||
@ -1582,6 +1597,7 @@ where
|
||||
audio_handler.handle_frame(af);
|
||||
}
|
||||
MediaData::AudioFormat(f) => {
|
||||
log::debug!("recved audio format, sample rate={}", f.sample_rate);
|
||||
audio_handler.handle_format(f);
|
||||
}
|
||||
_ => {}
|
||||
@ -1592,7 +1608,7 @@ where
|
||||
}
|
||||
log::info!("Audio decoder loop exits");
|
||||
});
|
||||
return (video_sender, audio_sender);
|
||||
audio_sender
|
||||
}
|
||||
|
||||
/// Handle latency test.
|
||||
@ -1934,6 +1950,8 @@ pub enum Data {
|
||||
RecordScreen(bool, i32, i32, String),
|
||||
ElevateDirect,
|
||||
ElevateWithLogon(String, String),
|
||||
NewVoiceCall,
|
||||
CloseVoiceCall,
|
||||
}
|
||||
|
||||
/// Keycode for key events.
|
||||
|
@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use hbb_common::{
|
||||
log,
|
||||
message_proto::{video_frame, VideoFrame},
|
||||
message_proto::{video_frame, VideoFrame, Message, VoiceCallRequest, VoiceCallResponse}, get_time,
|
||||
};
|
||||
|
||||
const MAX_LATENCY: i64 = 500;
|
||||
@ -18,6 +18,7 @@ pub struct LatencyController {
|
||||
last_video_remote_ts: i64, // generated on remote device
|
||||
update_time: Instant,
|
||||
allow_audio: bool,
|
||||
audio_only: bool
|
||||
}
|
||||
|
||||
impl Default for LatencyController {
|
||||
@ -26,6 +27,7 @@ impl Default for LatencyController {
|
||||
last_video_remote_ts: Default::default(),
|
||||
update_time: Instant::now(),
|
||||
allow_audio: Default::default(),
|
||||
audio_only: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -36,6 +38,11 @@ impl LatencyController {
|
||||
Arc::new(Mutex::new(LatencyController::default()))
|
||||
}
|
||||
|
||||
/// Set whether this [LatencyController] should be working in audio only mode.
|
||||
pub fn set_audio_only(&mut self, only: bool) {
|
||||
self.audio_only = only;
|
||||
}
|
||||
|
||||
/// Update the latency controller with the latest video timestamp.
|
||||
pub fn update_video(&mut self, timestamp: i64) {
|
||||
self.last_video_remote_ts = timestamp;
|
||||
@ -46,7 +53,11 @@ impl LatencyController {
|
||||
pub fn check_audio(&mut self, timestamp: i64) -> bool {
|
||||
// Compute audio latency.
|
||||
let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts;
|
||||
let latency = expected - timestamp;
|
||||
let latency = if self.audio_only {
|
||||
expected
|
||||
} else {
|
||||
expected - timestamp
|
||||
};
|
||||
// Set MAX and MIN, avoid fixing too frequently.
|
||||
if self.allow_audio {
|
||||
if latency.abs() > MAX_LATENCY {
|
||||
@ -59,6 +70,9 @@ impl LatencyController {
|
||||
self.allow_audio = true;
|
||||
}
|
||||
}
|
||||
// No video frame here, which means the update time is not up to date.
|
||||
// We manually update the time here.
|
||||
self.update_time = Instant::now();
|
||||
self.allow_audio
|
||||
}
|
||||
}
|
||||
@ -101,3 +115,24 @@ pub struct QualityStatus {
|
||||
pub target_bitrate: Option<i32>,
|
||||
pub codec_format: Option<CodecFormat>,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_voice_call_request(is_connect: bool) -> Message {
|
||||
let mut req = VoiceCallRequest::new();
|
||||
req.is_connect = is_connect;
|
||||
req.req_timestamp = get_time();
|
||||
let mut msg = Message::new();
|
||||
msg.set_voice_call_request(req);
|
||||
msg
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Message {
|
||||
let mut resp = VoiceCallResponse::new();
|
||||
resp.accepted = accepted;
|
||||
resp.req_timestamp = request_timestamp;
|
||||
resp.ack_timestamp = get_time();
|
||||
let mut msg = Message::new();
|
||||
msg.set_voice_call_response(resp);
|
||||
msg
|
||||
}
|
@ -1,17 +1,10 @@
|
||||
use crate::client::{
|
||||
Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30,
|
||||
SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED,
|
||||
};
|
||||
use crate::common;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
|
||||
use std::collections::HashMap;
|
||||
use std::num::NonZeroI64;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(windows)]
|
||||
use clipboard::{cliprdr::CliprdrClientContext, ContextSend};
|
||||
|
||||
use crate::ui_session_interface::{InvokeUiSession, Session};
|
||||
use crate::{client::Data, client::Interface};
|
||||
|
||||
use hbb_common::config::{PeerConfig, TransferSerde};
|
||||
use hbb_common::fs::{
|
||||
can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult,
|
||||
@ -20,6 +13,7 @@ use hbb_common::fs::{
|
||||
use hbb_common::message_proto::permission_info::Permission;
|
||||
use hbb_common::protobuf::Message as _;
|
||||
use hbb_common::rendezvous_proto::ConnType;
|
||||
use hbb_common::tokio::sync::mpsc::error::TryRecvError;
|
||||
#[cfg(windows)]
|
||||
use hbb_common::tokio::sync::Mutex as TokioMutex;
|
||||
use hbb_common::tokio::{
|
||||
@ -27,12 +21,20 @@ use hbb_common::tokio::{
|
||||
sync::mpsc,
|
||||
time::{self, Duration, Instant, Interval},
|
||||
};
|
||||
use hbb_common::{allow_err, message_proto::*, sleep};
|
||||
use hbb_common::{allow_err, get_time, message_proto::*, sleep};
|
||||
use hbb_common::{fs, log, Stream};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use crate::client::{
|
||||
new_voice_call_request, Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender,
|
||||
QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED,
|
||||
SERVER_KEYBOARD_ENABLED,
|
||||
};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
|
||||
use crate::common::{get_default_sound_input, set_sound_input};
|
||||
use crate::ui_session_interface::{InvokeUiSession, Session};
|
||||
use crate::{audio_service, common, ConnInner, CLIENT_SERVER};
|
||||
use crate::{client::Data, client::Interface};
|
||||
|
||||
pub struct Remote<T: InvokeUiSession> {
|
||||
handler: Session<T>,
|
||||
@ -40,6 +42,9 @@ pub struct Remote<T: InvokeUiSession> {
|
||||
audio_sender: MediaSender,
|
||||
receiver: mpsc::UnboundedReceiver<Data>,
|
||||
sender: mpsc::UnboundedSender<Data>,
|
||||
// Stop sending local audio to remote client.
|
||||
stop_voice_call_sender: Option<std::sync::mpsc::Sender<()>>,
|
||||
voice_call_request_timestamp: Option<NonZeroI64>,
|
||||
old_clipboard: Arc<Mutex<String>>,
|
||||
read_jobs: Vec<fs::TransferJob>,
|
||||
write_jobs: Vec<fs::TransferJob>,
|
||||
@ -81,6 +86,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
data_count: Arc::new(AtomicUsize::new(0)),
|
||||
frame_count,
|
||||
video_format: CodecFormat::Unknown,
|
||||
stop_voice_call_sender: None,
|
||||
voice_call_request_timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,6 +100,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
} else {
|
||||
ConnType::default()
|
||||
};
|
||||
|
||||
match Client::start(
|
||||
&self.handler.id,
|
||||
key,
|
||||
@ -212,6 +220,10 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
log::debug!("Exit io_loop of id={}", self.handler.id);
|
||||
// Stop client audio server.
|
||||
if let Some(s) = self.stop_voice_call_sender.take() {
|
||||
s.send(()).ok();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.handler
|
||||
@ -253,6 +265,81 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_voice_call(&mut self) {
|
||||
let voice_call_sender = std::mem::replace(&mut self.stop_voice_call_sender, None);
|
||||
if let Some(stopper) = voice_call_sender {
|
||||
let _ = stopper.send(());
|
||||
}
|
||||
}
|
||||
|
||||
// Start a voice call recorder, records audio and send to remote
|
||||
fn start_voice_call(&mut self) -> Option<std::sync::mpsc::Sender<()>> {
|
||||
if self.handler.is_file_transfer() || self.handler.is_port_forward() {
|
||||
return None;
|
||||
}
|
||||
// Switch to default input device
|
||||
let default_sound_device = get_default_sound_input();
|
||||
if let Some(device) = default_sound_device {
|
||||
set_sound_input(device);
|
||||
}
|
||||
// Create a channel to receive error or closed message
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel();
|
||||
// Create a stand-alone inner, add subscribe to audio service
|
||||
let conn_id = CLIENT_SERVER.write().unwrap().get_new_id();
|
||||
let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None);
|
||||
// now we subscribe
|
||||
CLIENT_SERVER.write().unwrap().subscribe(
|
||||
audio_service::NAME,
|
||||
client_conn_inner.clone(),
|
||||
true,
|
||||
);
|
||||
let tx_audio = self.sender.clone();
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
// check if client is closed
|
||||
match rx.try_recv() {
|
||||
Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
log::debug!("Exit voice call audio service of client");
|
||||
// unsubscribe
|
||||
CLIENT_SERVER.write().unwrap().subscribe(
|
||||
audio_service::NAME,
|
||||
client_conn_inner,
|
||||
false,
|
||||
);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match rx_audio_data.try_recv() {
|
||||
Ok((_instant, msg)) => match &msg.union {
|
||||
Some(message::Union::AudioFrame(frame)) => {
|
||||
let mut msg = Message::new();
|
||||
msg.set_audio_frame(frame.clone());
|
||||
tx_audio.send(Data::Message(msg)).ok();
|
||||
log::debug!("send audio frame {}", frame.timestamp);
|
||||
}
|
||||
Some(message::Union::Misc(misc)) => {
|
||||
let mut msg = Message::new();
|
||||
msg.set_misc(misc.clone());
|
||||
tx_audio.send(Data::Message(msg)).ok();
|
||||
log::debug!("send audio misc {:?}", misc.audio_format());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(err) => {
|
||||
if err == TryRecvError::Empty {
|
||||
// ignore
|
||||
} else {
|
||||
log::debug!("Failed to record local audio channel: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Some(tx)
|
||||
}
|
||||
|
||||
fn start_clipboard(&mut self) -> Option<std::sync::mpsc::Sender<()>> {
|
||||
if self.handler.is_file_transfer() || self.handler.is_port_forward() {
|
||||
return None;
|
||||
@ -654,6 +741,22 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
msg.set_misc(misc);
|
||||
allow_err!(peer.send(&msg).await);
|
||||
}
|
||||
Data::NewVoiceCall => {
|
||||
let msg = new_voice_call_request(true);
|
||||
// Save the voice call request timestamp for the further validation.
|
||||
self.voice_call_request_timestamp = Some(
|
||||
NonZeroI64::new(msg.voice_call_request().req_timestamp)
|
||||
.unwrap_or(NonZeroI64::new(get_time()).unwrap()),
|
||||
);
|
||||
allow_err!(peer.send(&msg).await);
|
||||
self.handler.on_voice_call_waiting();
|
||||
}
|
||||
Data::CloseVoiceCall => {
|
||||
self.stop_voice_call();
|
||||
let msg = new_voice_call_request(false);
|
||||
self.handler.on_voice_call_closed("Closed manually by the peer");
|
||||
allow_err!(peer.send(&msg).await);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
true
|
||||
@ -1146,6 +1249,34 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
self.handler
|
||||
.msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link);
|
||||
}
|
||||
Some(message::Union::VoiceCallRequest(request)) => {
|
||||
if request.is_connect {
|
||||
// TODO: maybe we will do a voice call from the peer in the future.
|
||||
} else {
|
||||
log::debug!("The remote has requested to close the voice call");
|
||||
if let Some(sender) = self.stop_voice_call_sender.take() {
|
||||
allow_err!(sender.send(()));
|
||||
self.handler.on_voice_call_closed("");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(message::Union::VoiceCallResponse(response)) => {
|
||||
let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None);
|
||||
if let Some(ts) = ts {
|
||||
if response.req_timestamp != ts.get() {
|
||||
log::debug!("Possible encountering a voice call attack.");
|
||||
} else {
|
||||
if response.accepted {
|
||||
// The peer accepted the voice call.
|
||||
self.handler.on_voice_call_started();
|
||||
self.stop_voice_call_sender = self.start_voice_call();
|
||||
} else {
|
||||
// The peer refused the voice call.
|
||||
self.handler.on_voice_call_closed("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ use hbb_common::{
|
||||
// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
||||
use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all};
|
||||
|
||||
use crate::ui_interface::{set_option, get_option};
|
||||
|
||||
pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future<Output = ()>;
|
||||
|
||||
pub const CLIPBOARD_NAME: &'static str = "clipboard";
|
||||
@ -105,6 +107,54 @@ pub fn check_clipboard(
|
||||
None
|
||||
}
|
||||
|
||||
/// Set sound input device.
|
||||
pub fn set_sound_input(device: String) {
|
||||
let prior_device = get_option("audio-input".to_owned());
|
||||
if prior_device != device {
|
||||
log::info!("switch to audio input device {}", device);
|
||||
std::thread::spawn(move || {
|
||||
set_option("audio-input".to_owned(), device);
|
||||
});
|
||||
} else {
|
||||
log::info!("audio input is already set to {}", device);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get system's default sound input device name.
|
||||
#[inline]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn get_default_sound_input() -> Option<String> {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
let host = cpal::default_host();
|
||||
let dev = host.default_input_device();
|
||||
return if let Some(dev) = dev {
|
||||
match dev.name() {
|
||||
Ok(name) => Some(name),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let input = crate::platform::linux::get_default_pa_source();
|
||||
return if let Some(input) = input {
|
||||
Some(input.1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
pub fn get_default_sound_input() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc<Mutex<String>>>) {
|
||||
let content = if clipboard.compress {
|
||||
@ -715,5 +765,5 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> Strin
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_common {
|
||||
use super::*;
|
||||
|
||||
}
|
||||
|
@ -246,8 +246,6 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
} else if args[0] == "--cm" {
|
||||
// call connection manager to establish connections
|
||||
// meanwhile, return true to call flutter window to show control panel
|
||||
#[cfg(feature = "flutter")]
|
||||
crate::flutter::connection_manager::start_listen_ipc_thread();
|
||||
crate::ui_interface::start_option_status_sync();
|
||||
}
|
||||
}
|
||||
|
@ -394,6 +394,22 @@ impl InvokeUiSession for FlutterHandler {
|
||||
fn switch_back(&self, peer_id: &str) {
|
||||
self.push_event("switch_back", [("peer_id", peer_id)].into());
|
||||
}
|
||||
|
||||
fn on_voice_call_started(&self) {
|
||||
self.push_event("on_voice_call_started", [].into());
|
||||
}
|
||||
|
||||
fn on_voice_call_closed(&self, reason: &str) {
|
||||
self.push_event("on_voice_call_closed", [("reason", reason)].into())
|
||||
}
|
||||
|
||||
fn on_voice_call_waiting(&self) {
|
||||
self.push_event("on_voice_call_waiting", [].into());
|
||||
}
|
||||
|
||||
fn on_voice_call_incoming(&self) {
|
||||
self.push_event("on_voice_call_incoming", [].into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new remote session with the given id.
|
||||
@ -521,6 +537,11 @@ pub mod connection_manager {
|
||||
fn show_elevation(&self, show: bool) {
|
||||
self.push_event("show_elevation", vec![("show", &show.to_string())]);
|
||||
}
|
||||
|
||||
fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
|
||||
let client_json = serde_json::to_string(&client).unwrap_or("".into());
|
||||
self.push_event("update_voice_call_state", vec![("client", &client_json)]);
|
||||
}
|
||||
}
|
||||
|
||||
impl FlutterHandler {
|
||||
@ -528,9 +549,11 @@ pub mod connection_manager {
|
||||
let mut h: HashMap<&str, &str> = event.iter().cloned().collect();
|
||||
assert!(h.get("name").is_none());
|
||||
h.insert("name", name);
|
||||
|
||||
|
||||
if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) {
|
||||
s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned()));
|
||||
} else {
|
||||
println!("Push event {} failed. No {} event stream found.", name, super::APP_TYPE_CM);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ use crate::{
|
||||
common::make_fd_to_json,
|
||||
flutter::{session_add, session_start_},
|
||||
};
|
||||
use crate::common::is_keyboard_mode_supported;
|
||||
use crate::common::{get_default_sound_input, is_keyboard_mode_supported};
|
||||
use crate::flutter::{self, SESSIONS};
|
||||
use crate::ui_interface::{self, *};
|
||||
|
||||
@ -520,6 +520,13 @@ pub fn main_get_sound_inputs() -> Vec<String> {
|
||||
vec![String::from("")]
|
||||
}
|
||||
|
||||
pub fn main_get_default_sound_input() -> Option<String> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
return get_default_sound_input();
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
None
|
||||
}
|
||||
|
||||
pub fn main_get_hostname() -> SyncReturn<String> {
|
||||
SyncReturn(crate::common::hostname())
|
||||
}
|
||||
@ -819,6 +826,26 @@ pub fn session_new_rdp(id: String) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_request_voice_call(id: String) {
|
||||
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||
session.request_voice_call();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_close_voice_call(id: String) {
|
||||
if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) {
|
||||
session.close_voice_call();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) {
|
||||
crate::ui_cm_interface::handle_incoming_voice_call(id, accept);
|
||||
}
|
||||
|
||||
pub fn cm_close_voice_call(id: i32) {
|
||||
crate::ui_cm_interface::close_voice_call(id);
|
||||
}
|
||||
|
||||
pub fn main_get_last_remote_id() -> String {
|
||||
LocalConfig::get_remote_id()
|
||||
}
|
||||
@ -1246,12 +1273,22 @@ pub fn main_is_login_wayland() -> SyncReturn<bool> {
|
||||
SyncReturn(is_login_wayland())
|
||||
}
|
||||
|
||||
pub fn main_start_pa() {
|
||||
#[cfg(target_os = "linux")]
|
||||
std::thread::spawn(crate::ipc::start_pa);
|
||||
}
|
||||
|
||||
pub fn main_hide_docker() -> SyncReturn<bool> {
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::platform::macos::hide_dock();
|
||||
SyncReturn(true)
|
||||
}
|
||||
|
||||
pub fn cm_start_listen_ipc_thread() {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
crate::flutter::connection_manager::start_listen_ipc_thread();
|
||||
}
|
||||
|
||||
/// Start an ipc server for receiving the url scheme.
|
||||
///
|
||||
/// * Should only be called in the main flutter window.
|
||||
@ -1264,6 +1301,7 @@ pub fn main_start_ipc_url_server() {
|
||||
/// Send a url scheme throught the ipc.
|
||||
///
|
||||
/// * macOS only
|
||||
#[allow(unused_variables)]
|
||||
pub fn send_url_scheme(url: String) {
|
||||
#[cfg(target_os = "macos")]
|
||||
thread::spawn(move || crate::ui::macos::handle_url_scheme(url));
|
||||
|
@ -210,7 +210,11 @@ pub enum Data {
|
||||
DataPortableService(DataPortableService),
|
||||
SwitchSidesRequest(String),
|
||||
SwitchSidesBack,
|
||||
UrlLink(String)
|
||||
UrlLink(String),
|
||||
VoiceCallIncoming,
|
||||
StartVoiceCall,
|
||||
VoiceCallResponse(bool),
|
||||
CloseVoiceCall(String),
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"),
|
||||
("Always use software rendering", "使用软件渲染"),
|
||||
("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"),
|
||||
("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"),
|
||||
("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"),
|
||||
("Wait", "等待"),
|
||||
("Elevation Error", "提权失败"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "帧率"),
|
||||
("Auto", "自动"),
|
||||
("Other Default Options", "其它默认选项"),
|
||||
("Voice call", "语音通话"),
|
||||
("Text chat", "文字聊天"),
|
||||
("Stop voice call", "停止语音聊天"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."),
|
||||
("Always use software rendering", "Software-Rendering immer verwenden"),
|
||||
("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."),
|
||||
("Wait", "Warten"),
|
||||
("Elevation Error", "Berechtigungsfehler"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "fps"),
|
||||
("Auto", "Automatisch"),
|
||||
("Other Default Options", "Weitere Standardoptionen"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."),
|
||||
("request_elevation_tip","You can also request elevation if there is someone on the remote side."),
|
||||
("wait_accept_uac_tip","Please wait for the remote user to accept the UAC dialog."),
|
||||
("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk.")
|
||||
("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."),
|
||||
("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions.")
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."),
|
||||
("Always use software rendering", "Usar siempre renderizado por software"),
|
||||
("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."),
|
||||
("Wait", "Esperar"),
|
||||
("Elevation Error", "Error de elevación"),
|
||||
@ -435,7 +436,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Strong", "Fuerte"),
|
||||
("Switch Sides", "Intercambiar lados"),
|
||||
("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"),
|
||||
("Closed as expected", "Cerrado como se esperaba"),
|
||||
("Closed as expected", ""),
|
||||
("Display", "Pantalla"),
|
||||
("Default View Style", "Estilo de vista predeterminado"),
|
||||
("Default Scroll Style", "Estilo de desplazamiento predeterminado"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", "Otras opciones predeterminadas"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."),
|
||||
("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"),
|
||||
("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."),
|
||||
("Wait", "صبر کنید"),
|
||||
("Elevation Error", "خطای ارتفاع"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "FPS"),
|
||||
("Auto", "خودکار"),
|
||||
("Other Default Options", "سایر گزینه های پیش فرض"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."),
|
||||
("Always use software rendering", "Utiliser toujours le rendu logiciel"),
|
||||
("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."),
|
||||
("Wait", "En cours"),
|
||||
("Elevation Error", "Erreur d'augmentation des privilèges"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "FPS"),
|
||||
("Auto", "Auto"),
|
||||
("Other Default Options", "Autres options par défaut"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."),
|
||||
("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"),
|
||||
("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"),
|
||||
("Wait", "Περιμένετε"),
|
||||
("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "Se si dispone di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver nouveau e la scelta di utilizzare il rendering software possono aiutare. È necessario un riavvio del software."),
|
||||
("Always use software rendering", "Usa sempre il render Software"),
|
||||
("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."),
|
||||
("Wait", "Attendi"),
|
||||
("Elevation Error", "Errore durante l'elevazione dei diritti"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "FPS"),
|
||||
("Auto", "Auto"),
|
||||
("Other Default Options", "Altre Opzioni Predefinite"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."),
|
||||
("Always use software rendering", "Zawsze używaj renderowania programowego"),
|
||||
("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."),
|
||||
("Wait", "Czekaj"),
|
||||
("Elevation Error", "Błąd przy podnoszeniu uprawnień"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "FPS"),
|
||||
("Auto", "Auto"),
|
||||
("Other Default Options", "Inne opcje domyślne"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."),
|
||||
("Always use software rendering", "Использовать программную визуализацию"),
|
||||
("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."),
|
||||
("Wait", "Ждите"),
|
||||
("Elevation Error", "Ошибка повышения прав"),
|
||||
@ -434,8 +435,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Medium", "Средний"),
|
||||
("Strong", "Стойкий"),
|
||||
("Switch Sides", "Переключить стороны"),
|
||||
("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"),
|
||||
("Closed as expected", "Закрыто по ожиданию"),
|
||||
("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"),
|
||||
("Closed as expected", ""),
|
||||
("Display", "Отображение"),
|
||||
("Default View Style", "Стиль отображения по умолчанию"),
|
||||
("Default Scroll Style", "Стиль прокрутки по умолчанию"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "FPS"),
|
||||
("Auto", "Авто"),
|
||||
("Other Default Options", "Другие параметры по умолчанию"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"),
|
||||
("Always use software rendering", "使用軟件渲染"),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"),
|
||||
("Wait", "等待"),
|
||||
("Elevation Error", "提權失敗"),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", "幀率"),
|
||||
("Auto", "自動"),
|
||||
("Other Default Options", "其它默認選項"),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("software_render_tip", ""),
|
||||
("Always use software rendering", ""),
|
||||
("config_input", ""),
|
||||
("config_microphone", ""),
|
||||
("request_elevation_tip", ""),
|
||||
("Wait", ""),
|
||||
("Elevation Error", ""),
|
||||
@ -445,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("FPS", ""),
|
||||
("Auto", ""),
|
||||
("Other Default Options", ""),
|
||||
("Voice call", ""),
|
||||
("Text chat", ""),
|
||||
("Stop voice call", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -534,6 +534,24 @@ pub fn get_pa_sources() -> Vec<(String, String)> {
|
||||
out
|
||||
}
|
||||
|
||||
pub fn get_default_pa_source() -> Option<(String, String)> {
|
||||
use pulsectl::controllers::*;
|
||||
match SourceController::create() {
|
||||
Ok(mut handler) => {
|
||||
if let Ok(dev) = handler.get_default_device() {
|
||||
return Some((
|
||||
dev.name.unwrap_or("".to_owned()),
|
||||
dev.description.unwrap_or("".to_owned()),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to get_pa_source: {:?}", err);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn lock_screen() {
|
||||
std::process::Command::new("xdg-screensaver")
|
||||
.arg("lock")
|
||||
|
@ -65,6 +65,13 @@ type ConnMap = HashMap<i32, ConnInner>;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CHILD_PROCESS: Childs = Default::default();
|
||||
pub static ref CONN_COUNT: Arc<Mutex<usize>> = Default::default();
|
||||
// A client server used to provide local services(audio, video, clipboard, etc.)
|
||||
// for all initiative connections.
|
||||
//
|
||||
// [Note]
|
||||
// Now we use this [`CLIENT_SERVER`] to do following operations:
|
||||
// - record local audio, and send to remote
|
||||
pub static ref CLIENT_SERVER: ServerPtr = new();
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
@ -316,6 +323,13 @@ impl Server {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get a new unique id
|
||||
pub fn get_new_id(&mut self) -> i32 {
|
||||
let new_id = self.id_count;
|
||||
self.id_count += 1;
|
||||
new_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
|
@ -5,7 +5,11 @@ use crate::clipboard_file::*;
|
||||
use crate::common::update_clipboard;
|
||||
#[cfg(windows)]
|
||||
use crate::portable_service::client as portable_client;
|
||||
use crate::video_service;
|
||||
use crate::{
|
||||
client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response},
|
||||
common::{get_default_sound_input, set_sound_input},
|
||||
video_service,
|
||||
};
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel};
|
||||
use crate::{ipc, VERSION};
|
||||
@ -32,7 +36,10 @@ use serde_json::{json, value::Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{atomic::AtomicI64, mpsc as std_mpsc};
|
||||
use std::{
|
||||
num::NonZeroI64,
|
||||
sync::{atomic::AtomicI64, mpsc as std_mpsc},
|
||||
};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use system_shutdown;
|
||||
|
||||
@ -90,12 +97,19 @@ pub struct Connection {
|
||||
recording: bool,
|
||||
last_test_delay: i64,
|
||||
lock_after_session_end: bool,
|
||||
show_remote_cursor: bool, // by peer
|
||||
show_remote_cursor: bool,
|
||||
// by peer
|
||||
ip: String,
|
||||
disable_clipboard: bool, // by peer
|
||||
disable_audio: bool, // by peer
|
||||
enable_file_transfer: bool, // by peer
|
||||
tx_input: std_mpsc::Sender<MessageInput>, // handle input messages
|
||||
disable_clipboard: bool,
|
||||
// by peer
|
||||
disable_audio: bool,
|
||||
// by peer
|
||||
enable_file_transfer: bool,
|
||||
// by peer
|
||||
audio_sender: Option<MediaSender>,
|
||||
// audio by the remote peer/client
|
||||
tx_input: std_mpsc::Sender<MessageInput>,
|
||||
// handle input messages
|
||||
video_ack_required: bool,
|
||||
peer_info: (String, String),
|
||||
server_audit_conn: String,
|
||||
@ -106,6 +120,14 @@ pub struct Connection {
|
||||
#[cfg(windows)]
|
||||
portable: PortableState,
|
||||
from_switch: bool,
|
||||
voice_call_request_timestamp: Option<NonZeroI64>,
|
||||
audio_input_device_before_voice_call: Option<String>,
|
||||
}
|
||||
|
||||
impl ConnInner {
|
||||
pub fn new(id: i32, tx: Option<Sender>, tx_video: Option<Sender>) -> Self {
|
||||
Self { id, tx, tx_video }
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscriber for ConnInner {
|
||||
@ -203,6 +225,9 @@ impl Connection {
|
||||
#[cfg(windows)]
|
||||
portable: Default::default(),
|
||||
from_switch: false,
|
||||
audio_sender: None,
|
||||
voice_call_request_timestamp: None,
|
||||
audio_input_device_before_voice_call: None,
|
||||
};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
tokio::spawn(async move {
|
||||
@ -367,6 +392,16 @@ impl Connection {
|
||||
msg.set_misc(misc);
|
||||
conn.send(msg).await;
|
||||
}
|
||||
ipc::Data::VoiceCallResponse(accepted) => {
|
||||
conn.handle_voice_call(accepted).await;
|
||||
}
|
||||
ipc::Data::CloseVoiceCall(_reason) => {
|
||||
log::debug!("Close the voice call from the ipc.");
|
||||
conn.close_voice_call().await;
|
||||
// Notify the peer that we closed the voice call.
|
||||
let msg = new_voice_call_request(false);
|
||||
conn.send(msg).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
@ -637,15 +672,15 @@ impl Connection {
|
||||
.collect();
|
||||
if !whitelist.is_empty()
|
||||
&& whitelist
|
||||
.iter()
|
||||
.filter(|x| x == &"0.0.0.0")
|
||||
.next()
|
||||
.is_none()
|
||||
.iter()
|
||||
.filter(|x| x == &"0.0.0.0")
|
||||
.next()
|
||||
.is_none()
|
||||
&& whitelist
|
||||
.iter()
|
||||
.filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip())))
|
||||
.next()
|
||||
.is_none()
|
||||
.iter()
|
||||
.filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip())))
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
self.send_login_error("Your ip is blocked by the peer")
|
||||
.await;
|
||||
@ -771,7 +806,7 @@ impl Connection {
|
||||
};
|
||||
self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type}));
|
||||
#[allow(unused_mut)]
|
||||
let mut username = crate::platform::get_active_username();
|
||||
let mut username = crate::platform::get_active_username();
|
||||
let mut res = LoginResponse::new();
|
||||
let mut pi = PeerInfo {
|
||||
username: username.clone(),
|
||||
@ -798,7 +833,7 @@ impl Connection {
|
||||
h265,
|
||||
..Default::default()
|
||||
})
|
||||
.into();
|
||||
.into();
|
||||
}
|
||||
|
||||
if self.port_forward_socket.is_some() {
|
||||
@ -842,7 +877,7 @@ impl Connection {
|
||||
privacy_mode: video_service::is_privacy_mode_supported(),
|
||||
..Default::default()
|
||||
})
|
||||
.into();
|
||||
.into();
|
||||
|
||||
let mut sub_service = false;
|
||||
if self.file_transfer.is_some() {
|
||||
@ -1125,7 +1160,7 @@ impl Connection {
|
||||
"Failed to access remote {}, please make sure if it is open",
|
||||
addr
|
||||
))
|
||||
.await;
|
||||
.await;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1289,12 +1324,12 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
Some(message::Union::Clipboard(cb)) =>
|
||||
{
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if self.clipboard {
|
||||
update_clipboard(cb, None);
|
||||
{
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if self.clipboard {
|
||||
update_clipboard(cb, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(message::Union::Cliprdr(_clip)) => {
|
||||
if self.file_transfer_enabled() {
|
||||
#[cfg(windows)]
|
||||
@ -1477,15 +1512,15 @@ impl Connection {
|
||||
}
|
||||
|
||||
Some(misc::Union::RestartRemoteDevice(_)) =>
|
||||
{
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if self.restart {
|
||||
match system_shutdown::reboot() {
|
||||
Ok(_) => log::info!("Restart by the peer"),
|
||||
Err(e) => log::error!("Failed to restart:{}", e),
|
||||
{
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if self.restart {
|
||||
match system_shutdown::reboot() {
|
||||
Ok(_) => log::info!("Restart by the peer"),
|
||||
Err(e) => log::error!("Failed to restart:{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(misc::Union::ElevationRequest(r)) => match r.union {
|
||||
Some(elevation_request::Union::Direct(_)) => {
|
||||
#[cfg(windows)]
|
||||
@ -1495,8 +1530,8 @@ impl Connection {
|
||||
err = portable_client::start_portable_service(
|
||||
portable_client::StartPara::Direct,
|
||||
)
|
||||
.err()
|
||||
.map_or("".to_string(), |e| e.to_string());
|
||||
.err()
|
||||
.map_or("".to_string(), |e| e.to_string());
|
||||
}
|
||||
self.portable.elevation_requested = err.is_empty();
|
||||
let mut misc = Misc::new();
|
||||
@ -1514,8 +1549,8 @@ impl Connection {
|
||||
err = portable_client::start_portable_service(
|
||||
portable_client::StartPara::Logon(_r.username, _r.password),
|
||||
)
|
||||
.err()
|
||||
.map_or("".to_string(), |e| e.to_string());
|
||||
.err()
|
||||
.map_or("".to_string(), |e| e.to_string());
|
||||
}
|
||||
self.portable.elevation_requested = err.is_empty();
|
||||
let mut misc = Misc::new();
|
||||
@ -1527,6 +1562,18 @@ impl Connection {
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Some(misc::Union::AudioFormat(format)) => {
|
||||
if !self.disable_audio {
|
||||
// Drop the audio sender previously.
|
||||
drop(std::mem::replace(&mut self.audio_sender, None));
|
||||
// Start a audio thread to play the audio sent by peer.
|
||||
let latency_controller = LatencyController::new();
|
||||
// No video frame will be sent here, so we need to disable latency controller, or audio check may fail.
|
||||
latency_controller.lock().unwrap().set_audio_only(true);
|
||||
self.audio_sender = Some(start_audio_thread(Some(latency_controller)));
|
||||
allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format)));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "flutter")]
|
||||
Some(misc::Union::SwitchSidesRequest(s)) => {
|
||||
if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) {
|
||||
@ -1536,7 +1583,7 @@ impl Connection {
|
||||
"--switch_uuid",
|
||||
uuid.to_string().as_ref(),
|
||||
])
|
||||
.ok();
|
||||
.ok();
|
||||
self.send_close_reason_no_retry("Closed as expected").await;
|
||||
self.on_close("switch sides", false).await;
|
||||
return false;
|
||||
@ -1544,12 +1591,68 @@ impl Connection {
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Some(message::Union::AudioFrame(frame)) => {
|
||||
if !self.disable_audio {
|
||||
if let Some(sender) = &self.audio_sender {
|
||||
allow_err!(sender.send(MediaData::AudioFrame(frame)));
|
||||
} else {
|
||||
log::warn!("Processing audio frame without the voice call audio sender.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(message::Union::VoiceCallRequest(request)) => {
|
||||
if request.is_connect {
|
||||
self.voice_call_request_timestamp = Some(
|
||||
NonZeroI64::new(request.req_timestamp)
|
||||
.unwrap_or(NonZeroI64::new(get_time()).unwrap()),
|
||||
);
|
||||
// Notify the connection manager.
|
||||
self.send_to_cm(Data::VoiceCallIncoming);
|
||||
} else {
|
||||
self.close_voice_call().await;
|
||||
}
|
||||
}
|
||||
Some(message::Union::VoiceCallResponse(_response)) => {
|
||||
// TODO: Maybe we can do a voice call from cm directly.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn handle_voice_call(&mut self, accepted: bool) {
|
||||
if let Some(ts) = self.voice_call_request_timestamp.take() {
|
||||
let msg = new_voice_call_response(ts.get(), accepted);
|
||||
if accepted {
|
||||
// Backup the default input device.
|
||||
let audio_input_device = Config::get_option("audio-input");
|
||||
log::debug!("Backup the sound input device {}", audio_input_device);
|
||||
self.audio_input_device_before_voice_call = Some(audio_input_device);
|
||||
// Switch to default input device
|
||||
let default_sound_device = get_default_sound_input();
|
||||
if let Some(device) = default_sound_device {
|
||||
set_sound_input(device);
|
||||
}
|
||||
self.send_to_cm(Data::StartVoiceCall);
|
||||
} else {
|
||||
self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
|
||||
}
|
||||
self.send(msg).await;
|
||||
} else {
|
||||
log::warn!("Possible a voice call attack.");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn close_voice_call(&mut self) {
|
||||
// Restore to the prior audio device.
|
||||
if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) {
|
||||
set_sound_input(sound_input);
|
||||
}
|
||||
// Notify the connection manager that the voice call has been closed.
|
||||
self.send_to_cm(Data::CloseVoiceCall("".to_owned()));
|
||||
}
|
||||
|
||||
async fn update_option(&mut self, o: &OptionMessage) {
|
||||
log::info!("Option update: {:?}", o);
|
||||
if let Ok(q) = o.image_quality.enum_value() {
|
||||
@ -1718,13 +1821,13 @@ impl Connection {
|
||||
lock_screen().await;
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let data = if self.chat_unanswered {
|
||||
let data = if self.chat_unanswered {
|
||||
ipc::Data::Disconnected
|
||||
} else {
|
||||
ipc::Data::Close
|
||||
};
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
let data = ipc::Data::Close;
|
||||
let data = ipc::Data::Close;
|
||||
self.tx_to_cm.send(data).ok();
|
||||
self.port_forward_socket.take();
|
||||
}
|
||||
|
@ -95,6 +95,9 @@ pub fn start(args: &mut [String]) {
|
||||
frame.event_handler(UI {});
|
||||
frame.sciter_handler(UIHostHandler {});
|
||||
page = "index.html";
|
||||
// Start pulse audio local server.
|
||||
#[cfg(target_os = "linux")]
|
||||
std::thread::spawn(crate::ipc::start_pa);
|
||||
} else if args[0] == "--install" {
|
||||
frame.event_handler(UI {});
|
||||
frame.sciter_handler(UIHostHandler {});
|
||||
|
11
src/ui/cm.rs
11
src/ui/cm.rs
@ -55,6 +55,17 @@ impl InvokeUiCM for SciterHandler {
|
||||
fn show_elevation(&self, show: bool) {
|
||||
self.call("showElevation", &make_args!(show));
|
||||
}
|
||||
|
||||
fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
|
||||
self.call(
|
||||
"updateVoiceCallState",
|
||||
&make_args!(
|
||||
client.id,
|
||||
client.in_voice_call,
|
||||
client.incoming_voice_call
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl SciterHandler {
|
||||
|
@ -378,7 +378,7 @@ class Header: Reactor.Component {
|
||||
togglePrivacyMode(me.id);
|
||||
} else if (me.id == "show-quality-monitor") {
|
||||
toggleQualityMonitor(me.id);
|
||||
}else if (me.attributes.hasClass("toggle-option")) {
|
||||
} else if (me.attributes.hasClass("toggle-option")) {
|
||||
handler.toggle_option(me.id);
|
||||
toggleMenuState();
|
||||
} else if (!me.attributes.hasClass("selected")) {
|
||||
@ -434,6 +434,9 @@ function toggleMenuState() {
|
||||
var c = handler.get_option("codec-preference");
|
||||
if (!c) c = "auto";
|
||||
values.push(c);
|
||||
var a = handler.get_audio_mode();
|
||||
if (!a) a = "guest-to-host";
|
||||
values.push(a);
|
||||
for (var el in $$(menu#display-options li)) {
|
||||
el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0);
|
||||
}
|
||||
|
@ -6,12 +6,12 @@ use std::{
|
||||
|
||||
use sciter::{
|
||||
dom::{
|
||||
event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK},
|
||||
Element, HELEMENT,
|
||||
Element,
|
||||
event::{BEHAVIOR_EVENTS, EVENT_GROUPS, EventReason, PHASE_MASK}, HELEMENT,
|
||||
},
|
||||
make_args,
|
||||
video::{video_destination, AssetPtr, COLOR_SPACE},
|
||||
Value,
|
||||
video::{AssetPtr, COLOR_SPACE, video_destination},
|
||||
};
|
||||
|
||||
use hbb_common::{
|
||||
@ -266,6 +266,22 @@ impl InvokeUiSession for SciterHandler {
|
||||
}
|
||||
|
||||
fn switch_back(&self, _id: &str) {}
|
||||
|
||||
fn on_voice_call_started(&self) {
|
||||
self.call("onVoiceCallStart", &make_args!());
|
||||
}
|
||||
|
||||
fn on_voice_call_closed(&self, reason: &str) {
|
||||
self.call("onVoiceCallClosed", &make_args!(reason));
|
||||
}
|
||||
|
||||
fn on_voice_call_waiting(&self) {
|
||||
self.call("onVoiceCallWaiting", &make_args!());
|
||||
}
|
||||
|
||||
fn on_voice_call_incoming(&self) {
|
||||
self.call("onVoiceCallIncoming", &make_args!());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SciterSession(Session<SciterHandler>);
|
||||
@ -420,6 +436,8 @@ impl sciter::EventHandler for SciterSession {
|
||||
fn supported_hwcodec();
|
||||
fn change_prefer_codec();
|
||||
fn restart_remote_device();
|
||||
fn request_voice_call();
|
||||
fn close_voice_call();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,8 @@ pub struct Client {
|
||||
pub restart: bool,
|
||||
pub recording: bool,
|
||||
pub from_switch: bool,
|
||||
pub in_voice_call: bool,
|
||||
pub incoming_voice_call: bool,
|
||||
#[serde(skip)]
|
||||
tx: UnboundedSender<Data>,
|
||||
}
|
||||
@ -88,6 +90,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized {
|
||||
fn change_language(&self);
|
||||
|
||||
fn show_elevation(&self, show: bool);
|
||||
|
||||
fn update_voice_call_state(&self, client: &Client);
|
||||
}
|
||||
|
||||
impl<T: InvokeUiCM> Deref for ConnectionManager<T> {
|
||||
@ -138,6 +142,8 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||
recording,
|
||||
from_switch,
|
||||
tx,
|
||||
in_voice_call: false,
|
||||
incoming_voice_call: false
|
||||
};
|
||||
CLIENTS
|
||||
.write()
|
||||
@ -180,6 +186,30 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||
fn show_elevation(&self, show: bool) {
|
||||
self.ui_handler.show_elevation(show);
|
||||
}
|
||||
|
||||
fn voice_call_started(&self, id: i32) {
|
||||
if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
|
||||
client.incoming_voice_call = false;
|
||||
client.in_voice_call = true;
|
||||
self.ui_handler.update_voice_call_state(client);
|
||||
}
|
||||
}
|
||||
|
||||
fn voice_call_incoming(&self, id: i32) {
|
||||
if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
|
||||
client.incoming_voice_call = true;
|
||||
client.in_voice_call = false;
|
||||
self.ui_handler.update_voice_call_state(client);
|
||||
}
|
||||
}
|
||||
|
||||
fn voice_call_closed(&self, id: i32, _reason: &str) {
|
||||
if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
|
||||
client.incoming_voice_call = false;
|
||||
client.in_voice_call = false;
|
||||
self.ui_handler.update_voice_call_state(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -389,6 +419,15 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show)) => {
|
||||
self.cm.show_elevation(show);
|
||||
}
|
||||
Data::StartVoiceCall => {
|
||||
self.cm.voice_call_started(self.conn_id);
|
||||
}
|
||||
Data::VoiceCallIncoming => {
|
||||
self.cm.voice_call_incoming(self.conn_id);
|
||||
}
|
||||
Data::CloseVoiceCall(reason) => {
|
||||
self.cm.voice_call_closed(self.conn_id, reason.as_str());
|
||||
}
|
||||
_ => {
|
||||
|
||||
}
|
||||
@ -805,3 +844,17 @@ pub fn elevate_portable(_id: i32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn handle_incoming_voice_call(id: i32, accept: bool) {
|
||||
if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
|
||||
allow_err!(client.tx.send(Data::VoiceCallResponse(accept)));
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn close_voice_call(id: i32) {
|
||||
if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
|
||||
allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned())));
|
||||
};
|
||||
}
|
@ -1,26 +1,30 @@
|
||||
use crate::client::io_loop::Remote;
|
||||
use crate::client::{
|
||||
check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay,
|
||||
input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key,
|
||||
LoginConfigHandler, QualityStatus, KEY_MAP,
|
||||
};
|
||||
use crate::common::{self, GrabState};
|
||||
use crate::keyboard;
|
||||
use crate::{client::Data, client::Interface};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY};
|
||||
use hbb_common::rendezvous_proto::ConnType;
|
||||
use hbb_common::tokio::{self, sync::mpsc};
|
||||
use hbb_common::{allow_err, message_proto::*};
|
||||
use hbb_common::{fs, get_version_number, log, Stream};
|
||||
use rdev::{Event, EventType::*};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use rdev::{Event, EventType::*};
|
||||
use uuid::Uuid;
|
||||
|
||||
use hbb_common::{allow_err, message_proto::*};
|
||||
use hbb_common::{fs, get_version_number, log, Stream};
|
||||
use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY};
|
||||
use hbb_common::rendezvous_proto::ConnType;
|
||||
use hbb_common::tokio::{self, sync::mpsc};
|
||||
|
||||
use crate::{client::Data, client::Interface};
|
||||
use crate::client::{
|
||||
check_if_retry, FileManager, handle_hash, handle_login_error, handle_login_from_ui,
|
||||
handle_test_delay, input_os_password, Key, KEY_MAP, load_config, LoginConfigHandler,
|
||||
QualityStatus, send_mouse, start_video_audio_threads,
|
||||
};
|
||||
use crate::client::io_loop::Remote;
|
||||
use crate::common::{self, GrabState};
|
||||
use crate::keyboard;
|
||||
|
||||
pub static IS_IN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@ -653,6 +657,14 @@ impl<T: InvokeUiSession> Session<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_voice_call(&self) {
|
||||
self.send(Data::NewVoiceCall);
|
||||
}
|
||||
|
||||
pub fn close_voice_call(&self) {
|
||||
self.send(Data::CloseVoiceCall);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
|
||||
@ -693,6 +705,10 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
|
||||
fn clipboard(&self, content: String);
|
||||
fn cancel_msgbox(&self, tag: &str);
|
||||
fn switch_back(&self, id: &str);
|
||||
fn on_voice_call_started(&self);
|
||||
fn on_voice_call_closed(&self, reason: &str);
|
||||
fn on_voice_call_waiting(&self);
|
||||
fn on_voice_call_incoming(&self);
|
||||
}
|
||||
|
||||
impl<T: InvokeUiSession> Deref for Session<T> {
|
||||
|
Loading…
Reference in New Issue
Block a user