从 Web 到桌面:基于 Tauri 2.0 + Vue 3 打造 vivo 线下门店「大头贴」拍照体验系统

作者:vivo 互联网大前端团队- Yang Maoxiang

本文介绍 vivo 线下门店「大头贴」拍照合成打印一体化桌面应用软件的技术方案。该项目基于 Tauri 2.0 + Rust + Vue 3 构建,实现了手机实时投屏、智能拍照、Live Photo 处理、模板合成、视频生成、跨平台打印等核心能力,为门店用户提供沉浸式拍照体验。

1分钟看图掌握核心要点👇

一、项目背景

vivo线下门店需搭建一套沉浸式拍照体验系统------用户在门店使用vivo手机进行拍照,深度体验 vivo 影像系统的技术优势(如蔡司光学镜头、人像效果、Live Photo 动态照片等),拍摄画面实时投屏到大屏幕,用户选择心仪的照片后自动合成精美模板,最终可以打印成实体照片并支持扫码获取电子版。

这套系统面临几个核心挑战:

  • **实时性要求高:**手机画面需要毫秒级低延迟投屏到大屏,确保拍照体验流畅
  • **跨平台部署:**需同时支持 macOS 和 Windows 门店设备
  • **设备交互复杂:**需要与 Android 手机、打印机等硬件深度交互
  • **多媒体处理能力:**涉及照片合成、Live Photo 视频处理、HEVC 转码等
  • **稳定性要求极高:**门店设备长时间运行,需保持软件运行稳定

经过技术选型评估,我们最终选择了 Tauri 2.0 作为桌面应用框架。

二、为什么选择 Tauri 而不是 Electron?

选择 Tauri 的关键决策因素:

  • **Rust 后端:**ADB 命令调用、FFmpeg 转码、进程管理等系统级操作,Rust 的性能和安全性远超 Node.js。
  • **更小的包体积:**Tauri 框架本身仅 ~8MB(使用系统 WebView),集成 scrcpy、FFmpeg 等工具链后总计 71MB,而 Electron 方案还需额外承担 ~150MB 的 Chromium 开销。
  • **低内存占用:**门店设备配置有限,低内存占用意味着更好的稳定性。
  • **原生窗口控制:**Tauri 提供了对系统窗口的精细控制能力,这对 scrcpy 窗口嵌入至关重要。

三、整体架构设计

3.1 技术栈全景

3.2 核心业务流程

系统支持两种拍照模式:

  • **四宫格模式:**拍摄 4 张照片,合成为经典的「人生四宫格」大头贴
  • **报纸机模式:**单张横屏拍摄,支持 Live Photo 动态照片

四、核心实现方案

核心技术挑战主要集中在三个模块:Rust侧的进程管理与系统调用、前后端协同的模板合成,以及跨平台差异的封装适配。

4.1 手机实时投屏------scrcpy 集成与窗口控制

通过 Rust 后端管理 scrcpy(开源的 Android 投屏工具)进程,实现手机画面到大屏的低延迟实时投屏,主要难点在进程生命周期管理、性能调优和窗口精准控制上。

进程状态管理

使用 Rust 的 Mutex<Option> 管理

scrcpy 进程生命周期:

rust 复制代码
    static SCRCPY_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
    
    // 启动前先关闭旧进程(并等待其释放 ADB 连接)
    if let Ok(mut guard)= SCRCPY_PROCESS.lock() {
        if let Some(mut old_child)= guard.take() {
            let _ = old_child.kill();
            let _ = old_child.wait(); // 等待进程退出,释放 ADB 连接
        }
    }

确保同一时间只有一个 scrcpy 投屏进程在运行,防止多进程冲突、端口占用、设备连接异常,保证投屏稳定,否则多个 scrcpy 实例会抢占 ADB 连接,导致投屏失败。

性能调优参数

针对门店场景的画质与延迟需求,我们定制了 scrcpy 启动参数:

