21 Commits

Author SHA1 Message Date
Valmo Trindade
b8bb01e985 improved video feed path generator to use player's name and role 2025-04-07 04:46:53 -03:00
Valmo Trindade
10d0b6f86c separed video stream oss in scopes 2025-04-07 01:28:10 -03:00
Valmo Trindade
db16d94596 added scopes for windows and linux on video stream 2025-04-06 17:53:47 -03:00
Valmo Trindade
9fc7a0e1bd added windows target to video stop callback 2025-04-06 16:46:33 -03:00
Valmo Trindade
f9b5860f9b Added handling for letting only windows calls work and throw a callback when called form a linux on video streams 2025-04-06 16:24:40 -03:00
Valmo Trindade
51b1a06e6c updated project version 2025-04-06 06:03:08 -03:00
Valmo Trindade
63c60bc81a made ffmpeg thread running with no window 2025-04-06 06:02:53 -03:00
Valmo Trindade
10683d9e2f Added function and editor module for handling the video stream paramethers 2025-04-06 05:42:30 -03:00
Valmo Trindade
871d6ec9d3 Added extension calls for starting and stopping a RTSP Video Stream for being used by OTS MEDIAMTX 2025-04-06 05:38:41 -03:00
Valmo Trindade
6eca9e92b6 added loop breaker for Disconnected TCP socket 2025-04-06 05:37:59 -03:00
Valmo Trindade
a8f6b04221 Improved TCP Socket Callbacks 2025-04-06 04:20:25 -03:00
Valmo Trindade
7831b25062 linted tcp socket error callback messages 2025-04-06 04:01:13 -03:00
Valmo Trindade
c490483abb added more extension callback EH for handling error msgs in game 2025-04-06 04:00:26 -03:00
Valmo Trindade
1719a16894 fixed Kunduz map size on convert function 2025-04-03 12:26:35 -03:00
Valmo Trindade
8e4b967f7d improved error callback messages 2025-04-03 12:26:07 -03:00
Valmo Trindade
91eeee9989 Added Kunduz map integration 2025-03-30 23:20:22 -03:00
Valmo Trindade
48512f587b Fixed Southen sahrani convertion call 2025-03-30 23:20:04 -03:00
Valmo Trindade
ddf60c439c switched to v1 2025-03-21 18:52:02 -03:00
Valmo Trindade
8e1fc2170a Changed default port to TCP 8088 2025-03-21 18:51:48 -03:00
Valmo Trindade
25628016ee Merge branch 'short_name' 2025-03-21 18:46:33 -03:00
Valmo Trindade
2ac29f50a6 added function to handle names for vehicles 2025-02-19 17:48:05 -03:00
15 changed files with 399 additions and 33 deletions

View File

