从 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<Child>> 管理
scrcpy 进程生命周期:
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 启动参数:
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 单位一致的物理像素位置。
// 前端使用 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 窗口嵌入到主应用窗口内部的能力,提供更沉浸的体验:
#[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 调用会严重影响性能,因此我们设计了三层缓存策略:
// 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 配置,定义了画布尺寸和每个照片槽位的精确坐标:
// 模板配置结构(后台下发)
{
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)生成——相关运营人员在模板图上可以手动绘制和调整照片槽位。编辑器的核心设计是图片像素坐标系,所有标注数据基于图片原始分辨率,与视图缩放完全解耦:
// 屏幕坐标 → 图片像素坐标的转换
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 实现(前端):
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 滤镜组合实现同一效果:
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 视频组成。检测算法需要处理两种情况:
// 批量获取相册中所有 .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 支持不够友好,因此我们实现了自动转码机制:
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 命令直接发送到默认打印机
#[cfg(target_os = "macos")]
{
Command::new("lpr")
.arg("-o").arg("print-color-mode=color")
.arg(&file_path)
.output()
}
Windows:通过 PowerShell 调用 System.
Drawing 实现精细控制
#[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 层完成封装。
4.5 Deep Link 协议与单实例保护
运营人员在悟空系统配置好相关活动后,在浏览器中访问活动页面并选择对应门店,点击「启动」按钮即可通过 URL Scheme 唤起桌面应用,自动携带活动ID、门店信息等参数:
vivo-photo://callback?{业务参数}
为确保整个系统在门店端稳定运行,我们基于 tauri-plugin-single-instance 和 tauri-plugin-deep-link 两个插件,实现了一套完整的单实例管控 + Deep Link 全链路处理机制:
后端(Rust)单实例拦截与参数转发
通过 single-instance 插件保证应用全局只能运行一个实例,当第二个实例启动时,自动拦截并将启动参数转发给已运行的主实例,同时聚焦主窗口,避免多开冲突。
// 单实例保护:第二个实例启动时,转发参数给已运行实例
.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 处理
前端配套实现了完整的协议监听与处理逻辑,覆盖所有触发场景:
// 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 模式:
#[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 条件编译来解决平台差异:
// 隐藏 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 插件,我们封装了两套请求方法:
// 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 插件实现了一套带签名验证的安全自动检测更新机制,确保门店端应用能够稳定、安全地完成版本升级。
整体更新流程如下:
- 修改 tauri.conf.json 中的版本号(如 1.0.0 → 1.0.1)
- 执行 pnpm tauri build 构建新版本安装包
- 使用 Tauri CLI 生成 Ed25519 签名文件(.sig)
- 将安装包和签名文件上传至 CDN,更新 latest.json 配置
- 客户端启动时自动检测更新,下载后验证签名并静默安装
安全保障:更新包采用 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 在与系统硬件深度交互的桌面应用场景中的可行性与稳定性。

浙公网安备 33010602011771号