rust 复制代码
    let mut args = vec![
        "-s".to_string(), device_id.clone(),
        "--stay-awake".to_string(),           // 保持设备唤醒,防止 CPU 休眠
        "--disable-screensaver".to_string(),   // 禁止息屏,保持屏幕常亮
        "--no-audio".to_string(),              // 禁用音频捕获,避免音频权限错误
        "--video-codec=h264".to_string(),      // H.264 编码,兼容性最佳
        "--max-fps=60".to_string(),            // 60 FPS 高帧率
        "--video-bit-rate=8M".to_string(),     // 8Mbps 高码率保证画质
        "--max-size=1920".to_string(),         // 限制最大分辨率,减少传输数据量
        "--window-borderless".to_string(),     // 去除窗口边框
        "--always-on-top".to_string(),         // 窗口置顶
    ];

这组参数平衡了画质和延迟:h264 编码确保硬件解码可用,8Mbps 码率保证清晰度,max-size=1920 在不损失视觉效果的前提下显著降低传输带宽。

窗口精准定位算法

scrcpy 窗口需要精确嵌入到应用界面中,与前端 UI 对齐,这里有一个容易忽视的问题:前端使用 postcss-px-to-viewport 做响应式适配(设计稿宽度 720px),Rust 后端需要计算出与前端 vw 单位一致的物理像素位置。

rust 复制代码
    // 前端使用 postcss-px-to-viewport,设计稿宽度 720px
    // Rust 后端需要动态计算与前端 vw 单位一致的位置
    let design_width = 720.0;
    let logical_width = inner_size.width as f64 / scale_factor;
    let top_bar_height_logical = SCRCPY_TOP_MARGIN * logical_width / design_width;
    
    // 竖屏模式:高度撑满可用区域,宽度按手机宽高比计算
    let scrcpy_width = (available_height as f64 * 0.454) as i32; // 1080/2376 ≈ 0.454
    let scrcpy_x = position.x + (inner_size.width as i32 - scrcpy_width) / 2; // 水平居中

这套算法实现了以下效果:

  • **竖屏模式:**scrcpy 窗口高度撑满可用区域,宽度按手机比例自适应,水平居中。
  • **横屏模式:**scrcpy 窗口宽度撑满应用,高度按比例计算,垂直居中。
  • **DPI 感知:**通过 scale_factor 正确处理 Windows 高分屏(125%、150% 缩放)。

Windows 窗口嵌入(Win32 API)

在 Windows 平台,我们实现了将 scrcpy 窗口嵌入到主应用窗口内部的能力,提供更沉浸的体验:

scss 复制代码
    #[cfg(target_os = "windows")]
    fn embed_scrcpy_window(parent_hwnd: isize, ...) -> Option<isize> {
        unsafe {
            // 1. 修改窗口样式:移除边框,设置为子窗口
            let style = GetWindowLongPtrA(scrcpy_hwnd, GWL_STYLE);
            let new_style = (style & !WS_POPUP & !WS_CAPTION & !WS_THICKFRAME) | WS_CHILD;
            SetWindowLongPtrA(scrcpy_hwnd, GWL_STYLE, new_style);
    
            // 2. 设置父窗口
            SetParent(scrcpy_hwnd, parent_hwnd);
    
            // 3. 调整位置和大小
            SetWindowPos(scrcpy_hwnd, HWND_TOPMOST, x, y, width, height, ...);
        }
    }

通过 EnumWindows + 唯一窗口标题的方式定位 scrcpy 窗口句柄,再通过 Win32 API 修改窗口样式并嵌入。

整个投屏模块的难点不在单个环节,而在于这四个环节必须串起来才能稳定工作:进程只能存在一个、参数配置、窗口位置要和前端 UI 像素级对齐、嵌入时须等待窗口就绪,任何一个环节都会影响实际投屏效果。

4.2 多级缓存策略------减少 ADB 命令开销

与 Android 设备通信依赖 ADB,每次调用都是一次进程创建,但是在高频轮询场景(如拍照计数、设备状态检测)下,频繁的 ADB 调用会严重影响性能,因此我们设计了三层缓存策略:

