170 lines
6.2 KiB
TypeScript
170 lines
6.2 KiB
TypeScript
// =============================================================================
|
||
// 企微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 API(Chrome 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,
|
||
}
|
||
}
|