mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-13 09:40:30 +08:00
feat: 支持 turn 中转,可以远程访问 h264/webrtc #197
This commit is contained in:
parent
aae4e936db
commit
c8305cc65d
25
.github/workflows/build_img.yaml
vendored
25
.github/workflows/build_img.yaml
vendored
@ -50,6 +50,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Inject TURN config (optional)
|
||||||
|
if: ${{ env.TURN_HOST != '' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p configs/kvmd/override.d
|
||||||
|
cat > configs/kvmd/override.d/turn.yaml <<EOF
|
||||||
|
janus:
|
||||||
|
stun:
|
||||||
|
host: ${TURN_HOST}
|
||||||
|
port: ${TURN_PORT}
|
||||||
|
local_ice_servers:
|
||||||
|
- urls:
|
||||||
|
- "stun:${TURN_HOST}:${TURN_PORT}"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=udp"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=tcp"
|
||||||
|
username: "${TURN_USER}"
|
||||||
|
credential: "${TURN_PASS}"
|
||||||
|
EOF
|
||||||
|
env:
|
||||||
|
TURN_HOST: ${{ secrets.TURN_HOST }}
|
||||||
|
TURN_PORT: ${{ secrets.TURN_PORT }}
|
||||||
|
TURN_USER: ${{ secrets.TURN_USER }}
|
||||||
|
TURN_PASS: ${{ secrets.TURN_PASS }}
|
||||||
|
|
||||||
- name: Set build environment
|
- name: Set build environment
|
||||||
id: build_env
|
id: build_env
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -184,4 +207,4 @@ jobs:
|
|||||||
if [ "${{ github.event.inputs.create_release }}" = "true" ]; then
|
if [ "${{ github.event.inputs.create_release }}" = "true" ]; then
|
||||||
echo "| **Release** | [${{ env.RELEASE_TAG }}](${{ steps.release.outputs.url }}) |" >> $GITHUB_STEP_SUMMARY
|
echo "| **Release** | [${{ env.RELEASE_TAG }}](${{ steps.release.outputs.url }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
48
.github/workflows/docker-build.yaml
vendored
48
.github/workflows/docker-build.yaml
vendored
@ -47,6 +47,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Inject TURN config (optional)
|
||||||
|
if: ${{ env.TURN_HOST != '' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p configs/kvmd/override.d
|
||||||
|
cat > configs/kvmd/override.d/turn.yaml <<EOF
|
||||||
|
janus:
|
||||||
|
stun:
|
||||||
|
host: ${TURN_HOST}
|
||||||
|
port: ${TURN_PORT}
|
||||||
|
local_ice_servers:
|
||||||
|
- urls:
|
||||||
|
- "stun:${TURN_HOST}:${TURN_PORT}"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=udp"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=tcp"
|
||||||
|
username: "${TURN_USER}"
|
||||||
|
credential: "${TURN_PASS}"
|
||||||
|
EOF
|
||||||
|
env:
|
||||||
|
TURN_HOST: ${{ secrets.TURN_HOST }}
|
||||||
|
TURN_PORT: ${{ secrets.TURN_PORT }}
|
||||||
|
TURN_USER: ${{ secrets.TURN_USER }}
|
||||||
|
TURN_PASS: ${{ secrets.TURN_PASS }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
@ -117,6 +140,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Inject TURN config (optional)
|
||||||
|
if: ${{ env.TURN_HOST != '' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p configs/kvmd/override.d
|
||||||
|
cat > configs/kvmd/override.d/turn.yaml <<EOF
|
||||||
|
janus:
|
||||||
|
stun:
|
||||||
|
host: ${TURN_HOST}
|
||||||
|
port: ${TURN_PORT}
|
||||||
|
local_ice_servers:
|
||||||
|
- urls:
|
||||||
|
- "stun:${TURN_HOST}:${TURN_PORT}"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=udp"
|
||||||
|
- "turn:${TURN_HOST}:${TURN_PORT}?transport=tcp"
|
||||||
|
username: "${TURN_USER}"
|
||||||
|
credential: "${TURN_PASS}"
|
||||||
|
EOF
|
||||||
|
env:
|
||||||
|
TURN_HOST: ${{ secrets.TURN_HOST }}
|
||||||
|
TURN_PORT: ${{ secrets.TURN_PORT }}
|
||||||
|
TURN_USER: ${{ secrets.TURN_USER }}
|
||||||
|
TURN_PASS: ${{ secrets.TURN_PASS }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
@ -191,4 +237,4 @@ jobs:
|
|||||||
echo "- **Platforms**: ${{ github.event.inputs.platforms }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Platforms**: ${{ github.event.inputs.platforms }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Aliyun Enabled**: ${{ github.event.inputs.enable_aliyun }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Aliyun Enabled**: ${{ github.event.inputs.enable_aliyun }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Tags**:" >> $GITHUB_STEP_SUMMARY
|
echo "- **Tags**:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /' >> $GITHUB_STEP_SUMMARY
|
echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /' >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@ -157,10 +157,6 @@ media:
|
|||||||
|
|
||||||
jpeg:
|
jpeg:
|
||||||
sink: 'kvmd::ustreamer::jpeg'
|
sink: 'kvmd::ustreamer::jpeg'
|
||||||
janus:
|
|
||||||
stun:
|
|
||||||
host: stun.cloudflare.com
|
|
||||||
port: 3478
|
|
||||||
|
|
||||||
otgnet:
|
otgnet:
|
||||||
commands:
|
commands:
|
||||||
|
|||||||
@ -81,6 +81,7 @@ from ..validators.net import valid_port
|
|||||||
from ..validators.net import valid_ports_list
|
from ..validators.net import valid_ports_list
|
||||||
from ..validators.net import valid_mac
|
from ..validators.net import valid_mac
|
||||||
from ..validators.net import valid_ssl_ciphers
|
from ..validators.net import valid_ssl_ciphers
|
||||||
|
from ..validators.net import valid_ice_servers
|
||||||
|
|
||||||
from ..validators.hid import valid_hid_key
|
from ..validators.hid import valid_hid_key
|
||||||
from ..validators.hid import valid_hid_mouse_output
|
from ..validators.hid import valid_hid_mouse_output
|
||||||
@ -860,6 +861,7 @@ def _get_config_scheme() -> dict:
|
|||||||
], type=valid_command),
|
], type=valid_command),
|
||||||
"cmd_remove": Option([], type=valid_options),
|
"cmd_remove": Option([], type=valid_options),
|
||||||
"cmd_append": Option([], type=valid_options),
|
"cmd_append": Option([], type=valid_options),
|
||||||
|
"local_ice_servers": Option([], type=valid_ice_servers, unpack_as="ice_servers"),
|
||||||
},
|
},
|
||||||
|
|
||||||
"watchdog": {
|
"watchdog": {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import asyncio
|
|||||||
import asyncio.subprocess
|
import asyncio.subprocess
|
||||||
import socket
|
import socket
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import netifaces
|
import netifaces
|
||||||
|
|
||||||
@ -43,6 +45,7 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
cmd_remove: list[str],
|
cmd_remove: list[str],
|
||||||
cmd_append: list[str],
|
cmd_append: list[str],
|
||||||
|
ice_servers: list[dict[str, Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.__stun = Stun(stun_host, stun_port, stun_timeout, stun_retries, stun_retries_delay)
|
self.__stun = Stun(stun_host, stun_port, stun_timeout, stun_retries, stun_retries_delay)
|
||||||
@ -52,6 +55,7 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
self.__check_retries_delay = check_retries_delay
|
self.__check_retries_delay = check_retries_delay
|
||||||
|
|
||||||
self.__cmd = tools.build_cmd(cmd, cmd_remove, cmd_append)
|
self.__cmd = tools.build_cmd(cmd, cmd_remove, cmd_append)
|
||||||
|
self.__ice_servers = ice_servers
|
||||||
|
|
||||||
self.__janus_task: (asyncio.Task | None) = None
|
self.__janus_task: (asyncio.Task | None) = None
|
||||||
self.__janus_proc: (asyncio.subprocess.Process | None) = None # pylint: disable=no-member
|
self.__janus_proc: (asyncio.subprocess.Process | None) = None # pylint: disable=no-member
|
||||||
@ -173,13 +177,25 @@ class JanusRunner: # pylint: disable=too-many-instance-attributes
|
|||||||
part.format(**placeholders)
|
part.format(**placeholders)
|
||||||
for part in cmd
|
for part in cmd
|
||||||
]
|
]
|
||||||
self.__janus_proc = await aioproc.run_process(
|
env = {}
|
||||||
cmd=cmd,
|
ice_payload = self.__build_ice_payload(netcfg)
|
||||||
env={"JANUS_USTREAMER_WEB_ICE_URL": f"stun:{netcfg.stun_host}:{netcfg.stun_port}"},
|
if ice_payload:
|
||||||
)
|
env["JANUS_USTREAMER_WEB_ICE_URL"] = ice_payload
|
||||||
|
self.__janus_proc = await aioproc.run_process(cmd=cmd, env=env or None)
|
||||||
get_logger(0).info("Started Janus pid=%d: %s", self.__janus_proc.pid, tools.cmdfmt(cmd))
|
get_logger(0).info("Started Janus pid=%d: %s", self.__janus_proc.pid, tools.cmdfmt(cmd))
|
||||||
|
|
||||||
async def __kill_janus_proc(self) -> None:
|
async def __kill_janus_proc(self) -> None:
|
||||||
if self.__janus_proc:
|
if self.__janus_proc:
|
||||||
await aioproc.kill_process(self.__janus_proc, 5, get_logger(0))
|
await aioproc.kill_process(self.__janus_proc, 5, get_logger(0))
|
||||||
self.__janus_proc = None
|
self.__janus_proc = None
|
||||||
|
|
||||||
|
def __build_ice_payload(self, netcfg: _Netcfg) -> (str | None):
|
||||||
|
if self.__ice_servers:
|
||||||
|
try:
|
||||||
|
return f"json:{json.dumps(self.__ice_servers, ensure_ascii=False)}"
|
||||||
|
except Exception as ex: # pragma: no cover
|
||||||
|
get_logger(0).error("Can't encode ICE servers: %s", tools.efmt(ex))
|
||||||
|
return None
|
||||||
|
if netcfg.stun_host and netcfg.stun_port:
|
||||||
|
return f"stun:{netcfg.stun_host}:{netcfg.stun_port}"
|
||||||
|
return None
|
||||||
|
|||||||
@ -136,7 +136,12 @@ class Stun:
|
|||||||
return (StunNatType.FULL_CONE_NAT, resp)
|
return (StunNatType.FULL_CONE_NAT, resp)
|
||||||
|
|
||||||
if first.changed is None:
|
if first.changed is None:
|
||||||
raise RuntimeError(f"Changed addr is None: {first}")
|
get_logger(0).warning(
|
||||||
|
"STUN server %s:%d responded without CHANGED-ADDRESS; skipping NAT type detection",
|
||||||
|
self.__host,
|
||||||
|
self.__port,
|
||||||
|
)
|
||||||
|
return (StunNatType.ERROR, first)
|
||||||
resp = await self.__make_request("Change request [ext_ip != src_ip]", first.changed, b"")
|
resp = await self.__make_request("Change request [ext_ip != src_ip]", first.changed, b"")
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
return (StunNatType.CHANGED_ADDR_ERROR, resp)
|
return (StunNatType.CHANGED_ADDR_ERROR, resp)
|
||||||
|
|||||||
@ -120,3 +120,39 @@ def valid_ssl_ciphers(arg: Any) -> str:
|
|||||||
def valid_url(arg: Any) -> str:
|
def valid_url(arg: Any) -> str:
|
||||||
# XXX: VERY primitive
|
# XXX: VERY primitive
|
||||||
return check_re_match(arg, "HTTP(S) URL", r"^https?://[\[\w]+\S*")
|
return check_re_match(arg, "HTTP(S) URL", r"^https?://[\[\w]+\S*")
|
||||||
|
|
||||||
|
|
||||||
|
def valid_ice_servers(arg: Any) -> list[dict[str, Any]]:
|
||||||
|
name = "ICE servers list"
|
||||||
|
if arg is None:
|
||||||
|
return []
|
||||||
|
if not isinstance(arg, list):
|
||||||
|
raise_error(arg, name)
|
||||||
|
servers: list[dict[str, Any]] = []
|
||||||
|
for item in arg:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise_error(item, "ICE server entry")
|
||||||
|
urls = item.get("urls")
|
||||||
|
if isinstance(urls, str):
|
||||||
|
urls_list = [valid_stripped_string_not_empty(urls, "ICE server URL")]
|
||||||
|
elif isinstance(urls, list):
|
||||||
|
urls_list = [
|
||||||
|
valid_stripped_string_not_empty(url, "ICE server URL")
|
||||||
|
for url in urls
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
raise_error(urls, "ICE server URLs")
|
||||||
|
if not urls_list:
|
||||||
|
raise_error(urls, "ICE server URLs")
|
||||||
|
server: dict[str, Any] = {"urls": urls_list}
|
||||||
|
username = item.get("username")
|
||||||
|
if username is not None:
|
||||||
|
server["username"] = valid_stripped_string_not_empty(username, "ICE username")
|
||||||
|
credential = item.get("credential")
|
||||||
|
if credential is not None:
|
||||||
|
server["credential"] = valid_stripped_string_not_empty(credential, "ICE credential")
|
||||||
|
credential_type = item.get("credential_type") or item.get("credentialType")
|
||||||
|
if credential_type is not None:
|
||||||
|
server["credentialType"] = valid_stripped_string_not_empty(credential_type, "ICE credentialType")
|
||||||
|
servers.append(server)
|
||||||
|
return servers
|
||||||
|
|||||||
@ -111,7 +111,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __organizeH
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var __ensureJanus = function(internal) {
|
var __ensureJanus = function(internal) {
|
||||||
if (__janus === null && !__stop && (!__ensuring || internal)) {
|
if (__janus === null && !__stop && (!__ensuring || internal)) {
|
||||||
__ensuring = true;
|
__ensuring = true;
|
||||||
__setInactive();
|
__setInactive();
|
||||||
@ -131,10 +131,75 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __organizeH
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var __decodeIcePayload = function(payload) {
|
||||||
|
if (typeof payload !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let data = payload.trim();
|
||||||
|
if (data.startsWith("json:")) {
|
||||||
|
data = data.slice(5).trim();
|
||||||
|
}
|
||||||
|
if (data.startsWith("{") || data.startsWith("[")) {
|
||||||
|
try {
|
||||||
|
let parsed = JSON.parse(data);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
} else if (parsed && Array.isArray(parsed.servers)) {
|
||||||
|
return parsed.servers;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
__logError("Can't parse ICE payload:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
var __normalizeIceEntry = function(entry) {
|
||||||
|
if (!entry || typeof entry !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let urls = entry.urls;
|
||||||
|
if (typeof urls === "string") {
|
||||||
|
urls = [urls];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(urls) || urls.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let normalized = {"urls": urls};
|
||||||
|
if (entry.username) {
|
||||||
|
normalized.username = entry.username;
|
||||||
|
}
|
||||||
|
if (entry.credential) {
|
||||||
|
normalized.credential = entry.credential;
|
||||||
|
}
|
||||||
|
if (entry.credentialType) {
|
||||||
|
normalized.credentialType = entry.credentialType;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
var __normalizeIceServers = function(payload) {
|
||||||
|
let parsed = __decodeIcePayload(payload);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let servers = [];
|
||||||
|
for (let entry of parsed) {
|
||||||
|
let normalized = __normalizeIceEntry(entry);
|
||||||
|
if (normalized) {
|
||||||
|
servers.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (servers.length > 0 ? servers : null);
|
||||||
|
};
|
||||||
|
|
||||||
var __getIceServers = function() {
|
var __getIceServers = function() {
|
||||||
if (__ice !== null && __ice.url) {
|
if (__ice !== null && __ice.url) {
|
||||||
__logInfo("Using the custom ICE Server got from uStreamer:", __ice);
|
let normalized = __normalizeIceServers(__ice.url);
|
||||||
|
if (normalized !== null) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
return [{"urls": __ice.url}];
|
return [{"urls": __ice.url}];
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user