录制
何时使用
在任何涉及录制器的操作时使用本技能:录制按钮、进行中的工具栏、权限提示、分块上传流程或摄像头气泡。如果你要添加对新源(例如,标签页捕获、iPhone 连续互通相机)的支持,或是在服务端更改分块的最终化方式,本技能是你的指南。
涉及的数据模型
recordings— 当用户按下“录制”按钮或导入源时,该行即被创建。本地/文件录制的状态转换路径为uploading→processing→ready(或failed)。videoUrl、durationMs、videoSizeBytes、width、height、hasAudio、hasCamera会在上传流式传输时被填充。Loom 导入使用import-loom-recording,在导入后变为 ready 状态,可能是 Clips 托管的 MP4 文件,或者在 MP4 导出不可用时使用 Loom 嵌入。application_state.record-intent— 代理(agent)在想要开始录制时写入此字段。UI 读取并清除它,然后提示用户授权。application_state.navigation— 当录制器激活时,设置为{ view: "record" }。
二进制上传使用自定义 API 路由(/api/uploads/:id/chunk 和 /api/uploads/:id/abort)而不是操作(actions),因为操作不适合处理二进制流式传输体。最终的分块会调用 finalize-recording。Loom URL 导入仅包含元数据,应通过 import-loom-recording 操作进行。
某些录制与会议相关联——当录制行中的 meeting_id 非空时,表示它是通过 start-meeting-recording 创建的,并且同时适用于 recording 和 meetings 技能。请参阅 meetings 技能以了解双向链接。
生命周期
- 意图。 用户点击“录制”(全局快捷键
Cmd+Shift+L)或代理调用pnpm action start-recording --mode=screen。代理版本将record-intent写入应用状态;UI 监听到并启动与用户点击相同的流程。 - 权限。 屏幕录制调用
navigator.mediaDevices.getDisplayMedia({ video, audio }),摄像头录制调用getUserMedia({ video, audio })。不要在没有用户手势的情况下提示权限。代理路径依赖于 UI 的按钮——我们永远不会绕过浏览器的权限模型。 - 创建行。 一旦流被授予,立即调用
create-recording插入一个状态为"uploading"且带有预生成 id 的行。该 id 将用于后续的每个分块上传。 - 录制。 使用
mimeType: "video/webm;codecs=vp9,opus"(回退到 vp8,然后是浏览器默认值)启动一个MediaRecorder。使用timeslice: 2000,以便每 2 秒接收一个分块。 - 上传每个分块。
ondataavailable将字节流式传输到/api/uploads/chunk,并附带标头X-Recording-Id和X-Chunk-Index,同时,作为尽力而为的备份,将原始分块独立地镜像到IndexedDB。一次瞬态交付失败会暂停上传,并在当前浏览器标签页保持打开期间仅恢复内存中的源;没有后台重发工作线程或关闭标签页后的恢复保证。IndexedDB写入失败不会阻塞捕获或上传。 - 实时转录。 与 MediaRecorder 并行,
useLiveTranscription运行 Web Speech API 以实时累积文本转录。在停止时,客户端调用save-browser-transcript立即持久化结果——无需 API 密钥。桌面端录制会优先使用本地 Whisper/macOS 语音(如果可用),在非 Mac 平台的 webview 中则回退到 Web Speech,最后才依赖上传转录。 - 最终化。 停止时,将最终分块发送到
/api/uploads/:id/chunk?isFinal=1。该路由调用finalize-recording,它会拼接分块,使媒体可跳转播放(见下文),在配置了存储时上传完成的媒体,将status转换为ready,然后启动request-transcript以获得更高质量的转录输出(请参阅ai-video-tools)。 - 立即导航。 桌面端录制器在“停止”开始最终化时立即打开
/r/:id。录制行已存在,因此页面可以在从uploading或processing轮询到ready状态时,显示标题、分享链接和上传进度。不要等待上传/最终化响应后再打开页面。
移动端配套应用生命周期
Agent-Native 移动应用使用相同的录制行和二进制上传路由,但使用原生捕获原语:
- 摄像头视频/导入使用
expo-camera/ 系统照片选择器;会议音频使用expo-audio并启用后台录制。 - 原生文件被复制到文档目录中,并在上传开始前写入一个带有类型信息的 AsyncStorage 捕获任务。
create-recording接收一个稳定的客户端生成的 id、sourceAppName: "Agent-Native Mobile"以及容器 MIME 类型。- 上传通过 Expo 的 FileHandle 最多读取 3 MiB,并在每次确认的 POST 后持久化下一个分块的索引。服务端 4 MiB 的限制仍然适用。
- 前台/恢复同步会轮询
/api/uploads/:id/status;可重试的失败会进行退避,并且永远不会丢弃本地文件。完成后可以发送一条本地通知,并且录制内容会出现在所有 Clips 应用中。
移动端音频 M4A 是一个 MP4 容器,并使用录制路由的 MP4 MIME 别名。字节不会被转换或完整加载到 JavaScript 内存中。
由于手机在执行任何网络操作之前会持久化每个音频/视频文件,并会从其持久队列中恢复有界的分块上传,因此瞬态上传失败永远不会导致捕获内容丢失。不要要求用户保持捕获屏幕打开或在上传失败后重新录制;应告知他们重新连接 Clips 并从移动端主屏幕重试已保存的任务。
移动端会议捕获仅使用麦克风;永远不要声称它具有桌面端麦克风加系统音频的保真度。请参阅 meetings 技能以了解这对于参会者归属意味着什么。
可跳转播放(不要直接交付原始 MediaRecorder 输出)
原始的 MediaRecorder 文件不适用于渐进式 HTTP 播放,会导致“剪辑需要数分钟加载”和“每次跳转都会重新缓冲”的问题,尽管文件下载正常:
- MP4 的
moov元数据原子写在mdat之后,因此播放器必须先获取整个文件才能开始播放或进行跳转。 - WebM 是一个没有 Cues(跳转索引)且片段时长未知的直播流,因此 Chrome 不会处理
currentTime = X,必须进行扫描/下载。
finalize-recording 在上传前修复了这个问题:MP4 获取纯 TypeScript 实现的 faststart(server/lib/faststart.ts),WebM 获取一个无损的 ffmpeg -c copy 重新混流,写入 SeekHead + Cues + 实际时长(server/lib/video-remux.ts)。两者都是尽力而为——失败时我们会上传原始文件,绝不阻塞 finalize。超过 CLIPS_INLINE_REMUX_MAX_BYTES(默认 200 MB)的录制会跳过内联处理,并在后台修复。
流式/可恢复上传路径将原始字节直接转发给提供商,无法内联重写它们,因此 finalize-recording 会为这些上传调度一个后台的 ensureRecordingSeekable 处理。
要修复在此功能之前(或通过流式)上传的剪辑,请调用 reprocess-recording 操作:--id、--ids='[...]' 或 --all --limit=N。它会重新获取提供商媒体,重写它,重新上传,并更新数据行。它是幂等的(已可跳转的剪辑会被跳过,除非使用 --force),并且只会操作调用者拥有的、托管在提供商的剪辑。这是当用户报告特定剪辑缓慢/缓冲时的正确工具。
可跳转性重新混流无法修复在视频轨道存在较大时间戳间隙时(移动浏览器在用户切换应用后暂停摄像头时常见)音频仍在继续的录制。对于冻结或似乎在声明时长之前停止的剪辑,请使用 --normalizeTimeline=true 调用 reprocess-recording。该显式模式使用相同的所有者限定的获取和上传流程,但会完全转码为恒定 30 fps 的 faststart MP4(H.264 + AAC)。它保留音频,并在缺失帧的间隙中复制最后一个解码的视频帧。该操作上传到一个新的媒体对象,并且仅在验证输出已存储后自动更新数据行;任何转码、音频验证、上传或并发更新失败都会使原始 URL 和格式保持不变。
Loom 导入
对 Loom 分享或嵌入 URL 使用 import-loom-recording。该操作验证 Loom URL,从 Loom 的公共端点读取 Loom oEmbed 元数据,并创建一个状态为 ready 的录制,其中包含 Loom 的嵌入 URL、缩略图、标题、时长和尺寸。当 Loom 在分享页面上暴露一个已签名的公共转录 JSON URL 时,该操作会将该转录导入 Clips 并存储标准化的片段;永远不要存储 Loom 的已签名 CDN URL。
当 Loom 暴露一个可下载的公共 MP4 时,import-loom-recording 会下载它,将字节重新上传到 Clips 存储,并创建一个已就绪、可播放的 Clips 托管录制,同时从分享页面导入 Loom 的公共转录(如果存在)。如果 Loom 允许公共播放但不暴露 MP4,该操作会保留一个以嵌入为后端的就绪录制,并在可用时仍然导入转录。
以嵌入为后端的 Loom 导入会渲染一个 Loom iframe 并隐藏原生 Clips 编辑器。重新上传的 Loom 导入支持 Clips 原生的修剪、导出、帧提取和基于上传的转录;当嵌入型导入需要这些功能时,请要求用户上传原始视频文件。
浏览器诊断和录制器安装选项
浏览器录制可以通过 createBrowserDiagnosticsCapture 保存有界、经过编辑的诊断信息:控制台消息以及 fetch/XHR 方法、URL、状态、时长和错误。浏览器录制器仅捕获录制器页面本身的活动。Clips Chrome 扩展是浏览器日志的活动标签页路径:它使用扩展捕获会话启动 /record,并传递 developerLogs=1/0,然后以 source 为 extension 保存诊断信息。
Web Store 列表是公开的,因此公共 Chrome 扩展选项默认显示。否则显示“下载桌面应用”的 UI 提示使用共享的安装选择组件(CaptureInstallButton / CaptureInstallInlineLink)提供两个选项:用于浏览器日志的 Chrome 扩展,以及用于最无缝原生捕获的桌面应用。设置 VITE_CLIPS_CHROME_EXTENSION_ENABLED=0 可再次隐藏 Chrome 选项,或设置 VITE_CLIPS_CHROME_EXTENSION_URL 指向不同的列表。
save-browser-diagnostics 是 UI/内部使用的。它存储有界的控制台日志以及 fetch/XHR 方法、URL 路径/查询键、状态和时长。它从不捕获标头、主体、cookie 或网络 URL 查询值。控制台文本在保留有用的非秘密值的同时,会编辑看起来像凭据的键和标头。当你拥有编辑权限时,使用 get-recording-player-data 获取完整的诊断负载;请参阅 video-sharing 技能以了解公共代理上下文暴露的更严格的编辑流。
Chrome 扩展
扩展位于 chrome-extension/。它使用 clipsExtensionId 和 clipsCaptureSessionId 启动 /record,录制器会将 CLIPS_CAPTURE_START / CLIPS_CAPTURE_STOP / CLIPS_CAPTURE_CANCEL 发送回扩展。它仅在被用户激活的标签页上使用 Chrome 调试器 API,并且仅在录制活动期间使用,并返回与 save-browser-diagnostics 保存的相同的经过编辑的诊断信息格式。
该扩展还增强了 GitHub 上的 Issue 和 PR markdown:一个狭窄的 github.com 内容脚本检测 Clips 的 /r/、/share/ 和 /embed/ 链接,然后在扩展拥有的预览 iframe 中渲染现有的 /embed/:id 播放器,这样视频无需离开 GitHub 即可播放。除非经过审慎的权限审查,否则请将此功能限制在 GitHub 上。
文件夹、空间和批量移动
单次和批量文件夹移动都使用 move-recording。对于单个剪辑传入 id,对于选中的剪辑传入 ids,传入 folderId: null 可将它们移动到库或空间的根目录。
截图
截图是一个 kind = "image"(在 shared/recording-kind.ts 中为 isImageRecording)的 recordings 行。它与剪辑共享文件夹、空间、分享、评论和搜索功能,但没有任何视频部分:没有转录、章节、胶片条、时间线或音频。使用 list-recordings --kind=image 列出它们。
- 捕获、标注和遮盖是浏览器工作(
create-screenshot、save-screenshot-edits,均为仅 UI)。代理通过get-recording-player-data或/api/agent-frame.jpg读取截图,后者会为任意atMs返回图片本身。 imageUrl是提供服务的图片;baseImageUrl是编辑器在其上绘制的无标记基底。两者都通过/api/thumbnail/:id提供,该端点应用密码、过期时间和遮盖保留。永远不要泄露存储 URL。- 遮盖分为两步,与视频类似:框被保存为待处理的叠加层(对观众隐藏),然后进行烧录。一次烧录会在
editsJson.burnInProgress中写入,然后删除原始文件,之后再清除该标记;unreclaimedUrls列出尚未删除的旧副本。两者都仅在服务端(server/lib/screenshot-edits.ts),并且绝不能到达观众。 - 视频编辑操作在
assertNativeRecordingMedia处拒绝截图;只有save-screenshot-edits能写入截图的editsJson。
暂停 / 恢复
所有主流浏览器都支持 MediaRecorder.pause() / .resume()。在暂停期间保持单个 MediaRecorder 实例——不要销毁流,否则会再次触发权限提示。暂停期间,上传工作线程会继续排空其缓冲区,以便在用户停止前赶上进度。
摄像头气泡
当模式为 screen+camera 时,“气泡”指两种不同的东西:
- 屏幕上的自拍预览。
CameraBubble渲染一个普通的、未被录制的<video>元素,用户可以在倒计时/录制期间拖拽它以调整自己的取景位置。它从未被捕获——它只是本地的取景 UI。 - 录制的合成画面。 一旦用户录制另一个窗口/应用,显示捕获无法包含那个 DOM 元素,因此保存的视频必须在
MediaRecorder开始时将摄像头圆圈烘焙进去。我们通过在启动时抓取摄像头流的快照并持续追踪其位置来实现这一点。
错误恢复
录制器的错误恢复策略:
- 权限拒绝。 如果用户在系统提示后拒绝访问,
getDisplayMedia或getUserMedia会拒绝 promise。我们会捕获该错误并显示一条友好的消息,建议他们重试或检查系统偏好设置。录制行不会被创建。 - MediaRecorder 错误。
MediaRecorder上的onerror事件指示底层流或编码器问题。我们会停止录制,将行标记为failed,并提示用户重试。 - 分块上传失败。 单个分块上传的
ondataavailable处理程序会捕获网络错误并进行重试(最多重试 3 次,使用指数退避)。如果所有重试都失败,我们会暂停录制,显示一个非阻塞通知,并在恢复连接后继续。如果录制在未完成上传的情况下停止,我们会尝试上传所有剩余分块;如果失败,则将行标记为failed。 - 移动端恢复。 移动应用在上传前持久化捕获文件,因此应用崩溃或系统终止后可以恢复。下次打开应用时,它会检查未完成的任务并提示用户重试或放弃。
- 不可恢复的错误。 某些错误(例如,存储配额已满、硬件编码器故障)被视为不可恢复。录制立即停止,行标记为
failed,并向用户显示解释性消息。