@@ -4,10 +4,10 @@ name = "ARMA Team Awareness Kit"
prefix = "armatak" prefix = "armatak"
[version] [version]
build = 0 build = 9
major = 0 major = 0
minor = 9 minor = 9
patch = 0 patch = 9
git_hash = 0 git_hash = 0
@@ -44,7 +44,22 @@ workshop = [
"1779063631", # Zeus enhanced "1779063631", # Zeus enhanced
"1673595418", # User Input Menus "1673595418", # User Input Menus
"1678581937", # Extended Function Viewer "1678581937", # Extended Function Viewer
"1231625987" #Debug Console "1231625987", #Debug Console
"751965892", # ACRE2
"843577117", # RHSUSAF
"843425103", # RHSAFRF
"843593391", # RHSGREF
"735566597", # Project OPFOR
"3030830594", # Western Dusk
"3147473073", # TOTT Core
"3147480843", # TOTT CAG
"3147476552", # TOTT Optics
"2975268929", # GPNVG
"583496184", # CUP Terrains
"421620913", # Kunduz
"2397360831", # USAF Core
"2397376046", # USAF Utility
"2226368165", # USAF AC130
] ]
parameters = [ parameters = [

2
Cargo.lock generated
View File

@@ -83,7 +83,7 @@ dependencies = [
[[package]] [[package]]
name = "armatak" name = "armatak"
version = "0.9.0" version = "0.9.9"
dependencies = [ dependencies = [
"arma-rs", "arma-rs",
"chrono", "chrono",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "armatak" name = "armatak"
version = "0.9.0" version = "0.9.9"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -4,6 +4,9 @@ class CfgFunctions {
class init { class init {
file = "\armatak\armatak\armatak_main\functions\fn_init.sqf"; file = "\armatak\armatak\armatak_main\functions\fn_init.sqf";
}; };
class video_init {
file = "\armatak\armatak\armatak_main\functions\fn_video_init.sqf";
};
class log_message { class log_message {
file = "\armatak\armatak\armatak_main\functions\fn_log_message.sqf"; file = "\armatak\armatak\armatak_main\functions\fn_log_message.sqf";
}; };
@@ -43,6 +46,10 @@ class CfgFunctions {
class extract_uuid { class extract_uuid {
file = "\armatak\armatak\armatak_main\functions\extract_data\fn_extract_uuid.sqf"; file = "\armatak\armatak\armatak_main\functions\extract_data\fn_extract_uuid.sqf";
}; };
class shorten_name {
file = "\armatak\armatak\armatak_main\functions\extract_data\fn_shorten_name.sqf";
};
class convert_location { class convert_location {
file = "\armatak\armatak\armatak_main\functions\map\fn_convert_location.sqf"; file = "\armatak\armatak\armatak_main\functions\map\fn_convert_location.sqf";
}; };
@@ -61,6 +68,9 @@ class CfgFunctions {
class convert_to_cucui { class convert_to_cucui {
file = "\armatak\armatak\armatak_main\functions\map\fn_convert_to_cucui.sqf"; file = "\armatak\armatak\armatak_main\functions\map\fn_convert_to_cucui.sqf";
}; };
class convert_to_kunduz {
file = "\armatak\armatak\armatak_main\functions\map\fn_convert_to_kunduz.sqf";
};
class convert_to_livonia { class convert_to_livonia {
file = "\armatak\armatak\armatak_main\functions\map\fn_convert_to_livonia.sqf"; file = "\armatak\armatak\armatak_main\functions\map\fn_convert_to_livonia.sqf";
}; };

View File

@@ -49,9 +49,6 @@ class CfgVehicles {
}; };
class Attributes: AttributesBase { class Attributes: AttributesBase {
class Units: Units {
property = "armatak_module_attached_units";
};
class armatak_module_tak_server_instance_address: Edit { class armatak_module_tak_server_instance_address: Edit {
property = "armatak_module_tak_server_instance_address"; property = "armatak_module_tak_server_instance_address";
displayname = "TAK Server Address"; displayname = "TAK Server Address";
@@ -64,7 +61,86 @@ class CfgVehicles {
displayname = "TAK Server TCP Port"; displayname = "TAK Server TCP Port";
tooltip = "TAK Server instance Port for TCP connection"; tooltip = "TAK Server instance Port for TCP connection";
typeName = "NUMBER"; typeName = "NUMBER";
defaultValue = "8080"; defaultValue = "8088";
};
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;
};
};
};
class armatak_module_video_stream_core: armatak_module_core {
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";
functionPriority = 1;
isGlobal = 0;
isTriggerActivated = 0;
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";
displayname = "MediaMTX Provider Address";
tooltip = "MediaMTX Provider Instance Address";
typeName = "STRING";
defaultValue = "localhost";
};
class armatak_module_mediamtx_video_stream_instance_port: 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";
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";
displayname = "MediaMTX Provider Password";
tooltip = "MediaMTX Provider Instance Password";
typeName = "STRING";
defaultValue = "password";
}; };
class ModuleDescription: ModuleDescription {}; class ModuleDescription: ModuleDescription {};
}; };

View File

@@ -7,7 +7,7 @@ params["_unit"];
private _callsign = ""; private _callsign = "";
if (roleDescription _unit != "") then { if (roleDescription _unit != "") then {
_callsign = name _unit + " | " + roleDescription _unit; _callsign = ([name _unit] call armatak_fnc_shorten_name) + " | " + roleDescription _unit;
} else { } else {
_callsign = name _unit; _callsign = name _unit;
@@ -20,7 +20,7 @@ if ((([_unit] call BIS_fnc_objectType) select 0) == "Vehicle") then {
_callsign = getText (configFile >> "CfgVehicles" >> typeOf _unit >> "displayName"); _callsign = getText (configFile >> "CfgVehicles" >> typeOf _unit >> "displayName");
if (!isNull driver _unit) then { if (!isNull driver _unit) then {
_callsign = getText (configFile >> "CfgVehicles" >> typeOf _unit >> "displayName") + " | " + name (driver _unit); _callsign = getText (configFile >> "CfgVehicles" >> typeOf _unit >> "displayName") + " | " + ([name (driver _unit)] call armatak_fnc_shorten_name);
}; };
}; };
@@ -34,10 +34,10 @@ if (unitIsUAV _unit) then {
} }
}; };
_pre_defined_callsign = _unit getVariable "_atak_callsign"; _atak_pre_defined_callsign = _unit getVariable "_atak_callsign";
if (!isNil "_pre_defined_callsign") then { if (!isNil "_atak_pre_defined_callsign") then {
_callsign = _pre_defined_callsign; _callsign = _atak_pre_defined_callsign;
}; };
_callsign _callsign

View File

@@ -0,0 +1,17 @@
params ["_fullName"];
private _nameParts = _fullName splitString " ";
private _nameCount = count _nameParts;
if (_nameCount == 1) then {
_fullName
} else {
private _firstName = _nameParts select 0;
private _lastName = _nameParts select (_nameCount - 1);
if ((count _lastName) >= 7) then {
_lastName
} else {
format ["%1 %2", _firstName select [0, 1], _lastName]
};
};

View File

@@ -12,7 +12,22 @@ if (isServer) exitWith {
params ["_name", "_function", "_data"]; params ["_name", "_function", "_data"];
if (_name == "armatak_tcp_socket") then { if (_name == "armatak_tcp_socket") then {
_warning = format ["<t color='#FF8021'>ARMATAK</t><br/> %1", _function]; _warning = format ["<t color='#00FF21'>ARMATAK</t><br/> %1", _function];
[[_warning, 1.5]] call CBA_fnc_notify;
};
if (_name == "armatak_tcp_socket_error") then {
_warning = format ["<t color='#FF0021'>ARMATAK</t><br/> %1", _function];
[[_warning, 1.5]] call CBA_fnc_notify;
};
if (_name == "armatak_video") then {
_warning = format ["<t color='#00FF21'>ARMATAK</t><br/> %1", _function];
[[_warning, 1.5]] call CBA_fnc_notify;
};
if (_name == "armatak_video_error") then {
_warning = format ["<t color='#FF0021'>ARMATAK</t><br/> %1", _function];
[[_warning, 1.5]] call CBA_fnc_notify; [[_warning, 1.5]] call CBA_fnc_notify;
}; };
}]; }];

