@agent-native/core 0.100.2 → 0.101.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +83 -0
- package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/ar-SA/template-plan.mdx +7 -0
- package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/de-DE/template-plan.mdx +7 -0
- package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/es-ES/template-plan.mdx +14 -0
- package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/fr-FR/template-plan.mdx +7 -0
- package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/hi-IN/template-plan.mdx +7 -0
- package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/ja-JP/template-plan.mdx +7 -0
- package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/ko-KR/template-plan.mdx +7 -0
- package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/pt-BR/template-plan.mdx +14 -0
- package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/zh-CN/template-plan.mdx +7 -0
- package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +48 -30
- package/corpus/core/docs/content/locales/zh-TW/template-plan.mdx +7 -0
- package/corpus/core/docs/content/pr-visual-recap.mdx +50 -32
- package/corpus/core/docs/content/template-plan.mdx +14 -0
- package/corpus/core/docs/content/toolkit-comments-review.mdx +16 -2
- package/corpus/core/package.json +4 -1
- package/corpus/core/src/a2a/index.ts +2 -1
- package/corpus/core/src/a2a/server.ts +34 -9
- package/corpus/core/src/agent/production-agent.ts +56 -10
- package/corpus/core/src/cli/create.ts +47 -19
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +2 -3
- package/corpus/core/src/cli/recap.ts +3 -4932
- package/corpus/core/src/cli/skills-content/visual-plan-skill.ts +6 -4
- package/corpus/core/src/cli/skills-content/visual-recap-skill.ts +7 -1
- package/corpus/core/src/client/AgentPanel.tsx +19 -17
- package/corpus/core/src/client/analytics.ts +8 -1
- package/corpus/core/src/client/chat/markdown-renderer.tsx +54 -8
- package/corpus/core/src/client/chat/message-components.tsx +8 -2
- package/corpus/core/src/client/clipboard.ts +28 -1
- package/corpus/core/src/client/guided-questions.tsx +4 -0
- package/corpus/core/src/client/i18n.tsx +2 -1
- package/corpus/core/src/client/index.ts +4 -0
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +108 -0
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +482 -150
- package/corpus/core/src/client/review/index.ts +8 -0
- package/corpus/core/src/client/review/use-review.ts +35 -1
- package/corpus/core/src/client/use-agent-chat-context.ts +3 -2
- package/corpus/core/src/credentials/index.ts +41 -8
- package/corpus/core/src/extensions/fetch-tool.ts +33 -3
- package/corpus/core/src/notifications/channels.ts +20 -6
- package/corpus/core/src/review/actions/create-review-comment.ts +2 -1
- package/corpus/core/src/review/actions/get-review-feedback.ts +6 -11
- package/corpus/core/src/review/actions/list-review-comments.ts +38 -4
- package/corpus/core/src/review/actions/reply-review-comment.ts +33 -22
- package/corpus/core/src/review/actions/resolve-review-thread.ts +29 -2
- package/corpus/core/src/review/actions/send-review-thread-to-agent.ts +62 -0
- package/corpus/core/src/review/identity.ts +101 -0
- package/corpus/core/src/review/index.ts +14 -0
- package/corpus/core/src/review/store.ts +315 -8
- package/corpus/core/src/review/types.ts +4 -0
- package/corpus/core/src/secrets/crypto.ts +26 -10
- package/corpus/core/src/secrets/storage.ts +16 -3
- package/corpus/core/src/server/action-discovery.ts +4 -0
- package/corpus/core/src/server/action-routes.ts +125 -4
- package/corpus/core/src/server/agent-chat/plugin-options.ts +16 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +61 -7
- package/corpus/core/src/server/derived-secret.ts +5 -1
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/core/src/vite/action-types-plugin.ts +4 -0
- package/corpus/templates/analytics/changelog/2026-07-14-daily-dashboard-email-captures-authenticate-with-a-sessi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-14-long-running-cross-app-analyses-now-continue-reliably-in-the.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +156 -3
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +8 -4
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +402 -142
- package/corpus/templates/assets/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/assets/app/i18n-data.ts +1 -0
- package/corpus/templates/assets/app/routes/library.tsx +187 -48
- package/corpus/templates/assets/changelog/2026-07-13-merged-the-candidates-panel-into-the-library-as-a-drafts-tab.md +6 -0
- package/corpus/templates/chat/app/lib/agent-page.tsx +40 -0
- package/corpus/templates/chat/app/routes/agent.tsx +5 -3
- package/corpus/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +6 -0
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +5 -0
- package/corpus/templates/clips/AGENTS.md +4 -0
- package/corpus/templates/clips/actions/get-feature-flags.ts +30 -0
- package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
- package/corpus/templates/clips/actions/request-transcript.ts +41 -1
- package/corpus/templates/clips/actions/view-screen.ts +5 -9
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +44 -81
- package/corpus/templates/clips/app/components/player/video-player.tsx +39 -1
- package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +6 -12
- package/corpus/templates/clips/app/hooks/use-mse-video-source.ts +146 -0
- package/corpus/templates/clips/app/i18n/ar-SA.ts +0 -8
- package/corpus/templates/clips/app/i18n/de-DE.ts +0 -9
- package/corpus/templates/clips/app/i18n/en-US.ts +0 -8
- package/corpus/templates/clips/app/i18n/es-ES.ts +0 -9
- package/corpus/templates/clips/app/i18n/fr-FR.ts +0 -9
- package/corpus/templates/clips/app/i18n/hi-IN.ts +0 -8
- package/corpus/templates/clips/app/i18n/ja-JP.ts +0 -9
- package/corpus/templates/clips/app/i18n/ko-KR.ts +0 -9
- package/corpus/templates/clips/app/i18n/pt-BR.ts +0 -9
- package/corpus/templates/clips/app/i18n/zh-CN.ts +0 -7
- package/corpus/templates/clips/app/i18n/zh-TW.ts +0 -7
- package/corpus/templates/clips/app/lib/capture-install-options.ts +21 -0
- package/corpus/templates/clips/app/lib/fmp4.ts +271 -0
- package/corpus/templates/clips/app/lib/mse-video-loader.ts +585 -0
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +0 -4
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -13
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +2 -37
- package/corpus/templates/clips/app/routes/download.tsx +3 -2
- package/corpus/templates/clips/changelog/2026-07-10-shared-clips-now-start-playing-instantly-instead-of-showing-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-14-clips-now-distinguishes-transcript-previews-from-incomplete-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-14-desktop-app-prompts-stay-hidden-after-you-download-the-insta.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-14-share-dialogs-show-separate-links-for-people-and-agents.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +5 -0
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +10 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +4 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +2518 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +353 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +558 -67
- package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +11 -1
- package/corpus/templates/clips/desktop/src-tauri/src/remote_flags.rs +188 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +1 -1
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +1 -0
- package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +5 -1
- package/corpus/templates/clips/shared/feature-flags.ts +41 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +3 -0
- package/corpus/templates/design/.agents/skills/design-review-feedback/SKILL.md +42 -0
- package/corpus/templates/design/AGENTS.md +6 -0
- package/corpus/templates/design/README.md +1 -0
- package/corpus/templates/design/actions/navigate.ts +4 -4
- package/corpus/templates/design/actions/view-screen.ts +116 -2
- package/corpus/templates/design/agent-native.app-skill.json +5 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +52 -59
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +13 -9
- package/corpus/templates/design/app/components/design/EditPanel.tsx +79 -11
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +78 -65
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +161 -0
- package/corpus/templates/design/app/components/design/ReviewStatusControl.tsx +96 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +166 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +1 -0
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +1077 -0
- package/corpus/templates/design/app/components/visual-editor/index.ts +1 -0
- package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +42 -0
- package/corpus/templates/design/app/hooks/use-design-systems.ts +2 -2
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +9 -6
- package/corpus/templates/design/app/hooks/use-question-flow.ts +7 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +56 -1
- package/corpus/templates/design/app/i18n/de-DE.ts +57 -1
- package/corpus/templates/design/app/i18n/en-US.ts +56 -1
- package/corpus/templates/design/app/i18n/es-ES.ts +56 -1
- package/corpus/templates/design/app/i18n/fr-FR.ts +57 -1
- package/corpus/templates/design/app/i18n/hi-IN.ts +56 -1
- package/corpus/templates/design/app/i18n/ja-JP.ts +56 -1
- package/corpus/templates/design/app/i18n/ko-KR.ts +56 -1
- package/corpus/templates/design/app/i18n/pt-BR.ts +57 -1
- package/corpus/templates/design/app/i18n/zh-CN.ts +56 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +51 -0
- package/corpus/templates/design/app/pages/Present.tsx +172 -14
- package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +23 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +10 -0
- package/corpus/templates/design/app/pages/present-review-state.ts +18 -0
- package/corpus/templates/design/app/public-routes.ts +8 -0
- package/corpus/templates/design/app/root.tsx +7 -6
- package/corpus/templates/design/changelog/2026-07-13-dragged-design-assets-now-appear-at-the-visible-drop-locatio.md +6 -0
- package/corpus/templates/design/changelog/2026-07-13-reviewers-can-pin-comments-on-shared-designs-and-apply-verif.md +6 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +10 -0
- package/corpus/templates/design/server/plugins/auth.ts +7 -5
- package/corpus/templates/design/server/plugins/review.ts +9 -0
- package/corpus/templates/design/shared/review-anchor.ts +83 -0
- package/corpus/templates/design/shared/review-summary.ts +25 -0
- package/corpus/templates/dispatch/changelog/2026-07-14-dispatch-now-distinguishes-connected-agents-from-locally-mou.md +6 -0
- package/corpus/templates/forms/.agents/skills/form-publishing/SKILL.md +2 -0
- package/corpus/templates/forms/AGENTS.md +4 -0
- package/corpus/templates/forms/actions/create-form.ts +2 -1
- package/corpus/templates/forms/actions/update-form.ts +5 -2
- package/corpus/templates/forms/app/i18n/ar-SA.ts +3 -0
- package/corpus/templates/forms/app/i18n/de-DE.ts +3 -0
- package/corpus/templates/forms/app/i18n/en-US.ts +3 -0
- package/corpus/templates/forms/app/i18n/es-ES.ts +3 -0
- package/corpus/templates/forms/app/i18n/fr-FR.ts +3 -0
- package/corpus/templates/forms/app/i18n/hi-IN.ts +3 -0
- package/corpus/templates/forms/app/i18n/ja-JP.ts +3 -0
- package/corpus/templates/forms/app/i18n/ko-KR.ts +3 -0
- package/corpus/templates/forms/app/i18n/pt-BR.ts +3 -0
- package/corpus/templates/forms/app/i18n/zh-CN.ts +3 -0
- package/corpus/templates/forms/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +19 -0
- package/corpus/templates/forms/changelog/2026-07-14-fixed-the-ask-forms-loading-input-appearing-above-the-welcom.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-14-form-social-previews-now-show-the-form-title-description-and.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-14-forms-can-email-the-form-owner-when-new-responses-arrive.md +6 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +17 -0
- package/corpus/templates/forms/server/lib/form-og-image.ts +64 -6
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +6 -2
- package/corpus/templates/forms/server/lib/response-email.ts +65 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/forms/server/routes/api/forms/og/[...slug]/og.png.get.ts +184 -0
- package/corpus/templates/forms/shared/types.ts +2 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +6 -4
- package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +7 -1
- package/dist/a2a/index.d.ts +2 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/server.d.ts +25 -0
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +30 -6
- package/dist/a2a/server.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +37 -12
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/create.d.ts +5 -3
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +44 -19
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +2 -2
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +2 -2
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +3 -562
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +3 -3875
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
- package/dist/cli/skills-content/visual-plan-skill.d.ts.map +1 -1
- package/dist/cli/skills-content/visual-plan-skill.js +6 -4
- package/dist/cli/skills-content/visual-plan-skill.js.map +1 -1
- package/dist/cli/skills-content/visual-recap-skill.d.ts +1 -1
- package/dist/cli/skills-content/visual-recap-skill.d.ts.map +1 -1
- package/dist/cli/skills-content/visual-recap-skill.js +7 -1
- package/dist/cli/skills-content/visual-recap-skill.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +3 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/analytics.d.ts +5 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +3 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/chat/markdown-renderer.d.ts +1 -0
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +36 -1
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +5 -2
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/clipboard.d.ts +3 -1
- package/dist/client/clipboard.d.ts.map +1 -1
- package/dist/client/clipboard.js +24 -1
- package/dist/client/clipboard.js.map +1 -1
- package/dist/client/guided-questions.d.ts +3 -1
- package/dist/client/guided-questions.d.ts.map +1 -1
- package/dist/client/guided-questions.js +2 -1
- package/dist/client/guided-questions.js.map +1 -1
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +3 -1
- package/dist/client/i18n.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +18 -0
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -0
- package/dist/client/review/ReviewCommentComposer.js +30 -0
- package/dist/client/review/ReviewCommentComposer.js.map +1 -0
- package/dist/client/review/ReviewThreadPanel.d.ts +28 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +151 -39
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/review/index.d.ts +3 -2
- package/dist/client/review/index.d.ts.map +1 -1
- package/dist/client/review/index.js +2 -1
- package/dist/client/review/index.js.map +1 -1
- package/dist/client/review/use-review.d.ts +23 -0
- package/dist/client/review/use-review.d.ts.map +1 -1
- package/dist/client/review/use-review.js +3 -0
- package/dist/client/review/use-review.js.map +1 -1
- package/dist/client/use-agent-chat-context.d.ts +1 -1
- package/dist/client/use-agent-chat-context.d.ts.map +1 -1
- package/dist/client/use-agent-chat-context.js +4 -2
- package/dist/client/use-agent-chat-context.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/credentials/index.d.ts +12 -7
- package/dist/credentials/index.d.ts.map +1 -1
- package/dist/credentials/index.js +33 -8
- package/dist/credentials/index.js.map +1 -1
- package/dist/extensions/fetch-tool.d.ts +13 -1
- package/dist/extensions/fetch-tool.d.ts.map +1 -1
- package/dist/extensions/fetch-tool.js +9 -2
- package/dist/extensions/fetch-tool.js.map +1 -1
- package/dist/notifications/channels.js +20 -4
- package/dist/notifications/channels.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/review/actions/create-review-comment.js +2 -1
- package/dist/review/actions/create-review-comment.js.map +1 -1
- package/dist/review/actions/get-review-feedback.js +6 -11
- package/dist/review/actions/get-review-feedback.js.map +1 -1
- package/dist/review/actions/list-review-comments.d.ts +1 -0
- package/dist/review/actions/list-review-comments.js +25 -4
- package/dist/review/actions/list-review-comments.js.map +1 -1
- package/dist/review/actions/reply-review-comment.js +12 -5
- package/dist/review/actions/reply-review-comment.js.map +1 -1
- package/dist/review/actions/resolve-review-thread.d.ts +3 -0
- package/dist/review/actions/resolve-review-thread.js +20 -3
- package/dist/review/actions/resolve-review-thread.js.map +1 -1
- package/dist/review/actions/send-review-thread-to-agent.d.ts +17 -0
- package/dist/review/actions/send-review-thread-to-agent.d.ts.map +1 -0
- package/dist/review/actions/send-review-thread-to-agent.js +49 -0
- package/dist/review/actions/send-review-thread-to-agent.js.map +1 -0
- package/dist/review/identity.d.ts +6 -0
- package/dist/review/identity.d.ts.map +1 -0
- package/dist/review/identity.js +68 -0
- package/dist/review/identity.js.map +1 -0
- package/dist/review/index.d.ts +3 -1
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/index.js +2 -1
- package/dist/review/index.js.map +1 -1
- package/dist/review/store.d.ts +36 -0
- package/dist/review/store.d.ts.map +1 -1
- package/dist/review/store.js +223 -8
- package/dist/review/store.js.map +1 -1
- package/dist/review/types.d.ts +4 -0
- package/dist/review/types.d.ts.map +1 -1
- package/dist/review/types.js.map +1 -1
- package/dist/secrets/crypto.d.ts +15 -7
- package/dist/secrets/crypto.d.ts.map +1 -1
- package/dist/secrets/crypto.js +23 -10
- package/dist/secrets/crypto.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +16 -3
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts +45 -0
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +71 -5
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-chat/plugin-options.d.ts +16 -0
- package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
- package/dist/server/agent-chat/plugin-options.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +20 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +39 -4
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/derived-secret.d.ts +1 -1
- package/dist/server/derived-secret.d.ts.map +1 -1
- package/dist/server/derived-secret.js.map +1 -1
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/vite/action-types-plugin.d.ts.map +1 -1
- package/dist/vite/action-types-plugin.js +4 -0
- package/dist/vite/action-types-plugin.js.map +1 -1
- package/docs/content/locales/ar-SA/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/ar-SA/template-plan.mdx +7 -0
- package/docs/content/locales/de-DE/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/de-DE/template-plan.mdx +7 -0
- package/docs/content/locales/es-ES/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/es-ES/template-plan.mdx +14 -0
- package/docs/content/locales/fr-FR/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/fr-FR/template-plan.mdx +7 -0
- package/docs/content/locales/hi-IN/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/hi-IN/template-plan.mdx +7 -0
- package/docs/content/locales/ja-JP/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/ja-JP/template-plan.mdx +7 -0
- package/docs/content/locales/ko-KR/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/ko-KR/template-plan.mdx +7 -0
- package/docs/content/locales/pt-BR/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/pt-BR/template-plan.mdx +14 -0
- package/docs/content/locales/zh-CN/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/zh-CN/template-plan.mdx +7 -0
- package/docs/content/locales/zh-TW/pr-visual-recap.mdx +48 -30
- package/docs/content/locales/zh-TW/template-plan.mdx +7 -0
- package/docs/content/pr-visual-recap.mdx +50 -32
- package/docs/content/template-plan.mdx +14 -0
- package/docs/content/toolkit-comments-review.mdx +16 -2
- package/package.json +4 -1
- package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +0 -68
- package/corpus/templates/forms/server/routes/api/forms/og/[slug]/og.png.get.ts +0 -45
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use chrono::Utc;
|
|
2
2
|
use serde::{Deserialize, Serialize};
|
|
3
3
|
use std::fs::File;
|
|
4
|
-
use std::io::{ErrorKind, Read};
|
|
4
|
+
use std::io::{ErrorKind, Read, Seek, SeekFrom};
|
|
5
5
|
use std::path::{Path, PathBuf};
|
|
6
6
|
use std::process::{Child, Command, Stdio};
|
|
7
7
|
use std::sync::{Arc, Condvar, Mutex, OnceLock};
|
|
@@ -24,15 +24,22 @@ use screencapturekit::shareable_content::SCShareableContent;
|
|
|
24
24
|
#[cfg(target_os = "macos")]
|
|
25
25
|
use screencapturekit::stream::{
|
|
26
26
|
configuration::SCStreamConfiguration, content_filter::SCContentFilter,
|
|
27
|
-
output_type::SCStreamOutputType, sc_stream::SCStream,
|
|
27
|
+
output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType, sc_stream::SCStream,
|
|
28
28
|
};
|
|
29
29
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
30
30
|
|
|
31
31
|
pub(crate) const QUICKTIME_RECORDING_MIME_TYPE: &str = "video/quicktime";
|
|
32
32
|
pub(crate) const MP4_RECORDING_MIME_TYPE: &str = "video/mp4";
|
|
33
|
+
/// Prefix tagging a capture-side finalize/write failure (segment-sink disk
|
|
34
|
+
/// write error, AVAssetWriter finalize failure/timeout) as opposed to a benign
|
|
35
|
+
/// `stop_capture` error. The upload path fails closed on this: the on-disk file
|
|
36
|
+
/// is incomplete even when its init-segment `moov` is present, so uploading
|
|
37
|
+
/// would silently publish a truncated clip.
|
|
38
|
+
const CAPTURE_FINALIZE_INCOMPLETE_PREFIX: &str = "capture finalize incomplete: ";
|
|
33
39
|
// Keep native chunks comfortably under serverless request/event limits.
|
|
34
|
-
const
|
|
35
|
-
|
|
40
|
+
const GCS_CHUNK_ALIGN_BYTES: usize = 256 * 1024;
|
|
41
|
+
const UPLOAD_CHUNK_BYTES: usize = 15 * GCS_CHUNK_ALIGN_BYTES; // 3.75 MiB
|
|
42
|
+
// Master switch for native transcoding/compression.
|
|
36
43
|
const COMPRESSION_ENABLED: bool = true;
|
|
37
44
|
const TRANSCODE_THRESHOLD_BYTES: u64 = 24 * 1024 * 1024;
|
|
38
45
|
const TARGET_UPLOAD_BYTES: u64 = 18 * 1024 * 1024;
|
|
@@ -100,6 +107,22 @@ fn audio_filter_chain(downmix: bool, denoise: bool, mic_pregain: bool) -> String
|
|
|
100
107
|
}
|
|
101
108
|
const NATIVE_CAPTURE_MAX_LONG_EDGE: u32 = 1280;
|
|
102
109
|
const NATIVE_CAPTURE_FPS: u32 = 24;
|
|
110
|
+
|
|
111
|
+
// Custom ScreenCaptureKit capture engine: AVAssetWriter fragmented-MP4
|
|
112
|
+
// writer, live audio mixer, and the AVFoundation FFI glue live in a child
|
|
113
|
+
// module; this file keeps session orchestration, upload, and recovery.
|
|
114
|
+
#[cfg(target_os = "macos")]
|
|
115
|
+
mod custom_capture;
|
|
116
|
+
#[cfg(target_os = "macos")]
|
|
117
|
+
use custom_capture::{
|
|
118
|
+
start_custom_screencapturekit_backend_at, CustomCaptureResume, CustomScreenCaptureWriter,
|
|
119
|
+
};
|
|
120
|
+
// Live chunk uploader: tails the fragmented MP4 and streams it during
|
|
121
|
+
// recording; attached/finalized/abandoned from the session logic here.
|
|
122
|
+
#[cfg(target_os = "macos")]
|
|
123
|
+
mod live_upload;
|
|
124
|
+
#[cfg(target_os = "macos")]
|
|
125
|
+
use live_upload::{attach_live_uploader_to_session, LiveUpload};
|
|
103
126
|
const AVCONVERT_PATH: &str = "/usr/bin/avconvert";
|
|
104
127
|
const AVCONVERT_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
|
105
128
|
const FFMPEG_TIMEOUT: Duration = Duration::from_secs(8 * 60);
|
|
@@ -229,6 +252,16 @@ struct NativeFullscreenSession {
|
|
|
229
252
|
/// warming up) but the recording output hasn't been attached yet, so
|
|
230
253
|
/// nothing is written to disk. `begin` attaches it and flips this false.
|
|
231
254
|
pending_recording_output: bool,
|
|
255
|
+
custom_pipeline: bool,
|
|
256
|
+
/// Active live-upload task: streams the growing fragmented MP4 to the
|
|
257
|
+
/// server in chunks during recording (custom pipeline only). `None` when
|
|
258
|
+
/// live upload is disabled or for local-only recordings.
|
|
259
|
+
#[cfg(target_os = "macos")]
|
|
260
|
+
live_upload: Option<LiveUpload>,
|
|
261
|
+
/// True if a live uploader was ever attached. If the uploader is later
|
|
262
|
+
/// abandoned (e.g. pause makes the recording multi-segment), the stop path
|
|
263
|
+
/// must reset already-uploaded chunks before re-uploading the whole file.
|
|
264
|
+
had_live_upload: bool,
|
|
232
265
|
/// Stop flag for the background disk-space monitor thread. Set to true
|
|
233
266
|
/// when the session is finalized or discarded so the thread exits cleanly.
|
|
234
267
|
disk_monitor_stop: Option<Arc<AtomicBool>>,
|
|
@@ -273,6 +306,24 @@ pub(crate) enum NativeFullscreenBackend {
|
|
|
273
306
|
/// never saw mic samples" from "samples existed but encoded silent".
|
|
274
307
|
mic_sample_count: Option<Arc<AtomicU64>>,
|
|
275
308
|
},
|
|
309
|
+
#[cfg(target_os = "macos")]
|
|
310
|
+
CustomScreenCaptureKit {
|
|
311
|
+
/// Behind `Arc<Mutex>` because the capture watchdog can swap in a
|
|
312
|
+
/// rebuilt stream after an interruption while the stop path also needs
|
|
313
|
+
/// to reach it. Lock is only held for brief `stop_capture`/swap calls.
|
|
314
|
+
stream: Arc<Mutex<SCStream>>,
|
|
315
|
+
writer: CustomScreenCaptureWriter,
|
|
316
|
+
mic_ready: Option<Arc<AtomicBool>>,
|
|
317
|
+
recording_enabled: Arc<AtomicBool>,
|
|
318
|
+
/// Signals the watchdog thread to stop supervising (and never rebuild
|
|
319
|
+
/// the stream again) as the session is being torn down.
|
|
320
|
+
watchdog_shutdown: Arc<AtomicBool>,
|
|
321
|
+
/// Handles for the soft pause/resume path: stop only the capture
|
|
322
|
+
/// source on pause and splice a fresh SCStream onto the same writer on
|
|
323
|
+
/// resume, keeping one continuous append-only file so live upload is
|
|
324
|
+
/// never interrupted.
|
|
325
|
+
resume: CustomCaptureResume,
|
|
326
|
+
},
|
|
276
327
|
}
|
|
277
328
|
|
|
278
329
|
/// Safety net for the `screencapture` fallback: if a session carrying a live
|
|
@@ -284,11 +335,25 @@ pub(crate) enum NativeFullscreenBackend {
|
|
|
284
335
|
/// the place to block on graceful finalization.
|
|
285
336
|
impl Drop for NativeFullscreenBackend {
|
|
286
337
|
fn drop(&mut self) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
338
|
+
match self {
|
|
339
|
+
NativeFullscreenBackend::Screencapture { child } => {
|
|
340
|
+
if matches!(child.try_wait(), Ok(None)) {
|
|
341
|
+
let _ = child.kill();
|
|
342
|
+
let _ = child.wait();
|
|
343
|
+
}
|
|
291
344
|
}
|
|
345
|
+
// Guarantee the capture watchdog stops supervising even if a session
|
|
346
|
+
// is dropped without going through the normal stop path (panic
|
|
347
|
+
// unwind, stale-session displacement). Otherwise it would keep the
|
|
348
|
+
// SCStream alive and rebuild it forever.
|
|
349
|
+
#[cfg(target_os = "macos")]
|
|
350
|
+
NativeFullscreenBackend::CustomScreenCaptureKit {
|
|
351
|
+
watchdog_shutdown, ..
|
|
352
|
+
} => {
|
|
353
|
+
watchdog_shutdown.store(true, Ordering::SeqCst);
|
|
354
|
+
}
|
|
355
|
+
#[cfg(target_os = "macos")]
|
|
356
|
+
NativeFullscreenBackend::ScreenCaptureKit { .. } => {}
|
|
292
357
|
}
|
|
293
358
|
}
|
|
294
359
|
}
|
|
@@ -584,6 +649,8 @@ struct SavedNativeRecording {
|
|
|
584
649
|
last_attempt_at: Option<String>,
|
|
585
650
|
last_error: Option<String>,
|
|
586
651
|
retry_count: u32,
|
|
652
|
+
#[serde(default)]
|
|
653
|
+
custom_pipeline: bool,
|
|
587
654
|
/// True when the SCK finalization callback reported an error, meaning the
|
|
588
655
|
/// MP4 is missing its moov atom and cannot be recovered by retrying.
|
|
589
656
|
#[serde(default)]
|
|
@@ -870,6 +937,10 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
870
937
|
mic_device_id: Option<String>,
|
|
871
938
|
mic_device_label: Option<String>,
|
|
872
939
|
capture_region: Option<NativeCaptureRegion>,
|
|
940
|
+
// Local-only recordings never upload, so their live-upload creds stay
|
|
941
|
+
// unresolved regardless of what the shared session holds.
|
|
942
|
+
local_only: Option<bool>,
|
|
943
|
+
has_camera: Option<bool>,
|
|
873
944
|
) -> Result<NativeFullscreenStartInfo, String> {
|
|
874
945
|
#[cfg(not(target_os = "macos"))]
|
|
875
946
|
{
|
|
@@ -882,12 +953,40 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
882
953
|
mic_device_id,
|
|
883
954
|
mic_device_label,
|
|
884
955
|
capture_region,
|
|
956
|
+
local_only,
|
|
957
|
+
has_camera,
|
|
885
958
|
);
|
|
886
959
|
return Err("Native full-screen recording is currently macOS-only.".into());
|
|
887
960
|
}
|
|
888
961
|
|
|
889
962
|
#[cfg(target_os = "macos")]
|
|
890
963
|
{
|
|
964
|
+
// Live-upload credentials (server URL + cookie + auth token) live in the
|
|
965
|
+
// shared session state the renderer keeps warm via
|
|
966
|
+
// `meetings_watcher_set_session`. Read them from there instead of taking
|
|
967
|
+
// them as command args. Local-only recordings never upload, so leave
|
|
968
|
+
// their creds `None` regardless of what the session currently holds.
|
|
969
|
+
let (server_url, auth_token, cookie) = if local_only.unwrap_or(false) {
|
|
970
|
+
(None, None, None)
|
|
971
|
+
} else {
|
|
972
|
+
let snapshot = app
|
|
973
|
+
.try_state::<crate::meetings_watcher::MeetingsWatcherState>()
|
|
974
|
+
.map(|s| s.session_snapshot())
|
|
975
|
+
.unwrap_or_default();
|
|
976
|
+
(
|
|
977
|
+
snapshot.server_url,
|
|
978
|
+
snapshot.auth_token,
|
|
979
|
+
snapshot.session_cookie,
|
|
980
|
+
)
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// Best-effort, non-blocking: keep the server-controlled feature flag
|
|
984
|
+
// cache warm using the session creds this call already has. Never
|
|
985
|
+
// delays recording start — a stale/default cache is used for THIS
|
|
986
|
+
// recording if the fetch hasn't landed yet; the result applies to
|
|
987
|
+
// the next one.
|
|
988
|
+
crate::remote_flags::spawn_refresh(server_url.clone(), cookie.clone(), auth_token.clone());
|
|
989
|
+
|
|
891
990
|
let is_warmed = {
|
|
892
991
|
let guard = state.inner.lock().map_err(|e| e.to_string())?;
|
|
893
992
|
guard
|
|
@@ -896,7 +995,7 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
896
995
|
.unwrap_or(false)
|
|
897
996
|
};
|
|
898
997
|
if !is_warmed {
|
|
899
|
-
|
|
998
|
+
let info = start_native_session_locked(
|
|
900
999
|
&app,
|
|
901
1000
|
&state,
|
|
902
1001
|
&recording_id,
|
|
@@ -906,7 +1005,23 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
906
1005
|
mic_device_label,
|
|
907
1006
|
capture_region,
|
|
908
1007
|
false,
|
|
909
|
-
)
|
|
1008
|
+
)?;
|
|
1009
|
+
{
|
|
1010
|
+
let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
|
|
1011
|
+
if let Some(session) = guard.as_mut() {
|
|
1012
|
+
attach_live_uploader_to_session(
|
|
1013
|
+
&app,
|
|
1014
|
+
session,
|
|
1015
|
+
&recording_id,
|
|
1016
|
+
server_url.as_deref(),
|
|
1017
|
+
auth_token.as_deref(),
|
|
1018
|
+
cookie.as_deref(),
|
|
1019
|
+
include_audio,
|
|
1020
|
+
has_camera.unwrap_or(false),
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return Ok(info);
|
|
910
1025
|
}
|
|
911
1026
|
|
|
912
1027
|
// Grab the mic-ready diagnostics without holding the lock during the wait.
|
|
@@ -920,6 +1035,9 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
920
1035
|
}) => mic_ready
|
|
921
1036
|
.as_ref()
|
|
922
1037
|
.map(|ready| (Arc::clone(ready), mic_sample_count.clone())),
|
|
1038
|
+
Some(NativeFullscreenBackend::CustomScreenCaptureKit { mic_ready, .. }) => {
|
|
1039
|
+
mic_ready.as_ref().map(|ready| (Arc::clone(ready), None))
|
|
1040
|
+
}
|
|
923
1041
|
_ => None,
|
|
924
1042
|
})
|
|
925
1043
|
};
|
|
@@ -945,13 +1063,20 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
945
1063
|
.ok_or_else(|| "No native full-screen recording is active.".to_string())?;
|
|
946
1064
|
let width = session.width;
|
|
947
1065
|
let height = session.height;
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1066
|
+
match session.backend.as_ref() {
|
|
1067
|
+
Some(NativeFullscreenBackend::ScreenCaptureKit {
|
|
1068
|
+
stream, recording, ..
|
|
1069
|
+
}) => {
|
|
1070
|
+
stream
|
|
1071
|
+
.add_recording_output(recording)
|
|
1072
|
+
.map_err(|e| format!("add recording output failed: {e:?}"))?;
|
|
1073
|
+
}
|
|
1074
|
+
Some(NativeFullscreenBackend::CustomScreenCaptureKit {
|
|
1075
|
+
recording_enabled, ..
|
|
1076
|
+
}) => {
|
|
1077
|
+
recording_enabled.store(true, Ordering::SeqCst);
|
|
1078
|
+
}
|
|
1079
|
+
_ => {}
|
|
955
1080
|
}
|
|
956
1081
|
if mic_ready_before_attach.is_some() || mic_samples_before_attach.is_some() {
|
|
957
1082
|
eprintln!(
|
|
@@ -975,6 +1100,17 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
975
1100
|
session.current_segment_started_at = now;
|
|
976
1101
|
session.pending_recording_output = false;
|
|
977
1102
|
|
|
1103
|
+
attach_live_uploader_to_session(
|
|
1104
|
+
&app,
|
|
1105
|
+
session,
|
|
1106
|
+
&recording_id,
|
|
1107
|
+
server_url.as_deref(),
|
|
1108
|
+
auth_token.as_deref(),
|
|
1109
|
+
cookie.as_deref(),
|
|
1110
|
+
include_audio,
|
|
1111
|
+
has_camera.unwrap_or(false),
|
|
1112
|
+
);
|
|
1113
|
+
|
|
978
1114
|
Ok(NativeFullscreenStartInfo {
|
|
979
1115
|
recording_id,
|
|
980
1116
|
width,
|
|
@@ -1008,7 +1144,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1008
1144
|
// promptly instead of letting it run through the merge window and push the
|
|
1009
1145
|
// saved transcript past the clip's real end. See recorder.ts `handle.stop()`.
|
|
1010
1146
|
let StoppedSession {
|
|
1011
|
-
session,
|
|
1147
|
+
mut session,
|
|
1012
1148
|
duration_ms,
|
|
1013
1149
|
stop_outcome,
|
|
1014
1150
|
consolidate_outcome,
|
|
@@ -1069,6 +1205,28 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1069
1205
|
)?;
|
|
1070
1206
|
let stop_error = stop_outcome.err();
|
|
1071
1207
|
if let Some(stop_err) = &stop_error {
|
|
1208
|
+
// Capture-side finalize/write failure: the on-disk file is incomplete
|
|
1209
|
+
// even though its init-segment moov is present (segmented fMP4 always
|
|
1210
|
+
// carries one). Never upload — the live uploader already streamed a
|
|
1211
|
+
// prefix, and finishing here would report success on a truncated clip
|
|
1212
|
+
// and delete the local copy. Fail closed and keep the retry copy.
|
|
1213
|
+
if stop_err.starts_with(CAPTURE_FINALIZE_INCOMPLETE_PREFIX) {
|
|
1214
|
+
saved.last_error = Some(stop_err.clone());
|
|
1215
|
+
write_saved_recording_metadata(&app, &saved)?;
|
|
1216
|
+
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
1217
|
+
let error = format!(
|
|
1218
|
+
"{stop_err}. The clip was saved locally and can be retried from the Clips menu."
|
|
1219
|
+
);
|
|
1220
|
+
emit_native_upload_finished(
|
|
1221
|
+
&app,
|
|
1222
|
+
&server_url,
|
|
1223
|
+
&recording_id,
|
|
1224
|
+
false,
|
|
1225
|
+
Some(error.clone()),
|
|
1226
|
+
Some(&saved.file_path),
|
|
1227
|
+
);
|
|
1228
|
+
return Err(error);
|
|
1229
|
+
}
|
|
1072
1230
|
saved.last_error = Some(stop_err.clone());
|
|
1073
1231
|
// Only mark corrupt when the SCK delegate explicitly called recording_did_fail
|
|
1074
1232
|
// (error contains "finalize failed"). Transient stop_capture /
|
|
@@ -1144,13 +1302,121 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1144
1302
|
write_saved_recording_metadata(&app, &saved)?;
|
|
1145
1303
|
emit_native_upload_progress(&app, "preparing", "Optimizing clip", None, None);
|
|
1146
1304
|
|
|
1305
|
+
#[cfg(target_os = "macos")]
|
|
1306
|
+
eprintln!(
|
|
1307
|
+
"[live-upload] stop_and_upload for {recording_id}: live_upload_active={} had_live_upload={} custom_pipeline={}",
|
|
1308
|
+
session.live_upload.is_some(),
|
|
1309
|
+
session.had_live_upload,
|
|
1310
|
+
session.custom_pipeline
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
// Live-upload path: most of the file already streamed to the server
|
|
1314
|
+
// during recording. The writer produces delegate-fed segments that WE
|
|
1315
|
+
// append to the local file (AVFoundation never rewrites it — see the
|
|
1316
|
+
// "Segmented output" section in custom_capture.rs), so the streamed byte
|
|
1317
|
+
// ranges are stable and the uploader can safely drain the tail + send the
|
|
1318
|
+
// final post instead of re-uploading the whole file.
|
|
1319
|
+
#[cfg(target_os = "macos")]
|
|
1320
|
+
if let Some(live) = session.live_upload.take() {
|
|
1321
|
+
eprintln!(
|
|
1322
|
+
"[live-upload] stop: signalling finalize for {recording_id} (duration_ms={duration_ms})"
|
|
1323
|
+
);
|
|
1324
|
+
emit_native_upload_progress(&app, "uploading", "Uploading clip", None, None);
|
|
1325
|
+
live.ctrl
|
|
1326
|
+
.duration_ms
|
|
1327
|
+
.store(duration_ms as u64, Ordering::SeqCst);
|
|
1328
|
+
live.ctrl.finalize.store(true, Ordering::SeqCst);
|
|
1329
|
+
let result = match live.result_rx.await {
|
|
1330
|
+
Ok(inner) => inner,
|
|
1331
|
+
Err(_) => Err("live upload task ended unexpectedly".to_string()),
|
|
1332
|
+
};
|
|
1333
|
+
eprintln!(
|
|
1334
|
+
"[live-upload] stop: finalize result for {recording_id}: {}",
|
|
1335
|
+
match &result {
|
|
1336
|
+
Ok(bytes) => format!("ok ({bytes} bytes)"),
|
|
1337
|
+
Err(e) => format!("error: {e}"),
|
|
1338
|
+
}
|
|
1339
|
+
);
|
|
1340
|
+
return match result {
|
|
1341
|
+
Ok(bytes) => {
|
|
1342
|
+
clear_saved_recording_after_success(&app, &saved);
|
|
1343
|
+
emit_native_upload_finished(&app, &server_url, &recording_id, true, None, None);
|
|
1344
|
+
Ok(NativeFullscreenUploadResult {
|
|
1345
|
+
recording_id,
|
|
1346
|
+
duration_ms,
|
|
1347
|
+
width: session.width,
|
|
1348
|
+
height: session.height,
|
|
1349
|
+
bytes,
|
|
1350
|
+
})
|
|
1351
|
+
}
|
|
1352
|
+
Err(err) => {
|
|
1353
|
+
saved.last_attempt_at = Some(now_iso());
|
|
1354
|
+
saved.last_error = Some(err.clone());
|
|
1355
|
+
saved.retry_count = saved.retry_count.saturating_add(1);
|
|
1356
|
+
let _ = write_saved_recording_metadata(&app, &saved);
|
|
1357
|
+
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
1358
|
+
let error = format!(
|
|
1359
|
+
"{err}. The clip was saved locally and can be retried from the Clips menu."
|
|
1360
|
+
);
|
|
1361
|
+
emit_native_upload_finished(
|
|
1362
|
+
&app,
|
|
1363
|
+
&server_url,
|
|
1364
|
+
&recording_id,
|
|
1365
|
+
false,
|
|
1366
|
+
Some(error.clone()),
|
|
1367
|
+
Some(&saved.file_path),
|
|
1368
|
+
);
|
|
1369
|
+
Err(error)
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// If a live upload was started but later abandoned (e.g. the recording was
|
|
1375
|
+
// paused and became multi-segment), the server holds partial/stale chunks.
|
|
1376
|
+
// Clear them before re-uploading the whole consolidated file. A failed
|
|
1377
|
+
// reset is fatal for this path: uploading from index 0 over leftover
|
|
1378
|
+
// higher-index chunks corrupts the server-side reassembly, so keep the
|
|
1379
|
+
// clip parked locally instead (manual retry resets again first).
|
|
1380
|
+
let auth_token = auth_token.unwrap_or_default();
|
|
1381
|
+
let cookie = cookie.unwrap_or_default();
|
|
1382
|
+
if session.had_live_upload {
|
|
1383
|
+
if let Err(err) = reset_upload_chunks(
|
|
1384
|
+
&server_url,
|
|
1385
|
+
&recording_id,
|
|
1386
|
+
session.mime_type,
|
|
1387
|
+
&auth_token,
|
|
1388
|
+
&cookie,
|
|
1389
|
+
)
|
|
1390
|
+
.await
|
|
1391
|
+
{
|
|
1392
|
+
eprintln!("[live-upload] stop: reset of stale chunks failed for {recording_id}: {err}");
|
|
1393
|
+
saved.last_attempt_at = Some(now_iso());
|
|
1394
|
+
saved.last_error = Some(err.clone());
|
|
1395
|
+
saved.retry_count = saved.retry_count.saturating_add(1);
|
|
1396
|
+
let _ = write_saved_recording_metadata(&app, &saved);
|
|
1397
|
+
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
1398
|
+
let error = format!(
|
|
1399
|
+
"{err}. The clip was saved locally and can be retried from the Clips menu."
|
|
1400
|
+
);
|
|
1401
|
+
emit_native_upload_finished(
|
|
1402
|
+
&app,
|
|
1403
|
+
&server_url,
|
|
1404
|
+
&recording_id,
|
|
1405
|
+
false,
|
|
1406
|
+
Some(error.clone()),
|
|
1407
|
+
Some(&saved.file_path),
|
|
1408
|
+
);
|
|
1409
|
+
return Err(error);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1147
1413
|
let result = upload_recording_file(
|
|
1148
1414
|
&app,
|
|
1149
1415
|
&session,
|
|
1150
1416
|
server_url.clone(),
|
|
1151
1417
|
recording_id.clone(),
|
|
1152
|
-
auth_token
|
|
1153
|
-
cookie
|
|
1418
|
+
auth_token,
|
|
1419
|
+
cookie,
|
|
1154
1420
|
upload_mode,
|
|
1155
1421
|
duration_ms,
|
|
1156
1422
|
has_audio,
|
|
@@ -1197,12 +1463,17 @@ pub async fn native_fullscreen_recording_stop_and_save(
|
|
|
1197
1463
|
file_role: String,
|
|
1198
1464
|
) -> Result<NativeFullscreenSaveResult, String> {
|
|
1199
1465
|
let StoppedSession {
|
|
1200
|
-
session,
|
|
1466
|
+
mut session,
|
|
1201
1467
|
duration_ms,
|
|
1202
1468
|
stop_outcome,
|
|
1203
1469
|
consolidate_outcome,
|
|
1204
1470
|
multi_segment,
|
|
1205
1471
|
} = take_and_finalize_active_session(&state, |_session| {})?;
|
|
1472
|
+
// Saving locally — stop any in-flight live upload to the server.
|
|
1473
|
+
#[cfg(target_os = "macos")]
|
|
1474
|
+
if let Some(live) = session.live_upload.take() {
|
|
1475
|
+
live.ctrl.cancelled.store(true, Ordering::SeqCst);
|
|
1476
|
+
}
|
|
1206
1477
|
// Capture is finalized — drop the camera bubble now so the face
|
|
1207
1478
|
// doesn't linger while the clip saves (mirrors the upload path).
|
|
1208
1479
|
let _ = crate::clips::close_bubble(app.clone()).await;
|
|
@@ -1329,8 +1600,65 @@ pub async fn native_fullscreen_recording_pause(
|
|
|
1329
1600
|
if session.paused_at.is_some() {
|
|
1330
1601
|
return Ok(());
|
|
1331
1602
|
}
|
|
1603
|
+
eprintln!(
|
|
1604
|
+
"[clips-tray] pause requested for {}: segment #{} ran {}ms",
|
|
1605
|
+
session.restart.safe_id,
|
|
1606
|
+
session.restart.segment_counter,
|
|
1607
|
+
session.current_segment_started_at.elapsed().as_millis()
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
// Custom pipeline in live-upload (segmented) mode does a SOFT pause: stop
|
|
1611
|
+
// only the capture source and keep the writer, file, and live uploader
|
|
1612
|
+
// alive. Resume splices a fresh SCStream onto the same append-only file, so
|
|
1613
|
+
// there are no segments to concatenate and the upload is never interrupted.
|
|
1614
|
+
#[cfg(target_os = "macos")]
|
|
1615
|
+
{
|
|
1616
|
+
let soft_paused =
|
|
1617
|
+
if let Some(NativeFullscreenBackend::CustomScreenCaptureKit {
|
|
1618
|
+
resume, writer, ..
|
|
1619
|
+
}) = session.backend.as_ref()
|
|
1620
|
+
{
|
|
1621
|
+
if writer.segmented() && writer.is_started() {
|
|
1622
|
+
resume.pause();
|
|
1623
|
+
true
|
|
1624
|
+
} else {
|
|
1625
|
+
false
|
|
1626
|
+
}
|
|
1627
|
+
} else {
|
|
1628
|
+
false
|
|
1629
|
+
};
|
|
1630
|
+
if soft_paused {
|
|
1631
|
+
session.paused_at = Some(Instant::now());
|
|
1632
|
+
eprintln!(
|
|
1633
|
+
"[clips-tray] soft-paused {} (writer/file/live upload kept alive)",
|
|
1634
|
+
session.restart.safe_id
|
|
1635
|
+
);
|
|
1636
|
+
return Ok(());
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Fallback hard-pause path for pipelines the soft pause above does NOT
|
|
1641
|
+
// cover: the stock recorder, the custom pipeline with live upload off, and
|
|
1642
|
+
// a pause before the first frame. (When live upload is on, the soft pause
|
|
1643
|
+
// handles it and returns before reaching here, keeping the uploader alive.)
|
|
1644
|
+
//
|
|
1645
|
+
// Live upload assumes a single append-only file — the uploader tails one
|
|
1646
|
+
// growing file by byte offset. In these pipelines, pause finalizes that
|
|
1647
|
+
// file and resume starts a brand-new one (multi-segment), which stop later
|
|
1648
|
+
// stitches together into different bytes, so those offsets would be
|
|
1649
|
+
// meaningless. Abandon the in-flight uploader here; the stop path then
|
|
1650
|
+
// resets the partial chunks and re-uploads the consolidated file whole.
|
|
1651
|
+
#[cfg(target_os = "macos")]
|
|
1652
|
+
if let Some(live) = session.live_upload.take() {
|
|
1653
|
+
eprintln!(
|
|
1654
|
+
"[clips-tray] pause: abandoning live upload for {} (recording becomes multi-segment)",
|
|
1655
|
+
session.restart.safe_id
|
|
1656
|
+
);
|
|
1657
|
+
live.ctrl.cancelled.store(true, Ordering::SeqCst);
|
|
1658
|
+
}
|
|
1332
1659
|
if session.backend.is_none() {
|
|
1333
1660
|
// No active backend means we're already paused (or never started).
|
|
1661
|
+
eprintln!("[clips-tray] pause: no active backend; marking paused only");
|
|
1334
1662
|
session.paused_at = Some(Instant::now());
|
|
1335
1663
|
return Ok(());
|
|
1336
1664
|
}
|
|
@@ -1340,6 +1668,17 @@ pub async fn native_fullscreen_recording_pause(
|
|
|
1340
1668
|
}
|
|
1341
1669
|
recover_from_unusable_current_segment(session, "pause", true);
|
|
1342
1670
|
session.paused_at = Some(Instant::now());
|
|
1671
|
+
let current_segment_bytes = session
|
|
1672
|
+
.segments
|
|
1673
|
+
.last()
|
|
1674
|
+
.and_then(|path| std::fs::metadata(path).ok())
|
|
1675
|
+
.map(|meta| meta.len());
|
|
1676
|
+
eprintln!(
|
|
1677
|
+
"[clips-tray] paused {}: {} segment(s) on disk, finalized segment size={:?} bytes",
|
|
1678
|
+
session.restart.safe_id,
|
|
1679
|
+
session.segments.len(),
|
|
1680
|
+
current_segment_bytes
|
|
1681
|
+
);
|
|
1343
1682
|
Ok(())
|
|
1344
1683
|
}
|
|
1345
1684
|
|
|
@@ -1360,11 +1699,53 @@ pub async fn native_fullscreen_recording_resume(
|
|
|
1360
1699
|
return Ok(());
|
|
1361
1700
|
};
|
|
1362
1701
|
|
|
1702
|
+
// Soft-resume counterpart to the custom-pipeline soft pause: the backend
|
|
1703
|
+
// was never torn down, so build a fresh SCStream onto the same writer and
|
|
1704
|
+
// rebase its timestamps past the pause gap instead of starting a new
|
|
1705
|
+
// segment file.
|
|
1706
|
+
#[cfg(target_os = "macos")]
|
|
1707
|
+
{
|
|
1708
|
+
let paused_for = paused_at.elapsed();
|
|
1709
|
+
let soft_resumed =
|
|
1710
|
+
if let Some(NativeFullscreenBackend::CustomScreenCaptureKit {
|
|
1711
|
+
resume, writer, ..
|
|
1712
|
+
}) = session.backend.as_ref()
|
|
1713
|
+
{
|
|
1714
|
+
if writer.segmented() && writer.is_started() {
|
|
1715
|
+
resume.resume(paused_for)?;
|
|
1716
|
+
true
|
|
1717
|
+
} else {
|
|
1718
|
+
false
|
|
1719
|
+
}
|
|
1720
|
+
} else {
|
|
1721
|
+
false
|
|
1722
|
+
};
|
|
1723
|
+
if soft_resumed {
|
|
1724
|
+
session.paused_total = session
|
|
1725
|
+
.paused_total
|
|
1726
|
+
.checked_add(paused_for)
|
|
1727
|
+
.unwrap_or(session.paused_total);
|
|
1728
|
+
session.paused_at = None;
|
|
1729
|
+
eprintln!(
|
|
1730
|
+
"[clips-tray] soft-resumed {} after {}ms paused (single continuous file, paused_total={}ms)",
|
|
1731
|
+
session.restart.safe_id,
|
|
1732
|
+
paused_for.as_millis(),
|
|
1733
|
+
session.paused_total.as_millis()
|
|
1734
|
+
);
|
|
1735
|
+
return Ok(());
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1363
1739
|
let restart = session.restart.clone();
|
|
1364
1740
|
let next_counter = restart.segment_counter.saturating_add(1);
|
|
1365
1741
|
let extension = native_extension_for_mime_type(session.mime_type);
|
|
1366
1742
|
let segment_path = segment_path_for(&app, &restart.safe_id, extension, next_counter)?;
|
|
1367
1743
|
let _ = std::fs::remove_file(&segment_path);
|
|
1744
|
+
eprintln!(
|
|
1745
|
+
"[clips-tray] resume requested for {}: starting segment #{next_counter} -> {}",
|
|
1746
|
+
restart.safe_id,
|
|
1747
|
+
segment_path.display()
|
|
1748
|
+
);
|
|
1368
1749
|
|
|
1369
1750
|
// Re-check disk space before starting the new segment. The mid-recording
|
|
1370
1751
|
// monitor warns but does not block, so space can drop below the hard limit
|
|
@@ -1403,6 +1784,13 @@ pub async fn native_fullscreen_recording_resume(
|
|
|
1403
1784
|
.checked_add(paused_at.elapsed())
|
|
1404
1785
|
.unwrap_or(session.paused_total);
|
|
1405
1786
|
session.paused_at = None;
|
|
1787
|
+
eprintln!(
|
|
1788
|
+
"[clips-tray] resumed {}: segment #{next_counter} live after {}ms paused (segments={}, paused_total={}ms)",
|
|
1789
|
+
restart.safe_id,
|
|
1790
|
+
paused_at.elapsed().as_millis(),
|
|
1791
|
+
session.segments.len(),
|
|
1792
|
+
session.paused_total.as_millis()
|
|
1793
|
+
);
|
|
1406
1794
|
Ok(())
|
|
1407
1795
|
}
|
|
1408
1796
|
|
|
@@ -1577,7 +1965,7 @@ fn take_and_finalize_active_session(
|
|
|
1577
1965
|
if let Err(err) = &consolidate_outcome {
|
|
1578
1966
|
eprintln!("[clips-tray] segment consolidation failed: {err}");
|
|
1579
1967
|
}
|
|
1580
|
-
|
|
1968
|
+
eprintln!(
|
|
1581
1969
|
"[clips-tray] consolidate done (ok={}, segments={}, multi={multi_segment}); {}",
|
|
1582
1970
|
consolidate_outcome.is_ok(),
|
|
1583
1971
|
session.segments.len(),
|
|
@@ -1674,6 +2062,10 @@ fn recover_from_unusable_current_segment(
|
|
|
1674
2062
|
/// session displaced by a new start). Finalizes any active backend and
|
|
1675
2063
|
/// deletes every on-disk artifact — segment files and the final path.
|
|
1676
2064
|
fn discard_session(session: &mut NativeFullscreenSession) {
|
|
2065
|
+
#[cfg(target_os = "macos")]
|
|
2066
|
+
if let Some(live) = session.live_upload.take() {
|
|
2067
|
+
live.ctrl.cancelled.store(true, Ordering::SeqCst);
|
|
2068
|
+
}
|
|
1677
2069
|
if let Some(stop) = &session.disk_monitor_stop {
|
|
1678
2070
|
stop.store(true, Ordering::Relaxed);
|
|
1679
2071
|
}
|
|
@@ -1713,22 +2105,34 @@ fn start_segment_backend(
|
|
|
1713
2105
|
) -> Result<(NativeFullscreenBackend, Option<u32>, Option<u32>), String> {
|
|
1714
2106
|
#[cfg(target_os = "macos")]
|
|
1715
2107
|
{
|
|
1716
|
-
//
|
|
1717
|
-
//
|
|
1718
|
-
|
|
1719
|
-
let
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2108
|
+
// safe_id isn't needed on macOS — the segment path is pre-computed by
|
|
2109
|
+
// the caller. Consume to silence the unused-variable warning.
|
|
2110
|
+
let _ = safe_id;
|
|
2111
|
+
let sck_result = if crate::remote_flags::current().use_custom_sck_pipeline {
|
|
2112
|
+
start_custom_screencapturekit_backend_at(
|
|
2113
|
+
app,
|
|
2114
|
+
segment_path,
|
|
2115
|
+
include_audio,
|
|
2116
|
+
capture_system_audio,
|
|
2117
|
+
mic_device_id,
|
|
2118
|
+
mic_device_label,
|
|
2119
|
+
target_display_id,
|
|
2120
|
+
capture_region,
|
|
2121
|
+
false,
|
|
2122
|
+
)
|
|
2123
|
+
} else {
|
|
2124
|
+
start_screencapturekit_backend_at(
|
|
2125
|
+
segment_path,
|
|
2126
|
+
include_audio,
|
|
2127
|
+
capture_system_audio,
|
|
2128
|
+
mic_device_id,
|
|
2129
|
+
mic_device_label,
|
|
2130
|
+
target_display_id,
|
|
2131
|
+
capture_region,
|
|
2132
|
+
false,
|
|
2133
|
+
)
|
|
2134
|
+
};
|
|
2135
|
+
match sck_result {
|
|
1732
2136
|
Ok((backend, w, h)) => return Ok((backend, w, h)),
|
|
1733
2137
|
Err(sck_err) => {
|
|
1734
2138
|
if include_audio {
|
|
@@ -2016,6 +2420,15 @@ fn consolidate_segments_into_path(session: &mut NativeFullscreenSession) -> Resu
|
|
|
2016
2420
|
#[cfg(target_os = "macos")]
|
|
2017
2421
|
{
|
|
2018
2422
|
let segments = session.segments.clone();
|
|
2423
|
+
for (i, segment) in segments.iter().enumerate() {
|
|
2424
|
+
let size = std::fs::metadata(segment).map(|m| m.len()).unwrap_or(0);
|
|
2425
|
+
eprintln!(
|
|
2426
|
+
"[clips-tray] consolidate input segment {}/{}: {} ({size} bytes)",
|
|
2427
|
+
i + 1,
|
|
2428
|
+
segments.len(),
|
|
2429
|
+
segment.display()
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
2019
2432
|
// Concatenate into a temp sibling first so a failure mid-export
|
|
2020
2433
|
// doesn't leave a half-written file at the real output path.
|
|
2021
2434
|
let target_stem = session
|
|
@@ -2771,6 +3184,7 @@ fn saved_recording_from_path(
|
|
|
2771
3184
|
last_attempt_at: None,
|
|
2772
3185
|
last_error: None,
|
|
2773
3186
|
retry_count: 0,
|
|
3187
|
+
custom_pipeline: session.custom_pipeline,
|
|
2774
3188
|
corrupt: false,
|
|
2775
3189
|
})
|
|
2776
3190
|
}
|
|
@@ -2825,6 +3239,11 @@ fn clear_saved_recording_after_success(app: &AppHandle, saved: &SavedNativeRecor
|
|
|
2825
3239
|
"[clips-tray] upload succeeded for {}, but local pending recording cleanup failed: {err}",
|
|
2826
3240
|
saved.recording_id
|
|
2827
3241
|
);
|
|
3242
|
+
} else {
|
|
3243
|
+
eprintln!(
|
|
3244
|
+
"[clips-tray] upload succeeded for {}; removed local recording file",
|
|
3245
|
+
saved.recording_id
|
|
3246
|
+
);
|
|
2828
3247
|
}
|
|
2829
3248
|
}
|
|
2830
3249
|
|
|
@@ -2905,8 +3324,10 @@ fn start_screencapturekit_recording(
|
|
|
2905
3324
|
let target_display_id = tray_display_id(app);
|
|
2906
3325
|
let path = pending_recording_path(app, safe_id, "mp4")?;
|
|
2907
3326
|
let _ = std::fs::remove_file(&path);
|
|
3327
|
+
let use_custom_sck_pipeline = crate::remote_flags::current().use_custom_sck_pipeline;
|
|
2908
3328
|
eprintln!(
|
|
2909
|
-
"[clips-tray] starting ScreenCaptureKit recording -> {}",
|
|
3329
|
+
"[clips-tray] starting ScreenCaptureKit recording. use_custom_sck_pipeline = {}, path -> {}",
|
|
3330
|
+
use_custom_sck_pipeline,
|
|
2910
3331
|
path.display()
|
|
2911
3332
|
);
|
|
2912
3333
|
let check_path = path.parent().unwrap_or(&path);
|
|
@@ -2924,16 +3345,30 @@ fn start_screencapturekit_recording(
|
|
|
2924
3345
|
);
|
|
2925
3346
|
}
|
|
2926
3347
|
}
|
|
2927
|
-
let (backend, width, height) =
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3348
|
+
let (backend, width, height) = if use_custom_sck_pipeline {
|
|
3349
|
+
start_custom_screencapturekit_backend_at(
|
|
3350
|
+
app,
|
|
3351
|
+
&path,
|
|
3352
|
+
include_audio,
|
|
3353
|
+
capture_system_audio,
|
|
3354
|
+
mic_device_id,
|
|
3355
|
+
mic_device_label,
|
|
3356
|
+
target_display_id,
|
|
3357
|
+
capture_region,
|
|
3358
|
+
defer_recording_output,
|
|
3359
|
+
)?
|
|
3360
|
+
} else {
|
|
3361
|
+
start_screencapturekit_backend_at(
|
|
3362
|
+
&path,
|
|
3363
|
+
include_audio,
|
|
3364
|
+
capture_system_audio,
|
|
3365
|
+
mic_device_id,
|
|
3366
|
+
mic_device_label,
|
|
3367
|
+
target_display_id,
|
|
3368
|
+
capture_region,
|
|
3369
|
+
defer_recording_output,
|
|
3370
|
+
)?
|
|
3371
|
+
};
|
|
2937
3372
|
let (fallback_width, fallback_height) = primary_monitor_size(app);
|
|
2938
3373
|
let mut session = new_fullscreen_session(
|
|
2939
3374
|
backend,
|
|
@@ -3030,6 +3465,13 @@ fn new_fullscreen_session(
|
|
|
3030
3465
|
height: Option<u32>,
|
|
3031
3466
|
restart: RestartInfo,
|
|
3032
3467
|
) -> NativeFullscreenSession {
|
|
3468
|
+
#[cfg(target_os = "macos")]
|
|
3469
|
+
let custom_pipeline = matches!(
|
|
3470
|
+
backend,
|
|
3471
|
+
NativeFullscreenBackend::CustomScreenCaptureKit { .. }
|
|
3472
|
+
);
|
|
3473
|
+
#[cfg(not(target_os = "macos"))]
|
|
3474
|
+
let custom_pipeline = false;
|
|
3033
3475
|
let now = Instant::now();
|
|
3034
3476
|
NativeFullscreenSession {
|
|
3035
3477
|
backend: Some(backend),
|
|
@@ -3046,6 +3488,10 @@ fn new_fullscreen_session(
|
|
|
3046
3488
|
paused_at: None,
|
|
3047
3489
|
restart,
|
|
3048
3490
|
pending_recording_output: false,
|
|
3491
|
+
custom_pipeline,
|
|
3492
|
+
#[cfg(target_os = "macos")]
|
|
3493
|
+
live_upload: None,
|
|
3494
|
+
had_live_upload: false,
|
|
3049
3495
|
disk_monitor_stop: None,
|
|
3050
3496
|
}
|
|
3051
3497
|
}
|
|
@@ -3143,6 +3589,40 @@ pub(crate) fn stop_native_recording(
|
|
|
3143
3589
|
match backend {
|
|
3144
3590
|
NativeFullscreenBackend::Screencapture { child } => stop_screencapture(child),
|
|
3145
3591
|
#[cfg(target_os = "macos")]
|
|
3592
|
+
NativeFullscreenBackend::CustomScreenCaptureKit {
|
|
3593
|
+
stream,
|
|
3594
|
+
writer,
|
|
3595
|
+
watchdog_shutdown,
|
|
3596
|
+
..
|
|
3597
|
+
} => {
|
|
3598
|
+
eprintln!(
|
|
3599
|
+
"[clips-tray] stopping custom capture (wait_for_finalize={wait_for_finalize})"
|
|
3600
|
+
);
|
|
3601
|
+
// Stop the watchdog first so it can't rebuild the stream out from
|
|
3602
|
+
// under us mid-teardown.
|
|
3603
|
+
watchdog_shutdown.store(true, Ordering::SeqCst);
|
|
3604
|
+
let stop_result = stream
|
|
3605
|
+
.lock()
|
|
3606
|
+
.map_err(|e| format!("custom ScreenCaptureKit stop lock poisoned: {e}"))
|
|
3607
|
+
.and_then(|guard| {
|
|
3608
|
+
guard
|
|
3609
|
+
.stop_capture()
|
|
3610
|
+
.map_err(|e| format!("custom ScreenCaptureKit stop failed: {e:?}"))
|
|
3611
|
+
});
|
|
3612
|
+
if let Err(err) = &stop_result {
|
|
3613
|
+
eprintln!("[clips-tray] custom capture stop_capture error: {err}");
|
|
3614
|
+
}
|
|
3615
|
+
let finish_result = writer.finish(wait_for_finalize);
|
|
3616
|
+
// The finalize/segment-write result reflects on-disk file integrity,
|
|
3617
|
+
// so it takes priority over a (possibly benign) stop_capture error
|
|
3618
|
+
// and is tagged so the upload path fails closed instead of
|
|
3619
|
+
// publishing a truncated clip.
|
|
3620
|
+
match finish_result {
|
|
3621
|
+
Ok(()) => stop_result,
|
|
3622
|
+
Err(err) => Err(format!("{CAPTURE_FINALIZE_INCOMPLETE_PREFIX}{err}")),
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
#[cfg(target_os = "macos")]
|
|
3146
3626
|
NativeFullscreenBackend::ScreenCaptureKit { stream, finish, .. } => {
|
|
3147
3627
|
// `remove_recording_output()` looks like the clean stop path, but
|
|
3148
3628
|
// on real machines it can block synchronously forever when the
|
|
@@ -3261,6 +3741,7 @@ async fn upload_recording_file(
|
|
|
3261
3741
|
has_audio,
|
|
3262
3742
|
session.restart.mic_captured_in_file,
|
|
3263
3743
|
session.restart.capture_system_audio,
|
|
3744
|
+
session.custom_pipeline,
|
|
3264
3745
|
)?;
|
|
3265
3746
|
let upload_result = upload_prepared_recording_file(
|
|
3266
3747
|
app,
|
|
@@ -3323,6 +3804,7 @@ fn prepare_saved_recording_file(
|
|
|
3323
3804
|
saved.has_audio,
|
|
3324
3805
|
saved.mic_captured,
|
|
3325
3806
|
saved.system_audio_captured,
|
|
3807
|
+
saved.custom_pipeline,
|
|
3326
3808
|
)?;
|
|
3327
3809
|
Ok((prepared, retry_combined_path))
|
|
3328
3810
|
}
|
|
@@ -3686,6 +4168,9 @@ async fn send_upload_post(
|
|
|
3686
4168
|
.map_err(|e| format!("native recording upload failed: {e}"))?;
|
|
3687
4169
|
let status = response.status();
|
|
3688
4170
|
let body = response.text().await.unwrap_or_default();
|
|
4171
|
+
eprintln!(
|
|
4172
|
+
"[live-upload] POST chunk #{index} (final={is_final}) -> {status} for {recording_id}"
|
|
4173
|
+
);
|
|
3689
4174
|
if !status.is_success() {
|
|
3690
4175
|
return Err(format!(
|
|
3691
4176
|
"native recording upload returned {status}: {}",
|
|
@@ -4073,10 +4558,14 @@ fn prepare_recording_file(
|
|
|
4073
4558
|
has_audio: bool,
|
|
4074
4559
|
mic_captured_audio: bool,
|
|
4075
4560
|
system_audio_captured: bool,
|
|
4561
|
+
// The file already contains a single (live-mixed or single-source) audio
|
|
4562
|
+
// track written by the custom pipeline; the L/R downmix repair assumes
|
|
4563
|
+
// mic and system on separate stereo channels and must not run on it.
|
|
4564
|
+
audio_premixed: bool,
|
|
4076
4565
|
) -> Result<PreparedRecordingFile, String> {
|
|
4077
4566
|
// Downmix only repairs mic+system L/R split. Applying it to mic-only
|
|
4078
4567
|
// capture halves speech energy (~6 dB) when SCK puts the mic on one channel.
|
|
4079
|
-
let downmix_audio = mic_captured_audio && system_audio_captured;
|
|
4568
|
+
let downmix_audio = mic_captured_audio && system_audio_captured && !audio_premixed;
|
|
4080
4569
|
let denoise_audio = mic_captured_audio;
|
|
4081
4570
|
// Pregain only for mic-only (no system audio compete) — SCK has no AGC.
|
|
4082
4571
|
let mic_pregain = mic_captured_audio && !system_audio_captured;
|
|
@@ -4119,7 +4608,13 @@ fn prepare_recording_file(
|
|
|
4119
4608
|
|
|
4120
4609
|
let mut smallest_attempt_bytes: Option<u64> = None;
|
|
4121
4610
|
let ffmpeg_path = resolve_ffmpeg_path();
|
|
4122
|
-
|
|
4611
|
+
if has_audio {
|
|
4612
|
+
if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
|
|
4613
|
+
let raw_summary =
|
|
4614
|
+
describe_audio_signal_probe(audio_signal_probe_with_ffmpeg(ffmpeg_path, path));
|
|
4615
|
+
eprintln!("[clips-tray] raw recording audio signal before prepare: {raw_summary}");
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4123
4618
|
if !COMPRESSION_ENABLED || source_bytes < TRANSCODE_THRESHOLD_BYTES {
|
|
4124
4619
|
if has_audio {
|
|
4125
4620
|
if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
|
|
@@ -4629,9 +5124,11 @@ fn normalize_audio_with_ffmpeg(
|
|
|
4629
5124
|
.arg("-i")
|
|
4630
5125
|
.arg(source)
|
|
4631
5126
|
.arg("-map")
|
|
4632
|
-
.arg("0:v:0")
|
|
4633
|
-
|
|
4634
|
-
|
|
5127
|
+
.arg("0:v:0");
|
|
5128
|
+
|
|
5129
|
+
command.arg("-map").arg("0:a?");
|
|
5130
|
+
|
|
5131
|
+
command
|
|
4635
5132
|
.arg("-c:v")
|
|
4636
5133
|
.arg("copy")
|
|
4637
5134
|
.arg("-c:a")
|
|
@@ -4687,16 +5184,6 @@ fn native_transcode_presets(
|
|
|
4687
5184
|
}
|
|
4688
5185
|
}
|
|
4689
5186
|
|
|
4690
|
-
fn native_preset_label(preset: &str) -> &'static str {
|
|
4691
|
-
match preset {
|
|
4692
|
-
"Preset1280x720" | "PresetAppleM4V720pHD" => "720p",
|
|
4693
|
-
"Preset960x540" => "540p",
|
|
4694
|
-
"Preset640x480" | "PresetAppleM4V480pSD" => "480p",
|
|
4695
|
-
"PresetAppleM4VCellular" => "mobile quality",
|
|
4696
|
-
_ => "a smaller preset",
|
|
4697
|
-
}
|
|
4698
|
-
}
|
|
4699
|
-
|
|
4700
5187
|
fn transcode_with_ffmpeg(
|
|
4701
5188
|
ffmpeg_path: &str,
|
|
4702
5189
|
source: &Path,
|
|
@@ -4719,9 +5206,9 @@ fn transcode_with_ffmpeg(
|
|
|
4719
5206
|
.arg("-i")
|
|
4720
5207
|
.arg(source)
|
|
4721
5208
|
.arg("-map")
|
|
4722
|
-
.arg("0:v:0")
|
|
4723
|
-
|
|
4724
|
-
|
|
5209
|
+
.arg("0:v:0");
|
|
5210
|
+
|
|
5211
|
+
command.arg("-map").arg("0:a?");
|
|
4725
5212
|
|
|
4726
5213
|
if let Some((scaled_w, scaled_h)) =
|
|
4727
5214
|
ffmpeg_scaled_dimensions(width, height, preset.max_long_edge, preset.max_short_edge)
|
|
@@ -5439,6 +5926,10 @@ mod segment_recovery_tests {
|
|
|
5439
5926
|
capture_region: None,
|
|
5440
5927
|
},
|
|
5441
5928
|
pending_recording_output: false,
|
|
5929
|
+
custom_pipeline: false,
|
|
5930
|
+
#[cfg(target_os = "macos")]
|
|
5931
|
+
live_upload: None,
|
|
5932
|
+
had_live_upload: false,
|
|
5442
5933
|
disk_monitor_stop: None,
|
|
5443
5934
|
}
|
|
5444
5935
|
}
|