rust 复制代码
    // 1. 工具路径缓存------避免每次查找 ADB/scrcpy 路径
    static ADB_PATH_CACHE: Mutex<Option<String>> = Mutex::new(None);
    static SCRCPY_PATH_CACHE: Mutex<Option<String>> = Mutex::new(None);
    
    // 2. 设备 ID 缓存------带 TTL 的设备信息缓存
    static DEVICE_ID_CACHE: Mutex<Option<(String, std::time::Instant)>> = Mutex::new(None);
    const DEVICE_CACHE_TTL_SECS: u64 = 5;
    
    // 3. 合并 ADB 命令------一次调用获取多项数据
    #[tauri::command]
    async fn get_photo_status() -> Result<(String, usize), String> {
        // 单次 shell 调用同时获取最新文件名和照片数量
        let script = r#"ls -t /sdcard/DCIM/Camera/*.jpg 2>/dev/null | head -1;
                        ls /sdcard/DCIM/Camera/ 2>/dev/null | grep -iE '\.(jpg|jpeg|png)$' | wc -l"#;
        // ...
    }

优化效果:

  • ADB 路径查找从每次 ~50ms 降低到首次缓存后 ~0ms
  • 设备状态轮询从 2 次 ADB 调用合并为 1 次
  • 整体 ADB 调用频率降低约 60%

这套缓存策略的思路核心:尽量把 ADB 调用次数压到最低,让前端轮询感受不到后端的进程创建开销。

4.3 素材处理与模板合成引擎

素材处理采用前后端分工:前端 Canvas 负责静态图合成,Rust 后端负责 HEVC 转码和 Live Photo 检测,Rust FFmpeg 负责视频合成。

双引擎合成架构(Canvas + FFmpeg)

模板合成要解决的问题是:将用户照片按指定位置、角度、尺寸嵌入模板,生成最终作品。根据是否包含 Live Photo,系统使用两套合成引擎:

1)JSON 驱动的坐标系统

模板合成的核心数据结构是一份 JSON 配置,定义了画布尺寸和每个照片槽位的精确坐标:

yaml 复制代码
    // 模板配置结构(后台下发)
    {
      imageWidth: 1080,          // 画布宽度
      imageHeight: 1920,         // 画布高度
      imgUrl: "模板图片URL",      // 含透明镂空区域的模板图
      annotations: [             // 照片槽位列表
        {
          data: { x: 54, y: 120, width: 480, height: 640 },
          type: "image"
        },
        // ... 支持四宫格(4 槽位)和单图(1 槽位)两种模式
      ]
    }

这份 JSON 同时驱动 Canvas 和 FFmpeg 两套引擎------前端和后端消费同一份配置,产出视觉一致的结果。

这份坐标数据由配套的模板标注编辑器 (基于 Canvas 2D)生成------相关运营人员在模板图上可以手动绘制和调整照片槽位。编辑器的核心设计是图片像素坐标系,所有标注数据基于图片原始分辨率,与视图缩放完全解耦:

arduino 复制代码
    // 屏幕坐标 → 图片像素坐标的转换
    getMousePos(e) {
        const rect = canvas.getBoundingClientRect()
        const x = (e.clientX - rect.left - offsetX) / scale
        const y = (e.clientY - rect.top - offsetY) / scale
        return { x, y }
    }

这样无论编辑器缩放到什么比例,输出的坐标都是基于图片原始分辨率的,编辑器所见即合成所得。编辑器还支持矩形绘制、旋转、等比缩放、辅助对齐线(距离 < 5px 自动吸附)、撤销/重做等操作。

完整的数据流转链路:

2)图层叠加策略

模板与照片合成方案采用"三明治"分层结构:

模板图的镂空区域正好露出下方用户所拍摄的照片。

3)object-fit: cover 双端一致实现

用户照片的宽高比与槽位不一定匹配,需要实现 CSS object-fit: cover 的等效裁剪------保持比例填满容器,居中裁剪溢出部分。这个算法需要在 Canvas 和 FFmpeg 中各实现一次且结果完全一致:

Canvas 实现(前端):