View File

@@ -0,0 +1,83 @@
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
armatak_module_mediamtx_video_stream_instance_address = _logic getVariable "armatak_module_mediamtx_video_stream_instance_address";
armatak_module_mediamtx_video_stream_instance_port = _logic getVariable "armatak_module_mediamtx_video_stream_instance_port";
armatak_module_mediamtx_video_stream_instance_auth_user = _logic getVariable "armatak_module_mediamtx_video_stream_instance_auth_user";
armatak_module_mediamtx_video_stream_instance_auth_pass = _logic getVariable "armatak_module_mediamtx_video_stream_instance_auth_pass";
missionNamespace setVariable ["armatak_mediamtx_video_stream_instance_address", armatak_module_mediamtx_video_stream_instance_address];
missionNamespace setVariable ["armatak_mediamtx_video_stream_instance_port", armatak_module_mediamtx_video_stream_instance_port];
missionNamespace setVariable ["armatak_mediamtx_video_stream_instance_auth_user", armatak_module_mediamtx_video_stream_instance_auth_user];
missionNamespace setVariable ["armatak_mediamtx_video_stream_instance_auth_pass", armatak_module_mediamtx_video_stream_instance_auth_pass];
_startAction = [
"ArmatakStartStream",
"Start Video Feed",
"",
{
_uuid = (_this select 0) call armatak_fnc_extract_uuid;
_uuid = (_this select 0) call armatak_fnc_extract_uuid;
_uuid_short = _uuid select [0, 8];
_role = roleDescription (_this select 0);
_name = name (_this select 0);
_role = [_role] call BIS_fnc_filterString;
_name = [_name] call BIS_fnc_filterString;
_stream_path = _name + "_" + _role + "_" + _uuid_short;
armatak_mediamtx_video_stream_instance_address = missionNamespace getVariable "armatak_mediamtx_video_stream_instance_address";
armatak_mediamtx_video_stream_instance_port = missionNamespace getVariable "armatak_mediamtx_video_stream_instance_port";
armatak_mediamtx_video_stream_instance_auth_user = missionNamespace getVariable "armatak_mediamtx_video_stream_instance_auth_user";
armatak_mediamtx_video_stream_instance_auth_pass = missionNamespace getVariable "armatak_mediamtx_video_stream_instance_auth_pass";
"armatak" callExtension ["video_stream:start", [armatak_mediamtx_video_stream_instance_address, armatak_mediamtx_video_stream_instance_port, _uuid, armatak_mediamtx_video_stream_instance_auth_user, armatak_mediamtx_video_stream_instance_auth_pass]];
(_this select 0) setVariable ["armatak_video_feed_is_streaming", true];
},
{
(_this select 0) getVariable "armatak_video_feed_is_streaming" == false
}
] call ace_interact_menu_fnc_createAction;
[
"Man",
1,
["ACE_SelfActions"],
_startAction,
true
] call ace_interact_menu_fnc_addActionToClass;
_stopAction = [
"ArmatakStopStream",
"Stop Video Feed",
"",
{
"armatak" callExtension ["video_stream:stop", []];
(_this select 0) setVariable ["armatak_video_feed_is_streaming", false];
},
{
(_this select 0) getVariable "armatak_video_feed_is_streaming"
}
] call ace_interact_menu_fnc_createAction;
[
"Man",
1,
["ACE_SelfActions"],
_stopAction,
true
] call ace_interact_menu_fnc_addActionToClass;
if (isMultiplayer) then {
{
_x setVariable ["armatak_video_feed_is_streaming", false];
} forEach playableUnits;
} else {
player setVariable ["armatak_video_feed_is_streaming", false];
};
};
true;

