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

170 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============================================================================
// 企微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,
}
}