ini 复制代码
    const drawImageCover = (ctx, img, x, y, w, h) => {
      const imgRatio = img.width / img.height
      const containerRatio = w / h
      let sx, sy, sWidth, sHeight
      if (imgRatio > containerRatio) {
        // 图片更宽 → 裁剪左右,保留上下
        sHeight = img.height
        sWidth = img.height * containerRatio
        sx = (img.width - sWidth) / 2// 居中取图
        sy = 0
      } else {
        // 图片更高 → 裁剪上下,保留左右
        sWidth = img.width
        sHeight = img.width / containerRatio
        sx = 0
        sy = (img.height - sHeight) / 2
      }
      ctx.drawImage(img, sx, sy, sWidth, sHeight, x, y, w, h)
    }

FFmpeg 实现(Rust 后端)------

用 scale + crop 滤镜组合实现同一效果:

ini 复制代码
    let cover_filter = format!(
        // scale: 短边撑满,长边溢出(-1 表示自动计算保持比例)
        "scale='if(gt(iw*{th},ih*{tw}),-1,{tw})':'if(gt(iw*{th},ih*{tw}),{th},-1)'\
         :flags=bilinear,\
         crop={tw}:{th}:(iw-{tw})/2:(ih-{th})/2,setsar=1",
        tw = target_w, th = target_h
    );

FFmpeg scale 表达式中的 if(gt(iw*th, ih*tw), ...) 和前端的 imgRatio > containerRatio 本质是同一个数学判断,只是写法不同,所以双端裁剪结果一致。

Live Photo 智能检测

vivo 手机拍摄的 Live Photo 由一张 .jpg 和一个同名 .mp4 视频组成。检测算法需要处理两种情况:

rust 复制代码
    // 批量获取相册中所有 .mp4 文件名,构建 HashSet 加速查找
    fn get_mp4_file_set(adb_path: &str) -> HashSet<String> { ... }
    
    // 检测一张照片是否是 Motion Photo
    fn is_motion_photo(filename: &str, mp4_set: &HashSet<String>) -> bool{
        // 策略一:文件名包含 "MVIMG" 前缀(旧版 vivo 命名规则)
        if filename.contains("MVIMG") { returntrue; }
        // 策略二:存在同名 .mp4 文件(新版 vivo 命名规则)
        let stem = filename.trim_end_matches(".jpg");
        mp4_set.contains(&format!("{}.mp4", stem))
    }

这里的关键优化是:仅在报纸机模式(需要 Live Photo)时才执行检测,四宫格模式直接跳过,避免无谓的 ADB 调用。同时通过 HashSet 将 O(N²) 的逐个查找优化为 O(N) 的批量比对。

HEVC → H.264 转码(硬件加速优先)

vivo 手机 Live Photo 的视频默认使用 HEVC (H.265) 编码,但 Web 端对 HEVC 支持不够友好,因此我们实现了自动转码机制:

rust 复制代码
    fn transcode_to_h264(video_data: &[u8]) -> Option<Vec<u8>> {
        // 硬件编码器优先级列表(按平台区分)
        let encoders = if cfg!(target_os = "macos") {
            vec!["h264_videotoolbox", "libx264"]  // macOS 使用 VideoToolbox 硬件加速
        } else {
            vec!["h264_nvenc", "h264_qsv", "h264_amf", "libx264"]  // Windows 依次尝试 NVIDIA/Intel/AMD
        };
    
        for encoder in &encoders {
            // 逐个尝试,硬件不可用时自动降级到软编码
            let args = if *encoder == "libx264" {
                // 软编码:极速预设 + CRF 质量控制
                vec!["-c:v", "libx264", "-preset", "ultrafast", "-crf", "23", ...]
            } else {
                // 硬编码:固定码率,速度极快
                vec!["-c:v", encoder, "-b:v", "5M", ...]
            };
            // ...
        }
    }

此外,对于报纸机的横屏镜像拍照模式,我们在缩略图获取命令中集成了 FFmpeg hflip 滤镜,一次调用完成拉取 + 翻转,避免额外的 IPC 开销。

整个模板合成模块的设计思路是「一份 JSON 配置驱动两套引擎」------同一份模板数据既能在前端 Canvas 实时预览,也能在Rust侧合成高质量视频,object-fit cover则保证了双端产出结果一致。

