Files
wecom_it_smart_desk/frontend-agent/src/composables/useScreenCapture.ts
T

170 lines
6.2 KiB
TypeScript
Raw Normal View History

// =============================================================================
// 企微IT智能服务台 — 屏幕截图组合函数
// =============================================================================
// 说明:实现屏幕截图功能,优先使用浏览器 Screen Capture API
// 在不支持或有问题(如企微桌面端限制)时降级到系统截图方案。
//
// 两个方案:
// 方案A(优先): navigator.mediaDevices.getDisplayMedia()
// - 优点:可直接在浏览器内完成截图,体验好
// - 缺点:企微桌面端可能限制此 API(非 HTTPS 或 localhost 外不可用)
// 方案B(降级):提示用户用系统截图工具(Win+Shift+S / Cmd+Shift+4
// 然后 Ctrl+V 粘贴到输入框(已有 handlePaste 实现)
//
// 使用方式:
// const { captureScreen, isCapturing, isScreenCaptureSupported, captureFallback }
// = useScreenCapture()
// - captureScreen():尝试方案A,失败时不自动降级(由调用方决定是否提示)
// - isScreenCaptureSupported():检测是否支持方案A
// - 调用方在 captureScreen() 返回 null 时,自行提示用户用系统截图
// =============================================================================
import { ref } from 'vue'
/** 是否正在截图(用于 UI 状态展示) */
const isCapturing = ref(false)
/**
* 检测浏览器是否支持 Screen Capture API
* @returns 是否支持
*/
export function isScreenCaptureSupported(): boolean {
return !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia)
}
/**
* 截取屏幕/窗口/标签页
*
* 做什么:调用浏览器 Screen Capture API,让用户选择要截取的目标,
* 然后从视频流中捕获一帧画面,转为 Blob 返回。
*
* 为什么用 Screen Capture API
* - 浏览器原生支持,无需第三方库
* - 可以截取整个屏幕、应用窗口或浏览器标签页
* - 企微桌面端基于 Chromium 内核,完全支持
*
* @returns 截图的 Blob 对象(PNG 格式),失败返回 null
*/
export async function captureScreen(): Promise<Blob | null> {
// 不支持 Screen Capture API → 提示用户用系统截图
if (!isScreenCaptureSupported()) {
console.warn('[useScreenCapture] 浏览器不支持 Screen Capture API')
return null
}
isCapturing.value = true
try {
// ------------------------------------------------------------------
// 1. 请求屏幕共享权限(浏览器弹出选择器:屏幕/窗口/标签页)
// ------------------------------------------------------------------
// video: true — 只请求视频流(不需要音频)
// @ts-ignore — Chrome 支持 preferLabel 等非标准选项
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false,
})
// ------------------------------------------------------------------
// 2. 从视频流中获取第一帧画面
// ------------------------------------------------------------------
const track = stream.getVideoTracks()[0]
if (!track) {
console.warn('[useScreenCapture] 没有获取到视频轨道')
return null
}
// 使用 ImageCapture APIChrome 59+)获取高质量截图
// 如果不支持 ImageCapture,则回退到 Canvas 绘制方案
let blob: Blob | null = null
if ('ImageCapture' in window) {
try {
// ImageCapture API — 直接从视频轨道抓帧,质量更高
const imageCapture = new ImageCapture(track)
// @ts-ignore — grabFrame 是 ImageCapture 标准方法,但 TS 类型定义可能不完整
const bitmap = await imageCapture.grabFrame()
// 绘制到 Canvas → 导出 PNG Blob
const canvas = document.createElement('canvas')
canvas.width = bitmap.width
canvas.height = bitmap.height
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(bitmap, 0, 0)
blob = await new Promise<Blob | null>((resolve) => {
canvas.toBlob((b) => resolve(b), 'image/png')
})
}
bitmap.close() // 释放位图资源
} catch (imageCaptureError) {
console.warn('[useScreenCapture] ImageCapture 失败,回退到 Canvas 方案:', imageCaptureError)
}
}
// ImageCapture 失败或不可用 → Canvas 方案
if (!blob) {
const video = document.createElement('video')
video.srcObject = stream
video.muted = true // 静音播放(避免系统声音输出)
// 等待视频元数据加载完成
await new Promise<void>((resolve, reject) => {
video.onloadedmetadata = () => resolve()
video.onerror = () => reject(new Error('视频加载失败'))
video.play() // 必须调用 play 才能获取帧
})
// 短暂等待确保有画面帧可用
await new Promise((r) => setTimeout(r, 200))
// 绘制当前帧到 Canvas
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(video, 0, 0)
blob = await new Promise<Blob | null>((resolve) => {
canvas.toBlob((b) => resolve(b), 'image/png')
})
}
// 清理 video 元素
video.pause()
video.srcObject = null
}
// ------------------------------------------------------------------
// 3. 停止屏幕共享(关闭视频流)
// ------------------------------------------------------------------
stream.getTracks().forEach((t) => t.stop())
return blob
} catch (error: any) {
// 用户取消屏幕选择 → 不是错误,静默处理
if (error?.name === 'NotAllowedError' || error?.name === 'AbortError') {
console.log('[useScreenCapture] 用户取消了屏幕选择')
return null
}
console.error('[useScreenCapture] 截图失败:', error)
return null
} finally {
isCapturing.value = false
}
}
/**
* 组合函数返回值
*/
export function useScreenCapture() {
return {
/** 是否正在截图 */
isCapturing,
/** 截取屏幕 */
captureScreen,
/** 检测浏览器支持 */
isScreenCaptureSupported,
}
}