mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-28 09:01:22 +02:00
feat(webrtc): add configurable STUN and TURN servers
Add backend config, RPC handlers, and an HTTP endpoint for WebRTC ICE servers. Replace hardcoded frontend STUN usage with server-provided ICE server configuration, and add access settings UI for STUN and TURN entries.
This commit is contained in:
12
config.go
12
config.go
@@ -21,6 +21,12 @@ type WakeOnLanDevice struct {
|
|||||||
MacAddress string `json:"macAddress"`
|
MacAddress string `json:"macAddress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TurnServer struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Credential string `json:"credential"`
|
||||||
|
}
|
||||||
|
|
||||||
// Constants for keyboard macro limits
|
// Constants for keyboard macro limits
|
||||||
const (
|
const (
|
||||||
MaxMacrosPerDevice = 25
|
MaxMacrosPerDevice = 25
|
||||||
@@ -81,6 +87,7 @@ func (m *KeyboardMacro) Validate() error {
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
STUN string `json:"stun"`
|
STUN string `json:"stun"`
|
||||||
|
TurnServers []TurnServer `json:"turn_servers"`
|
||||||
JigglerEnabled bool `json:"jiggler_enabled"`
|
JigglerEnabled bool `json:"jiggler_enabled"`
|
||||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||||
IncludePreRelease bool `json:"include_pre_release"`
|
IncludePreRelease bool `json:"include_pre_release"`
|
||||||
@@ -185,6 +192,7 @@ const sdConfigPath = "/mnt/sdcard/kvm_config.json"
|
|||||||
|
|
||||||
var defaultConfig = &Config{
|
var defaultConfig = &Config{
|
||||||
STUN: "stun:stun.l.google.com:19302",
|
STUN: "stun:stun.l.google.com:19302",
|
||||||
|
TurnServers: []TurnServer{},
|
||||||
AutoUpdateEnabled: false, // Set a default value
|
AutoUpdateEnabled: false, // Set a default value
|
||||||
ActiveExtension: "",
|
ActiveExtension: "",
|
||||||
KeyboardMacros: []KeyboardMacro{},
|
KeyboardMacros: []KeyboardMacro{},
|
||||||
@@ -299,6 +307,10 @@ func LoadConfig() {
|
|||||||
loadedConfig.Firewall = defaultConfig.Firewall
|
loadedConfig.Firewall = defaultConfig.Firewall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if loadedConfig.TurnServers == nil {
|
||||||
|
loadedConfig.TurnServers = []TurnServer{}
|
||||||
|
}
|
||||||
|
|
||||||
config = &loadedConfig
|
config = &loadedConfig
|
||||||
|
|
||||||
logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
|
logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
|
||||||
|
|||||||
58
jsonrpc.go
58
jsonrpc.go
@@ -852,6 +852,60 @@ func rpcSetConfigRaw(configStr string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RtcServersConfig struct {
|
||||||
|
STUN string `json:"stun"`
|
||||||
|
DefaultSTUN string `json:"defaultStun"`
|
||||||
|
TurnServers []TurnServer `json:"turnServers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetRtcServersConfig() (RtcServersConfig, error) {
|
||||||
|
return RtcServersConfig{
|
||||||
|
STUN: config.STUN,
|
||||||
|
DefaultSTUN: DefaultSTUN,
|
||||||
|
TurnServers: config.TurnServers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetStunServer(stun string) error {
|
||||||
|
config.STUN = stun
|
||||||
|
return SaveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetTurnServersParams struct {
|
||||||
|
Servers []TurnServer `json:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetTurnServers(params SetTurnServersParams) error {
|
||||||
|
config.TurnServers = params.Servers
|
||||||
|
if config.TurnServers == nil {
|
||||||
|
config.TurnServers = []TurnServer{}
|
||||||
|
}
|
||||||
|
return SaveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
type IceServerJSON struct {
|
||||||
|
URLs []string `json:"urls"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Credential string `json:"credential,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetIceServers() ([]IceServerJSON, error) {
|
||||||
|
raw := buildICEServers()
|
||||||
|
out := make([]IceServerJSON, 0, len(raw))
|
||||||
|
for _, server := range raw {
|
||||||
|
credential := ""
|
||||||
|
if server.Credential != nil {
|
||||||
|
credential = fmt.Sprintf("%v", server.Credential)
|
||||||
|
}
|
||||||
|
out = append(out, IceServerJSON{
|
||||||
|
URLs: server.URLs,
|
||||||
|
Username: server.Username,
|
||||||
|
Credential: credential,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func rpcGetActiveExtension() (string, error) {
|
func rpcGetActiveExtension() (string, error) {
|
||||||
return config.ActiveExtension, nil
|
return config.ActiveExtension, nil
|
||||||
}
|
}
|
||||||
@@ -1406,6 +1460,10 @@ var rpcHandlers = map[string]RPCHandler{
|
|||||||
"resetConfig": {Func: rpcResetConfig},
|
"resetConfig": {Func: rpcResetConfig},
|
||||||
"getConfigRaw": {Func: rpcGetConfigRaw},
|
"getConfigRaw": {Func: rpcGetConfigRaw},
|
||||||
"setConfigRaw": {Func: rpcSetConfigRaw, Params: []string{"configStr"}},
|
"setConfigRaw": {Func: rpcSetConfigRaw, Params: []string{"configStr"}},
|
||||||
|
"getRtcServersConfig": {Func: rpcGetRtcServersConfig},
|
||||||
|
"setStunServer": {Func: rpcSetStunServer, Params: []string{"stun"}},
|
||||||
|
"setTurnServers": {Func: rpcSetTurnServers, Params: []string{"params"}},
|
||||||
|
"getIceServers": {Func: rpcGetIceServers},
|
||||||
"setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}},
|
"setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}},
|
||||||
"getDisplayRotation": {Func: rpcGetDisplayRotation},
|
"getDisplayRotation": {Func: rpcGetDisplayRotation},
|
||||||
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const comboboxVariants = cva({
|
|||||||
variants: { size: sizes },
|
variants: { size: sizes },
|
||||||
});
|
});
|
||||||
|
|
||||||
type BaseProps = React.ComponentProps<typeof HeadlessCombobox>;
|
type BaseProps = React.ComponentProps<typeof HeadlessCombobox<ComboboxOption>>;
|
||||||
|
|
||||||
interface ComboboxProps extends Omit<BaseProps, "displayValue"> {
|
interface ComboboxProps extends Omit<BaseProps, "displayValue"> {
|
||||||
displayValue: (option: ComboboxOption) => string;
|
displayValue: (option: ComboboxOption) => string;
|
||||||
|
|||||||
@@ -205,7 +205,8 @@ export function MacroStepCard({
|
|||||||
)}
|
)}
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<Combobox
|
<Combobox
|
||||||
onChange={(value: { value: string; label: string }) => {
|
onChange={value => {
|
||||||
|
if (!value) return;
|
||||||
onKeySelect(value);
|
onKeySelect(value);
|
||||||
onKeyQueryChange('');
|
onKeyQueryChange('');
|
||||||
}}
|
}}
|
||||||
@@ -239,4 +240,4 @@ export function MacroStepCard({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { LogDialog } from "@components/LogDialog";
|
|||||||
import { Dialog } from "@/layout/components_setting/access/auth";
|
import { Dialog } from "@/layout/components_setting/access/auth";
|
||||||
import AutoHeight from "@components/AutoHeight";
|
import AutoHeight from "@components/AutoHeight";
|
||||||
import FirewallSettings from "./FirewallSettings";
|
import FirewallSettings from "./FirewallSettings";
|
||||||
|
import WebRtcServersSettings from "./WebRtcServers";
|
||||||
|
|
||||||
export interface TailScaleResponse {
|
export interface TailScaleResponse {
|
||||||
state: string;
|
state: string;
|
||||||
@@ -997,6 +998,20 @@ function AccessContent({ setOpenDialog }: { setOpenDialog: (open: boolean) => vo
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsSectionHeader
|
||||||
|
title={$at("WebRTC Servers")}
|
||||||
|
description={$at("STUN and TURN servers used for peer connections")}
|
||||||
|
/>
|
||||||
|
<GridCard>
|
||||||
|
<AutoHeight>
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
<WebRtcServersSettings />
|
||||||
|
</div>
|
||||||
|
</AutoHeight>
|
||||||
|
</GridCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsSectionHeader
|
<SettingsSectionHeader
|
||||||
title={$at("Remote")}
|
title={$at("Remote")}
|
||||||
|
|||||||
159
ui/src/layout/components_setting/access/WebRtcServers.tsx
Normal file
159
ui/src/layout/components_setting/access/WebRtcServers.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useReactAt } from "i18n-auto-extractor/react";
|
||||||
|
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { InputField } from "@components/InputField";
|
||||||
|
import { SettingsItem } from "@components/Settings/SettingsView";
|
||||||
|
|
||||||
|
interface TurnServer {
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
credential: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RtcServersConfig {
|
||||||
|
stun: string;
|
||||||
|
defaultStun: string;
|
||||||
|
turnServers: TurnServer[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WebRtcServersSettings() {
|
||||||
|
const { $at } = useReactAt();
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
|
const [stun, setStun] = useState("");
|
||||||
|
const [defaultStun, setDefaultStun] = useState("");
|
||||||
|
const [turnServers, setTurnServers] = useState<TurnServer[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
send("getRtcServersConfig", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`${$at("Failed to load WebRTC servers")}: ${resp.error.data || $at("Unknown error")}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = resp.result as RtcServersConfig;
|
||||||
|
setDefaultStun(cfg.defaultStun);
|
||||||
|
setStun(cfg.stun || cfg.defaultStun);
|
||||||
|
setTurnServers(cfg.turnServers ?? []);
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
const saveStun = (value: string) => {
|
||||||
|
send("setStunServer", { stun: value }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`${$at("Failed to save STUN")}: ${resp.error.data || $at("Unknown error")}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStun(value);
|
||||||
|
notifications.success($at("STUN server saved"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistTurnServers = (servers: TurnServer[]) => {
|
||||||
|
send("setTurnServers", { params: { servers } }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`${$at("Failed to save TURN")}: ${resp.error.data || $at("Unknown error")}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTurnServers(servers);
|
||||||
|
notifications.success($at("TURN servers saved"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTurnRow = (index: number, field: keyof TurnServer, value: string) => {
|
||||||
|
setTurnServers(prev => prev.map((server, i) => (i === index ? { ...server, [field]: value } : server)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTurnRow = () => {
|
||||||
|
setTurnServers(prev => [...prev, { url: "", username: "", credential: "" }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTurnRow = (index: number) => {
|
||||||
|
persistTurnServers(turnServers.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<SettingsItem
|
||||||
|
title={$at("STUN Server")}
|
||||||
|
description={$at("Public STUN server for NAT traversal")}
|
||||||
|
noCol
|
||||||
|
>
|
||||||
|
<div className="flex w-full max-w-2xl flex-col gap-2 sm:flex-row">
|
||||||
|
<InputField
|
||||||
|
value={stun}
|
||||||
|
onChange={e => setStun(e.target.value)}
|
||||||
|
placeholder={defaultStun}
|
||||||
|
className="min-w-0"
|
||||||
|
/>
|
||||||
|
<div className="flex shrink-0 gap-2">
|
||||||
|
<Button size="MD" theme="primary" text={$at("Save")} onClick={() => saveStun(stun)} />
|
||||||
|
<Button
|
||||||
|
size="MD"
|
||||||
|
theme="light"
|
||||||
|
text={$at("Restore Default")}
|
||||||
|
onClick={() => saveStun(defaultStun)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="text-base font-semibold text-black dark:text-white">
|
||||||
|
{$at("TURN Servers")}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-slate-700 dark:text-slate-300">
|
||||||
|
{$at("Used as relay when direct peer-to-peer connection fails")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{turnServers.length === 0 && (
|
||||||
|
<div className="text-sm text-slate-500 dark:text-slate-400">{$at("No TURN servers configured")}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{turnServers.map((server, index) => (
|
||||||
|
<div key={index} className="grid gap-2 lg:grid-cols-[minmax(220px,1fr)_minmax(120px,180px)_minmax(120px,180px)_auto]">
|
||||||
|
<InputField
|
||||||
|
value={server.url}
|
||||||
|
onChange={e => updateTurnRow(index, "url", e.target.value)}
|
||||||
|
placeholder="turn:turn.example.com:3478"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
value={server.username}
|
||||||
|
onChange={e => updateTurnRow(index, "username", e.target.value)}
|
||||||
|
placeholder={$at("Username")}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
value={server.credential}
|
||||||
|
onChange={e => updateTurnRow(index, "credential", e.target.value)}
|
||||||
|
placeholder={$at("Credential")}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="MD"
|
||||||
|
theme="lightDanger"
|
||||||
|
text={$at("Delete")}
|
||||||
|
onClick={() => deleteTurnRow(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button size="MD" theme="light" text={$at("Add TURN Server")} onClick={addTurnRow} />
|
||||||
|
<Button
|
||||||
|
size="MD"
|
||||||
|
theme="primary"
|
||||||
|
text={$at("Save TURN Servers")}
|
||||||
|
onClick={() => persistTurnServers(turnServers)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useVpnStore } from "@/hooks/stores";
|
useVpnStore } from "@/hooks/stores";
|
||||||
import { JsonRpcRequest, useJsonRpc, resetHttpSessionId } from "@/hooks/useJsonRpc";
|
import { JsonRpcRequest, useJsonRpc, resetHttpSessionId } from "@/hooks/useJsonRpc";
|
||||||
|
import api from "@/api";
|
||||||
import Modal from "@components/Modal";
|
import Modal from "@components/Modal";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import {
|
import {
|
||||||
@@ -351,6 +352,18 @@ export default function MobileHome() {
|
|||||||
try {
|
try {
|
||||||
console.log("[setupPeerConnection] Creating peer connection");
|
console.log("[setupPeerConnection] Creating peer connection");
|
||||||
setLoadingMessage("Creating peer connection...");
|
setLoadingMessage("Creating peer connection...");
|
||||||
|
let fetchedIceServers: RTCIceServer[] = [];
|
||||||
|
if (!iceConfig?.iceServers) {
|
||||||
|
try {
|
||||||
|
const res = await api.GET("/api/ice-servers");
|
||||||
|
const data = await res.json();
|
||||||
|
fetchedIceServers = data.iceServers ?? [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error("failed to fetch ICE servers, fallback", e);
|
||||||
|
fetchedIceServers = [{ urls: ["stun:stun.l.google.com:19302"] }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pc = new RTCPeerConnection({
|
pc = new RTCPeerConnection({
|
||||||
// We only use STUN or TURN servers if we're in the cloud
|
// We only use STUN or TURN servers if we're in the cloud
|
||||||
//...(isInCloud && iceConfig?.iceServers
|
//...(isInCloud && iceConfig?.iceServers
|
||||||
@@ -358,13 +371,7 @@ export default function MobileHome() {
|
|||||||
// : {}),
|
// : {}),
|
||||||
...(iceConfig?.iceServers
|
...(iceConfig?.iceServers
|
||||||
? { iceServers: [iceConfig?.iceServers] }
|
? { iceServers: [iceConfig?.iceServers] }
|
||||||
: {
|
: { iceServers: fetchedIceServers }),
|
||||||
iceServers: [
|
|
||||||
{
|
|
||||||
urls: ['stun:stun.l.google.com:19302']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setPeerConnectionState(pc.connectionState);
|
setPeerConnectionState(pc.connectionState);
|
||||||
|
|||||||
@@ -363,6 +363,18 @@ export default function PCHome() {
|
|||||||
try {
|
try {
|
||||||
console.log("[setupPeerConnection] Creating peer connection");
|
console.log("[setupPeerConnection] Creating peer connection");
|
||||||
setLoadingMessage("Creating peer connection...");
|
setLoadingMessage("Creating peer connection...");
|
||||||
|
let fetchedIceServers: RTCIceServer[] = [];
|
||||||
|
if (!iceConfig?.iceServers) {
|
||||||
|
try {
|
||||||
|
const res = await api.GET("/api/ice-servers");
|
||||||
|
const data = await res.json();
|
||||||
|
fetchedIceServers = data.iceServers ?? [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error("failed to fetch ICE servers, fallback", e);
|
||||||
|
fetchedIceServers = [{ urls: ["stun:stun.l.google.com:19302"] }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pc = new RTCPeerConnection({
|
pc = new RTCPeerConnection({
|
||||||
// We only use STUN or TURN servers if we're in the cloud
|
// We only use STUN or TURN servers if we're in the cloud
|
||||||
//...(isInCloud && iceConfig?.iceServers
|
//...(isInCloud && iceConfig?.iceServers
|
||||||
@@ -370,13 +382,7 @@ export default function PCHome() {
|
|||||||
// : {}),
|
// : {}),
|
||||||
...(iceConfig?.iceServers
|
...(iceConfig?.iceServers
|
||||||
? { iceServers: [iceConfig?.iceServers] }
|
? { iceServers: [iceConfig?.iceServers] }
|
||||||
: {
|
: { iceServers: fetchedIceServers }),
|
||||||
iceServers: [
|
|
||||||
{
|
|
||||||
urls: ['stun:stun.l.google.com:19302']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setPeerConnectionState(pc.connectionState);
|
setPeerConnectionState(pc.connectionState);
|
||||||
|
|||||||
@@ -571,5 +571,23 @@
|
|||||||
"65f1314580": "Confirm your password",
|
"65f1314580": "Confirm your password",
|
||||||
"af21497286": "Set Password",
|
"af21497286": "Set Password",
|
||||||
"c3f88872d6": "This password will be used to secure your device data and protect against unauthorized access.",
|
"c3f88872d6": "This password will be used to secure your device data and protect against unauthorized access.",
|
||||||
"06a7b3bf6e": "All data remains on your local device."
|
"06a7b3bf6e": "All data remains on your local device.",
|
||||||
}
|
"d6d56d5972": "WebRTC Servers",
|
||||||
|
"8bea04c48e": "STUN and TURN servers used for peer connections",
|
||||||
|
"4cd48aed7f": "STUN server saved",
|
||||||
|
"f6f10b4517": "TURN servers saved",
|
||||||
|
"d235995f96": "STUN Server",
|
||||||
|
"fc1bd2c935": "Public STUN server for NAT traversal",
|
||||||
|
"289929755b": "Restore Default",
|
||||||
|
"0268827609": "TURN Servers",
|
||||||
|
"b3c14a0273": "Used as relay when direct peer-to-peer connection fails",
|
||||||
|
"bbc48fb751": "No TURN servers configured",
|
||||||
|
"f6039d44b2": "Username",
|
||||||
|
"03bc142e64": "Credential",
|
||||||
|
"ca3e8baee9": "Add TURN Server",
|
||||||
|
"0acde1b3e3": "Save TURN Servers",
|
||||||
|
"6f20995c95": "Failed to load WebRTC servers",
|
||||||
|
"6ef7ce5b80": "Failed to save STUN",
|
||||||
|
"4b5d050e51": "Failed to save TURN",
|
||||||
|
"aee9784c03": "Unknown error"
|
||||||
|
}
|
||||||
|
|||||||
@@ -571,5 +571,23 @@
|
|||||||
"65f1314580": "确认您的密码",
|
"65f1314580": "确认您的密码",
|
||||||
"af21497286": "设置密码",
|
"af21497286": "设置密码",
|
||||||
"c3f88872d6": "此密码将用于保护您的设备数据并防止未经授权的访问。",
|
"c3f88872d6": "此密码将用于保护您的设备数据并防止未经授权的访问。",
|
||||||
"06a7b3bf6e": "所有数据保留在您的本地设备上。"
|
"06a7b3bf6e": "所有数据保留在您的本地设备上。",
|
||||||
}
|
"d6d56d5972": "WebRTC 服务器",
|
||||||
|
"8bea04c48e": "用于点对点连接的 STUN 和 TURN 服务器",
|
||||||
|
"4cd48aed7f": "STUN 服务器已保存",
|
||||||
|
"f6f10b4517": "TURN 服务器已保存",
|
||||||
|
"d235995f96": "STUN 服务器",
|
||||||
|
"fc1bd2c935": "用于 NAT 穿透的公共 STUN 服务器",
|
||||||
|
"289929755b": "恢复默认",
|
||||||
|
"0268827609": "TURN 服务器",
|
||||||
|
"b3c14a0273": "当直接点对点连接失败时用作中继",
|
||||||
|
"bbc48fb751": "未配置 TURN 服务器",
|
||||||
|
"f6039d44b2": "用户名",
|
||||||
|
"03bc142e64": "凭据",
|
||||||
|
"ca3e8baee9": "添加 TURN 服务器",
|
||||||
|
"0acde1b3e3": "保存 TURN 服务器",
|
||||||
|
"6f20995c95": "加载 WebRTC 服务器失败",
|
||||||
|
"6ef7ce5b80": "保存 STUN 失败",
|
||||||
|
"4b5d050e51": "保存 TURN 失败",
|
||||||
|
"aee9784c03": "未知错误"
|
||||||
|
}
|
||||||
|
|||||||
11
web.go
11
web.go
@@ -168,6 +168,7 @@ func setupRouter() *gin.Engine {
|
|||||||
protected.GET("/storage/download", handleDownloadHttp)
|
protected.GET("/storage/download", handleDownloadHttp)
|
||||||
protected.GET("/storage/sd-download", handleSDDownloadHttp)
|
protected.GET("/storage/sd-download", handleSDDownloadHttp)
|
||||||
protected.POST("/api/rpc", handleRpcRequest)
|
protected.POST("/api/rpc", handleRpcRequest)
|
||||||
|
protected.GET("/api/ice-servers", handleGetIceServers)
|
||||||
protected.GET("/terminal/ws", handleTerminalWS)
|
protected.GET("/terminal/ws", handleTerminalWS)
|
||||||
protected.GET("/serial/ws", handleSerialWS)
|
protected.GET("/serial/ws", handleSerialWS)
|
||||||
protected.GET("/video/stream", handleVideoStream)
|
protected.GET("/video/stream", handleVideoStream)
|
||||||
@@ -907,6 +908,16 @@ func handleRpcRequest(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleGetIceServers(c *gin.Context) {
|
||||||
|
LoadConfig()
|
||||||
|
servers, err := rpcGetIceServers()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"iceServers": servers})
|
||||||
|
}
|
||||||
|
|
||||||
func handleVideoStream(c *gin.Context) {
|
func handleVideoStream(c *gin.Context) {
|
||||||
logger.Info().Msg("HTTP video stream request received")
|
logger.Info().Msg("HTTP video stream request received")
|
||||||
|
|
||||||
|
|||||||
38
webrtc.go
38
webrtc.go
@@ -34,6 +34,33 @@ type SessionConfig struct {
|
|||||||
Logger *zerolog.Logger
|
Logger *zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultSTUN = "stun:stun.l.google.com:19302"
|
||||||
|
|
||||||
|
func buildICEServers() []webrtc.ICEServer {
|
||||||
|
if config == nil {
|
||||||
|
LoadConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
stunURL := config.STUN
|
||||||
|
if stunURL == "" {
|
||||||
|
stunURL = DefaultSTUN
|
||||||
|
}
|
||||||
|
|
||||||
|
servers := []webrtc.ICEServer{{URLs: []string{stunURL}}}
|
||||||
|
for _, turnServer := range config.TurnServers {
|
||||||
|
if turnServer.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
servers = append(servers, webrtc.ICEServer{
|
||||||
|
URLs: []string{turnServer.URL},
|
||||||
|
Username: turnServer.Username,
|
||||||
|
Credential: turnServer.Credential,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
||||||
b, err := base64.StdEncoding.DecodeString(offerStr)
|
b, err := base64.StdEncoding.DecodeString(offerStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,16 +113,7 @@ func newSession(sessionConfig SessionConfig) (*Session, error) {
|
|||||||
scopedLogger = webrtcLogger
|
scopedLogger = webrtcLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
iceServers := []webrtc.ICEServer{
|
iceServers := buildICEServers()
|
||||||
{
|
|
||||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if config.STUN != "" {
|
|
||||||
iceServers = append(iceServers, webrtc.ICEServer{
|
|
||||||
URLs: []string{config.STUN},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api := webrtc.NewAPI(webrtc.WithSettingEngine(webrtcSettingEngine))
|
api := webrtc.NewAPI(webrtc.WithSettingEngine(webrtcSettingEngine))
|
||||||
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
|
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
|
||||||
|
|||||||
Reference in New Issue
Block a user