4.4 跨平台打印适配

系统需要支持多种打印场景(6×8 照片纸、A3/A4 报纸、自动横竖方向),且 macOS 和 Windows 的打印 API 完全不同。

**macOS:**通过 lpr 命令直接发送到默认打印机

css 复制代码
    #[cfg(target_os = "macos")]
    {
        Command::new("lpr")
            .arg("-o").arg("print-color-mode=color")
            .arg(&file_path)
            .output()
    }

**Windows:**通过 PowerShell 调用 System.

Drawing 实现精细控制

php 复制代码
    #[cfg(target_os = "windows")]
    {
        // 完整的打印控制:
        // 1. EXIF 旋转校正
        // 2. 纸张自动检测(6x8/A3/A4)
        // 3. 横竖方向自适应
        // 4. object-fit: contain 居中绘制
        // 5. 高质量双三次插值缩放
        let ps_script = format!(r#"
            Add-Type -AssemblyName System.Drawing
            $img = [System.Drawing.Image]::FromFile('{file}')
            # 处理 EXIF 旋转...
            # 自动检测纸张...
            # 计算 contain 模式的缩放和居中...
            $pd.Print()
        "#);
    }

两个平台的打印 API 存在很大的差异,但对前端来说只需一次 invoke('print_image'),所有平台差异都在 Rust 层完成封装。

运营人员在悟空系统配置好相关活动后,在浏览器中访问活动页面并选择对应门店,点击「启动」按钮即可通过 URL Scheme 唤起桌面应用,自动携带活动ID、门店信息等参数:

arduino 复制代码
vivo-photo://callback?{业务参数}

为确保整个系统在门店端稳定运行,我们基于 tauri-plugin-single-instance 和 tauri-plugin-deep-link 两个插件,实现了一套完整的单实例管控 + Deep Link 全链路处理机制:

后端(Rust)单实例拦截与参数转发

通过 single-instance 插件保证应用全局只能运行一个实例,当第二个实例启动时,自动拦截并将启动参数转发给已运行的主实例,同时聚焦主窗口,避免多开冲突。

javascript 复制代码
    // 单实例保护:第二个实例启动时,转发参数给已运行实例
    .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
        if let Some(url) = args.get(1) {
            if url.starts_with("vivo-photo://") {
                let _ = app.emit("deep-link://new-url", vec![url.clone()]);
            }
        }
        // 聚焦到已有窗口
        if let Some(window) = app.get_webview_window("main") {
            let _ = window.set_focus();
        }
    }))

前端(Web)全场景 Deep Link 处理

前端配套实现了完整的协议监听与处理逻辑,覆盖所有触发场景:

scss 复制代码
    // 1. 首次启动获取 Deep Link(通过 URL Scheme 启动时)
    const initialUrls = await getCurrent()
    if (initialUrls?.length > 0) await handleDeepLink(initialUrls)
    
    // 2. 运行时监听新的 Deep Link
    unlistenDeepLink = await onOpenUrl(handleDeepLink)
    
    // 3. 单实例转发的 Deep Link
    unlistenSingleInstance = await listen('deep-link://new-url', event => {
      handleDeepLink(event.payload)
    })

这套方案完整覆盖了三大核心场景:

  • 通过 URL Scheme 首次冷启动应用
  • 应用运行时接收新的 Deep Link 请求
  • 应用运行过程中拦截第二实例并转发协议参数

最终实现了应用软件的一键启动:门店导购仅需在活动站点中选择对应门店后点击启动按钮,客户端即可自动拉起,并根据活动参数实时更新活动配置,大幅提升门店使用效率。

4.6 异步架构------不阻塞 UI 线程

Tauri 的 IPC 命令默认在主线程执行,耗时操作会导致 UI 卡顿,因此我们对所有可能耗时的操作都采用了 async + spawn_blocking 模式:

