mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
⭐feat(runner): Improve robustness and argument handling (#285)
## Description Made argument parsing and handling much nicer with clap features. Changed to tracing package for logging and made other improvements around to hopefully make things more robust and logical. Default audio-capture-method is now PipeWire since it seems to work perfectly fine with latest gstreamer 🎉 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Improved command-line argument parsing with stricter validation, type safety, and clearer help messages. - Enhanced GPU selection and logging, including explicit GPU info logging and support for negative GPU indices for auto-selection. - Added support for new audio and video codec and encoder enums, providing safer and more flexible codec handling. - **Bug Fixes** - Improved error handling and logging throughout the application, unifying logs under the `tracing` system for better diagnostics. - Fixed issues with directory ownership and environment variable handling in startup scripts. - **Refactor** - Replaced string-based parsing and manual conversions with strongly typed enums and value parsers. - Updated logging from `println!` and `log` macros to the `tracing` crate for consistency. - Simplified and unified the handling of pipeline and element references in the signaling and data channel logic. - **Chores** - Updated and cleaned up dependencies, including switching from `log` to `tracing` and upgrading the `webrtc` crate. - Removed unused or redundant code and environment variables for improved maintainability. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
d7e6da12ac
commit
ae364f69bd
@@ -168,8 +168,7 @@ ENV USER="nestri" \
|
|||||||
USER_PWD="nestri1234" \
|
USER_PWD="nestri1234" \
|
||||||
XDG_RUNTIME_DIR=/run/user/1000 \
|
XDG_RUNTIME_DIR=/run/user/1000 \
|
||||||
HOME=/home/nestri \
|
HOME=/home/nestri \
|
||||||
NVIDIA_DRIVER_CAPABILITIES=all \
|
NVIDIA_DRIVER_CAPABILITIES=all
|
||||||
NVIDIA_VISIBLE_DEVICES=all
|
|
||||||
|
|
||||||
RUN mkdir -p /home/${USER} && \
|
RUN mkdir -p /home/${USER} && \
|
||||||
groupadd -g ${GID} ${USER} && \
|
groupadd -g ${GID} ${USER} && \
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ log() {
|
|||||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ensures user directory ownership
|
||||||
|
chown_user_directory() {
|
||||||
|
local user_group="${USER}:${GID}"
|
||||||
|
if ! chown -R -h --no-preserve-root "$user_group" "${HOME}" 2>/dev/null; then
|
||||||
|
echo "Error: Failed to change ownership of ${HOME} to ${user_group}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# Waits for a given socket to be ready
|
# Waits for a given socket to be ready
|
||||||
wait_for_socket() {
|
wait_for_socket() {
|
||||||
local socket_path="$1"
|
local socket_path="$1"
|
||||||
@@ -110,6 +120,13 @@ install_nvidia_driver() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function log_gpu_info {
|
||||||
|
log "Detected GPUs:"
|
||||||
|
for vendor in "${!vendor_devices[@]}"; do
|
||||||
|
log "> $vendor: ${vendor_devices[$vendor]}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
# Wait for required sockets
|
# Wait for required sockets
|
||||||
wait_for_socket "/run/dbus/system_bus_socket" "DBus" || exit 1
|
wait_for_socket "/run/dbus/system_bus_socket" "DBus" || exit 1
|
||||||
@@ -126,10 +143,11 @@ main() {
|
|||||||
log "Error: Failed to detect GPU information."
|
log "Error: Failed to detect GPU information."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
log_gpu_info
|
||||||
|
|
||||||
# Handle NVIDIA GPU
|
# Handle NVIDIA GPU
|
||||||
if [[ -n "${vendor_devices[nvidia]:-}" ]]; then
|
if [[ -n "${vendor_devices[nvidia]:-}" ]]; then
|
||||||
log "NVIDIA GPU detected, applying driver fix..."
|
log "NVIDIA GPU(s) detected, applying driver fix..."
|
||||||
|
|
||||||
# Determine NVIDIA driver version
|
# Determine NVIDIA driver version
|
||||||
local nvidia_driver_version=""
|
local nvidia_driver_version=""
|
||||||
@@ -180,8 +198,6 @@ main() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
log "No NVIDIA GPU detected, skipping driver fix."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Make sure gamescope has CAP_SYS_NICE capabilities if available
|
# Make sure gamescope has CAP_SYS_NICE capabilities if available
|
||||||
@@ -195,6 +211,10 @@ main() {
|
|||||||
log "Skipping CAP_SYS_NICE for gamescope, capability not available..."
|
log "Skipping CAP_SYS_NICE for gamescope, capability not available..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Handle user directory permissions
|
||||||
|
log "Ensuring user directory permissions..."
|
||||||
|
chown_user_directory || exit 1
|
||||||
|
|
||||||
# Switch to nestri user
|
# Switch to nestri user
|
||||||
log "Switching to nestri user for application startup..."
|
log "Switching to nestri user for application startup..."
|
||||||
if [[ ! -x /etc/nestri/entrypoint_nestri.sh ]]; then
|
if [[ ! -x /etc/nestri/entrypoint_nestri.sh ]]; then
|
||||||
|
|||||||
@@ -5,16 +5,6 @@ log() {
|
|||||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensures user directory ownership
|
|
||||||
chown_user_directory() {
|
|
||||||
local user_group="$(id -nu):$(id -ng)"
|
|
||||||
chown -f "$user_group" ~ 2>/dev/null ||
|
|
||||||
sudo chown -f "$user_group" ~ 2>/dev/null ||
|
|
||||||
chown -R -f -h --no-preserve-root "$user_group" ~ 2>/dev/null ||
|
|
||||||
sudo chown -R -f -h --no-preserve-root "$user_group" ~ 2>/dev/null ||
|
|
||||||
log "Warning: Failed to change user directory permissions, there may be permission issues, continuing..."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parses resolution string
|
# Parses resolution string
|
||||||
parse_resolution() {
|
parse_resolution() {
|
||||||
local resolution="$1"
|
local resolution="$1"
|
||||||
@@ -156,7 +146,6 @@ main_loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
chown_user_directory
|
|
||||||
load_envs
|
load_envs
|
||||||
parse_resolution "${RESOLUTION:-1920x1080}" || exit 1
|
parse_resolution "${RESOLUTION:-1920x1080}" || exit 1
|
||||||
restart_chain
|
restart_chain
|
||||||
|
|||||||
@@ -11,6 +11,3 @@ export PROTON_NO_FSYNC=1
|
|||||||
|
|
||||||
# Sleeker Mangohud preset :)
|
# Sleeker Mangohud preset :)
|
||||||
export MANGOHUD_CONFIG=preset=2
|
export MANGOHUD_CONFIG=preset=2
|
||||||
|
|
||||||
# Our preferred prefix
|
|
||||||
export WINEPREFIX=/home/${USER}/.nestripfx/
|
|
||||||
|
|||||||
529
packages/server/Cargo.lock
generated
529
packages/server/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,17 +10,18 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main", features = ["v1_26"] }
|
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main", features = ["v1_26"] }
|
||||||
gst-webrtc = { package = "gstreamer-webrtc", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main", features = ["v1_26"] }
|
gst-webrtc = { package = "gstreamer-webrtc", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", branch = "main", features = ["v1_26"] }
|
||||||
gstrswebrtc = { package = "gst-plugin-webrtc", git = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs", branch = "main", features = ["v1_22"] }
|
gstrswebrtc = { package = "gst-plugin-webrtc", git = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs", branch = "main" }
|
||||||
serde = {version = "1.0", features = ["derive"] }
|
serde = {version = "1.0", features = ["derive"] }
|
||||||
tokio = { version = "1.44", features = ["full"] }
|
tokio = { version = "1.44", features = ["full"] }
|
||||||
clap = { version = "4.5", features = ["env"] }
|
clap = { version = "4.5", features = ["env"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
webrtc = "0.12"
|
webrtc = "0.13"
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
rustls = { version = "0.23", features = ["ring"] }
|
rustls = { version = "0.23", features = ["ring"] }
|
||||||
tokio-tungstenite = { version = "0.26", features = ["native-tls"] }
|
tokio-tungstenite = { version = "0.26", features = ["native-tls"] }
|
||||||
log = { version = "0.4", features = ["std"] }
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use clap::{Arg, Command};
|
use crate::args::encoding_args::AudioCaptureMethod;
|
||||||
|
use crate::enc_helper::{AudioCodec, EncoderType, VideoCodec};
|
||||||
|
use clap::{Arg, Command, value_parser};
|
||||||
|
use clap::builder::{BoolishValueParser, NonEmptyStringValueParser};
|
||||||
|
|
||||||
pub mod app_args;
|
pub mod app_args;
|
||||||
pub mod device_args;
|
pub mod device_args;
|
||||||
@@ -19,6 +22,7 @@ impl Args {
|
|||||||
.long("verbose")
|
.long("verbose")
|
||||||
.env("VERBOSE")
|
.env("VERBOSE")
|
||||||
.help("Enable verbose output")
|
.help("Enable verbose output")
|
||||||
|
.value_parser(BoolishValueParser::new())
|
||||||
.default_value("false"),
|
.default_value("false"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -26,7 +30,8 @@ impl Args {
|
|||||||
.short('d')
|
.short('d')
|
||||||
.long("debug")
|
.long("debug")
|
||||||
.env("DEBUG")
|
.env("DEBUG")
|
||||||
.help("Enable additional debugging information and features")
|
.help("Enable additional debugging features")
|
||||||
|
.value_parser(BoolishValueParser::new())
|
||||||
.default_value("false"),
|
.default_value("false"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -34,6 +39,7 @@ impl Args {
|
|||||||
.short('u')
|
.short('u')
|
||||||
.long("relay-url")
|
.long("relay-url")
|
||||||
.env("RELAY_URL")
|
.env("RELAY_URL")
|
||||||
|
.value_parser(NonEmptyStringValueParser::new())
|
||||||
.help("Nestri relay URL"),
|
.help("Nestri relay URL"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -42,6 +48,7 @@ impl Args {
|
|||||||
.long("resolution")
|
.long("resolution")
|
||||||
.env("RESOLUTION")
|
.env("RESOLUTION")
|
||||||
.help("Display/stream resolution in 'WxH' format")
|
.help("Display/stream resolution in 'WxH' format")
|
||||||
|
.value_parser(NonEmptyStringValueParser::new())
|
||||||
.default_value("1280x720"),
|
.default_value("1280x720"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -50,6 +57,7 @@ impl Args {
|
|||||||
.long("framerate")
|
.long("framerate")
|
||||||
.env("FRAMERATE")
|
.env("FRAMERATE")
|
||||||
.help("Display/stream framerate")
|
.help("Display/stream framerate")
|
||||||
|
.value_parser(value_parser!(u32).range(5..240))
|
||||||
.default_value("60"),
|
.default_value("60"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -63,7 +71,7 @@ impl Args {
|
|||||||
.short('g')
|
.short('g')
|
||||||
.long("gpu-vendor")
|
.long("gpu-vendor")
|
||||||
.env("GPU_VENDOR")
|
.env("GPU_VENDOR")
|
||||||
.help("GPU to find by vendor (e.g. 'nvidia')")
|
.help("GPU to use by vendor")
|
||||||
.required(false),
|
.required(false),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -71,7 +79,7 @@ impl Args {
|
|||||||
.short('n')
|
.short('n')
|
||||||
.long("gpu-name")
|
.long("gpu-name")
|
||||||
.env("GPU_NAME")
|
.env("GPU_NAME")
|
||||||
.help("GPU to find by name (e.g. 'rtx 3060')")
|
.help("GPU to use by name")
|
||||||
.required(false),
|
.required(false),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -79,14 +87,15 @@ impl Args {
|
|||||||
.short('i')
|
.short('i')
|
||||||
.long("gpu-index")
|
.long("gpu-index")
|
||||||
.env("GPU_INDEX")
|
.env("GPU_INDEX")
|
||||||
.help("GPU index, if multiple similar GPUs are present")
|
.help("GPU to use by index")
|
||||||
.default_value("0"),
|
.value_parser(value_parser!(i32).range(-1..))
|
||||||
|
.default_value("-1")
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("gpu-card-path")
|
Arg::new("gpu-card-path")
|
||||||
.long("gpu-card-path")
|
.long("gpu-card-path")
|
||||||
.env("GPU_CARD_PATH")
|
.env("GPU_CARD_PATH")
|
||||||
.help("Force a specific GPU by card/render path (e.g. '/dev/dri/card0')")
|
.help("Force a specific GPU by /dev/dri/ card or render path")
|
||||||
.required(false)
|
.required(false)
|
||||||
.conflicts_with_all(["gpu-vendor", "gpu-name", "gpu-index"]),
|
.conflicts_with_all(["gpu-vendor", "gpu-name", "gpu-index"]),
|
||||||
)
|
)
|
||||||
@@ -95,27 +104,30 @@ impl Args {
|
|||||||
.short('c')
|
.short('c')
|
||||||
.long("video-codec")
|
.long("video-codec")
|
||||||
.env("VIDEO_CODEC")
|
.env("VIDEO_CODEC")
|
||||||
.help("Preferred video codec ('h264', 'h265', 'av1')")
|
.help("Preferred video codec")
|
||||||
|
.value_parser(value_parser!(VideoCodec))
|
||||||
.default_value("h264"),
|
.default_value("h264"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("video-encoder")
|
Arg::new("video-encoder")
|
||||||
.long("video-encoder")
|
.long("video-encoder")
|
||||||
.env("VIDEO_ENCODER")
|
.env("VIDEO_ENCODER")
|
||||||
.help("Override video encoder (e.g. 'vah264enc')"),
|
.help("Override video encoder"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("video-rate-control")
|
Arg::new("video-rate-control")
|
||||||
.long("video-rate-control")
|
.long("video-rate-control")
|
||||||
.env("VIDEO_RATE_CONTROL")
|
.env("VIDEO_RATE_CONTROL")
|
||||||
.help("Rate control method ('cqp', 'vbr', 'cbr')")
|
.help("Rate control method")
|
||||||
.default_value("vbr"),
|
.value_parser(value_parser!(encoding_args::RateControlMethod))
|
||||||
|
.default_value("cbr"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("video-cqp")
|
Arg::new("video-cqp")
|
||||||
.long("video-cqp")
|
.long("video-cqp")
|
||||||
.env("VIDEO_CQP")
|
.env("VIDEO_CQP")
|
||||||
.help("Constant Quantization Parameter (CQP) quality")
|
.help("Constant Quantization Parameter (CQP) quality")
|
||||||
|
.value_parser(value_parser!(u32).range(1..51))
|
||||||
.default_value("26"),
|
.default_value("26"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -123,6 +135,7 @@ impl Args {
|
|||||||
.long("video-bitrate")
|
.long("video-bitrate")
|
||||||
.env("VIDEO_BITRATE")
|
.env("VIDEO_BITRATE")
|
||||||
.help("Target bitrate in kbps")
|
.help("Target bitrate in kbps")
|
||||||
|
.value_parser(value_parser!(u32).range(1..))
|
||||||
.default_value("6000"),
|
.default_value("6000"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -130,27 +143,31 @@ impl Args {
|
|||||||
.long("video-bitrate-max")
|
.long("video-bitrate-max")
|
||||||
.env("VIDEO_BITRATE_MAX")
|
.env("VIDEO_BITRATE_MAX")
|
||||||
.help("Maximum bitrate in kbps")
|
.help("Maximum bitrate in kbps")
|
||||||
|
.value_parser(value_parser!(u32).range(1..))
|
||||||
.default_value("8000"),
|
.default_value("8000"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("video-encoder-type")
|
Arg::new("video-encoder-type")
|
||||||
.long("video-encoder-type")
|
.long("video-encoder-type")
|
||||||
.env("VIDEO_ENCODER_TYPE")
|
.env("VIDEO_ENCODER_TYPE")
|
||||||
.help("Encoder type ('hardware', 'software')")
|
.help("Encoder type")
|
||||||
|
.value_parser(value_parser!(EncoderType))
|
||||||
.default_value("hardware"),
|
.default_value("hardware"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("audio-capture-method")
|
Arg::new("audio-capture-method")
|
||||||
.long("audio-capture-method")
|
.long("audio-capture-method")
|
||||||
.env("AUDIO_CAPTURE_METHOD")
|
.env("AUDIO_CAPTURE_METHOD")
|
||||||
.help("Audio capture method ('pipewire', 'pulseaudio', 'alsa')")
|
.help("Audio capture method")
|
||||||
.default_value("pulseaudio"),
|
.value_parser(value_parser!(AudioCaptureMethod))
|
||||||
|
.default_value("pipewire"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("audio-codec")
|
Arg::new("audio-codec")
|
||||||
.long("audio-codec")
|
.long("audio-codec")
|
||||||
.env("AUDIO_CODEC")
|
.env("AUDIO_CODEC")
|
||||||
.help("Preferred audio codec ('opus', 'aac')")
|
.help("Preferred audio codec")
|
||||||
|
.value_parser(value_parser!(AudioCodec))
|
||||||
.default_value("opus"),
|
.default_value("opus"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -163,14 +180,16 @@ impl Args {
|
|||||||
Arg::new("audio-rate-control")
|
Arg::new("audio-rate-control")
|
||||||
.long("audio-rate-control")
|
.long("audio-rate-control")
|
||||||
.env("AUDIO_RATE_CONTROL")
|
.env("AUDIO_RATE_CONTROL")
|
||||||
.help("Rate control method ('cqp', 'vbr', 'cbr')")
|
.help("Rate control method")
|
||||||
.default_value("vbr"),
|
.value_parser(value_parser!(encoding_args::RateControlMethod))
|
||||||
|
.default_value("cbr"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("audio-bitrate")
|
Arg::new("audio-bitrate")
|
||||||
.long("audio-bitrate")
|
.long("audio-bitrate")
|
||||||
.env("AUDIO_BITRATE")
|
.env("AUDIO_BITRATE")
|
||||||
.help("Target bitrate in kbps")
|
.help("Target bitrate in kbps")
|
||||||
|
.value_parser(value_parser!(u32).range(1..))
|
||||||
.default_value("128"),
|
.default_value("128"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -178,6 +197,7 @@ impl Args {
|
|||||||
.long("audio-bitrate-max")
|
.long("audio-bitrate-max")
|
||||||
.env("AUDIO_BITRATE_MAX")
|
.env("AUDIO_BITRATE_MAX")
|
||||||
.help("Maximum bitrate in kbps")
|
.help("Maximum bitrate in kbps")
|
||||||
|
.value_parser(value_parser!(u32).range(1..))
|
||||||
.default_value("192"),
|
.default_value("192"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -185,6 +205,7 @@ impl Args {
|
|||||||
.long("dma-buf")
|
.long("dma-buf")
|
||||||
.env("DMA_BUF")
|
.env("DMA_BUF")
|
||||||
.help("Use DMA-BUF for pipeline")
|
.help("Use DMA-BUF for pipeline")
|
||||||
|
.value_parser(BoolishValueParser::new())
|
||||||
.default_value("false"),
|
.default_value("false"),
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ pub struct AppArgs {
|
|||||||
impl AppArgs {
|
impl AppArgs {
|
||||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||||
Self {
|
Self {
|
||||||
verbose: matches.get_one::<String>("verbose").unwrap() == "true"
|
verbose: matches.get_one::<bool>("verbose").unwrap_or(&false).clone(),
|
||||||
|| matches.get_one::<String>("verbose").unwrap() == "1",
|
debug: matches.get_one::<bool>("debug").unwrap_or(&false).clone(),
|
||||||
debug: matches.get_one::<String>("debug").unwrap() == "true"
|
|
||||||
|| matches.get_one::<String>("debug").unwrap() == "1",
|
|
||||||
resolution: {
|
resolution: {
|
||||||
let res = matches
|
let res = matches
|
||||||
.get_one::<String>("resolution")
|
.get_one::<String>("resolution")
|
||||||
@@ -39,11 +37,7 @@ impl AppArgs {
|
|||||||
(1280, 720)
|
(1280, 720)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
framerate: matches
|
framerate: matches.get_one::<u32>("framerate").unwrap_or(&60).clone(),
|
||||||
.get_one::<String>("framerate")
|
|
||||||
.unwrap()
|
|
||||||
.parse::<u32>()
|
|
||||||
.unwrap_or(60),
|
|
||||||
relay_url: matches
|
relay_url: matches
|
||||||
.get_one::<String>("relay-url")
|
.get_one::<String>("relay-url")
|
||||||
.expect("relay url cannot be empty")
|
.expect("relay url cannot be empty")
|
||||||
@@ -53,19 +47,18 @@ impl AppArgs {
|
|||||||
.get_one::<String>("room")
|
.get_one::<String>("room")
|
||||||
.unwrap_or(&rand::random::<u32>().to_string())
|
.unwrap_or(&rand::random::<u32>().to_string())
|
||||||
.clone(),
|
.clone(),
|
||||||
dma_buf: matches.get_one::<String>("dma-buf").unwrap() == "true"
|
dma_buf: matches.get_one::<bool>("dma-buf").unwrap_or(&false).clone(),
|
||||||
|| matches.get_one::<String>("dma-buf").unwrap() == "1",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
println!("AppArgs:");
|
tracing::info!("AppArgs:");
|
||||||
println!("> verbose: {}", self.verbose);
|
tracing::info!("> verbose: {}", self.verbose);
|
||||||
println!("> debug: {}", self.debug);
|
tracing::info!("> debug: {}", self.debug);
|
||||||
println!("> resolution: {}x{}", self.resolution.0, self.resolution.1);
|
tracing::info!("> resolution: '{}x{}'", self.resolution.0, self.resolution.1);
|
||||||
println!("> framerate: {}", self.framerate);
|
tracing::info!("> framerate: {}", self.framerate);
|
||||||
println!("> relay_url: {}", self.relay_url);
|
tracing::info!("> relay_url: '{}'", self.relay_url);
|
||||||
println!("> room: {}", self.room);
|
tracing::info!("> room: '{}'", self.room);
|
||||||
println!("> dma_buf: {}", self.dma_buf);
|
tracing::info!("> dma_buf: {}", self.dma_buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ pub struct DeviceArgs {
|
|||||||
pub gpu_vendor: String,
|
pub gpu_vendor: String,
|
||||||
/// GPU name (e.g. "a770")
|
/// GPU name (e.g. "a770")
|
||||||
pub gpu_name: String,
|
pub gpu_name: String,
|
||||||
/// GPU index, if multiple same GPUs are present
|
/// GPU index, if multiple same GPUs are present, -1 for auto-selection
|
||||||
pub gpu_index: u32,
|
pub gpu_index: i32,
|
||||||
/// GPU card/render path, sets card explicitly from such path
|
/// GPU card/render path, sets card explicitly from such path
|
||||||
pub gpu_card_path: String,
|
pub gpu_card_path: String,
|
||||||
}
|
}
|
||||||
@@ -20,10 +20,9 @@ impl DeviceArgs {
|
|||||||
.unwrap_or(&"".to_string())
|
.unwrap_or(&"".to_string())
|
||||||
.clone(),
|
.clone(),
|
||||||
gpu_index: matches
|
gpu_index: matches
|
||||||
.get_one::<String>("gpu-index")
|
.get_one::<i32>("gpu-index")
|
||||||
.unwrap()
|
.unwrap_or(&-1)
|
||||||
.parse::<u32>()
|
.clone(),
|
||||||
.unwrap(),
|
|
||||||
gpu_card_path: matches
|
gpu_card_path: matches
|
||||||
.get_one::<String>("gpu-card-path")
|
.get_one::<String>("gpu-card-path")
|
||||||
.unwrap_or(&"".to_string())
|
.unwrap_or(&"".to_string())
|
||||||
@@ -32,17 +31,10 @@ impl DeviceArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
println!("DeviceArgs:");
|
tracing::info!("DeviceArgs:");
|
||||||
println!("> gpu_vendor: {}", self.gpu_vendor);
|
tracing::info!("> gpu_vendor: '{}'", self.gpu_vendor);
|
||||||
println!("> gpu_name: {}", self.gpu_name);
|
tracing::info!("> gpu_name: '{}'", self.gpu_name);
|
||||||
println!("> gpu_index: {}", self.gpu_index);
|
tracing::info!("> gpu_index: {}", self.gpu_index);
|
||||||
println!(
|
tracing::info!("> gpu_card_path: '{}'", self.gpu_card_path);
|
||||||
"> gpu_card_path: {}",
|
|
||||||
if self.gpu_card_path.is_empty() {
|
|
||||||
"Auto-Selection"
|
|
||||||
} else {
|
|
||||||
&self.gpu_card_path
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
use crate::enc_helper::Codec::{Audio, Video};
|
||||||
|
use crate::enc_helper::{AudioCodec, Codec, EncoderType, VideoCodec};
|
||||||
|
use clap::ValueEnum;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct RateControlCQP {
|
pub struct RateControlCQP {
|
||||||
@@ -8,14 +12,42 @@ pub struct RateControlCQP {
|
|||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct RateControlVBR {
|
pub struct RateControlVBR {
|
||||||
/// Target bitrate in kbps
|
/// Target bitrate in kbps
|
||||||
pub target_bitrate: i32,
|
pub target_bitrate: u32,
|
||||||
/// Maximum bitrate in kbps
|
/// Maximum bitrate in kbps
|
||||||
pub max_bitrate: i32,
|
pub max_bitrate: u32,
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct RateControlCBR {
|
pub struct RateControlCBR {
|
||||||
/// Target bitrate in kbps
|
/// Target bitrate in kbps
|
||||||
pub target_bitrate: i32,
|
pub target_bitrate: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, ValueEnum)]
|
||||||
|
pub enum RateControlMethod {
|
||||||
|
CQP,
|
||||||
|
VBR,
|
||||||
|
CBR,
|
||||||
|
}
|
||||||
|
impl RateControlMethod {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
RateControlMethod::CQP => "cqp",
|
||||||
|
RateControlMethod::VBR => "vbr",
|
||||||
|
RateControlMethod::CBR => "cbr",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for RateControlMethod {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"cqp" => Ok(RateControlMethod::CQP),
|
||||||
|
"vbr" => Ok(RateControlMethod::VBR),
|
||||||
|
"cbr" => Ok(RateControlMethod::CBR),
|
||||||
|
_ => Err(format!("Invalid rate control method: {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
@@ -30,7 +62,7 @@ pub enum RateControl {
|
|||||||
|
|
||||||
pub struct EncodingOptionsBase {
|
pub struct EncodingOptionsBase {
|
||||||
/// Codec (e.g. "h264", "opus" etc.)
|
/// Codec (e.g. "h264", "opus" etc.)
|
||||||
pub codec: String,
|
pub codec: Codec,
|
||||||
/// Overridable encoder (e.g. "vah264lpenc", "opusenc" etc.)
|
/// Overridable encoder (e.g. "vah264lpenc", "opusenc" etc.)
|
||||||
pub encoder: String,
|
pub encoder: String,
|
||||||
/// Rate control method (e.g. "cqp", "vbr", "cbr")
|
/// Rate control method (e.g. "cqp", "vbr", "cbr")
|
||||||
@@ -38,28 +70,21 @@ pub struct EncodingOptionsBase {
|
|||||||
}
|
}
|
||||||
impl EncodingOptionsBase {
|
impl EncodingOptionsBase {
|
||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
println!("> Codec: {}", self.codec);
|
tracing::info!("> Codec: '{}'", self.codec.as_str());
|
||||||
println!(
|
tracing::info!("> Encoder: '{}'", self.encoder);
|
||||||
"> Encoder: {}",
|
|
||||||
if self.encoder.is_empty() {
|
|
||||||
"Auto-Selection"
|
|
||||||
} else {
|
|
||||||
&self.encoder
|
|
||||||
}
|
|
||||||
);
|
|
||||||
match &self.rate_control {
|
match &self.rate_control {
|
||||||
RateControl::CQP(cqp) => {
|
RateControl::CQP(cqp) => {
|
||||||
println!("> Rate Control: CQP");
|
tracing::info!("> Rate Control: CQP");
|
||||||
println!("-> Quality: {}", cqp.quality);
|
tracing::info!("-> Quality: {}", cqp.quality);
|
||||||
}
|
}
|
||||||
RateControl::VBR(vbr) => {
|
RateControl::VBR(vbr) => {
|
||||||
println!("> Rate Control: VBR");
|
tracing::info!("> Rate Control: VBR");
|
||||||
println!("-> Target Bitrate: {}", vbr.target_bitrate);
|
tracing::info!("-> Target Bitrate: {}", vbr.target_bitrate);
|
||||||
println!("-> Max Bitrate: {}", vbr.max_bitrate);
|
tracing::info!("-> Max Bitrate: {}", vbr.max_bitrate);
|
||||||
}
|
}
|
||||||
RateControl::CBR(cbr) => {
|
RateControl::CBR(cbr) => {
|
||||||
println!("> Rate Control: CBR");
|
tracing::info!("> Rate Control: CBR");
|
||||||
println!("-> Target Bitrate: {}", cbr.target_bitrate);
|
tracing::info!("-> Target Bitrate: {}", cbr.target_bitrate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,63 +92,62 @@ impl EncodingOptionsBase {
|
|||||||
|
|
||||||
pub struct VideoEncodingOptions {
|
pub struct VideoEncodingOptions {
|
||||||
pub base: EncodingOptionsBase,
|
pub base: EncodingOptionsBase,
|
||||||
/// Encoder type (e.g. "hardware", "software")
|
pub encoder_type: EncoderType,
|
||||||
pub encoder_type: String,
|
|
||||||
}
|
}
|
||||||
impl VideoEncodingOptions {
|
impl VideoEncodingOptions {
|
||||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base: EncodingOptionsBase {
|
base: EncodingOptionsBase {
|
||||||
codec: matches.get_one::<String>("video-codec").unwrap().clone(),
|
codec: Video(
|
||||||
|
matches
|
||||||
|
.get_one::<VideoCodec>("video-codec")
|
||||||
|
.unwrap_or(&VideoCodec::H264)
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
encoder: matches
|
encoder: matches
|
||||||
.get_one::<String>("video-encoder")
|
.get_one::<String>("video-encoder")
|
||||||
.unwrap_or(&"".to_string())
|
.unwrap_or(&"".to_string())
|
||||||
.clone(),
|
.clone(),
|
||||||
rate_control: match matches
|
rate_control: match matches
|
||||||
.get_one::<String>("video-rate-control")
|
.get_one::<RateControlMethod>("video-rate-control")
|
||||||
.unwrap()
|
.unwrap_or(&RateControlMethod::CBR)
|
||||||
.as_str()
|
|
||||||
{
|
{
|
||||||
"cqp" => RateControl::CQP(RateControlCQP {
|
RateControlMethod::CQP => RateControl::CQP(RateControlCQP {
|
||||||
quality: matches
|
quality: matches
|
||||||
.get_one::<String>("video-cqp")
|
.get_one::<String>("video-cqp")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parse::<u32>()
|
.parse::<u32>()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
}),
|
}),
|
||||||
"cbr" => RateControl::CBR(RateControlCBR {
|
RateControlMethod::CBR => RateControl::CBR(RateControlCBR {
|
||||||
target_bitrate: matches
|
target_bitrate: matches
|
||||||
.get_one::<String>("video-bitrate")
|
.get_one::<u32>("video-bitrate")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parse::<i32>()
|
.clone(),
|
||||||
.unwrap(),
|
|
||||||
}),
|
}),
|
||||||
"vbr" => RateControl::VBR(RateControlVBR {
|
RateControlMethod::VBR => RateControl::VBR(RateControlVBR {
|
||||||
target_bitrate: matches
|
target_bitrate: matches
|
||||||
.get_one::<String>("video-bitrate")
|
.get_one::<u32>("video-bitrate")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parse::<i32>()
|
.clone(),
|
||||||
.unwrap(),
|
|
||||||
max_bitrate: matches
|
max_bitrate: matches
|
||||||
.get_one::<String>("video-bitrate-max")
|
.get_one::<u32>("video-bitrate-max")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parse::<i32>()
|
.clone(),
|
||||||
.unwrap(),
|
|
||||||
}),
|
}),
|
||||||
_ => panic!("Invalid rate control method for video"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
encoder_type: matches
|
encoder_type: matches
|
||||||
.get_one::<String>("video-encoder-type")
|
.get_one::<EncoderType>("video-encoder-type")
|
||||||
.unwrap_or(&"hardware".to_string())
|
.unwrap_or(&EncoderType::HARDWARE)
|
||||||
.clone(),
|
.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
println!("Video Encoding Options:");
|
tracing::info!("Video Encoding Options:");
|
||||||
self.base.debug_print();
|
self.base.debug_print();
|
||||||
println!("> Encoder Type: {}", self.encoder_type);
|
tracing::info!("> Encoder Type: {}", self.encoder_type.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Deref for VideoEncodingOptions {
|
impl Deref for VideoEncodingOptions {
|
||||||
@@ -134,18 +158,30 @@ impl Deref for VideoEncodingOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Clone, ValueEnum)]
|
||||||
pub enum AudioCaptureMethod {
|
pub enum AudioCaptureMethod {
|
||||||
PulseAudio,
|
PULSEAUDIO,
|
||||||
PipeWire,
|
PIPEWIRE,
|
||||||
ALSA,
|
ALSA,
|
||||||
}
|
}
|
||||||
impl AudioCaptureMethod {
|
impl AudioCaptureMethod {
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
AudioCaptureMethod::PulseAudio => "pulseaudio",
|
AudioCaptureMethod::PULSEAUDIO => "PulseAudio",
|
||||||
AudioCaptureMethod::PipeWire => "pipewire",
|
AudioCaptureMethod::PIPEWIRE => "PipeWire",
|
||||||
AudioCaptureMethod::ALSA => "alsa",
|
AudioCaptureMethod::ALSA => "ALSA",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for AudioCaptureMethod {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"pulseaudio" => Ok(AudioCaptureMethod::PULSEAUDIO),
|
||||||
|
"pipewire" => Ok(AudioCaptureMethod::PIPEWIRE),
|
||||||
|
"alsa" => Ok(AudioCaptureMethod::ALSA),
|
||||||
|
_ => Err(format!("Invalid audio capture method: {}", s)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,56 +194,50 @@ impl AudioEncodingOptions {
|
|||||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||||
Self {
|
Self {
|
||||||
base: EncodingOptionsBase {
|
base: EncodingOptionsBase {
|
||||||
codec: matches.get_one::<String>("audio-codec").unwrap().clone(),
|
codec: Audio(
|
||||||
|
matches
|
||||||
|
.get_one::<AudioCodec>("audio-codec")
|
||||||
|
.unwrap_or(&AudioCodec::OPUS)
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
encoder: matches
|
encoder: matches
|
||||||
.get_one::<String>("audio-encoder")
|
.get_one::<String>("audio-encoder")
|
||||||
.unwrap_or(&"".to_string())
|
.unwrap_or(&"".to_string())
|
||||||
.clone(),
|
.clone(),
|
||||||
rate_control: match matches
|
rate_control: match matches
|
||||||
.get_one::<String>("audio-rate-control")
|
.get_one::<RateControlMethod>("audio-rate-control")
|
||||||
.unwrap()
|
.unwrap_or(&RateControlMethod::CBR)
|
||||||
.as_str()
|
|
||||||
{
|
{
|
||||||
"cbr" => RateControl::CBR(RateControlCBR {
|
RateControlMethod::CBR => RateControl::CBR(RateControlCBR {
|
||||||
target_bitrate: matches
|
target_bitrate: matches
|
||||||
.get_one::<String>("audio-bitrate")
|
.get_one::<u32>("audio-bitrate")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parse::<i32>()
|
.clone(),
|
||||||
.unwrap(),
|
|
||||||
}),
|
}),
|
||||||
"vbr" => RateControl::VBR(RateControlVBR {
|
RateControlMethod::VBR => RateControl::VBR(RateControlVBR {
|
||||||
target_bitrate: matches
|
target_bitrate: matches
|
||||||
.get_one::<String>("audio-bitrate")
|
.get_one::<u32>("audio-bitrate")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parse::<i32>()
|
.clone(),
|
||||||
.unwrap(),
|
|
||||||
max_bitrate: matches
|
max_bitrate: matches
|
||||||
.get_one::<String>("audio-bitrate-max")
|
.get_one::<u32>("audio-bitrate-max")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parse::<i32>()
|
.clone(),
|
||||||
.unwrap(),
|
|
||||||
}),
|
}),
|
||||||
_ => panic!("Invalid rate control method for audio"),
|
wot => panic!("Invalid rate control method for audio: {}", wot.as_str()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
capture_method: match matches
|
capture_method: matches
|
||||||
.get_one::<String>("audio-capture-method")
|
.get_one::<AudioCaptureMethod>("audio-capture-method")
|
||||||
.unwrap()
|
.unwrap_or(&AudioCaptureMethod::PIPEWIRE)
|
||||||
.as_str()
|
.clone(),
|
||||||
{
|
|
||||||
"pulseaudio" => AudioCaptureMethod::PulseAudio,
|
|
||||||
"pipewire" => AudioCaptureMethod::PipeWire,
|
|
||||||
"alsa" => AudioCaptureMethod::ALSA,
|
|
||||||
// Default to PulseAudio
|
|
||||||
_ => AudioCaptureMethod::PulseAudio,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
println!("Audio Encoding Options:");
|
tracing::info!("Audio Encoding Options:");
|
||||||
self.base.debug_print();
|
self.base.debug_print();
|
||||||
println!("> Capture Method: {}", self.capture_method.as_str());
|
tracing::info!("> Capture Method: {}", self.capture_method.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Deref for AudioEncodingOptions {
|
impl Deref for AudioEncodingOptions {
|
||||||
@@ -233,7 +263,7 @@ impl EncodingArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
println!("Encoding Arguments:");
|
tracing::info!("Encoding Arguments:");
|
||||||
self.video.debug_print();
|
self.video.debug_print();
|
||||||
self.audio.debug_print();
|
self.audio.debug_print();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,70 @@
|
|||||||
use crate::args::encoding_args::RateControl;
|
use crate::args::encoding_args::RateControl;
|
||||||
use crate::gpu::{self, GPUInfo, get_gpu_by_card_path, get_gpus_by_vendor};
|
use crate::gpu::{self, GPUInfo, get_gpu_by_card_path, get_gpus_by_vendor};
|
||||||
|
use clap::ValueEnum;
|
||||||
use gst::prelude::*;
|
use gst::prelude::*;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
#[derive(Debug, Eq, PartialEq, Clone, ValueEnum)]
|
||||||
|
pub enum AudioCodec {
|
||||||
|
OPUS,
|
||||||
|
}
|
||||||
|
impl AudioCodec {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::OPUS => "Opus",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for AudioCodec {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"opus" => Ok(Self::OPUS),
|
||||||
|
_ => Err(format!("Invalid audio codec: {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, ValueEnum)]
|
||||||
pub enum VideoCodec {
|
pub enum VideoCodec {
|
||||||
H264,
|
H264,
|
||||||
H265,
|
H265,
|
||||||
AV1,
|
AV1,
|
||||||
UNKNOWN,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoCodec {
|
impl VideoCodec {
|
||||||
pub fn to_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::H264 => "H.264",
|
Self::H264 => "H.264",
|
||||||
Self::H265 => "H.265",
|
Self::H265 => "H.265",
|
||||||
Self::AV1 => "AV1",
|
Self::AV1 => "AV1",
|
||||||
Self::UNKNOWN => "Unknown",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for VideoCodec {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
pub fn from_str(s: &str) -> Self {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
"h264" | "h.264" | "avc" => Self::H264,
|
"h264" | "h.264" | "avc" => Ok(Self::H264),
|
||||||
"h265" | "h.265" | "hevc" | "hev1" => Self::H265,
|
"h265" | "h.265" | "hevc" | "hev1" => Ok(Self::H265),
|
||||||
"av1" => Self::AV1,
|
"av1" => Ok(Self::AV1),
|
||||||
_ => Self::UNKNOWN,
|
_ => Err(format!("Invalid video codec: {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
|
pub enum Codec {
|
||||||
|
Audio(AudioCodec),
|
||||||
|
Video(VideoCodec),
|
||||||
|
}
|
||||||
|
impl Codec {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Audio(codec) => codec.as_str(),
|
||||||
|
Self::Video(codec) => codec.as_str(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,27 +92,17 @@ impl EncoderAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
#[derive(Debug, Eq, PartialEq, Clone, ValueEnum)]
|
||||||
pub enum EncoderType {
|
pub enum EncoderType {
|
||||||
SOFTWARE,
|
SOFTWARE,
|
||||||
HARDWARE,
|
HARDWARE,
|
||||||
UNKNOWN,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EncoderType {
|
impl EncoderType {
|
||||||
pub fn to_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::SOFTWARE => "Software",
|
Self::SOFTWARE => "Software",
|
||||||
Self::HARDWARE => "Hardware",
|
Self::HARDWARE => "Hardware",
|
||||||
Self::UNKNOWN => "Unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_str(s: &str) -> Self {
|
|
||||||
match s.to_lowercase().as_str() {
|
|
||||||
"software" => Self::SOFTWARE,
|
|
||||||
"hardware" => Self::HARDWARE,
|
|
||||||
_ => Self::UNKNOWN,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +150,7 @@ impl VideoEncoderInfo {
|
|||||||
for (key, value) in &self.parameters {
|
for (key, value) in &self.parameters {
|
||||||
if element.has_property(key) {
|
if element.has_property(key) {
|
||||||
if verbose {
|
if verbose {
|
||||||
println!("Setting property {} to {}", key, value);
|
tracing::debug!("Setting property {} to {}", key, value);
|
||||||
}
|
}
|
||||||
element.set_property_from_str(key, value);
|
element.set_property_from_str(key, value);
|
||||||
}
|
}
|
||||||
@@ -145,19 +174,15 @@ fn get_encoder_api(encoder: &str, encoder_type: &EncoderType) -> EncoderAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
EncoderType::SOFTWARE => EncoderAPI::SOFTWARE,
|
EncoderType::SOFTWARE => EncoderAPI::SOFTWARE,
|
||||||
_ => EncoderAPI::UNKNOWN,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codec_from_encoder_name(name: &str) -> Option<VideoCodec> {
|
fn codec_from_encoder_name(name: &str) -> Option<VideoCodec> {
|
||||||
if name.contains("h264") {
|
match name.to_lowercase() {
|
||||||
Some(VideoCodec::H264)
|
n if n.contains("h264") => Some(VideoCodec::H264),
|
||||||
} else if name.contains("h265") {
|
n if n.contains("h265") => Some(VideoCodec::H265),
|
||||||
Some(VideoCodec::H265)
|
n if n.contains("av1") => Some(VideoCodec::AV1),
|
||||||
} else if name.contains("av1") {
|
_ => None,
|
||||||
Some(VideoCodec::AV1)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +297,6 @@ pub fn encoder_low_latency_params(
|
|||||||
let usage = match encoder_optz.codec {
|
let usage = match encoder_optz.codec {
|
||||||
VideoCodec::H264 | VideoCodec::H265 => "ultra-low-latency",
|
VideoCodec::H264 | VideoCodec::H265 => "ultra-low-latency",
|
||||||
VideoCodec::AV1 => "low-latency",
|
VideoCodec::AV1 => "low-latency",
|
||||||
_ => "",
|
|
||||||
};
|
};
|
||||||
if !usage.is_empty() {
|
if !usage.is_empty() {
|
||||||
encoder_optz.set_parameter("usage", usage);
|
encoder_optz.set_parameter("usage", usage);
|
||||||
@@ -378,8 +402,8 @@ pub fn get_compatible_encoders() -> Vec<VideoEncoderInfo> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
log::error!(
|
tracing::error!(
|
||||||
"Panic occurred while querying properties for {}",
|
"Error occurred while querying properties for {}",
|
||||||
encoder_name
|
encoder_name
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
@@ -401,16 +425,20 @@ pub fn get_compatible_encoders() -> Vec<VideoEncoderInfo> {
|
|||||||
/// * `encoders` - A vector containing information about each encoder.
|
/// * `encoders` - A vector containing information about each encoder.
|
||||||
/// * `name` - A string slice that holds the encoder name.
|
/// * `name` - A string slice that holds the encoder name.
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `Option<EncoderInfo>` - A reference to an EncoderInfo struct if found.
|
/// * `Result<EncoderInfo, Box<dyn Error>>` - A Result containing EncoderInfo if found, or an error.
|
||||||
pub fn get_encoder_by_name(
|
pub fn get_encoder_by_name(
|
||||||
encoders: &Vec<VideoEncoderInfo>,
|
encoders: &Vec<VideoEncoderInfo>,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Option<VideoEncoderInfo> {
|
) -> Result<VideoEncoderInfo, Box<dyn Error>> {
|
||||||
let name = name.to_lowercase();
|
let name = name.to_lowercase();
|
||||||
encoders
|
if let Some(encoder) = encoders
|
||||||
.iter()
|
.iter()
|
||||||
.find(|encoder| encoder.name.to_lowercase() == name)
|
.find(|encoder| encoder.name.to_lowercase() == name)
|
||||||
.cloned()
|
{
|
||||||
|
Ok(encoder.clone())
|
||||||
|
} else {
|
||||||
|
Err(format!("Encoder '{}' not found", name).into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to get encoders from vector by video codec.
|
/// Helper to get encoders from vector by video codec.
|
||||||
@@ -453,15 +481,23 @@ pub fn get_encoders_by_type(
|
|||||||
/// * `codec` - Desired codec.
|
/// * `codec` - Desired codec.
|
||||||
/// * `encoder_type` - Desired encoder type.
|
/// * `encoder_type` - Desired encoder type.
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `Option<EncoderInfo>` - Best-case compatible encoder.
|
/// * `Result<VideoEncoderInfo, Box<dyn Error>>` - A Result containing the best compatible encoder if found, or an error.
|
||||||
pub fn get_best_compatible_encoder(
|
pub fn get_best_compatible_encoder(
|
||||||
encoders: &Vec<VideoEncoderInfo>,
|
encoders: &Vec<VideoEncoderInfo>,
|
||||||
codec: VideoCodec,
|
codec: &Codec,
|
||||||
encoder_type: EncoderType,
|
encoder_type: &EncoderType,
|
||||||
) -> Option<VideoEncoderInfo> {
|
) -> Result<VideoEncoderInfo, Box<dyn Error>> {
|
||||||
let mut best_encoder: Option<VideoEncoderInfo> = None;
|
let mut best_encoder: Option<VideoEncoderInfo> = None;
|
||||||
let mut best_score: i32 = 0;
|
let mut best_score: i32 = 0;
|
||||||
|
|
||||||
|
let codec = match codec {
|
||||||
|
Codec::Video(c) => c.clone(),
|
||||||
|
Codec::Audio(_) => {
|
||||||
|
// Only for video currently
|
||||||
|
return Err("Attempted to get best compatible video encoder with audio codec".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Filter by codec and type first
|
// Filter by codec and type first
|
||||||
let encoders = get_encoders_by_videocodec(encoders, &codec);
|
let encoders = get_encoders_by_videocodec(encoders, &codec);
|
||||||
let encoders = get_encoders_by_type(&encoders, &encoder_type);
|
let encoders = get_encoders_by_type(&encoders, &encoder_type);
|
||||||
@@ -498,5 +534,9 @@ pub fn get_best_compatible_encoder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
best_encoder
|
if let Some(encoder) = best_encoder {
|
||||||
|
Ok(encoder)
|
||||||
|
} else {
|
||||||
|
Err("No compatible encoder found".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,3 +161,11 @@ pub fn get_gpu_by_card_path(gpus: &[GPUInfo], path: &str) -> Option<GPUInfo> {
|
|||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_gpu_by_index(gpus: &[GPUInfo], index: i32) -> Option<GPUInfo> {
|
||||||
|
if index < 0 || index as usize >= gpus.len() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(gpus[index as usize].clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod proto;
|
|||||||
mod websocket;
|
mod websocket;
|
||||||
|
|
||||||
use crate::args::encoding_args;
|
use crate::args::encoding_args;
|
||||||
|
use crate::enc_helper::EncoderType;
|
||||||
use crate::gpu::GPUVendor;
|
use crate::gpu::GPUVendor;
|
||||||
use crate::nestrisink::NestriSignaller;
|
use crate::nestrisink::NestriSignaller;
|
||||||
use crate::websocket::NestriWebSocket;
|
use crate::websocket::NestriWebSocket;
|
||||||
@@ -20,16 +21,16 @@ use std::str::FromStr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// Handles gathering GPU information and selecting the most suitable GPU
|
// Handles gathering GPU information and selecting the most suitable GPU
|
||||||
fn handle_gpus(args: &args::Args) -> Option<gpu::GPUInfo> {
|
fn handle_gpus(args: &args::Args) -> Result<gpu::GPUInfo, Box<dyn Error>> {
|
||||||
println!("Gathering GPU information..");
|
tracing::info!("Gathering GPU information..");
|
||||||
let gpus = gpu::get_gpus();
|
let gpus = gpu::get_gpus();
|
||||||
if gpus.is_empty() {
|
if gpus.is_empty() {
|
||||||
println!("No GPUs found");
|
return Err("No GPUs found".into());
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
for gpu in &gpus {
|
for (i, gpu) in gpus.iter().enumerate() {
|
||||||
println!(
|
tracing::info!(
|
||||||
"> [GPU] Vendor: '{}', Card Path: '{}', Render Path: '{}', Device Name: '{}'",
|
"> [GPU:{}] Vendor: '{}', Card Path: '{}', Render Path: '{}', Device Name: '{}'",
|
||||||
|
i,
|
||||||
gpu.vendor_string(),
|
gpu.vendor_string(),
|
||||||
gpu.card_path(),
|
gpu.card_path(),
|
||||||
gpu.render_path(),
|
gpu.render_path(),
|
||||||
@@ -50,9 +51,12 @@ fn handle_gpus(args: &args::Args) -> Option<gpu::GPUInfo> {
|
|||||||
if !args.device.gpu_name.is_empty() {
|
if !args.device.gpu_name.is_empty() {
|
||||||
filtered_gpus = gpu::get_gpus_by_device_name(&filtered_gpus, &args.device.gpu_name);
|
filtered_gpus = gpu::get_gpus_by_device_name(&filtered_gpus, &args.device.gpu_name);
|
||||||
}
|
}
|
||||||
if args.device.gpu_index != 0 {
|
if args.device.gpu_index > -1 {
|
||||||
// get single GPU by index
|
// get single GPU by index
|
||||||
gpu = filtered_gpus.get(args.device.gpu_index as usize).cloned();
|
gpu = gpu::get_gpu_by_index(&filtered_gpus, args.device.gpu_index).or_else(|| {
|
||||||
|
tracing::warn!("GPU index {} is out of range", args.device.gpu_index);
|
||||||
|
None
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// get first GPU
|
// get first GPU
|
||||||
gpu = filtered_gpus
|
gpu = filtered_gpus
|
||||||
@@ -61,35 +65,33 @@ fn handle_gpus(args: &args::Args) -> Option<gpu::GPUInfo> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if gpu.is_none() {
|
if gpu.is_none() {
|
||||||
println!(
|
return Err(format!(
|
||||||
"No GPU found with the specified parameters: vendor='{}', name='{}', index='{}', card_path='{}'",
|
"No GPU found with the specified parameters: vendor='{}', name='{}', index='{}', card_path='{}'",
|
||||||
args.device.gpu_vendor,
|
args.device.gpu_vendor,
|
||||||
args.device.gpu_name,
|
args.device.gpu_name,
|
||||||
args.device.gpu_index,
|
args.device.gpu_index,
|
||||||
args.device.gpu_card_path
|
args.device.gpu_card_path
|
||||||
);
|
).into());
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
let gpu = gpu.unwrap();
|
let gpu = gpu.unwrap();
|
||||||
println!("Selected GPU: '{}'", gpu.device_name());
|
tracing::info!("Selected GPU: '{}'", gpu.device_name());
|
||||||
Some(gpu)
|
Ok(gpu)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles picking video encoder
|
// Handles picking video encoder
|
||||||
fn handle_encoder_video(args: &args::Args) -> Option<enc_helper::VideoEncoderInfo> {
|
fn handle_encoder_video(args: &args::Args) -> Result<enc_helper::VideoEncoderInfo, Box<dyn Error>> {
|
||||||
println!("Getting compatible video encoders..");
|
tracing::info!("Getting compatible video encoders..");
|
||||||
let video_encoders = enc_helper::get_compatible_encoders();
|
let video_encoders = enc_helper::get_compatible_encoders();
|
||||||
if video_encoders.is_empty() {
|
if video_encoders.is_empty() {
|
||||||
println!("No compatible video encoders found");
|
return Err("No compatible video encoders found".into());
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
for encoder in &video_encoders {
|
for encoder in &video_encoders {
|
||||||
println!(
|
tracing::info!(
|
||||||
"> [Video Encoder] Name: '{}', Codec: '{}', API: '{}', Type: '{}', Device: '{}'",
|
"> [Video Encoder] Name: '{}', Codec: '{}', API: '{}', Type: '{}', Device: '{}'",
|
||||||
encoder.name,
|
encoder.name,
|
||||||
encoder.codec.to_str(),
|
encoder.codec.as_str(),
|
||||||
encoder.encoder_api.to_str(),
|
encoder.encoder_api.to_str(),
|
||||||
encoder.encoder_type.to_str(),
|
encoder.encoder_type.as_str(),
|
||||||
if let Some(gpu) = &encoder.gpu_info {
|
if let Some(gpu) = &encoder.gpu_info {
|
||||||
gpu.device_name()
|
gpu.device_name()
|
||||||
} else {
|
} else {
|
||||||
@@ -101,26 +103,16 @@ fn handle_encoder_video(args: &args::Args) -> Option<enc_helper::VideoEncoderInf
|
|||||||
let video_encoder;
|
let video_encoder;
|
||||||
if !args.encoding.video.encoder.is_empty() {
|
if !args.encoding.video.encoder.is_empty() {
|
||||||
video_encoder =
|
video_encoder =
|
||||||
enc_helper::get_encoder_by_name(&video_encoders, &args.encoding.video.encoder);
|
enc_helper::get_encoder_by_name(&video_encoders, &args.encoding.video.encoder)?;
|
||||||
} else {
|
} else {
|
||||||
video_encoder = enc_helper::get_best_compatible_encoder(
|
video_encoder = enc_helper::get_best_compatible_encoder(
|
||||||
&video_encoders,
|
&video_encoders,
|
||||||
enc_helper::VideoCodec::from_str(&args.encoding.video.codec),
|
&args.encoding.video.codec,
|
||||||
enc_helper::EncoderType::from_str(&args.encoding.video.encoder_type),
|
&args.encoding.video.encoder_type,
|
||||||
);
|
)?;
|
||||||
}
|
}
|
||||||
if video_encoder.is_none() {
|
tracing::info!("Selected video encoder: '{}'", video_encoder.name);
|
||||||
println!(
|
Ok(video_encoder)
|
||||||
"No video encoder found with the specified parameters: name='{}', vcodec='{}', type='{}'",
|
|
||||||
args.encoding.video.encoder,
|
|
||||||
args.encoding.video.codec,
|
|
||||||
args.encoding.video.encoder_type
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let video_encoder = video_encoder.unwrap();
|
|
||||||
println!("Selected video encoder: '{}'", video_encoder.name);
|
|
||||||
Some(video_encoder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles picking preferred settings for video encoder
|
// Handles picking preferred settings for video encoder
|
||||||
@@ -141,16 +133,16 @@ fn handle_encoder_video_settings(
|
|||||||
encoding_args::RateControl::VBR(vbr) => {
|
encoding_args::RateControl::VBR(vbr) => {
|
||||||
optimized_encoder = enc_helper::encoder_vbr_params(
|
optimized_encoder = enc_helper::encoder_vbr_params(
|
||||||
&optimized_encoder,
|
&optimized_encoder,
|
||||||
vbr.target_bitrate as u32,
|
vbr.target_bitrate,
|
||||||
vbr.max_bitrate as u32,
|
vbr.max_bitrate,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
encoding_args::RateControl::CBR(cbr) => {
|
encoding_args::RateControl::CBR(cbr) => {
|
||||||
optimized_encoder =
|
optimized_encoder =
|
||||||
enc_helper::encoder_cbr_params(&optimized_encoder, cbr.target_bitrate as u32);
|
enc_helper::encoder_cbr_params(&optimized_encoder, cbr.target_bitrate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!(
|
tracing::info!(
|
||||||
"Selected video encoder settings: '{}'",
|
"Selected video encoder settings: '{}'",
|
||||||
optimized_encoder.get_parameters_string()
|
optimized_encoder.get_parameters_string()
|
||||||
);
|
);
|
||||||
@@ -165,16 +157,23 @@ fn handle_encoder_audio(args: &args::Args) -> String {
|
|||||||
} else {
|
} else {
|
||||||
args.encoding.audio.encoder.clone()
|
args.encoding.audio.encoder.clone()
|
||||||
};
|
};
|
||||||
println!("Selected audio encoder: '{}'", audio_encoder);
|
tracing::info!("Selected audio encoder: '{}'", audio_encoder);
|
||||||
audio_encoder
|
audio_encoder
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
let args = args::Args::new();
|
let mut args = args::Args::new();
|
||||||
if args.app.verbose {
|
if args.app.verbose {
|
||||||
|
// Make sure tracing has INFO level
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(tracing::Level::INFO)
|
||||||
|
.init();
|
||||||
|
|
||||||
args.debug_print();
|
args.debug_print();
|
||||||
|
} else {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
rustls::crypto::ring::default_provider()
|
rustls::crypto::ring::default_provider()
|
||||||
@@ -192,33 +191,39 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
// Setup our websocket
|
// Setup our websocket
|
||||||
let nestri_ws = Arc::new(NestriWebSocket::new(ws_url).await?);
|
let nestri_ws = Arc::new(NestriWebSocket::new(ws_url).await?);
|
||||||
log::set_max_level(log::LevelFilter::Info);
|
|
||||||
log::set_boxed_logger(Box::new(nestri_ws.clone())).unwrap();
|
|
||||||
|
|
||||||
gst::init()?;
|
gst::init()?;
|
||||||
gstrswebrtc::plugin_register_static()?;
|
gstrswebrtc::plugin_register_static()?;
|
||||||
|
|
||||||
// Handle GPU selection
|
// Handle GPU selection
|
||||||
let gpu = handle_gpus(&args);
|
let gpu = match handle_gpus(&args) {
|
||||||
if gpu.is_none() {
|
Ok(gpu) => gpu,
|
||||||
log::error!("Failed to find a suitable GPU. Exiting..");
|
Err(e) => {
|
||||||
return Err("Failed to find a suitable GPU. Exiting..".into());
|
tracing::error!("Failed to find a suitable GPU: {}", e);
|
||||||
|
return Err(e);
|
||||||
}
|
}
|
||||||
let gpu = gpu.unwrap();
|
};
|
||||||
|
|
||||||
if args.app.dma_buf {
|
if args.app.dma_buf {
|
||||||
log::warn!(
|
if args.encoding.video.encoder_type != EncoderType::HARDWARE {
|
||||||
|
tracing::warn!("DMA-BUF is only supported with hardware encoders, disabling DMA-BUF..");
|
||||||
|
args.app.dma_buf = false;
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
"DMA-BUF is experimental, it may or may not improve performance, or even work at all."
|
"DMA-BUF is experimental, it may or may not improve performance, or even work at all."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle video encoder selection
|
// Handle video encoder selection
|
||||||
let video_encoder_info = handle_encoder_video(&args);
|
let mut video_encoder_info = match handle_encoder_video(&args) {
|
||||||
if video_encoder_info.is_none() {
|
Ok(encoder) => encoder,
|
||||||
log::error!("Failed to find a suitable video encoder. Exiting..");
|
Err(e) => {
|
||||||
return Err("Failed to find a suitable video encoder. Exiting..".into());
|
tracing::error!("Failed to find a suitable video encoder: {}", e);
|
||||||
|
return Err(e);
|
||||||
}
|
}
|
||||||
let mut video_encoder_info = video_encoder_info.unwrap();
|
};
|
||||||
|
|
||||||
// Handle video encoder settings
|
// Handle video encoder settings
|
||||||
video_encoder_info = handle_encoder_video_settings(&args, &video_encoder_info);
|
video_encoder_info = handle_encoder_video_settings(&args, &video_encoder_info);
|
||||||
|
|
||||||
@@ -232,10 +237,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
/* Audio */
|
/* Audio */
|
||||||
// Audio Source Element
|
// Audio Source Element
|
||||||
let audio_source = match args.encoding.audio.capture_method {
|
let audio_source = match args.encoding.audio.capture_method {
|
||||||
encoding_args::AudioCaptureMethod::PulseAudio => {
|
encoding_args::AudioCaptureMethod::PULSEAUDIO => {
|
||||||
gst::ElementFactory::make("pulsesrc").build()?
|
gst::ElementFactory::make("pulsesrc").build()?
|
||||||
}
|
}
|
||||||
encoding_args::AudioCaptureMethod::PipeWire => {
|
encoding_args::AudioCaptureMethod::PIPEWIRE => {
|
||||||
gst::ElementFactory::make("pipewiresrc").build()?
|
gst::ElementFactory::make("pipewiresrc").build()?
|
||||||
}
|
}
|
||||||
encoding_args::AudioCaptureMethod::ALSA => gst::ElementFactory::make("alsasrc").build()?,
|
encoding_args::AudioCaptureMethod::ALSA => gst::ElementFactory::make("alsasrc").build()?,
|
||||||
@@ -257,8 +262,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
audio_encoder.set_property(
|
audio_encoder.set_property(
|
||||||
"bitrate",
|
"bitrate",
|
||||||
&match &args.encoding.audio.rate_control {
|
&match &args.encoding.audio.rate_control {
|
||||||
encoding_args::RateControl::CBR(cbr) => cbr.target_bitrate * 1000i32,
|
encoding_args::RateControl::CBR(cbr) => cbr.target_bitrate.saturating_mul(1000) as i32,
|
||||||
encoding_args::RateControl::VBR(vbr) => vbr.target_bitrate * 1000i32,
|
encoding_args::RateControl::VBR(vbr) => vbr.target_bitrate.saturating_mul(1000) as i32,
|
||||||
_ => 128000i32,
|
_ => 128000i32,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -269,7 +274,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
/* Video */
|
/* Video */
|
||||||
// Video Source Element
|
// Video Source Element
|
||||||
let video_source = gst::ElementFactory::make("waylanddisplaysrc").build()?;
|
let video_source = Arc::new(gst::ElementFactory::make("waylanddisplaysrc").build()?);
|
||||||
video_source.set_property_from_str("render-node", gpu.render_path());
|
video_source.set_property_from_str("render-node", gpu.render_path());
|
||||||
|
|
||||||
// Caps Filter Element (resolution, fps)
|
// Caps Filter Element (resolution, fps)
|
||||||
@@ -323,7 +328,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
/* Output */
|
/* Output */
|
||||||
// WebRTC sink Element
|
// WebRTC sink Element
|
||||||
let signaller = NestriSignaller::new(nestri_ws.clone(), pipeline.clone());
|
let signaller = NestriSignaller::new(nestri_ws.clone(), video_source.clone());
|
||||||
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone()));
|
let webrtcsink = BaseWebRTCSink::with_signaller(Signallable::from(signaller.clone()));
|
||||||
webrtcsink.set_property_from_str("stun-server", "stun://stun.l.google.com:19302");
|
webrtcsink.set_property_from_str("stun-server", "stun://stun.l.google.com:19302");
|
||||||
webrtcsink.set_property_from_str("congestion-control", "disabled");
|
webrtcsink.set_property_from_str("congestion-control", "disabled");
|
||||||
@@ -456,9 +461,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let result = run_pipeline(pipeline.clone()).await;
|
let result = run_pipeline(pipeline.clone()).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => log::info!("All tasks finished"),
|
Ok(_) => tracing::info!("All tasks finished"),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error occurred in one of the tasks: {}", e);
|
tracing::error!("Error occurred in one of the tasks: {}", e);
|
||||||
return Err("Error occurred in one of the tasks".into());
|
return Err("Error occurred in one of the tasks".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,7 +476,7 @@ async fn run_pipeline(pipeline: Arc<gst::Pipeline>) -> Result<(), Box<dyn Error>
|
|||||||
|
|
||||||
{
|
{
|
||||||
if let Err(e) = pipeline.set_state(gst::State::Playing) {
|
if let Err(e) = pipeline.set_state(gst::State::Playing) {
|
||||||
log::error!("Failed to start pipeline: {}", e);
|
tracing::error!("Failed to start pipeline: {}", e);
|
||||||
return Err("Failed to start pipeline".into());
|
return Err("Failed to start pipeline".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,12 +484,12 @@ async fn run_pipeline(pipeline: Arc<gst::Pipeline>) -> Result<(), Box<dyn Error>
|
|||||||
// Wait for EOS or error (don't lock the pipeline indefinitely)
|
// Wait for EOS or error (don't lock the pipeline indefinitely)
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = tokio::signal::ctrl_c() => {
|
_ = tokio::signal::ctrl_c() => {
|
||||||
log::info!("Pipeline interrupted via Ctrl+C");
|
tracing::info!("Pipeline interrupted via Ctrl+C");
|
||||||
}
|
}
|
||||||
result = listen_for_gst_messages(bus) => {
|
result = listen_for_gst_messages(bus) => {
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => log::info!("Pipeline finished with EOS"),
|
Ok(_) => tracing::info!("Pipeline finished with EOS"),
|
||||||
Err(err) => log::error!("Pipeline error: {}", err),
|
Err(err) => tracing::error!("Pipeline error: {}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,7 +509,7 @@ async fn listen_for_gst_messages(bus: gst::Bus) -> Result<(), Box<dyn Error>> {
|
|||||||
while let Some(msg) = bus_stream.next().await {
|
while let Some(msg) = bus_stream.next().await {
|
||||||
match msg.view() {
|
match msg.view() {
|
||||||
gst::MessageView::Eos(_) => {
|
gst::MessageView::Eos(_) => {
|
||||||
log::info!("Received EOS");
|
tracing::info!("Received EOS");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
gst::MessageView::Error(err) => {
|
gst::MessageView::Error(err) => {
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ pub fn encode_message<T: Serialize>(message: &T) -> Result<String, Box<dyn Error
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_message(data: String) -> Result<MessageBase, Box<dyn Error + Send + Sync>> {
|
pub fn decode_message(data: String) -> Result<MessageBase, Box<dyn Error + Send + Sync>> {
|
||||||
println!("Data: {}", data);
|
|
||||||
let base_message: MessageBase = serde_json::from_str(&data)?;
|
let base_message: MessageBase = serde_json::from_str(&data)?;
|
||||||
Ok(base_message)
|
Ok(base_message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
|||||||
|
|
||||||
pub struct Signaller {
|
pub struct Signaller {
|
||||||
nestri_ws: PLRwLock<Option<Arc<NestriWebSocket>>>,
|
nestri_ws: PLRwLock<Option<Arc<NestriWebSocket>>>,
|
||||||
pipeline: PLRwLock<Option<Arc<gst::Pipeline>>>,
|
wayland_src: PLRwLock<Option<Arc<gst::Element>>>,
|
||||||
data_channel: AtomicRefCell<Option<gst_webrtc::WebRTCDataChannel>>,
|
data_channel: AtomicRefCell<Option<gst_webrtc::WebRTCDataChannel>>,
|
||||||
}
|
}
|
||||||
impl Default for Signaller {
|
impl Default for Signaller {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
nestri_ws: PLRwLock::new(None),
|
nestri_ws: PLRwLock::new(None),
|
||||||
pipeline: PLRwLock::new(None),
|
wayland_src: PLRwLock::new(None),
|
||||||
data_channel: AtomicRefCell::new(None),
|
data_channel: AtomicRefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,12 +38,12 @@ impl Signaller {
|
|||||||
*self.nestri_ws.write() = Some(nestri_ws);
|
*self.nestri_ws.write() = Some(nestri_ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pipeline(&self, pipeline: Arc<gst::Pipeline>) {
|
pub fn set_wayland_src(&self, wayland_src: Arc<gst::Element>) {
|
||||||
*self.pipeline.write() = Some(pipeline);
|
*self.wayland_src.write() = Some(wayland_src);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pipeline(&self) -> Option<Arc<gst::Pipeline>> {
|
pub fn get_wayland_src(&self) -> Option<Arc<gst::Element>> {
|
||||||
self.pipeline.read().clone()
|
self.wayland_src.read().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_data_channel(&self, data_channel: gst_webrtc::WebRTCDataChannel) {
|
pub fn set_data_channel(&self, data_channel: gst_webrtc::WebRTCDataChannel) {
|
||||||
@@ -159,8 +159,8 @@ impl Signaller {
|
|||||||
);
|
);
|
||||||
if let Some(data_channel) = data_channel {
|
if let Some(data_channel) = data_channel {
|
||||||
gst::info!(gst::CAT_DEFAULT, "Data channel created");
|
gst::info!(gst::CAT_DEFAULT, "Data channel created");
|
||||||
if let Some(pipeline) = signaller.imp().get_pipeline() {
|
if let Some(wayland_src) = signaller.imp().get_wayland_src() {
|
||||||
setup_data_channel(&data_channel, &pipeline);
|
setup_data_channel(&data_channel, &*wayland_src);
|
||||||
signaller.imp().set_data_channel(data_channel);
|
signaller.imp().set_data_channel(data_channel);
|
||||||
} else {
|
} else {
|
||||||
gst::error!(gst::CAT_DEFAULT, "Wayland display source not set");
|
gst::error!(gst::CAT_DEFAULT, "Wayland display source not set");
|
||||||
@@ -201,7 +201,7 @@ impl SignallableImpl for Signaller {
|
|||||||
// Wait for a reconnection notification
|
// Wait for a reconnection notification
|
||||||
reconnected_notify.notified().await;
|
reconnected_notify.notified().await;
|
||||||
|
|
||||||
println!("Reconnected to relay, re-negotiating...");
|
tracing::warn!("Reconnected to relay, re-negotiating...");
|
||||||
gst::warning!(gst::CAT_DEFAULT, "Reconnected to relay, re-negotiating...");
|
gst::warning!(gst::CAT_DEFAULT, "Reconnected to relay, re-negotiating...");
|
||||||
|
|
||||||
// Emit "session-ended" first to make sure the element is cleaned up
|
// Emit "session-ended" first to make sure the element is cleaned up
|
||||||
@@ -255,7 +255,7 @@ impl SignallableImpl for Signaller {
|
|||||||
};
|
};
|
||||||
if let Ok(encoded) = encode_message(&join_msg) {
|
if let Ok(encoded) = encode_message(&join_msg) {
|
||||||
if let Err(e) = nestri_ws.send_message(encoded) {
|
if let Err(e) = nestri_ws.send_message(encoded) {
|
||||||
eprintln!("Failed to send join message: {:?}", e);
|
tracing::error!("Failed to send join message: {:?}", e);
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to send join message: {:?}", e);
|
gst::error!(gst::CAT_DEFAULT, "Failed to send join message: {:?}", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -283,7 +283,7 @@ impl SignallableImpl for Signaller {
|
|||||||
};
|
};
|
||||||
if let Ok(encoded) = encode_message(&sdp_message) {
|
if let Ok(encoded) = encode_message(&sdp_message) {
|
||||||
if let Err(e) = nestri_ws.send_message(encoded) {
|
if let Err(e) = nestri_ws.send_message(encoded) {
|
||||||
eprintln!("Failed to send SDP message: {:?}", e);
|
tracing::error!("Failed to send SDP message: {:?}", e);
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to send SDP message: {:?}", e);
|
gst::error!(gst::CAT_DEFAULT, "Failed to send SDP message: {:?}", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -319,7 +319,7 @@ impl SignallableImpl for Signaller {
|
|||||||
};
|
};
|
||||||
if let Ok(encoded) = encode_message(&ice_message) {
|
if let Ok(encoded) = encode_message(&ice_message) {
|
||||||
if let Err(e) = nestri_ws.send_message(encoded) {
|
if let Err(e) = nestri_ws.send_message(encoded) {
|
||||||
eprintln!("Failed to send ICE message: {:?}", e);
|
tracing::error!("Failed to send ICE message: {:?}", e);
|
||||||
gst::error!(gst::CAT_DEFAULT, "Failed to send ICE message: {:?}", e);
|
gst::error!(gst::CAT_DEFAULT, "Failed to send ICE message: {:?}", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -361,8 +361,8 @@ impl ObjectImpl for Signaller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_data_channel(data_channel: &gst_webrtc::WebRTCDataChannel, pipeline: &gst::Pipeline) {
|
fn setup_data_channel(data_channel: &gst_webrtc::WebRTCDataChannel, wayland_src: &gst::Element) {
|
||||||
let pipeline = pipeline.clone();
|
let wayland_src = wayland_src.clone();
|
||||||
|
|
||||||
data_channel.connect_on_message_data(move |_data_channel, data| {
|
data_channel.connect_on_message_data(move |_data_channel, data| {
|
||||||
if let Some(data) = data {
|
if let Some(data) = data {
|
||||||
@@ -371,15 +371,15 @@ fn setup_data_channel(data_channel: &gst_webrtc::WebRTCDataChannel, pipeline: &g
|
|||||||
if let Some(input_msg) = message_input.data {
|
if let Some(input_msg) = message_input.data {
|
||||||
// Process the input message and create an event
|
// Process the input message and create an event
|
||||||
if let Some(event) = handle_input_message(input_msg) {
|
if let Some(event) = handle_input_message(input_msg) {
|
||||||
// Send the event to pipeline, result bool is ignored
|
// Send the event to wayland source, result bool is ignored
|
||||||
let _ = pipeline.send_event(event);
|
let _ = wayland_src.send_event(event);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Failed to parse InputMessage");
|
tracing::error!("Failed to parse InputMessage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to decode MessageInput: {:?}", e);
|
tracing::error!("Failed to decode MessageInput: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NestriSignaller {
|
impl NestriSignaller {
|
||||||
pub fn new(nestri_ws: Arc<NestriWebSocket>, pipeline: Arc<gst::Pipeline>) -> Self {
|
pub fn new(nestri_ws: Arc<NestriWebSocket>, wayland_src: Arc<gst::Element>) -> Self {
|
||||||
let obj: Self = glib::Object::new();
|
let obj: Self = glib::Object::new();
|
||||||
obj.imp().set_nestri_ws(nestri_ws);
|
obj.imp().set_nestri_ws(nestri_ws);
|
||||||
obj.imp().set_pipeline(pipeline);
|
obj.imp().set_wayland_src(wayland_src);
|
||||||
obj
|
obj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use crate::messages::{MessageBase, MessageLog, decode_message, encode_message};
|
use crate::messages::decode_message;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use futures_util::sink::SinkExt;
|
use futures_util::sink::SinkExt;
|
||||||
use futures_util::stream::{SplitSink, SplitStream};
|
use futures_util::stream::{SplitSink, SplitStream};
|
||||||
use log::{Level, Log, Metadata, Record};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
@@ -63,7 +62,7 @@ impl NestriWebSocket {
|
|||||||
return Ok(ws_stream);
|
return Ok(ws_stream);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to connect to WebSocket, retrying: {:?}", e);
|
tracing::error!("Failed to connect to WebSocket, retrying: {:?}", e);
|
||||||
sleep(Duration::from_secs(3)).await; // Wait before retrying
|
sleep(Duration::from_secs(3)).await; // Wait before retrying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +86,7 @@ impl NestriWebSocket {
|
|||||||
let mut ws_read = match ws_read_option {
|
let mut ws_read = match ws_read_option {
|
||||||
Some(ws_read) => ws_read,
|
Some(ws_read) => ws_read,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Reader is None, cannot proceed");
|
tracing::error!("Reader is None, cannot proceed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -101,7 +100,7 @@ impl NestriWebSocket {
|
|||||||
let base_message = match decode_message(data.to_string()) {
|
let base_message = match decode_message(data.to_string()) {
|
||||||
Ok(base_message) => base_message,
|
Ok(base_message) => base_message,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to decode message: {:?}", e);
|
tracing::error!("Failed to decode message: {:?}", e);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -113,7 +112,7 @@ impl NestriWebSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
tracing::error!(
|
||||||
"Error receiving message: {:?}, reconnecting in 3 seconds...",
|
"Error receiving message: {:?}, reconnecting in 3 seconds...",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
@@ -150,10 +149,10 @@ impl NestriWebSocket {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error sending message: {:?}", e);
|
tracing::error!("Error sending message: {:?}", e);
|
||||||
// Attempt to reconnect
|
// Attempt to reconnect
|
||||||
if let Err(e) = self_clone.reconnect().await {
|
if let Err(e) = self_clone.reconnect().await {
|
||||||
eprintln!("Error during reconnection: {:?}", e);
|
tracing::error!("Error during reconnection: {:?}", e);
|
||||||
// Wait before retrying
|
// Wait before retrying
|
||||||
sleep(Duration::from_secs(3)).await;
|
sleep(Duration::from_secs(3)).await;
|
||||||
continue;
|
continue;
|
||||||
@@ -161,10 +160,10 @@ impl NestriWebSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Writer is None, cannot send message");
|
tracing::error!("Writer is None, cannot send message");
|
||||||
// Attempt to reconnect
|
// Attempt to reconnect
|
||||||
if let Err(e) = self_clone.reconnect().await {
|
if let Err(e) = self_clone.reconnect().await {
|
||||||
eprintln!("Error during reconnection: {:?}", e);
|
tracing::error!("Error during reconnection: {:?}", e);
|
||||||
// Wait before retrying
|
// Wait before retrying
|
||||||
sleep(Duration::from_secs(3)).await;
|
sleep(Duration::from_secs(3)).await;
|
||||||
continue;
|
continue;
|
||||||
@@ -196,7 +195,7 @@ impl NestriWebSocket {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to reconnect to WebSocket: {:?}", e);
|
tracing::error!("Failed to reconnect to WebSocket: {:?}", e);
|
||||||
sleep(Duration::from_secs(3)).await; // Wait before retrying
|
sleep(Duration::from_secs(3)).await; // Wait before retrying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,39 +223,3 @@ impl NestriWebSocket {
|
|||||||
self.reconnected_notify.clone()
|
self.reconnected_notify.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Log for NestriWebSocket {
|
|
||||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
|
||||||
metadata.level() <= Level::Info
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log(&self, record: &Record) {
|
|
||||||
if self.enabled(record.metadata()) {
|
|
||||||
let level = record.level().to_string();
|
|
||||||
let message = record.args().to_string();
|
|
||||||
let time = chrono::Local::now().to_rfc3339();
|
|
||||||
|
|
||||||
// Print to console as well
|
|
||||||
println!("{}: {}", level, message);
|
|
||||||
|
|
||||||
// Encode and send the log message
|
|
||||||
let log_message = MessageLog {
|
|
||||||
base: MessageBase {
|
|
||||||
payload_type: "log".to_string(),
|
|
||||||
latency: None,
|
|
||||||
},
|
|
||||||
level,
|
|
||||||
message,
|
|
||||||
time,
|
|
||||||
};
|
|
||||||
if let Ok(encoded_message) = encode_message(&log_message) {
|
|
||||||
if let Err(e) = self.send_message(encoded_message) {
|
|
||||||
eprintln!("Failed to send log message: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&self) {
|
|
||||||
// No-op for this logger
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user