From 7b5510698ea16900e9dd02a6dd0a99b2a9f6301a Mon Sep 17 00:00:00 2001 From: Valmo Trindade Date: Thu, 22 May 2025 03:24:10 -0300 Subject: [PATCH] refactored video streaming module --- addons/video/CfgVehicles.hpp | 63 +++++------------- addons/video/functions/fnc_videoParser.sqf | 16 ++--- src/video_stream.rs | 77 +++++++++++++++------- 3 files changed, 80 insertions(+), 76 deletions(-) diff --git a/addons/video/CfgVehicles.hpp b/addons/video/CfgVehicles.hpp index 08dbe5e..188535d 100644 --- a/addons/video/CfgVehicles.hpp +++ b/addons/video/CfgVehicles.hpp @@ -1,5 +1,6 @@ class CfgVehicles { - class Logic; + class Logic; + class Module_F : Logic { class AttributesBase @@ -18,84 +19,54 @@ class CfgVehicles { class AnyBrain; }; }; - class armatak_module_core; - class armatak_module_video_stream_core: armatak_module_core { + class EGVAR(server,moduleBase); + class GVAR(videoModule): EGVAR(server,moduleBase) { scope = 2; - displayname = "ARMATAK MediaMTX Video Feed Parser"; - icon = "\a3\Modules_F_Curator\Data\iconcuratorsetcamera_ca.paa"; - category = "armatak_module_category"; - function = "armatak_fnc_video_init"; + scopeCurator = 0; + displayname = "Video Streaming Handler"; + icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa"; + category = QEGVAR(main,moduleCategory); + function = QFUNC(videoParser); functionPriority = 1; isGlobal = 0; - isTriggerActivated = 0; + isTriggerActivated = 1; isDisposable = 1; is3den = 0; curatorCanAttach = 0; curatorInfoType = "RscDisplayAttributeModuleNuke"; - canSetArea = 0; canSetAreaShape = 0; canSetAreaHeight = 0; - class AttributesValues { - size3[] = { 1, 1, -1 }; - isRectangle = 0; - }; - class Attributes: AttributesBase { - class armatak_module_mediamtx_video_stream_instance_address: Edit { - property = "armatak_module_mediamtx_video_stream_instance_address"; + class GVAR(instanceAddress): Edit { + property = QGVAR(instanceAddress); displayname = "MediaMTX Provider Address"; tooltip = "MediaMTX Provider Instance Address"; typeName = "STRING"; defaultValue = "localhost"; }; - class armatak_module_mediamtx_video_stream_instance_port: Edit { + class GVAR(instancePort): Edit { property = "armatak_module_mediamtx_video_stream_instance_port"; displayname = "MediaMTX Provider Port"; tooltip = "MediaMTX Provider Port for handling video streams"; typeName = "STRING"; defaultValue = "8554"; }; - class armatak_module_mediamtx_video_stream_instance_auth_user: Edit { - property = "armatak_module_mediamtx_video_stream_instance_auth_user"; + class GVAR(instanceAuthUser): Edit { + property = QGVAR(instanceAuthUser); displayname = "MediaMTX Provider Username"; tooltip = "MediaMTX Provider Instance Username"; typeName = "STRING"; defaultValue = "administrator"; }; - class armatak_module_mediamtx_video_stream_instance_auth_pass: Edit { - property = "armatak_module_mediamtx_video_stream_instance_auth_pass"; + class GVAR(instanceAuthPassword): Edit { + property = QGVAR(instanceAuthPassword); displayname = "MediaMTX Provider Password"; tooltip = "MediaMTX Provider Instance Password"; typeName = "STRING"; defaultValue = "password"; }; - class ModuleDescription: ModuleDescription {}; - }; - - class ModuleDescription: ModuleDescription { - description = "Generate the initial ARMATAK configuration, syncronizing all players to the TAK server instance"; - sync[] = {"LocationArea_F"}; - - class LocationArea_F { - description[] = { - "First line", - "Second line" - }; - position = 1; - direction = 1; - optional = 1; - duplicate = 1; - synced[] = { "BluforUnit", "AnyBrain" }; - }; - class BluforUnit - { - description = "Short description"; - displayName = "Any BLUFOR unit"; - icon = "iconMan"; - side = 1; - }; }; }; }; \ No newline at end of file diff --git a/addons/video/functions/fnc_videoParser.sqf b/addons/video/functions/fnc_videoParser.sqf index 77b1592..8bf8c07 100644 --- a/addons/video/functions/fnc_videoParser.sqf +++ b/addons/video/functions/fnc_videoParser.sqf @@ -7,15 +7,15 @@ params [ ]; if (isServer) exitWith { - private _instance_address = GETVAR(_logic,instance_address,false); - private _instance_port = GETVAR(_logic,instance_port,false); - private _instance_auth_user = GETVAR(_logic,instance_auth_user,false); - private _instance_auth_pass = GETVAR(_logic,instance_auth_pass,false); + private _instance_address = GETVAR(_logic,GVAR(instanceAddress),false); + private _instance_port = GETVAR(_logic,GVAR(instancePort),false); + private _instance_auth_user = GETVAR(_logic,GVAR(instanceAuthUser),false); + private _instance_auth_pass = GETVAR(_logic,GVAR(instanceAuthPassword),false); - SETMVAR(GVAR(instance_address),_instance_address); - SETMVAR(GVAR(instance_port),_instance_port); - SETMVAR(GVAR(instance_auth_user),_instance_auth_user); - SETMVAR(GVAR(instance_auth_pass),_instance_auth_pass); + SETMVAR(GVAR(instanceAddress),_instance_address); + SETMVAR(GVAR(instancePort),_instance_port); + SETMVAR(GVAR(instanceAuthUser),_instance_auth_user); + SETMVAR(GVAR(instanceAuthPassword),_instance_auth_pass); _startAction = [ QGVAR(startStream), diff --git a/src/video_stream.rs b/src/video_stream.rs index a60d5a5..18aadbf 100644 --- a/src/video_stream.rs +++ b/src/video_stream.rs @@ -4,6 +4,7 @@ use std::process::Command; use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::Mutex; use std::thread; +use std::time::Duration; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; @@ -29,11 +30,14 @@ pub fn start_stream( "VIDEO ERROR", "Screen capture is only supported on Windows", ); + return "screen capture unsupported"; } #[cfg(target_os = "windows")] { - let (tx, rx): (Sender<()>, Receiver<()>) = mpsc::channel(); + let (stop_tx, stop_rx): (Sender<()>, Receiver<()>) = mpsc::channel(); + let (status_tx, status_rx): (Sender>, Receiver>) = mpsc::channel(); + let rtsp_url = if username.is_empty() || password.is_empty() { format!("rtsp://{}:{}/{}", address, port, stream_path) } else { @@ -47,7 +51,6 @@ pub fn start_stream( thread::spawn(move || { let mut cmd = Command::new("ffmpeg"); - cmd.args(&[ "-f", "gdigrab", @@ -60,30 +63,57 @@ pub fn start_stream( &rtsp_url_clone, ]); - let mut child = cmd.creation_flags(CREATE_NO_WINDOW).spawn().unwrap(); + // Try to spawn ffmpeg process + let child_result = cmd.creation_flags(CREATE_NO_WINDOW).spawn(); - if rx.recv().is_err() { - let _ = ctx.callback_null("VIDEO ERROR", "Error receiving stop signal"); - } - - if let Err(e) = child.kill() { - let _ = ctx.callback_data( - "VIDEO ERROR", - "Failed to Stop FFmpeg", - e.to_string(), - ); + match child_result { + Ok(mut child) => { + let _ = status_tx.send(Ok(())); + if stop_rx.recv().is_err() {} + let _ = child.kill(); + } + Err(e) => { + let _ = status_tx.send(Err(format!("Failed to start FFmpeg: {}", e))); + // Return early, nothing else to do + } } }); + // Save the stop channel so we can stop later match STREAM_CTRL.lock() { - Ok(mut lock) => *lock = Some(tx), + Ok(mut lock) => *lock = Some(stop_tx), Err(e) => { - eprintln!("Failed to acquire lock: {}", e); + let _ = ctx.callback_data( + "VIDEO ERROR", + "Failed to acquire lock for stream control", + e.to_string(), + ); + return "stream control lock error"; + } + } + + // Wait up to 2 seconds to see if ffmpeg started correctly + match status_rx.recv_timeout(Duration::from_secs(2)) { + Ok(Ok(())) => { + let _ = ctx.callback_null("VIDEO", "FFmpeg started successfully"); + "starting video stream" + } + Ok(Err(e)) => { + let _ = ctx.callback_data("VIDEO ERROR", "FFmpeg failed to start", e); + "ffmpeg failed to start" + } + Err(_) => { + let _ = ctx.callback_null("VIDEO ERROR", "FFmpeg did not respond in time"); + "ffmpeg did not respond" } } } - "starting video stream" + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + { + ctx.callback_null("VIDEO ERROR", "Screen capture is only supported on Windows"); + "unsupported platform" + } } pub fn stop_stream(ctx: Context) -> &'static str { @@ -96,20 +126,23 @@ pub fn stop_stream(ctx: Context) -> &'static str { "Failed to send stop signal", e.to_string(), ); + "error sending stop" + } else { + let _ = ctx.callback_null("VIDEO", "Sent stop signal to FFmpeg"); + "stopping video stream" } } else { - let _ = - ctx.callback_null("VIDEO ERROR", "Tried to stop a nonexistent stream"); + let _ = ctx.callback_null("VIDEO ERROR", "Tried to stop a nonexistent stream"); + "no stream to stop" } } Err(e) => { let _ = ctx.callback_data( "VIDEO ERROR", - "Failed to acquire lock", + "Failed to acquire lock for stop", e.to_string(), ); + "lock error" } } - - "stopping video stream" -} +} \ No newline at end of file