rust 复制代码
    #[tauri::command]
    async fn get_photo_thumbnail(filename: String, flip: Option<bool>) -> Result<String, String> {
        tauri::async_runtime::spawn_blocking(move || {
            // 耗时操作在独立线程池中执行
            let adb_path = find_adb()?;
            let output = create_command(&adb_path)
                .args(&["exec-out", "cat", &source])
                .output()?;
            // Base64 编码(大图片可能需要数百毫秒)
            let base64_image = base64_encode(&output.stdout);
            Ok(format!("data:image/jpeg;base64,{}", base64_image))
        }).await
        .map_err(|e| format!("异步任务失败: {}", e))?
    }

我们让所有 ADB 调用、FFmpeg 转码、Base64 编码等耗时操作都采用了这种模式,确保主线程只做 IPC 调度和 UI 渲染。

4.7 条件编译------一套代码适配双平台

由于大量系统级操作在 macOS 和 Windows 上实现是完全不同的,因此我们通过 Rust 的 cfg 条件编译来解决平台差异:

rust 复制代码
   // 隐藏 Windows 控制台窗口
    #[cfg(target_os = "windows")]
    fn create_command(program: &str) -> Command {
        let mut cmd = Command::new(program);
        cmd.creation_flags(CREATE_NO_WINDOW); // 0x08000000
        cmd
    }
    
    #[cfg(not(target_os = "windows"))]
    fn create_command(program: &str) -> Command {
        Command::new(program)
    }
    
    // 文件保存路径策略
    #[cfg(target_os = "windows")]
    let save_dir = base_path.join("vivo大头贴").join("vivo大头贴素材");
    // Windows: AppData\Local(避免 Win11 权限弹窗)
    
    #[cfg(not(target_os = "windows"))]
    let save_dir = base_path.join("vivo大头贴素材");
    // macOS: ~/Desktop(用户方便访问)

项目中共有 20+ 处条件编译分支,覆盖进程创建、文件路径、窗口嵌入、打印、FFmpeg 查找、窗口装饰等所有平台差异点,确保一套代码同时适配 macOS 和 Windows。

4.8 双模式 HTTP 请求封装

桌面应用的网络请求面临两个特殊问题:CORS 跨域限制和 Cookie 管理。基于 Tauri HTTP 插件,我们封装了两套请求方法:

javascript 复制代码
    // request 模式:使用 Tauri HTTP 插件,无 CORS 限制,不带 Cookie
    exportconst get = (url, params) => request(url, { method: 'GET', params })
    
    // authRequest 模式:携带 Cookie(手动管理 Set-Cookie)
    exportconst authGet = (url, params) => authRequest(url, { method: 'GET', params })

Tauri HTTP 插件的请求由 Rust 发起,天然绕过浏览器 CORS 限制,然而对于需要登录态的接口,我们通过手动解析 Set-Cookie 响应头并在后续请求中携带相关Cookie。

五、软件安全更新

我们基于 Tauri Updater 插件实现了一套带签名验证的安全自动检测更新机制,确保门店端应用能够稳定、安全地完成版本升级。

整体更新流程如下:

  1. 修改 tauri.conf.json 中的版本号(如 1.0.0 → 1.0.1)
  2. 执行 pnpm tauri build 构建新版本安装包
  3. 使用 Tauri CLI 生成 Ed25519 签名文件(.sig)
  4. 将安装包和签名文件上传至 CDN,更新 latest.json 配置
  5. 客户端启动时自动检测更新,下载后验证签名并静默安装

**安全保障:**更新包采用 Ed25519 非对称加密算法进行签名校验。私钥仅在构建机器内部使用,不参与任何线上分发流程;公钥则硬编码至应用二进制中,用于安装前对更新包进行合法性校验。任何未签名、签名不匹配或被篡改的安装包都会被直接拒绝安装,从机制上保证更新链路安全可信。

**更新体验:**桌面端提供软件启动时版本更新检查,发现新版本后弹窗展示版本信息和更新日志,用户确认后自动下载安装并提示重启,全程无需手动处理安装包,保证用户体验。

六、性能优化总结

在门店沉浸式拍照系统的开发中,低延迟、高稳定是核心体验要求。针对投屏、拍照、转码等高频操作的性能瓶颈,我们围绕进程开销、IO 效率、计算调度、硬件加速四个方向,对核心链路做了系统性优化,关键优化项的前后对比如下:

