/* * ==================== 视频接口使用说明(页面调用 video_*) ==================== * * 一、调用顺序与返回值 * 应用启动:video_start();进入页面:video_play(...);退出页面:video_stop(...) * 或 video_stop_all();应用退出:video_shutdown()。 * 除 shutdown 外,下面返回 int 的 video_* 命令都立即返回: * > 0:请求号,表示已加入队列,不代表服务执行成功或视频已经出画。 * < 0:提交失败,可用 video_error_text(返回值) 查看原因。 * 命令按提交顺序在内部线程执行;字符串会复制,可直接传局部 string。 * 执行结果:video_result(请求号),用 video_finished / video_ok 判断。 * 最多允许 32 个未完成请求,满时返回 VIDEO_ERR_QUEUE_FULL。 * Windows 模拟器不执行实际视频操作,执行结果为 HVC_ERR_UNSUPPORTED。 * * 二、通用形参约束 * channel:视频通道号,1~4;单路用 1,四路分别用 1、2、3、4。 * url:非空视频源地址,最多 3000 字节,不得含内嵌空字符。 * 播放支持 rtsp://、storage:// 或服务允许的板端本地绝对路径。 * x、y:视频区域左上角的屏幕坐标,必须为非负偶数,单位:像素。 * width:视频显示宽度,必须大于 0 且为 32 的倍数,单位:像素。 * height:视频显示高度,必须为正偶数,单位:像素。 * 宽高表示屏幕上的最终显示区域,不是视频源分辨率;边界由服务检查。 * 面板占位时可直接传面板的 mSettedX、mSettedY、mSettedwidth、mSettedheight。 * rotation:顺时针旋转角度,仅允许 0、90、180、270,默认 0。 * storage:存储位置,"auto" 自动选择、"usb" U盘、"sd" SD卡。 * * 三、初始化与退出 * video_start(const VideoOptions& options = VideoOptions()) * 初始化命令队列和工作线程,并启动或连接视频服务。应用启动时调用一次, * 不要每次切页重复调用;shutdown 后可重新初始化。 * options:服务连接配置,可省略,使用下面的默认值: * host = "127.0.0.1":服务地址;同板使用回环地址,远程控制填板卡 IP。 * port = 8555:服务 TCP 端口。 * timeoutMs = 3000:普通命令 TCP 总超时,单位:毫秒。 * script = "/customer/hmi-video-next/run_player.sh":本机启动脚本绝对路径。 * 空字符串表示仅连接已有服务;远程连接时必须设为空字符串。 * manageUiWindows = true:启动脚本时启用服务的 UI 窗口管理选项。 * startupTimeoutMs = 10000:等待服务启动的最长时间,范围 500~60000 毫秒。 * probeTimeoutMs = 10000:视频源探测超时,范围 1~60000 毫秒。 * 返回值:请求号或负数错误码,启动结果也通过 video_result 查询。 * * video_shutdown() * 无形参、无返回值。停止接收请求,取消尚未执行的请求,等待当前请求结束, * 回收工作线程。只在应用退出时调用,可能等待网络超时。 * 不会停止独立的视频服务、已有播放或录像;需要时先提交对应停止命令, * 并等其完成后再调用 shutdown,避免排队的停止命令被取消。 * * 四、播放与显示控制(返回值均为请求号或负数错误码) * video_play(int channel, const string& url, int x, int y, * int width, int height, int rotation = 0) * 在指定通道播放视频,并设置显示位置、大小、旋转角度。 * channel、url、x、y、width、height、rotation:含义和限制见“通用形参约束”。 * * video_refresh(int channel, const string& url, int x, int y, * int width, int height, int rotation = 0) * 先停止该通道,停止成功后再按给定参数重新播放,适合手动重连或重建画面。 * 各形参含义与 video_play 完全相同;不影响其他通道。 * * video_set_source(int channel, const string& url) * 只保存或修改指定通道的视频源,不自动开始显示。 * channel:通道号;url:视频源地址。可先设置源,再开始录制,实现只录不显示。 * * video_set_rect(int channel, int x, int y, int width, int height) * 只修改指定通道的显示位置和大小,不更换视频源。 * channel:通道号;x、y:左上角坐标;width、height:最终显示宽高。 * * video_set_rotation(int channel, int rotation) * 修改指定通道的顺时针旋转角度,不影响其他通道。 * channel:通道号;rotation:0、90、180、270。 * * video_hide(int channel) * 隐藏指定通道的画面,连接和解码保持运行,不释放解码资源。 * channel:要隐藏的通道号。 * * video_show(int channel) * 恢复指定通道被 hide 隐藏的画面。 * channel:要恢复显示的通道号;已经 stop 的通道应使用 play 重新播放。 * * video_stop(int channel) * 停止指定通道并释放硬件解码资源,保留该通道的视频源地址。 * channel:要停止的通道号;明确停止录像请使用 video_record_stop。 * * video_clear(int channel) * 清除指定通道,释放资源,同时删除保存的视频源地址和录像请求。 * channel:要清除的通道号。 * * video_stop_all() * 无形参。一次停止全部视频通道,适合多路页面的停止按钮和退出清理。 * 明确停止所有录像请使用 video_record_stop_all。 * * 五、录像与截图(返回值均为请求号或负数错误码) * video_record_start(int channel, const string& storage = "auto", * int segmentSeconds = 60, int maxSegments = 100) * 开始指定通道的 RTSP 录像;应先播放或用 set_source 配置视频源。 * channel:录制通道号;storage:保存位置,默认自动选择。 * segmentSeconds:每段目标时长,默认 60 秒,范围 10~3600 秒; * 实际切段会等待关键帧。 * maxSegments:当前通道最多保留的录像段数,默认 100,范围 2~10000。 * * video_record_stop(int channel) * 停止指定通道的录像;不用于停止视频显示。 * channel:要停止录制的通道号。 * * video_record_stop_all() * 无形参。停止全部通道的录像;停止视频显示另用 video_stop_all。 * * video_snapshot(int channel, const string& text = "", * const string& storage = "auto", int fontSize = 24, * const string& position = "bottom") * 保存指定通道的一帧图片,可在保存的图片上叠加文字,不修改屏幕画面。 * channel:截图通道号。 * text:UTF-8 文字,最多 512 字节;默认空字符串表示不加文字; * 支持 \n 或 \r\n 换行,不接受制表符等其他 C0/C1 控制字符。 * storage:图片保存位置,默认 "auto"。 * fontSize:文字大小,默认 24,范围 12~96。 * position:文字位置,"top" 顶部或 "bottom" 底部,默认底部。 * 注意:请求执行成功仅表示服务受理;保存回复 snapshot.job_id,随后用 * video_status 查询同一截图任务,state 为 done 时 file 才是保存路径。 * 服务全局同时仅处理一张截图,忙碌时可能拒绝新的截图请求。 * * 六、服务查询与原始命令(返回值均为请求号或负数错误码) * video_status() * 无形参。查询全部通道、内存、录像、截图、重连等服务状态。 * 等请求完成后,从 video_result(请求号).reply 读取服务返回的 JSON。 * 播放请求成功表示服务受理,实际是否出画需结合状态中的 health 判断。 * * video_probe(const string& url) * 打开并探测视频源,查询编码、源分辨率及 decoder_compatible 等信息。 * url:待探测的视频源地址;使用 options.probeTimeoutMs 作为超时。 * 探测结果从 video_result(请求号).reply 读取。 * * video_exchange(const string& json) * 发送一条服务协议的原始 JSON 命令,普通业务优先用上面的专用函数。 * json:非空 JSON 对象文本,以 '{' 开头,最多 4095 字节,不能包含 * 实际回车、换行或内嵌空字符;字段含义由服务协议定义。 * * 七、本地结果查询(不发送网络请求,不等待命令执行完成) * video_result(int requestId) -> VideoResult * requestId:前面命令返回的正数请求号。返回该请求当前的结果副本: * state:UNKNOWN 未知/过期,PENDING 排队,RUNNING 执行中,DONE 已完成。 * code:执行结果码,仅 DONE 时有效;HVC_OK(0)表示执行成功。 * channel:对应通道号;全局命令为 0。 * operation:操作名称;reply:服务回复的原始 JSON,完成后读取。 * 仅保留最近 64 个请求结果;无效或已过期请求返回 VIDEO_UNKNOWN。 * * video_finished(const VideoResult& r) -> bool * r:video_result 返回的结果。已完成返回 true,否则返回 false; * 完成不等于成功,也可能是执行失败或取消。 * * video_ok(const VideoResult& r) -> bool * r:video_result 返回的结果。仅“已完成且 code == HVC_OK”返回 true。 * * video_busy() -> bool * 无形参。有排队或正在执行的请求返回 true,否则返回 false。 * 这是命令队列状态,不代表是否正在播放视频或录像。 * * video_error_text(int code) -> const char* * code:命令提交时的负数错误码,或 VideoResult.code。 * 返回对应的英文说明文本;服务具体拒绝原因还需读取 VideoResult.reply。 * * 下方 hvc_* 是内部同步 TCP 接口,已有单独注释;页面直接使用 video_* 即可。 */ #ifndef HMI_VIDEO_H #define HMI_VIDEO_H // 单文件视频接口(C++11)。初始化 video_start(),按钮调用 video_play() 等,退出 video_shutdown()。 // 普通操作立即入队;网络操作在共用工作线程中执行。多个源文件包含本文件共用同一队列。 #if defined(_WIN32) || defined(__MINGW32__) || defined(HMI_PLATFORM_WIN) #define HVC_WINDOWS_STUB 1 #else #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200112L #endif #define HVC_WINDOWS_STUB 0 #endif #include #include #include #include #include #include #include #include #include #include #include #define HVC_HOST_CAP 64 #define HVC_DEFAULT_PORT 8555 #define HVC_DEFAULT_TIMEOUT_MS 3000 typedef struct { char host[HVC_HOST_CAP]; /* 视频服务IP或主机名 */ int port; /* TCP端口,默认8555 */ int timeout_ms; /* 单次连接、发送和接收的总超时 */ } hvc_client_t; enum { HVC_OK = 0, HVC_ERR_ARGUMENT = -1, HVC_ERR_CONNECT = -2, HVC_ERR_TIMEOUT = -3, HVC_ERR_IO = -4, HVC_ERR_RESPONSE = -5, HVC_ERR_REJECTED = -6, HVC_ERR_UNSUPPORTED = -7 }; // ===== 页面使用的普通函数接口 ===== enum { VIDEO_ERR_NOT_STARTED = -100, VIDEO_ERR_QUEUE_FULL = -101, VIDEO_ERR_CANCELLED = -102, VIDEO_ERR_INTERNAL = -103 }; enum VideoState { VIDEO_UNKNOWN, VIDEO_PENDING, VIDEO_RUNNING, VIDEO_DONE }; struct VideoResult { VideoState state = VIDEO_UNKNOWN; int code = HVC_OK; int channel = 0; std::string operation; std::string reply; // 服务原始 JSON;仅完成后有意义 }; struct VideoOptions { std::string host = "127.0.0.1"; int port = HVC_DEFAULT_PORT; int timeoutMs = HVC_DEFAULT_TIMEOUT_MS; std::string script = "/customer/hmi-video-next/run_player.sh"; bool manageUiWindows = true; int startupTimeoutMs = 10000; int probeTimeoutMs = 10000; }; inline bool video_finished(const VideoResult& r) { return r.state == VIDEO_DONE; } inline bool video_ok(const VideoResult& r) { return video_finished(r) && r.code == HVC_OK; } inline int video_start(const VideoOptions& options = VideoOptions()); // 以下命令返回正数请求号(已入队)或负数错误码(未入队)。 // 字符串按值复制,可以直接传页面局部变量。不需要 reply 数组和线程。 inline int video_play(int channel, const std::string& url, int x, int y, int width, int height, int rotation = 0); inline int video_refresh(int channel, const std::string& url, int x, int y, int width, int height, int rotation = 0); inline int video_set_source(int channel, const std::string& url); inline int video_set_rect(int channel, int x, int y, int width, int height); inline int video_set_rotation(int channel, int rotation); inline int video_hide(int channel); inline int video_show(int channel); inline int video_stop(int channel); inline int video_clear(int channel); inline int video_stop_all(); inline int video_record_start(int channel, const std::string& storage = "auto", int segmentSeconds = 60, int maxSegments = 100); inline int video_record_stop(int channel); inline int video_record_stop_all(); // 截图命令完成仅表示服务受理;保存完成需 status 查询相同 job_id 的状态。 inline int video_snapshot(int channel, const std::string& text = "", const std::string& storage = "auto", int fontSize = 24, const std::string& position = "bottom"); inline int video_status(); inline int video_probe(const std::string& url); inline int video_exchange(const std::string& json); // 只读内存,页面定时器可安全调用,不等待网络。保留最近64个请求。 // VIDEO_UNKNOWN 表示请求号无效/已过期;VIDEO_PENDING/VIDEO_RUNNING 时 code 不是执行结果。 inline VideoResult video_result(int requestId); inline bool video_busy(); inline const char* video_error_text(int code); // 仅退出时使用:取消排队请求,等待当前请求结束,回收线程。 // 可能等待网络超时;不会停止独立的视频服务/播放/录像。 inline void video_shutdown(); // ===== 以下为内部实现,无需修改 ===== // 同步 TCP 基础接口。页面优先使用前面的 video_* 异步函数。 extern "C" { /* * 初始化客户端。 * client:由调用方长期保存的客户端结构。 * host:HMI与服务在同一板卡填"127.0.0.1";远程控制时填板卡IP。 * port:服务端口,默认8555。 * timeout_ms:单次TCP调用总超时,建议3000;码流探测建议调用方设为10000。 */ int hvc_client_init(hvc_client_t *client, const char *host, int port, int timeout_ms); /* * 保证本机视频服务已经启动,供HMI开机初始化线程调用。 * client:必须指向127.0.0.1、localhost或::1;端口会传给run_player.sh。 * script_path:板端run_player.sh的实际绝对路径。当前测试板安装在 * /customer/hmi-video-next;客户新装到/customer/hmi-video时, * 传入/customer/hmi-video/run_player.sh。接口内部不写死目录。 * manage_ui_windows:非0时添加--manage-ui-windows,适合尚未自行管理透明窗口的HMI。 * startup_timeout_ms:启动后等待TCP服务就绪的最长时间,范围500到60000毫秒。 * reply/capacity:接收status回复,建议至少16384字节。 * * 函数会先查询status:服务已运行时不会重复启动。未运行时使用fork/exec脱离 * HMI进程启动脚本,不会阻塞到播放器退出。不能从绘制、触摸等UI回调直接调用。 * Linux/ARM可用;Windows/MinGW模拟器返回HVC_ERR_UNSUPPORTED。 */ int hvc_service_ensure_started(const hvc_client_t *client, const char *script_path, int manage_ui_windows, int startup_timeout_ms, char *reply, size_t capacity); /* * 直接发送一条JSON请求。普通业务优先使用下面的类型化函数。 * request不能包含回车或换行;reply由调用方提供。 */ int hvc_exchange(const hvc_client_t *client, const char *request, char *reply, size_t reply_capacity); /* 读取全部通道、内存、MMA、录像及重连状态。4路建议reply至少16384字节。 */ int hvc_status(const hvc_client_t *client, char *reply, size_t capacity); /* 实际打开并探测RTSP,返回编码、源分辨率和decoder_compatible。 */ int hvc_probe(const hvc_client_t *client, const char *url, char *reply, size_t capacity); /* * 播放RTSP或板端本地视频。 * channel:通道号,SSD202D为1到4。 * url:rtsp://地址、storage://地址或允许的板端绝对路径。 * x/y:显示区域左上角坐标。 * width/height:屏幕上的显示大小,不是摄像头源分辨率。 * 本函数不修改该通道原有旋转角度;首次播放默认是0度。 * reply/capacity:接收服务JSON回复,普通命令建议至少2048字节。 */ int hvc_play(const hvc_client_t *client, int channel, const char *url, int x, int y, int width, int height, char *reply, size_t capacity); /* * 播放并在同一条命令中指定顺时针旋转角度。 * rotation:只允许0、90、180、270;width/height始终是屏幕最终显示区域。 * 旋转只作用于channel指定的通道,其他正在播放的通道不受影响。 */ int hvc_play_rotated(const hvc_client_t *client, int channel, const char *url, int x, int y, int width, int height, int rotation, char *reply, size_t capacity); /* * 只保存或修改视频源,不自动开始显示。 * 可先set_source,再record_start,实现只录像、不显示。 */ int hvc_set_source(const hvc_client_t *client, int channel, const char *url, char *reply, size_t capacity); /* 只修改位置和显示大小;URL不变。 */ int hvc_set_rect(const hvc_client_t *client, int channel, int x, int y, int width, int height, char *reply, size_t capacity); /* * 修改正在播放或已配置通道的顺时针旋转角度。 * 只允许0、90、180、270;只重建被修改通道,其他通道不受影响。 * 非0角度由板端MI_GFX旋转该路VDEC输出;0度使用VDEC直连DISP。 */ int hvc_set_rotation(const hvc_client_t *client, int channel, int rotation, char *reply, size_t capacity); /* 隐藏/恢复显示;RTSP连接和解码保持运行,适合HMI页面切换。 */ int hvc_hide(const hvc_client_t *client, int channel, char *reply, size_t capacity); int hvc_show(const hvc_client_t *client, int channel, char *reply, size_t capacity); /* stop释放硬解资源但保留URL;clear同时删除URL和录像请求。 */ int hvc_stop(const hvc_client_t *client, int channel, char *reply, size_t capacity); int hvc_clear(const hvc_client_t *client, int channel, char *reply, size_t capacity); int hvc_stop_all(const hvc_client_t *client, char *reply, size_t capacity); /* * 开始RTSP录像。 * storage:"auto"、"usb"或"sd"。 * segment_seconds:每段目标秒数,范围10到3600;实际会等待关键帧。 * max_segments:当前通道最多保留段数,范围2到10000。 */ int hvc_record_start(const hvc_client_t *client, int channel, const char *storage, int segment_seconds, int max_segments, char *reply, size_t capacity); int hvc_record_stop(const hvc_client_t *client, int channel, char *reply, size_t capacity); int hvc_record_stop_all(const hvc_client_t *client, char *reply, size_t capacity); /* * 保存当前通道的一帧图片,可选文字仅绘制在保存的图片上。 * text:UTF-8,最多512字节;NULL或空字符串表示不加文字。 * 支持LF或CRLF换行;不接受其他C0/C1控制字符(包括制表符)。 * storage:"auto"、"usb"或"sd";NULL默认"auto"。 * font_size:12到96;position:"top"或"bottom",NULL默认"bottom"。 * HVC_OK只表示已受理。保留回复snapshot中的job_id,通过hvc_status的 * channels[].snapshot等待相同job_id的state变为done或error; * done时file才是已保存的图片路径。JPEG质量使用服务默认值90。 * 服务全局同时仅处理一张截图,忙碌时返回HVC_ERR_REJECTED。 */ int hvc_snapshot(const hvc_client_t *client, int channel, const char *text, const char *storage, int font_size, const char *position, char *reply, size_t capacity); /* 把HVC错误码转换成英文短文本;服务拒绝原因仍应读取reply。 */ const char *hvc_result_string(int result); #ifndef HMI_VIDEO_CUSTOM_TRANSPORT #include #include #include #if !HVC_WINDOWS_STUB #include #include #include #include #include #include #include #include #endif #define HVC_REQUEST_CAP 4096 #if !HVC_WINDOWS_STUB inline long long hvc_detail_now_ms(void) { struct timespec value; if (clock_gettime(CLOCK_MONOTONIC, &value) != 0) return 0; return (long long)value.tv_sec * 1000 + value.tv_nsec / 1000000; } inline int hvc_detail_wait_fd(int fd, short events, long long deadline) { struct pollfd item; int result; long long remaining; memset(&item, 0, sizeof(item)); item.fd = fd; item.events = events; do { remaining = deadline - hvc_detail_now_ms(); if (remaining <= 0) return 0; result = poll(&item, 1, (int)remaining); } while (result < 0 && errno == EINTR); if (result <= 0) return result; if (item.revents & events) return 1; return -1; } inline int hvc_detail_connect_tcp(const hvc_client_t *client, long long deadline) { struct addrinfo hint, *addresses = NULL, *address; char port[16]; int fd = -1, timed_out = 0; memset(&hint, 0, sizeof(hint)); hint.ai_family = AF_UNSPEC; hint.ai_socktype = SOCK_STREAM; snprintf(port, sizeof(port), "%d", client->port); if (getaddrinfo(client->host, port, &hint, &addresses) != 0) return HVC_ERR_CONNECT; for (address = addresses; address; address = address->ai_next) { int error = 0, flags; socklen_t error_size = sizeof(error); fd = socket(address->ai_family, address->ai_socktype, address->ai_protocol); if (fd < 0) continue; flags = fcntl(fd, F_GETFL, 0); if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { close(fd); fd = -1; continue; } if (connect(fd, address->ai_addr, address->ai_addrlen) != 0 && errno != EINPROGRESS) { close(fd); fd = -1; continue; } error = hvc_detail_wait_fd(fd, POLLOUT, deadline); if (error == 0) { timed_out = 1; close(fd); fd = -1; continue; } if (error < 0 || getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &error_size) != 0 || error != 0) { close(fd); fd = -1; continue; } break; } freeaddrinfo(addresses); return fd >= 0 ? fd : (timed_out ? HVC_ERR_TIMEOUT : HVC_ERR_CONNECT); } inline int hvc_detail_reply_is_ok(const char *reply) { const char *value = strstr(reply, "\"ok\""); if (!value) return 0; value += 4; while (*value == ' ' || *value == '\t') value++; if (*value++ != ':') return 0; while (*value == ' ' || *value == '\t') value++; return !strncmp(value, "true", 4); } inline void hvc_detail_sleep_milliseconds(int milliseconds) { struct timespec delay; delay.tv_sec = milliseconds / 1000; delay.tv_nsec = (long)(milliseconds % 1000) * 1000000L; while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {} } inline int hvc_detail_is_local_host(const char *host) { return !strcmp(host, "127.0.0.1") || !strcmp(host, "localhost") || !strcmp(host, "::1"); } inline int hvc_detail_start_service_process(const char *script_path, int port, int manage_ui_windows) { char port_text[16]; pid_t launcher; int status; snprintf(port_text, sizeof(port_text), "%d", port); launcher = fork(); if (launcher < 0) return HVC_ERR_IO; if (launcher == 0) { pid_t service; int null_fd; if (setsid() < 0) _exit(126); service = fork(); if (service < 0) _exit(126); if (service > 0) _exit(0); null_fd = open("/dev/null", O_RDWR); if (null_fd >= 0) { (void)dup2(null_fd, STDIN_FILENO); (void)dup2(null_fd, STDOUT_FILENO); (void)dup2(null_fd, STDERR_FILENO); if (null_fd > STDERR_FILENO) close(null_fd); } if (manage_ui_windows) { execl("/bin/sh", "sh", script_path, "--listen", "0.0.0.0", "--port", port_text, "--manage-ui-windows", (char *)NULL); } else { execl("/bin/sh", "sh", script_path, "--listen", "0.0.0.0", "--port", port_text, (char *)NULL); } _exit(127); } while (waitpid(launcher, &status, 0) < 0) { if (errno != EINTR) return HVC_ERR_IO; } if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return HVC_ERR_IO; return HVC_OK; } #endif inline int hvc_client_init(hvc_client_t *client, const char *host, int port, int timeout_ms) { size_t length; if (!client || !host || !*host || port < 1 || port > 65535 || timeout_ms < 1 || timeout_ms > 60000) return HVC_ERR_ARGUMENT; length = strlen(host); if (length >= sizeof(client->host)) return HVC_ERR_ARGUMENT; memset(client, 0, sizeof(*client)); memcpy(client->host, host, length + 1); client->port = port; client->timeout_ms = timeout_ms; return HVC_OK; } inline int hvc_service_ensure_started(const hvc_client_t *client, const char *script_path, int manage_ui_windows, int startup_timeout_ms, char *reply, size_t capacity) { #if !HVC_WINDOWS_STUB hvc_client_t probe; long long deadline; int result; #endif if (!client || !client->host[0] || client->port < 1 || client->port > 65535 || client->timeout_ms < 1 || !script_path || script_path[0] != '/' || startup_timeout_ms < 500 || startup_timeout_ms > 60000 || !reply || capacity < 2) return HVC_ERR_ARGUMENT; reply[0] = 0; #if HVC_WINDOWS_STUB (void)manage_ui_windows; return HVC_ERR_UNSUPPORTED; #else if (!hvc_detail_is_local_host(client->host)) return HVC_ERR_ARGUMENT; result = hvc_status(client, reply, capacity); if (result == HVC_OK || result == HVC_ERR_REJECTED || result == HVC_ERR_RESPONSE) return result; if (result != HVC_ERR_CONNECT && result != HVC_ERR_TIMEOUT && result != HVC_ERR_IO) return result; if (access(script_path, R_OK) != 0) return HVC_ERR_IO; result = hvc_detail_start_service_process(script_path, client->port, manage_ui_windows != 0); if (result != HVC_OK) return result; deadline = hvc_detail_now_ms() + startup_timeout_ms; probe = *client; while (hvc_detail_now_ms() < deadline) { long long remaining = deadline - hvc_detail_now_ms(); probe.timeout_ms = remaining < 500 ? (int)remaining : 500; if (probe.timeout_ms < 1) break; result = hvc_status(&probe, reply, capacity); if (result == HVC_OK || result == HVC_ERR_REJECTED || result == HVC_ERR_RESPONSE) return result; if (result != HVC_ERR_CONNECT && result != HVC_ERR_TIMEOUT && result != HVC_ERR_IO) return result; remaining = deadline - hvc_detail_now_ms(); if (remaining > 0) hvc_detail_sleep_milliseconds(remaining < 200 ? (int)remaining : 200); } return HVC_ERR_TIMEOUT; #endif } inline int hvc_exchange(const hvc_client_t *client, const char *request, char *reply, size_t reply_capacity) { #if !HVC_WINDOWS_STUB int fd, result = HVC_ERR_IO; size_t length, sent = 0, used = 0; long long deadline; #endif if (!client || !client->host[0] || client->port < 1 || client->port > 65535 || client->timeout_ms < 1 || !request || request[0] != '{' || strchr(request, '\r') || strchr(request, '\n') || !reply || reply_capacity < 2) return HVC_ERR_ARGUMENT; reply[0] = 0; #if HVC_WINDOWS_STUB (void)request; return HVC_ERR_UNSUPPORTED; #else length = strlen(request); if (length == 0 || length >= HVC_REQUEST_CAP) return HVC_ERR_ARGUMENT; deadline = hvc_detail_now_ms() + client->timeout_ms; fd = hvc_detail_connect_tcp(client, deadline); if (fd < 0) return fd; while (sent <= length) { const char *data = sent == length ? "\n" : request + sent; size_t pending = sent == length ? 1 : length - sent; int ready = hvc_detail_wait_fd(fd, POLLOUT, deadline); int count; if (ready == 0) { result = HVC_ERR_TIMEOUT; goto done; } if (ready < 0) goto done; #ifdef MSG_NOSIGNAL count = (int)send(fd, data, pending, MSG_NOSIGNAL); #else count = (int)send(fd, data, pending, 0); #endif if (count > 0) sent += (size_t)count; else if (count == 0 || (errno != EAGAIN && errno != EINTR)) goto done; } while (used < reply_capacity - 1) { int ready = hvc_detail_wait_fd(fd, POLLIN, deadline); int count; if (ready == 0) { result = HVC_ERR_TIMEOUT; goto done; } if (ready < 0) goto done; count = (int)recv(fd, reply + used, reply_capacity - 1 - used, 0); if (count > 0) { char *line_end; used += (size_t)count; reply[used] = 0; line_end = strchr(reply, '\n'); if (line_end) { *line_end = 0; if (line_end > reply && line_end[-1] == '\r') line_end[-1] = 0; result = hvc_detail_reply_is_ok(reply) ? HVC_OK : HVC_ERR_REJECTED; goto done; } } else if (count == 0 || (errno != EAGAIN && errno != EINTR)) { result = HVC_ERR_RESPONSE; goto done; } } result = HVC_ERR_RESPONSE; done: close(fd); return result; #endif } inline int hvc_detail_escape_json(const char *source, char *output, size_t capacity) { static const char hex[] = "0123456789abcdef"; size_t used = 0; const unsigned char *p = (const unsigned char *)source; if (!source || !output || capacity == 0) return HVC_ERR_ARGUMENT; while (*p) { unsigned char value = *p++; size_t needed = value < 32 ? 6 : (value == '"' || value == '\\' ? 2 : 1); if (used + needed >= capacity) return HVC_ERR_ARGUMENT; if (value < 32) { output[used++] = '\\'; output[used++] = 'u'; output[used++] = '0'; output[used++] = '0'; output[used++] = hex[value >> 4]; output[used++] = hex[value & 15]; } else if (value == '"' || value == '\\') { output[used++] = '\\'; output[used++] = (char)value; } else { output[used++] = (char)value; } } output[used] = 0; return HVC_OK; } inline int hvc_detail_submit(const hvc_client_t *client, char *reply, size_t capacity, const char *format, ...) { char request[HVC_REQUEST_CAP]; va_list arguments; int length; va_start(arguments, format); length = vsnprintf(request, sizeof(request), format, arguments); va_end(arguments); if (length < 0 || (size_t)length >= sizeof(request)) return HVC_ERR_ARGUMENT; return hvc_exchange(client, request, reply, capacity); } inline int hvc_detail_valid_channel(int channel) { return channel >= 1 && channel <= 32; } inline int hvc_detail_valid_rotation(int rotation) { return rotation == 0 || rotation == 90 || rotation == 180 || rotation == 270; } inline int hvc_detail_simple_channel(const hvc_client_t *client, const char *operation, int channel, char *reply, size_t capacity) { if (!hvc_detail_valid_channel(channel)) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"%s\",\"channel\":%d}", operation, channel); } inline int hvc_status(const hvc_client_t *client, char *reply, size_t capacity) { return hvc_exchange(client, "{\"op\":\"status\"}", reply, capacity); } inline int hvc_probe(const hvc_client_t *client, const char *url, char *reply, size_t capacity) { char escaped[3072]; if (hvc_detail_escape_json(url, escaped, sizeof(escaped)) != HVC_OK) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"probe\",\"url\":\"%s\"}", escaped); } inline int hvc_play(const hvc_client_t *client, int channel, const char *url, int x, int y, int width, int height, char *reply, size_t capacity) { char escaped[3072]; if (!hvc_detail_valid_channel(channel) || x < 0 || y < 0 || width < 1 || height < 1 || hvc_detail_escape_json(url, escaped, sizeof(escaped)) != HVC_OK) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"play\",\"channel\":%d,\"url\":\"%s\"," "\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,\"visible\":true}", channel, escaped, x, y, width, height); } inline int hvc_play_rotated(const hvc_client_t *client, int channel, const char *url, int x, int y, int width, int height, int rotation, char *reply, size_t capacity) { char escaped[3072]; if (!hvc_detail_valid_channel(channel) || x < 0 || y < 0 || width < 1 || height < 1 || !hvc_detail_valid_rotation(rotation) || hvc_detail_escape_json(url, escaped, sizeof(escaped)) != HVC_OK) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"play\",\"channel\":%d,\"url\":\"%s\"," "\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d," "\"rotation\":%d,\"visible\":true}", channel, escaped, x, y, width, height, rotation); } inline int hvc_set_source(const hvc_client_t *client, int channel, const char *url, char *reply, size_t capacity) { char escaped[3072]; if (!hvc_detail_valid_channel(channel) || hvc_detail_escape_json(url, escaped, sizeof(escaped)) != HVC_OK) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"set\",\"channel\":%d,\"url\":\"%s\"}", channel, escaped); } inline int hvc_set_rect(const hvc_client_t *client, int channel, int x, int y, int width, int height, char *reply, size_t capacity) { if (!hvc_detail_valid_channel(channel) || x < 0 || y < 0 || width < 1 || height < 1) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"set\",\"channel\":%d,\"x\":%d,\"y\":%d," "\"w\":%d,\"h\":%d}", channel, x, y, width, height); } inline int hvc_set_rotation(const hvc_client_t *client, int channel, int rotation, char *reply, size_t capacity) { if (!hvc_detail_valid_channel(channel) || !hvc_detail_valid_rotation(rotation)) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"set\",\"channel\":%d,\"rotation\":%d}", channel, rotation); } inline int hvc_hide(const hvc_client_t *client, int channel, char *reply, size_t capacity) { return hvc_detail_simple_channel(client, "hide", channel, reply, capacity); } inline int hvc_show(const hvc_client_t *client, int channel, char *reply, size_t capacity) { return hvc_detail_simple_channel(client, "show", channel, reply, capacity); } inline int hvc_stop(const hvc_client_t *client, int channel, char *reply, size_t capacity) { return hvc_detail_simple_channel(client, "stop", channel, reply, capacity); } inline int hvc_clear(const hvc_client_t *client, int channel, char *reply, size_t capacity) { return hvc_detail_simple_channel(client, "clear", channel, reply, capacity); } inline int hvc_stop_all(const hvc_client_t *client, char *reply, size_t capacity) { return hvc_exchange(client, "{\"op\":\"stop_all\"}", reply, capacity); } inline int hvc_record_start(const hvc_client_t *client, int channel, const char *storage, int segment_seconds, int max_segments, char *reply, size_t capacity) { if (!hvc_detail_valid_channel(channel) || !storage || (strcmp(storage, "auto") && strcmp(storage, "usb") && strcmp(storage, "sd")) || segment_seconds < 10 || segment_seconds > 3600 || max_segments < 2 || max_segments > 10000) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"record_start\",\"channel\":%d,\"storage\":\"%s\"," "\"segment_seconds\":%d,\"max_segments\":%d}", channel, storage, segment_seconds, max_segments); } inline int hvc_record_stop(const hvc_client_t *client, int channel, char *reply, size_t capacity) { return hvc_detail_simple_channel(client, "record_stop", channel, reply, capacity); } inline int hvc_record_stop_all(const hvc_client_t *client, char *reply, size_t capacity) { return hvc_exchange(client, "{\"op\":\"record_stop_all\"}", reply, capacity); } inline int hvc_detail_valid_snapshot_text(const char *text) { const unsigned char *p = (const unsigned char *)text; size_t length = 0, offset = 0; while (length <= 512 && p[length]) ++length; if (length > 512) return 0; while (offset < length) { unsigned char first = p[offset++]; unsigned int codepoint; size_t trailing, i; if (first < 0x80) { if (first == '\r' && offset < length && p[offset] == '\n') { ++offset; continue; } if ((first < 0x20 && first != '\n') || first == 0x7f) return 0; continue; } if (first >= 0xc2 && first <= 0xdf) { trailing = 1; codepoint = first & 0x1f; } else if (first >= 0xe0 && first <= 0xef) { trailing = 2; codepoint = first & 0x0f; } else if (first >= 0xf0 && first <= 0xf4) { trailing = 3; codepoint = first & 0x07; } else { return 0; } if (trailing > length - offset) return 0; for (i = 0; i < trailing; ++i) { unsigned char next = p[offset++]; if ((next & 0xc0) != 0x80) return 0; codepoint = (codepoint << 6) | (next & 0x3f); } if ((trailing == 2 && codepoint < 0x800) || (trailing == 3 && codepoint < 0x10000) || (codepoint >= 0x80 && codepoint <= 0x9f) || (codepoint >= 0xd800 && codepoint <= 0xdfff) || codepoint > 0x10ffff) return 0; } return 1; } inline int hvc_snapshot(const hvc_client_t *client, int channel, const char *text, const char *storage, int font_size, const char *position, char *reply, size_t capacity) { char escaped[512 * 6 + 1]; if (!text) text = ""; if (!storage) storage = "auto"; if (!position) position = "bottom"; if (!hvc_detail_valid_channel(channel) || !hvc_detail_valid_snapshot_text(text) || (strcmp(storage, "auto") && strcmp(storage, "usb") && strcmp(storage, "sd")) || font_size < 12 || font_size > 96 || (strcmp(position, "top") && strcmp(position, "bottom")) || hvc_detail_escape_json(text, escaped, sizeof(escaped)) != HVC_OK) return HVC_ERR_ARGUMENT; return hvc_detail_submit(client, reply, capacity, "{\"op\":\"snapshot\",\"channel\":%d,\"text\":\"%s\"," "\"storage\":\"%s\",\"font_size\":%d,\"position\":\"%s\"}", channel, escaped, storage, font_size, position); } inline const char *hvc_result_string(int result) { switch (result) { case HVC_OK: return "success"; case HVC_ERR_ARGUMENT: return "invalid argument"; case HVC_ERR_CONNECT: return "TCP connection failed"; case HVC_ERR_TIMEOUT: return "TCP request timed out"; case HVC_ERR_IO: return "TCP send or receive failed"; case HVC_ERR_RESPONSE: return "invalid or oversized response"; case HVC_ERR_REJECTED: return "video service rejected the request"; case HVC_ERR_UNSUPPORTED: return "TCP control is disabled in Windows simulator"; default: return "unknown result"; } } #endif } namespace hmi_video_detail { typedef std::function Command; struct Job { int id; Command command; }; struct VideoData { std::mutex mutex; std::mutex lifecycle; std::condition_variable wake; std::thread worker; std::deque queue; std::map results; VideoOptions options; hvc_client_t client; bool accepting = false; bool stopping = false; bool ready = false; // 仅由内部工作线程访问 int pending = 0; int nextId = 0; }; inline VideoData& state() { static VideoData value; return value; } inline void trim(); inline int ensure(char*, size_t); inline void run(); inline int enqueue(const char* operation, int channel, const Command& command) { VideoData& s = state(); std::lock_guard lock(s.mutex); if (!s.accepting) return VIDEO_ERR_NOT_STARTED; if (s.pending >= 32) return VIDEO_ERR_QUEUE_FULL; if (s.nextId == INT_MAX) return VIDEO_ERR_INTERNAL; // 不重用请求号 const int id = ++s.nextId; VideoResult value; value.state = VIDEO_PENDING; value.channel = channel; value.operation = operation; s.results[id] = value; s.queue.push_back(Job{id, command}); ++s.pending; trim(); s.wake.notify_one(); return id; } inline void trim() { VideoData& s = state(); while (s.results.size() > 64) { std::map::iterator it = s.results.begin(); while (it != s.results.end() && !video_finished(it->second)) ++it; if (it == s.results.end()) break; s.results.erase(it); } } inline int ensure(char* reply, size_t capacity) { VideoData& s = state(); if (s.ready) return HVC_OK; int code = s.options.script.empty() ? hvc_status(&s.client, reply, capacity) : hvc_service_ensure_started(&s.client, s.options.script.c_str(), s.options.manageUiWindows, s.options.startupTimeoutMs, reply, capacity); s.ready = code == HVC_OK; return code; } inline void run() { VideoData& s = state(); for (;;) { Job job; { std::unique_lock lock(s.mutex); s.wake.wait(lock, [&s] { return s.stopping || !s.queue.empty(); }); if (s.stopping) return; job = s.queue.front(); s.queue.pop_front(); s.results[job.id].state = VIDEO_RUNNING; } char reply[32768] = {}; int code; try { code = ensure(reply, sizeof(reply)); if (code == HVC_OK && job.command) { reply[0] = 0; code = job.command(&s.client, reply, sizeof(reply)); } } catch (...) { code = VIDEO_ERR_INTERNAL; reply[0] = 0; } if (code == HVC_ERR_CONNECT || code == HVC_ERR_TIMEOUT || code == HVC_ERR_IO) s.ready = false; // 下一次调用重新检查服务,不重复执行失败命令 std::string operation; int channel = 0; { std::lock_guard lock(s.mutex); VideoResult& value = s.results[job.id]; value.code = code; value.reply = reply; value.state = VIDEO_DONE; --s.pending; operation = value.operation; channel = value.channel; } // 日志不持有队列锁,不输出可能含密码/隐私的JSON。 if (code != HVC_OK) std::fprintf(stderr, "HmiVideo %s ch=%d request=%d: %s\n", operation.c_str(), channel, job.id, video_error_text(code)); } } } inline int video_start(const VideoOptions& options) { hmi_video_detail::VideoData& s = hmi_video_detail::state(); std::lock_guard lifecycle(s.lifecycle); std::lock_guard lock(s.mutex); if (s.accepting || s.worker.joinable()) return HVC_ERR_ARGUMENT; if (options.startupTimeoutMs < 500 || options.startupTimeoutMs > 60000 || options.probeTimeoutMs < 1 || options.probeTimeoutMs > 60000 || options.host.find('\0') != std::string::npos || options.script.find('\0') != std::string::npos || (!options.script.empty() && (options.script[0] != '/' || (options.host != "127.0.0.1" && options.host != "localhost" && options.host != "::1")))) return HVC_ERR_ARGUMENT; int code = hvc_client_init(&s.client, options.host.c_str(), options.port, options.timeoutMs); if (code != HVC_OK) return code; if (s.nextId == INT_MAX) return VIDEO_ERR_INTERNAL; s.options = options; s.ready = false; s.stopping = false; static const bool cleanupRegistered = (std::atexit(video_shutdown) == 0); if (!cleanupRegistered) return VIDEO_ERR_INTERNAL; const int id = ++s.nextId; VideoResult value; value.state = VIDEO_PENDING; value.operation = "start"; s.results[id] = value; s.queue.push_back(hmi_video_detail::Job{id, hmi_video_detail::Command()}); s.pending = 1; try { s.worker = std::thread(hmi_video_detail::run); } catch (...) { s.queue.clear(); s.pending = 0; s.results[id].state = VIDEO_DONE; s.results[id].code = VIDEO_ERR_INTERNAL; return VIDEO_ERR_INTERNAL; } s.accepting = true; hmi_video_detail::trim(); return id; } inline void video_shutdown() { hmi_video_detail::VideoData& s = hmi_video_detail::state(); std::lock_guard lifecycle(s.lifecycle); { std::lock_guard lock(s.mutex); s.accepting = false; s.stopping = true; for (const hmi_video_detail::Job& job : s.queue) { VideoResult& value = s.results[job.id]; value.state = VIDEO_DONE; value.code = VIDEO_ERR_CANCELLED; --s.pending; } s.queue.clear(); s.wake.notify_all(); } if (s.worker.joinable()) s.worker.join(); } inline VideoResult video_result(int id) { hmi_video_detail::VideoData& s = hmi_video_detail::state(); std::lock_guard lock(s.mutex); std::map::const_iterator it = s.results.find(id); return it == s.results.end() ? VideoResult() : it->second; } inline bool video_busy() { hmi_video_detail::VideoData& s = hmi_video_detail::state(); std::lock_guard lock(s.mutex); return s.pending != 0; } inline const char* video_error_text(int code) { switch (code) { case VIDEO_ERR_NOT_STARTED: return "video wrapper is not started"; case VIDEO_ERR_QUEUE_FULL: return "video command queue is full"; case VIDEO_ERR_CANCELLED: return "video request cancelled on shutdown"; case VIDEO_ERR_INTERNAL: return "video wrapper internal error"; default: return hvc_result_string(code); } } namespace hmi_video_detail { inline bool validChannel(int channel) { return channel >= 1 && channel <= 4; } inline bool validRect(int x, int y, int width, int height) { return x >= 0 && y >= 0 && x % 2 == 0 && y % 2 == 0 && width > 0 && width % 32 == 0 && height > 0 && height % 2 == 0; } inline bool validRotation(int r) { return r == 0 || r == 90 || r == 180 || r == 270; } inline bool validString(const std::string& s, size_t max) { return s.size() <= max && s.find('\0') == std::string::npos; } inline bool validStorage(const std::string& s) { return s == "auto" || s == "usb" || s == "sd"; } } inline int video_play(int channel, const std::string& url, int x, int y, int w, int h, int rotation) { if (!hmi_video_detail::validChannel(channel) || !hmi_video_detail::validRect(x,y,w,h) || !hmi_video_detail::validRotation(rotation) || url.empty() || !hmi_video_detail::validString(url, 3000)) return HVC_ERR_ARGUMENT; return hmi_video_detail::enqueue("play", channel, [=](const hvc_client_t* c, char* r, size_t n) { return hvc_play_rotated(c, channel, url.c_str(), x,y,w,h,rotation,r,n); }); } inline int video_refresh(int channel, const std::string& url, int x, int y, int w, int h, int rotation) { if (!hmi_video_detail::validChannel(channel) || !hmi_video_detail::validRect(x,y,w,h) || !hmi_video_detail::validRotation(rotation) || url.empty() || !hmi_video_detail::validString(url, 3000)) return HVC_ERR_ARGUMENT; return hmi_video_detail::enqueue("refresh", channel, [=](const hvc_client_t* c, char* r, size_t n) { int code = hvc_stop(c, channel, r, n); return code == HVC_OK ? hvc_play_rotated(c,channel,url.c_str(),x,y,w,h,rotation,r,n) : code; }); } inline int video_set_source(int channel, const std::string& url) { if (!hmi_video_detail::validChannel(channel) || url.empty() || !hmi_video_detail::validString(url, 3000)) return HVC_ERR_ARGUMENT; return hmi_video_detail::enqueue("setSource", channel, [=](const hvc_client_t* c, char* r, size_t n) { return hvc_set_source(c,channel,url.c_str(),r,n); }); } inline int video_set_rect(int channel, int x, int y, int w, int h) { if (!hmi_video_detail::validChannel(channel) || !hmi_video_detail::validRect(x,y,w,h)) return HVC_ERR_ARGUMENT; return hmi_video_detail::enqueue("setRect", channel, [=](const hvc_client_t* c, char* r, size_t n) { return hvc_set_rect(c,channel,x,y,w,h,r,n); }); } inline int video_set_rotation(int channel, int rotation) { if (!hmi_video_detail::validChannel(channel) || !hmi_video_detail::validRotation(rotation)) return HVC_ERR_ARGUMENT; return hmi_video_detail::enqueue("setRotation", channel, [=](const hvc_client_t* c, char* r, size_t n) { return hvc_set_rotation(c,channel,rotation,r,n); }); } #define HMI_VIDEO_CHANNEL_METHOD(method, function) \ inline int video_##method(int channel) { \ if (!hmi_video_detail::validChannel(channel)) return HVC_ERR_ARGUMENT; \ return hmi_video_detail::enqueue(#method,channel,[=](const hvc_client_t* c,char* r,size_t n) { \ return function(c,channel,r,n); \ }); \ } HMI_VIDEO_CHANNEL_METHOD(hide, hvc_hide) HMI_VIDEO_CHANNEL_METHOD(show, hvc_show) HMI_VIDEO_CHANNEL_METHOD(stop, hvc_stop) HMI_VIDEO_CHANNEL_METHOD(clear, hvc_clear) HMI_VIDEO_CHANNEL_METHOD(record_stop, hvc_record_stop) #undef HMI_VIDEO_CHANNEL_METHOD #define HMI_VIDEO_GLOBAL_METHOD(method, function) \ inline int video_##method() { \ return hmi_video_detail::enqueue(#method,0,[](const hvc_client_t* c,char* r,size_t n) { \ return function(c,r,n); \ }); \ } HMI_VIDEO_GLOBAL_METHOD(stop_all, hvc_stop_all) HMI_VIDEO_GLOBAL_METHOD(record_stop_all, hvc_record_stop_all) HMI_VIDEO_GLOBAL_METHOD(status, hvc_status) #undef HMI_VIDEO_GLOBAL_METHOD inline int video_record_start(int channel, const std::string& storage, int seconds, int count) { if (!hmi_video_detail::validChannel(channel) || !hmi_video_detail::validStorage(storage) || seconds < 10 || seconds > 3600 || count < 2 || count > 10000) return HVC_ERR_ARGUMENT; return hmi_video_detail::enqueue("recordStart",channel,[=](const hvc_client_t* c,char* r,size_t n) { return hvc_record_start(c,channel,storage.c_str(),seconds,count,r,n); }); } inline int video_snapshot(int channel, const std::string& text, const std::string& storage, int fontSize, const std::string& position) { if (!hmi_video_detail::validChannel(channel) || !hmi_video_detail::validString(text,512) || !hmi_video_detail::validStorage(storage) || fontSize < 12 || fontSize > 96 || (position != "top" && position != "bottom")) return HVC_ERR_ARGUMENT; return hmi_video_detail::enqueue("snapshot",channel,[=](const hvc_client_t* c,char* r,size_t n) { return hvc_snapshot(c,channel,text.c_str(),storage.c_str(),fontSize,position.c_str(),r,n); }); } inline int video_probe(const std::string& url) { if (url.empty() || !hmi_video_detail::validString(url,3000)) return HVC_ERR_ARGUMENT; // options 在内部线程结束前保持不变;超时在内部线程中读取。 return hmi_video_detail::enqueue("probe",0,[=](const hvc_client_t* c,char* r,size_t n) { hvc_client_t probeClient = *c; probeClient.timeout_ms = hmi_video_detail::state().options.probeTimeoutMs; return hvc_probe(&probeClient,url.c_str(),r,n); }); } inline int video_exchange(const std::string& json) { if (json.empty() || json[0] != '{' || !hmi_video_detail::validString(json,4095) || json.find_first_of("\r\n") != std::string::npos) return HVC_ERR_ARGUMENT; return hmi_video_detail::enqueue("exchange",0,[=](const hvc_client_t* c,char* r,size_t n) { return hvc_exchange(c,json.c_str(),r,n); }); } #endif