View File

@@ -42,11 +42,14 @@ switch (toLower worldName) do {
_realLocation = _position call armatak_fnc_convert_to_united_sahrani; _realLocation = _position call armatak_fnc_convert_to_united_sahrani;
}; };
case "saralite": { case "saralite": {
_realLocation = _position call armatak_fnc_convert_to_united_sahrani; _realLocation = _position call armatak_fnc_convert_to_southen_sahrani;
}; };
case "enoch": { case "enoch": {
_realLocation = _position call armatak_fnc_convert_to_livonia; _realLocation = _position call armatak_fnc_convert_to_livonia;
}; };
case "kunduz": {
_realLocation = _position call armatak_fnc_convert_to_kunduz;
};
default { default {
_warning = format ["<t color='#FF8021'>ARMATAK</t><br/> %1", "Unsupported Map"]; _warning = format ["<t color='#FF8021'>ARMATAK</t><br/> %1", "Unsupported Map"];
[[_warning, 1.5]] call CBA_fnc_notify; [[_warning, 1.5]] call CBA_fnc_notify;

View File

@@ -0,0 +1,30 @@
params ["_longitudeInGame", "_latitudeInGame", "_altitude"];
private _mapWidth = 5120;
private _mapHeight = 5120;
// SW corner (used as origin)
private _SW_lat = 36.588437;
private _SW_lon = 68.834763;
// SE corner
private _SE_lat = 36.574950;
private _SE_lon = 68.899151;
// NW corner
private _NW_lat = 36.640080;
private _NW_lon = 68.847941;
private _edgeSE_lat = _SE_lat - _SW_lat;
private _edgeSE_lon = _SE_lon - _SW_lon;
private _edgeNW_lat = _NW_lat - _SW_lat;
private _edgeNW_lon = _NW_lon - _SW_lon;
private _fx = _longitudeInGame / _mapWidth;
private _fy = _latitudeInGame / _mapHeight;
private _realLat = _SW_lat + (_fx * _edgeSE_lat) + (_fy * _edgeNW_lat);
private _realLon = _SW_lon + (_fx * _edgeSE_lon) + (_fy * _edgeNW_lon);
[_realLat, _realLon, _altitude]

View File

@@ -1,4 +1,4 @@
#define build 0 #define build 0
#define major 0 #define major 1
#define minor 7 #define minor 0
#define patch 5 #define patch 0

View File

@@ -32,14 +32,14 @@ impl TcpClient {
let tcp_thread = thread::spawn(move || match TcpStream::connect(&address) { let tcp_thread = thread::spawn(move || match TcpStream::connect(&address) {
Ok(stream) => { Ok(stream) => {
info!("Connected to TCP server at {}", address); let _ = ctx.callback_data("armatak_tcp_socket", "Connected to TCP Server", address);
*connection_clone.lock().unwrap() = Some(stream); *connection_clone.lock().unwrap() = Some(stream);
} }
Err(e) => { Err(e) => {
let _ = ctx.callback_data( let _ = ctx.callback_data(
"armatak_tcp_socket", "armatak_tcp_socket_error",
"tak_socket_failed_connection", "TAK Socket connection failed",
format!("Failed to connect to TCP server: {}", e), e.to_string(),
); );
info!("Failed to connect to TCP server: {}", e); info!("Failed to connect to TCP server: {}", e);
} }
@@ -53,15 +53,17 @@ impl TcpClient {
info!("Failed to send message: {}", e); info!("Failed to send message: {}", e);
let _ = context.callback_data( let _ = context.callback_data(
"armatak_tcp_socket", "armatak_tcp_socket_error",
"tak_socket_disconnected", "TAK Socket disconnected",
e.to_string(), e.to_string(),
); );
running = false;
} }
} else { } else {
let _ = context.callback_null( let _ = context.callback_null(
"armatak_tcp_socket", "armatak_tcp_socket_error",
"tak_socket_not_active", "TAK Socket is not active",
); );
} }
} }
@@ -107,16 +109,14 @@ pub fn start(ctx: Context, address: String) -> &'static str {
let mut client_guard = TCP_CLIENT.lock().unwrap(); let mut client_guard = TCP_CLIENT.lock().unwrap();
*client_guard = Some(client); *client_guard = Some(client);
info!("TCP client started.");
"Starting TCP Client" "Starting TCP Client"
} }
pub fn send_payload(ctx: Context, payload: String) -> &'static str { pub fn send_payload(ctx: Context, payload: String) -> &'static str {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() { if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
info!("Sending payload: {}", payload);
client.send_payload(ctx, payload); client.send_payload(ctx, payload);
} else { } else {
let _ = ctx.callback_null("armatak_tcp_socket_error", "TCP Client is not running");
info!("TCP client is not running."); info!("TCP client is not running.");
} }
@@ -144,12 +144,12 @@ pub fn send_digital_pointer_cot(ctx: Context, cursor_over_time: DigitalPointerPa
"Sending Digital Pointer Cursor Over Time to TCP server" "Sending Digital Pointer Cursor Over Time to TCP server"
} }
pub fn stop() -> &'static str { pub fn stop(ctx: Context) -> &'static str {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() { if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
client.stop(); client.stop();
info!("TCP client stopped."); let _ = ctx.callback_null("armatak_tcp_socket", "TCP client stopped");
} else { } else {
info!("TCP client is not running."); let _ = ctx.callback_null("armatak_tcp_socket_error", "TCP client is not running");
} }
"Stopping TCP Client" "Stopping TCP Client"