经过以上这些优化,我们解决了门店场景里常见的投屏卡顿、拍照延迟、转码慢、UI 掉帧等问题:核心操作延迟从百毫秒级压到了几十毫秒甚至零开销,实现了 "拍完就看到" 的流畅体验;主线程全程不阻塞,大屏交互始终丝滑;硬件加速和异步调度大幅降低了门店设备的 CPU 负载,能保证长时间运行;缓存复用、批量查询等优化,也给后续多宫格照片处理等扩展留足了性能空间,最终落地了一套低延迟、高可靠、可规模化的门店影像体验系统。

七、踩坑与经验

在开发过程中,我们也遇到了一些值得分享的问题,具体如下:

1. scrcpy 和 ADB 版本冲突

scrcpy 内置了 ADB,如果系统 PATH 中也有 ADB,版本不一致会导致 adb server version doesn't match 错误。

**解决方案:**通过 cmd.env("ADB", adb_path) 显式指定 scrcpy 同目录的 ADB,并设置 cmd.current

_dir(scrcpy_dir) 作为工作目录。

2. Windows 窗口嵌入的时序问题

scrcpy 进程启动后窗口并不会立即创建,需要等待数百毫秒。

**解决方案:**我们通过 EnumWindows + 循环重试(最多 50 次,每次间隔 100ms)的方式等待窗口创建,并通过唯一窗口标题 wukong_scrcpy

_{pid} 实现精确定位。

3. Live Photo 视频转码的稳定性

从手机拉取的 HEVC 视频在转码过程中可能写入不完整,导致 MP4 文件损坏。

**解决方案:**我们增加了"文件大小稳定性检测"重试机制------等待文件大小不再变化后再进行转码,并验证输出文件是否为有效的 MP4 格式,这类边界情况在实际门店环境中可能会频繁出现,必须得进行兜底操作。

以上是我们在项目开发过程中遇到的几类典型问题及解决方案。通过这些适配与兜底处理,使得系统在门店实际运行中的稳定性和容错能力得到了明显提升。

八、总结

vivo 大头贴拍照体验系统基于 Tauri 2.0 + Rust 后端 + Vue 3 前端,实现了完整的门店拍照合成打印一体化体验闭环。

核心能力:

  • **低延迟实时投屏:**scrcpy 进程管控 + 窗口精准嵌入 + 性能参数调优
  • **智能拍照体验:**相机自动拉起 + 拍照状态监听 + Live Photo 智能检测
  • **双引擎模板合成:**Canvas 静态图合成 + FFmpeg 视频模版合成

工程保障:

  • 一套代码双平台运行:20+ 条件编译覆盖进程管理、窗口嵌入、打印等全量跨平台差异
  • 跨平台打印适配:macOS lpr + Windows PowerShell System.Drawing 双方案兼容
  • 安全自动更新:Ed25519 签名验签 + 自动检测更新
  • Deep Link 一键拉起:URL Scheme 协议 + 单实例保护

该系统已在部分 vivo 线下门店试点运行,验证了 Tauri 2.0 在与系统硬件深度交互的桌面应用场景中的可行性与稳定性。

相关推荐
光影少年1 小时前
React 合成事件机制、和原生事件区别、事件冒泡阻止
前端·react.js·掘金·金石计划
没有鸡汤吃不下饭1 小时前
告别手动对接口:我用 OpenAPI JSON 做了一个前端接口同步 Skill
前端·ai编程
空栈独白1 小时前
NestJS实战-前后端联调
前端
米饭同学i1 小时前
浏览器记住密码导致忘记密码页面输入框回显错乱?看这篇就够了
前端
孤舟望月1 小时前
NestJS实战-后端开发-全局配置
前端
陆枫Larry1 小时前
从一个按钮间距,聊透 CSS 的 gap 属性
前端
北冥有鱼1 小时前
mqtt 测试
前端·后端
张鑫旭2 小时前
都AI时代了,我为何还在学习前端基础知识?
前端
swipe2 小时前
正则表达式入门到进阶:从表单校验到手写模板引擎
前端·javascript·面试