View File

@@ -5,6 +5,7 @@ mod websocket;
mod util; mod util;
mod cot_router; mod cot_router;
mod cot_generator; mod cot_generator;
mod video_stream;
#[arma] #[arma]
pub fn init() -> Extension { pub fn init() -> Extension {
@@ -49,5 +50,11 @@ pub fn init() -> Extension {
.command("send_digital_pointer_cot", cot_router::send_digital_pointer_cot) .command("send_digital_pointer_cot", cot_router::send_digital_pointer_cot)
.command("stop", cot_router::stop) .command("stop", cot_router::stop)
) )
.group(
"video_stream",
Group::new()
.command("start", video_stream::start_stream)
.command("stop", video_stream::stop_stream)
)
.finish() .finish()
} }

110
src/video_stream.rs Normal file
View File

@@ -0,0 +1,110 @@
use arma_rs::Context;
use lazy_static::lazy_static;
use std::process::Command;
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::Mutex;
use std::thread;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
lazy_static! {
static ref STREAM_CTRL: Mutex<Option<Sender<()>>> = Mutex::new(None);
}
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
pub fn start_stream(
ctx: Context,
address: String,
port: String,
stream_path: String,
username: String,
password: String,
) -> &'static str {
#[cfg(target_os = "linux")]
{
ctx.callback_null(
"armatak_video_error",
"Screen capture is only supported on Windows",
);
}
#[cfg(target_os = "windows")]
{
let (tx, rx): (Sender<()>, Receiver<()>) = mpsc::channel();
let rtsp_url = format!(
"rtsp://{}:{}@{}:{}/{}",
username, password, address, port, stream_path
);
let rtsp_url_clone = rtsp_url.clone();
thread::spawn(move || {
let mut cmd = Command::new("ffmpeg");
cmd.args(&[
"-f",
"gdigrab",
"-i",
"desktop",
"-f",
"rtsp",
"-rtsp_transport",
"tcp",
&rtsp_url_clone,
]);
let mut child = cmd.creation_flags(CREATE_NO_WINDOW).spawn().unwrap();
if rx.recv().is_err() {
let _ = ctx.callback_null("armatak_video_error", "Error receiving stop signal");
}
if let Err(e) = child.kill() {
let _ = ctx.callback_data(
"armatak_video_error",
"Failed to Stop FFmpeg",
e.to_string(),
);
}
});
match STREAM_CTRL.lock() {
Ok(mut lock) => *lock = Some(tx),
Err(e) => {
eprintln!("Failed to acquire lock: {}", e);
}
}
}
"starting video stream"
}
pub fn stop_stream(ctx: Context) -> &'static str {
match STREAM_CTRL.lock() {
Ok(mut lock) => {
if let Some(tx) = lock.take() {
if let Err(e) = tx.send(()) {
let _ = ctx.callback_data(
"armatak_video_error",
"Failed to send stop signal",
e.to_string(),
);
}
} else {
let _ =
ctx.callback_null("armatak_video_error", "Tried to stop a nonexistent stream");
}
}
Err(e) => {
let _ = ctx.callback_data(
"armatak_video_error",
"Failed to acquire lock",
e.to_string(),
);
}
}
"stopping video stream"
}