From 8a9e04b8d4d66a13816a8a7dcad0ae7bb6cd85df Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Thu, 1 May 2025 23:46:35 +0700 Subject: [PATCH] UBERF-9731 Recorder initial version (#8800) * UBERF-9731 Recorder initial version Signed-off-by: Alexander Onnikov * fix: rush validate issues Signed-off-by: Alexander Onnikov --------- Signed-off-by: Alexander Onnikov --- common/config/rush/pnpm-lock.yaml | 88 +++- models/all/src/migration.ts | 4 +- models/export/src/migration.ts | 2 +- models/print/src/migration.ts | 2 +- models/recorder/package.json | 7 +- models/recorder/src/index.ts | 11 +- models/recorder/src/migration.ts | 49 ++ models/recorder/src/plugin.ts | 9 +- packages/theme/src/InvertedTheme.svelte | 22 + packages/theme/src/index.ts | 1 + packages/theme/styles/_colors.scss | 4 - plugins/media-assets/assets/icons.svg | 9 +- plugins/media-assets/src/index.ts | 4 +- .../components/MediaPopupCamPreview.svelte | 1 - plugins/media-resources/src/plugin.ts | 2 - plugins/media-resources/src/utils.ts | 4 +- plugins/media/src/plugin.ts | 10 +- plugins/media/src/types.ts | 4 +- plugins/media/src/utils.ts | 10 +- plugins/recorder-assets/lang/cs.json | 5 + plugins/recorder-assets/lang/de.json | 5 + plugins/recorder-assets/lang/en.json | 5 + plugins/recorder-assets/lang/es.json | 5 + plugins/recorder-assets/lang/fr.json | 5 + plugins/recorder-assets/lang/it.json | 15 +- plugins/recorder-assets/lang/ja.json | 5 + plugins/recorder-assets/lang/pt.json | 5 + plugins/recorder-assets/lang/ru.json | 5 + plugins/recorder-assets/lang/zh.json | 5 + plugins/recorder-resources/package.json | 7 +- .../src/components/Camera.svelte | 234 +++++----- .../src/components/Draggable.svelte | 153 +++++++ .../src/components/Panel.svelte | 417 ++++++++++-------- .../src/components/Recorder.svelte | 196 ++++++-- .../src/components/RecorderExt.svelte | 61 +++ .../src/components/RecordingPopup.svelte | 83 ++++ .../src/components/WorkbenchExtension.svelte | 14 + .../{Camera.svelte => CircleLarge.svelte} | 6 +- .../src/components/icons/CircleMedium.svelte | 27 ++ .../src/components/icons/CircleSmall.svelte | 27 ++ .../src/components/icons/Flip.svelte | 25 -- .../src/components/icons/Rec.svelte | 31 ++ .../src/components/icons/RecordOff.svelte | 25 ++ .../src/components/icons/RecordOn.svelte | 27 ++ .../icons/{Cancel.svelte => Restart.svelte} | 0 plugins/recorder-resources/src/index.ts | 7 + plugins/recorder-resources/src/plugin.ts | 23 +- plugins/recorder-resources/src/recorder.ts | 93 ++-- plugins/recorder-resources/src/recording.ts | 196 ++++++-- .../recorder-resources/src/screen-recorder.ts | 62 +-- plugins/recorder-resources/src/stores.ts | 21 + plugins/recorder-resources/src/stream.ts | 12 +- plugins/recorder-resources/src/types.ts | 22 + plugins/recorder-resources/src/uploader.ts | 83 ++-- plugins/recorder-resources/src/utils.ts | 56 +++ plugins/recorder/package.json | 1 + plugins/recorder/src/index.ts | 13 +- plugins/uploader-resources/package.json | 1 - 58 files changed, 1679 insertions(+), 547 deletions(-) create mode 100644 models/recorder/src/migration.ts create mode 100644 packages/theme/src/InvertedTheme.svelte create mode 100644 plugins/recorder-resources/src/components/Draggable.svelte create mode 100644 plugins/recorder-resources/src/components/RecorderExt.svelte create mode 100644 plugins/recorder-resources/src/components/RecordingPopup.svelte create mode 100644 plugins/recorder-resources/src/components/WorkbenchExtension.svelte rename plugins/recorder-resources/src/components/icons/{Camera.svelte => CircleLarge.svelte} (54%) create mode 100644 plugins/recorder-resources/src/components/icons/CircleMedium.svelte create mode 100644 plugins/recorder-resources/src/components/icons/CircleSmall.svelte delete mode 100644 plugins/recorder-resources/src/components/icons/Flip.svelte create mode 100644 plugins/recorder-resources/src/components/icons/Rec.svelte create mode 100644 plugins/recorder-resources/src/components/icons/RecordOff.svelte create mode 100644 plugins/recorder-resources/src/components/icons/RecordOn.svelte rename plugins/recorder-resources/src/components/icons/{Cancel.svelte => Restart.svelte} (100%) create mode 100644 plugins/recorder-resources/src/stores.ts create mode 100644 plugins/recorder-resources/src/types.ts create mode 100644 plugins/recorder-resources/src/utils.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index ef4441dde9..980ab51491 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -55,6 +55,9 @@ importers: '@hcengineering/communication-yaml': specifier: 0.1.172 version: 0.1.172(typescript@5.3.3) + '@hcengineering/drive': + specifier: ^0.6.32 + version: 0.6.490 '@hocuspocus/provider': specifier: ^2.15.2 version: 2.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6(yjs@13.6.23))(yjs@13.6.23) @@ -2083,9 +2086,6 @@ importers: snappyjs: specifier: ^0.7.0 version: 0.7.0 - stream-browserify: - specifier: ^3.0.0 - version: 3.0.0 style-loader: specifier: ^3.3.1 version: 3.3.4(webpack@5.97.1) @@ -2985,6 +2985,9 @@ packages: '@hcengineering/account-client@0.7.28': resolution: {integrity: sha512-k1dkioX+jYrLyAXLLLsBYVCdZKf3WjhnDHw/yeAQgLQQ/9W2ffLtDMZ8lYWV3XMQ3eWrUCodtTDOiAQgXd/LEQ==, tarball: https://npm.pkg.github.com/download/@hcengineering/account-client/0.7.28/32c9bc0dd297f00f4d5461467f25e072d01b9c76} + '@hcengineering/analytics@0.6.490': + resolution: {integrity: sha512-Jcdj9LXiIUxm75jRIaOrNlvTW48bd59ul2I0WB5NE3WHfAXKmk5FIBUd2dtTDhHMQUOZwUOKzVcxfYWIUs5HFQ==, tarball: https://npm.pkg.github.com/download/@hcengineering/analytics/0.6.490/e2ed2bc32ca04e170e2907e6b0c44a3b725d36bd} + '@hcengineering/analytics@0.7.28': resolution: {integrity: sha512-+CI/wJ8zgop+3mEcvhlB1UJBU6tIKq2Y7qTichZjGli8BckEIEwbD4j2beu2qXRSoTT2yUGWM9zpS9+fmH/DAw==, tarball: https://npm.pkg.github.com/download/@hcengineering/analytics/0.7.28/a8faf3d575bb28b9682b659495e738f8e0ca596e} @@ -3036,12 +3039,24 @@ packages: peerDependencies: typescript: ^5.6.3 + '@hcengineering/core@0.6.490': + resolution: {integrity: sha512-BnYeOeSBxjYcwAwGcXcTcmLbNo0ft2Pt7QoDlHV4KLuDCQkU0Xw6hUvDgMyz6irHKciCa0jBdZ2vOgvBTTe3ug==, tarball: https://npm.pkg.github.com/download/@hcengineering/core/0.6.490/562815e33c0dca6833a43d71745ebb12fa6e7cb9} + '@hcengineering/core@0.7.28': resolution: {integrity: sha512-qv4QE7mkect6JYlGMljW5CQ2uU80hFGq0E17lZ5rO79h1boWEKLJpINv+w3J2BdR/nNG7taIsdZg3yfl9fY4bg==, tarball: https://npm.pkg.github.com/download/@hcengineering/core/0.7.28/43b84c63262e2b8fe5554c1967812e2ce6bb56e1} + '@hcengineering/drive@0.6.490': + resolution: {integrity: sha512-UCcs/SQeXSP/PinDmyh/F6qMW3Ji4CI1nQYCLsxcMei6XHwXyMqfVppeqKpbmGVV+5HJOjAsbSaJe2EiLy6l5Q==, tarball: https://npm.pkg.github.com/download/@hcengineering/drive/0.6.490/270638c5412bb3cf2ac5f2a207896b069f87adb9} + + '@hcengineering/platform@0.6.490': + resolution: {integrity: sha512-jtCHMznMpbPJQaX2RTbKN0tR5e2YgSrIYWHREXgi8VIAIygMLNtzkkNkVX/2FgcOHK+Gs1aF3mf4gsikiKyyKw==, tarball: https://npm.pkg.github.com/download/@hcengineering/platform/0.6.490/45e5e6cbd7fcb12cf241c5ace3c027689052f454} + '@hcengineering/platform@0.7.28': resolution: {integrity: sha512-0zEifwsLdOs/M6TZ9xnPrQTTlhwW28sooiyL4ZfQsiC2AJUy05vLP5/W8V9eDLta+EiR5bjtlulGsg1GfPjUoQ==, tarball: https://npm.pkg.github.com/download/@hcengineering/platform/0.7.28/2126ff97284b679ceab3c86f24febd73a3a28712} + '@hcengineering/preference@0.6.490': + resolution: {integrity: sha512-gwX9U1T2H3A/yDoT2DLFn4SPD9Ki1mQ4bp3MhbPc6uc/cv8Dm2eD/nFM1viT2j03TrM8caKIpB0epONH3qj12g==, tarball: https://npm.pkg.github.com/download/@hcengineering/preference/0.6.490/9e1a4219a1cbec866faf43f7584abcda8fb9b6c2} + '@hcengineering/preference@0.7.28': resolution: {integrity: sha512-WwwOfjZ1TLdTfPhQGvSjo8dS63HyuE7BDVhfstxP8D+H4BI5UKYoev/6vlwQf/3d+jI1S4f2ivO//IDJAD7sGg==, tarball: https://npm.pkg.github.com/download/@hcengineering/preference/0.7.28/f1739991cc012d194fa18f99565ae5bd64f0ad07} @@ -3051,9 +3066,15 @@ packages: '@hcengineering/tags@0.7.28': resolution: {integrity: sha512-2KCVUu5N/GAkDsDRbdUi7gnBq4ze1Z3eCLoc8nz2G/zeAjAga8gmWkZEbXqAC8rNVqBQv72EKeJFGJyqnev6Xw==, tarball: https://npm.pkg.github.com/download/@hcengineering/tags/0.7.28/42545cc43e3c61e32ce96be87c245fa929f5cbb6} + '@hcengineering/theme@0.6.490': + resolution: {integrity: sha512-avJpw+/Sx0xZDctrFImANYBGK4TXdOphI158jcWgpC1ys6IBiRHKeeUtquTKud8q9mE06jb7TjOt9Oo/wrCGWg==, tarball: https://npm.pkg.github.com/download/@hcengineering/theme/0.6.490/2f8704cc5a089ce66e7d6dae4aa3ade6f3ea9b7a} + '@hcengineering/theme@0.7.28': resolution: {integrity: sha512-Hv3wQKHr327TC7wMHCBiG4bMtg8vSlb7+UFwdKI8ztEs3359meqkXu+exqi7Nl5334voXIz5ZFxjwZwOMzNSFA==, tarball: https://npm.pkg.github.com/download/@hcengineering/theme/0.7.28/557640a7ba2ffaa4d6e5411a54577f60dc528300} + '@hcengineering/ui@0.6.490': + resolution: {integrity: sha512-W/7ljW5a/nSfzHQjJRknlnUkKC9vGRkXUjOiBKIjMiJxw7NZwNyfv5quQxNswdARMYRrAfcfdxUOhEmIZYC2Lg==, tarball: https://npm.pkg.github.com/download/@hcengineering/ui/0.6.490/27bb6fcefc46410988abe75404bbd0465d0dd2a1} + '@hcengineering/ui@0.7.28': resolution: {integrity: sha512-QsUpO0+Idu1n2dC6KtorPJ6SSp30It8FHSuG3Y+Awv4td/ftVocOSFqhskbk+us9Yw9KqOYgkqhGYEHbfRfw4Q==, tarball: https://npm.pkg.github.com/download/@hcengineering/ui/0.7.28/82025fc775702ea8c8569611356662bfbf28eb74} @@ -4543,7 +4564,7 @@ packages: version: 0.0.0 '@rush-temp/model-recorder@file:projects/model-recorder.tgz': - resolution: {integrity: sha512-fRjS1msAMkf33rDm+7u/ywsOHfF0lu9VWivQjWiSnxBwakrn0Ztx3ZU/+XDNkj4CJMPd+lMPTdNx6gtuKUbZ6Q==, tarball: file:projects/model-recorder.tgz} + resolution: {integrity: sha512-zYwcCJVba+KhkMPQhgD7+ePk4S+zv9VyJ4h5PdIGgxfafKhugx5hcwVUthjVlgjyTs4merYQoTFl4ys30KatZA==, tarball: file:projects/model-recorder.tgz} version: 0.0.0 '@rush-temp/model-recruit@file:projects/model-recruit.tgz': @@ -4987,11 +5008,11 @@ packages: version: 0.0.0 '@rush-temp/recorder-resources@file:projects/recorder-resources.tgz': - resolution: {integrity: sha512-Juvy2FmXIbOQdKI94A3Q/2UTQ0aLuMRiCMni/o1Xdwg5g14Pm5TC0ZjkHairKYm1LMDmmUWiRtxOiud+klTSmQ==, tarball: file:projects/recorder-resources.tgz} + resolution: {integrity: sha512-LiFazTORRWULjLfy0S9ycqsfJejlenpfjZ2EBswBl5BNBDSuyJPUHC6ykOn/j3GYgeY/gibIrDs/KFcQq7I4rA==, tarball: file:projects/recorder-resources.tgz} version: 0.0.0 '@rush-temp/recorder@file:projects/recorder.tgz': - resolution: {integrity: sha512-vth2EZ1eZjV91sZVk3POK0QGcb6uoyMX7K2tiJcLWeIABOMfs4vaviuekXGqggE8awr51zxI8I7y+/SfYxEGoA==, tarball: file:projects/recorder.tgz} + resolution: {integrity: sha512-/Oi8ZzFzINaPy13dn0+UfkU+0VdCVzBOgXmArnYDqAq2ow503KqMf8Zv3/rW8oQOlYeS6Jdy6TlGp+IMX2CQuQ==, tarball: file:projects/recorder.tgz} version: 0.0.0 '@rush-temp/recruit-assets@file:projects/recruit-assets.tgz': @@ -5535,7 +5556,7 @@ packages: version: 0.0.0 '@rush-temp/uploader-resources@file:projects/uploader-resources.tgz': - resolution: {integrity: sha512-gsRiOemRCAlhK0K5xzASMWgbIQpVAY8cYqKCms/N4rCDUDVtJL/jTKixLhjZDfUdSe+J537cRsHoP+xm19JrUQ==, tarball: file:projects/uploader-resources.tgz} + resolution: {integrity: sha512-FyPfNKvLhjTBhJwTqdKd0eq1snWLDYrHxxCWwoByt671DiuP+eRd3SKL7WnALLYBGZCxlkNTVvoDrFHPFeehYw==, tarball: file:projects/uploader-resources.tgz} version: 0.0.0 '@rush-temp/uploader@file:projects/uploader.tgz': @@ -14904,6 +14925,10 @@ snapshots: '@hcengineering/core': 0.7.28 '@hcengineering/platform': 0.7.28 + '@hcengineering/analytics@0.6.490': + dependencies: + '@hcengineering/platform': 0.6.490 + '@hcengineering/analytics@0.7.28': dependencies: '@hcengineering/platform': 0.7.28 @@ -15030,16 +15055,39 @@ snapshots: js-yaml: 4.1.0 typescript: 5.7.3 + '@hcengineering/core@0.6.490': + dependencies: + '@hcengineering/analytics': 0.6.490 + '@hcengineering/platform': 0.6.490 + fast-equals: 5.2.2 + '@hcengineering/core@0.7.28': dependencies: '@hcengineering/analytics': 0.7.28 '@hcengineering/platform': 0.7.28 fast-equals: 5.2.2 + '@hcengineering/drive@0.6.490': + dependencies: + '@hcengineering/core': 0.6.490 + '@hcengineering/platform': 0.6.490 + '@hcengineering/preference': 0.6.490 + '@hcengineering/ui': 0.6.490 + + '@hcengineering/platform@0.6.490': + dependencies: + intl-messageformat: 10.7.14 + '@hcengineering/platform@0.7.28': dependencies: intl-messageformat: 10.7.14 + '@hcengineering/preference@0.6.490': + dependencies: + '@hcengineering/core': 0.6.490 + '@hcengineering/platform': 0.6.490 + '@hcengineering/ui': 0.6.490 + '@hcengineering/preference@0.7.28': dependencies: '@hcengineering/core': 0.7.28 @@ -15059,12 +15107,35 @@ snapshots: '@hcengineering/ui': 0.7.28 '@hcengineering/view': 0.7.28 + '@hcengineering/theme@0.6.490': + dependencies: + '@hcengineering/analytics': 0.6.490 + '@hcengineering/platform': 0.6.490 + svelte: 4.2.19 + '@hcengineering/theme@0.7.28': dependencies: '@hcengineering/analytics': 0.7.28 '@hcengineering/platform': 0.7.28 svelte: 4.2.19 + '@hcengineering/ui@0.6.490': + dependencies: + '@hcengineering/analytics': 0.6.490 + '@hcengineering/core': 0.6.490 + '@hcengineering/platform': 0.6.490 + '@hcengineering/theme': 0.6.490 + autolinker: 4.0.0 + date-fns: 2.30.0 + date-fns-tz: 2.0.0(date-fns@2.30.0) + dompurify: 3.2.3 + emoji-regex: 10.3.0 + emojibase: 16.0.0 + fast-equals: 5.2.2 + hls.js: 1.5.20 + plyr: 3.7.8 + svelte: 4.2.19 + '@hcengineering/ui@0.7.28': dependencies: '@hcengineering/analytics': 0.7.28 @@ -23492,7 +23563,6 @@ snapshots: prettier: 3.2.5 prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.19) sass: 1.71.1 - stream-browserify: 3.0.0 svelte: 4.2.19 svelte-check: 3.6.9(@babel/core@7.23.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(postcss@8.5.3)(sass@1.71.1)(svelte@4.2.19) svelte-eslint-parser: 0.33.1(svelte@4.2.19) @@ -23520,6 +23590,7 @@ snapshots: '@rush-temp/recorder@file:projects/recorder.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@20.11.19)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3))': dependencies: + '@hcengineering/drive': 0.6.490 '@types/jest': 29.5.12 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.7.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3) @@ -27416,7 +27487,6 @@ snapshots: prettier: 3.2.5 prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.19) sass: 1.71.1 - stream-browserify: 3.0.0 svelte: 4.2.19 svelte-check: 3.6.9(@babel/core@7.23.9)(postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@20.11.19)(typescript@5.3.3)))(postcss@8.5.3)(sass@1.71.1)(svelte@4.2.19) svelte-eslint-parser: 0.33.1(svelte@4.2.19) diff --git a/models/all/src/migration.ts b/models/all/src/migration.ts index 8025ba696b..fad99f9896 100644 --- a/models/all/src/migration.ts +++ b/models/all/src/migration.ts @@ -59,6 +59,7 @@ import { aiBotId, aiBotOperation } from '@hcengineering/model-ai-bot' import { chatId, chatOperation } from '@hcengineering/model-chat' import { inboxId, inboxOperation } from '@hcengineering/model-inbox' import { processId, processOperation } from '@hcengineering/model-process' +import { recorderId, recorderOperation } from '@hcengineering/model-recorder' export const migrateOperations: [string, MigrateOperation][] = [ ['core', coreOperation], @@ -106,5 +107,6 @@ export const migrateOperations: [string, MigrateOperation][] = [ [aiBotId, aiBotOperation], [chatId, chatOperation], [inboxId, inboxOperation], - [processId, processOperation] + [processId, processOperation], + [recorderId, recorderOperation] ] diff --git a/models/export/src/migration.ts b/models/export/src/migration.ts index 52d09a2cb3..0e50483528 100644 --- a/models/export/src/migration.ts +++ b/models/export/src/migration.ts @@ -15,7 +15,7 @@ import { type MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model' -export const loveOperation: MigrateOperation = { +export const exportOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise {}, async upgrade (state: Map>, client: () => Promise): Promise {} } diff --git a/models/print/src/migration.ts b/models/print/src/migration.ts index 0a67d7efdd..86451f4616 100644 --- a/models/print/src/migration.ts +++ b/models/print/src/migration.ts @@ -15,7 +15,7 @@ import { type MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model' -export const loveOperation: MigrateOperation = { +export const printOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise {}, async upgrade (state: Map>, client: () => Promise): Promise {} } diff --git a/models/recorder/package.json b/models/recorder/package.json index 651519ff02..e57280eddf 100644 --- a/models/recorder/package.json +++ b/models/recorder/package.json @@ -30,9 +30,14 @@ "dependencies": { "@hcengineering/model": "^0.6.11", "@hcengineering/model-core": "^0.6.0", + "@hcengineering/model-presentation": "^0.6.0", "@hcengineering/platform": "^0.6.11", + "@hcengineering/core": "^0.6.32", + "@hcengineering/drive": "^0.6.0", "@hcengineering/uploader": "^0.6.0", "@hcengineering/recorder": "^0.6.0", - "@hcengineering/recorder-resources": "^0.6.0" + "@hcengineering/recorder-resources": "^0.6.0", + "@hcengineering/workbench": "^0.6.16", + "@hcengineering/ui": "^0.6.15" } } diff --git a/models/recorder/src/index.ts b/models/recorder/src/index.ts index b3f3b5f773..5cfaf6986d 100644 --- a/models/recorder/src/index.ts +++ b/models/recorder/src/index.ts @@ -15,13 +15,15 @@ import { type Builder } from '@hcengineering/model' import core from '@hcengineering/model-core' +import presentation from '@hcengineering/model-presentation' +import uploader from '@hcengineering/uploader' +import workbench from '@hcengineering/workbench' import recorder from './plugin' -import uploader from '@hcengineering/uploader' - export { recorderId } from '@hcengineering/recorder' export { recorder as default } +export * from './migration' export function createModel (builder: Builder): void { builder.createDoc(uploader.class.UploadHandlerDefinition, core.space.Model, { @@ -29,4 +31,9 @@ export function createModel (builder: Builder): void { label: recorder.string.Record, icon: recorder.icon.Record }) + + builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { + extension: workbench.extensions.WorkbenchExtensions, + component: recorder.component.WorkbenchExtension + }) } diff --git a/models/recorder/src/migration.ts b/models/recorder/src/migration.ts new file mode 100644 index 0000000000..f9ab3cf814 --- /dev/null +++ b/models/recorder/src/migration.ts @@ -0,0 +1,49 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import drive from '@hcengineering/drive' +import { + type MigrateOperation, + type MigrationClient, + type MigrationUpgradeClient, + createDefaultSpace, + tryUpgrade +} from '@hcengineering/model' +import { recorderId } from '@hcengineering/recorder' +import recorder from './plugin' + +export const recorderOperation: MigrateOperation = { + async migrate (client: MigrationClient, mode): Promise {}, + async upgrade (state: Map>, client: () => Promise, mode): Promise { + await tryUpgrade(mode, state, client, recorderId, [ + { + state: 'create-drive', + func: async (client) => { + await createDefaultSpace( + client, + recorder.space.Drive, + { + name: 'Screen Recordings', + description: 'Screen recordings', + type: drive.spaceType.DefaultDrive, + autoJoin: true + }, + drive.class.Drive + ) + } + } + ]) + } +} diff --git a/models/recorder/src/plugin.ts b/models/recorder/src/plugin.ts index 919a51e5ea..cca99fe16d 100644 --- a/models/recorder/src/plugin.ts +++ b/models/recorder/src/plugin.ts @@ -13,8 +13,15 @@ // limitations under the License. // +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import core from '@hcengineering/core' import { mergeIds } from '@hcengineering/platform' import { recorderId } from '@hcengineering/recorder' import recorder from '@hcengineering/recorder-resources/src/plugin' +import { type AnyComponent } from '@hcengineering/ui/src/types' -export default mergeIds(recorderId, recorder, {}) +export default mergeIds(recorderId, recorder, { + component: { + WorkbenchExtension: '' as AnyComponent + } +}) diff --git a/packages/theme/src/InvertedTheme.svelte b/packages/theme/src/InvertedTheme.svelte new file mode 100644 index 0000000000..8b0089adb6 --- /dev/null +++ b/packages/theme/src/InvertedTheme.svelte @@ -0,0 +1,22 @@ + + + + +
+ +
diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts index 020cc9f2dd..350ba6ef7e 100644 --- a/packages/theme/src/index.ts +++ b/packages/theme/src/index.ts @@ -18,6 +18,7 @@ import '@hcengineering/platform-rig/profiles/ui/svelte' import { derived, writable } from 'svelte/store' export { default as Theme } from './Theme.svelte' +export { default as InvertedTheme } from './InvertedTheme.svelte' /** * @public diff --git a/packages/theme/styles/_colors.scss b/packages/theme/styles/_colors.scss index 75025e2ef1..ed7b505f73 100644 --- a/packages/theme/styles/_colors.scss +++ b/packages/theme/styles/_colors.scss @@ -361,8 +361,6 @@ --theme-link-preview-bg-color: #2B2D41; --theme-link-preview-description-color: #C1C9D6; --theme-link-preview-text-color:#FFFFFF; - --theme-recorder-panel-bg: #2A2939; - --theme-recorder-active-button: #A5BDFF0D; --theme-dialog-border-color: rgba(255, 255, 255, 0.1); --theme-dialog-background-color: #2a2938; @@ -475,8 +473,6 @@ --theme-list-divider-color: rgba(0, 0, 0, .07); --theme-list-subheader-divider: rgba(0, 0, 0, .06); - --theme-recorder-panel-bg: #FFFFFF; - --theme-list-button-color: #F2F2F4; --theme-list-button-hover: #E8E8EA; --theme-link-button-color: #E5E5E7; diff --git a/plugins/media-assets/assets/icons.svg b/plugins/media-assets/assets/icons.svg index 24d8110f07..c3bbda6538 100644 --- a/plugins/media-assets/assets/icons.svg +++ b/plugins/media-assets/assets/icons.svg @@ -3,7 +3,14 @@ + + + + - + + + + diff --git a/plugins/media-assets/src/index.ts b/plugins/media-assets/src/index.ts index 65ec6b015a..dc4d629369 100644 --- a/plugins/media-assets/src/index.ts +++ b/plugins/media-assets/src/index.ts @@ -20,5 +20,7 @@ const icons = require('../assets/icons.svg') as string // eslint-disable-line loadMetadata(media.icon, { Mic: `${icons}#mic`, - Cam: `${icons}#camera` + Cam: `${icons}#cam`, + MicOff: `${icons}#micoff`, + CamOff: `${icons}#camoff` }) diff --git a/plugins/media-resources/src/components/MediaPopupCamPreview.svelte b/plugins/media-resources/src/components/MediaPopupCamPreview.svelte index 5c1ff927cf..61d45249b7 100644 --- a/plugins/media-resources/src/components/MediaPopupCamPreview.svelte +++ b/plugins/media-resources/src/components/MediaPopupCamPreview.svelte @@ -35,7 +35,6 @@ await releaseStream(stream) try { - console.log('getUserMedia', constraints) const newStream = await navigator.mediaDevices.getUserMedia(constraints) if (selected.deviceId === device.deviceId) { stream = newStream diff --git a/plugins/media-resources/src/plugin.ts b/plugins/media-resources/src/plugin.ts index 11b33dc36f..9aaa8adf10 100644 --- a/plugins/media-resources/src/plugin.ts +++ b/plugins/media-resources/src/plugin.ts @@ -33,8 +33,6 @@ export default mergeIds(mediaId, media, { DefaultSpeaker: '' as IntlString, TurnOnCam: '' as IntlString, TurnOffCam: '' as IntlString, - TurnOnMic: '' as IntlString, - TurnOffMic: '' as IntlString, On: '' as IntlString, Off: '' as IntlString } diff --git a/plugins/media-resources/src/utils.ts b/plugins/media-resources/src/utils.ts index 6248dd00cb..36d7e922b7 100644 --- a/plugins/media-resources/src/utils.ts +++ b/plugins/media-resources/src/utils.ts @@ -99,12 +99,12 @@ class MediaSessionImpl registerSession(this) } - setCamera (state: CamState): void { + setCamera (state: CamState | undefined): void { this.state.camera = state this.emit('update', this.state) } - setMicrophone (state: MicState): void { + setMicrophone (state: MicState | undefined): void { this.state.microphone = state this.emit('update', this.state) } diff --git a/plugins/media/src/plugin.ts b/plugins/media/src/plugin.ts index b6519dc5cd..6a743525e5 100644 --- a/plugins/media/src/plugin.ts +++ b/plugins/media/src/plugin.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import type { Plugin, Asset } from '@hcengineering/platform' +import type { Plugin, Asset, IntlString } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { type ComponentExtensionId } from '@hcengineering/ui' @@ -27,7 +27,13 @@ export const mediaPlugin = plugin(mediaId, { }, icon: { Mic: '' as Asset, - Cam: '' as Asset + Cam: '' as Asset, + MicOff: '' as Asset, + CamOff: '' as Asset + }, + string: { + TurnOnMic: '' as IntlString, + TurnOffMic: '' as IntlString } }) diff --git a/plugins/media/src/types.ts b/plugins/media/src/types.ts index 8657bc6b0c..93b24260c9 100644 --- a/plugins/media/src/types.ts +++ b/plugins/media/src/types.ts @@ -35,12 +35,12 @@ export interface MediaSession extends TypedEmitter { /** * Enable or disable camera */ - setCamera: (state: CamState) => void + setCamera: (state: CamState | undefined) => void /** * Enable or disable microphone */ - setMicrophone: (state: MicState) => void + setMicrophone: (state: MicState | undefined) => void /** * Enable or disable media feature diff --git a/plugins/media/src/utils.ts b/plugins/media/src/utils.ts index 03443305db..4cdcfb9690 100644 --- a/plugins/media/src/utils.ts +++ b/plugins/media/src/utils.ts @@ -140,7 +140,7 @@ export function cleanupDeviceLabel (label: string): string { } export async function getCameraStream ( - deviceId: MediaDeviceInfo['deviceId'], + deviceId: MediaDeviceInfo['deviceId'] | undefined, constraints?: MediaStreamConstraints ): Promise { if (navigator?.mediaDevices === undefined) { @@ -151,17 +151,16 @@ export async function getCameraStream ( let video: MediaTrackConstraints | boolean if (constraints?.video === undefined) { - video = { deviceId } + video = deviceId !== undefined ? { deviceId: { exact: deviceId } } : true } else if (constraints.video === true) { - video = { deviceId } + video = deviceId !== undefined ? { deviceId: { exact: deviceId } } : true } else if (constraints.video === false) { video = false } else { - video = { deviceId, ...constraints.video } + video = { deviceId: { exact: deviceId }, ...constraints.video } } try { - console.info('Using media device', constraints) return await navigator.mediaDevices.getUserMedia({ ...constraints, video }) } catch (err: any) { console.warn('Failed to get camera stream', err) @@ -191,7 +190,6 @@ export async function getMicrophoneStream ( } try { - console.info('Using media device', constraints) return await navigator.mediaDevices.getUserMedia({ ...constraints, audio }) } catch (err: any) { console.warn('Failed to get microphone stream', err) diff --git a/plugins/recorder-assets/lang/cs.json b/plugins/recorder-assets/lang/cs.json index 4f9707cd5a..040b74ccfd 100644 --- a/plugins/recorder-assets/lang/cs.json +++ b/plugins/recorder-assets/lang/cs.json @@ -5,6 +5,11 @@ "Stop": "Zastavit", "Resume": "Pokračovat", "Record": "Nahrávat", + "CancelRecording": "Zrušit nahrávání", + "CancelRecordingConfirm": "Opravdu chcete zrušit nahrávání?", + "RestartRecording": "Restart nahrávání", + "RestartRecordingConfirm": "Opravdu chcete restartovat nahrávání?", + "RecordingTitle": "Nahrávání obrazovky {date}", "ClickToSkip": "Kliknutím přeskočíte" } } \ No newline at end of file diff --git a/plugins/recorder-assets/lang/de.json b/plugins/recorder-assets/lang/de.json index 374afbaa62..b4e83154d5 100644 --- a/plugins/recorder-assets/lang/de.json +++ b/plugins/recorder-assets/lang/de.json @@ -5,6 +5,11 @@ "Stop": "Stopp", "Resume": "Fortsetzen", "Record": "Aufnehmen", + "CancelRecording": "Aufnahme abbrechen", + "CancelRecordingConfirm": "Möchten Sie die Aufnahme abbrechen?", + "RestartRecording": "Aufnahme neu starten", + "RestartRecordingConfirm": "Möchten Sie die Aufnahme neu starten?", + "RecordingTitle": "Bildschirmaufnahme {date}", "ClickToSkip": "Zum Überspringen klicken" } } \ No newline at end of file diff --git a/plugins/recorder-assets/lang/en.json b/plugins/recorder-assets/lang/en.json index 1bc6ee2151..8d15056bda 100644 --- a/plugins/recorder-assets/lang/en.json +++ b/plugins/recorder-assets/lang/en.json @@ -5,6 +5,11 @@ "Stop": "Stop", "Resume": "Resume", "Record": "Record", + "CancelRecording": "Cancel recording", + "CancelRecordingConfirm": "Are you sure you want to cancel recording?", + "RestartRecording": "Restart recording", + "RestartRecordingConfirm": "Are you sure you want to restart recording?", + "RecordingTitle": "Screen Recording {date}", "ClickToSkip": "Click to skip" } } diff --git a/plugins/recorder-assets/lang/es.json b/plugins/recorder-assets/lang/es.json index c08d9b8ac4..f4b0f8211c 100644 --- a/plugins/recorder-assets/lang/es.json +++ b/plugins/recorder-assets/lang/es.json @@ -5,6 +5,11 @@ "Stop": "Detener", "Resume": "Reanudar", "Record": "Grabar", + "CancelRecording": "Cancelar grabación", + "CancelRecordingConfirm": "¿Está seguro de que desea cancelar la grabación?", + "RestartRecording": "Reiniciar grabación", + "RestartRecordingConfirm": "¿Está seguro de que desea reiniciar la grabación?", + "RecordingTitle": "Grabación de pantalla {date}", "ClickToSkip": "Haz clic para saltar" } } \ No newline at end of file diff --git a/plugins/recorder-assets/lang/fr.json b/plugins/recorder-assets/lang/fr.json index fda990af61..9645ee0b10 100644 --- a/plugins/recorder-assets/lang/fr.json +++ b/plugins/recorder-assets/lang/fr.json @@ -5,6 +5,11 @@ "Stop": "Arrêter", "Resume": "Reprendre", "Record": "Enregistrer", + "CancelRecording": "Annuler l'enregistrement", + "CancelRecordingConfirm": "Êtes-vous sûr de vouloir annuler l'enregistrement?", + "RestartRecording": "Redémarrer l'enregistrement", + "RestartRecordingConfirm": "Êtes-vous sûr de vouloir redémarrer l'enregistrement?", + "RecordingTitle": "Enregistrement d'écran {date}", "ClickToSkip": "Cliquer pour ignorer" } } \ No newline at end of file diff --git a/plugins/recorder-assets/lang/it.json b/plugins/recorder-assets/lang/it.json index 1bc6ee2151..6e77113603 100644 --- a/plugins/recorder-assets/lang/it.json +++ b/plugins/recorder-assets/lang/it.json @@ -1,10 +1,15 @@ { "string": { - "Cancel": "Cancel", - "Pause": "Pause", + "Cancel": "Annulla", + "Pause": "Pausa", "Stop": "Stop", - "Resume": "Resume", - "Record": "Record", - "ClickToSkip": "Click to skip" + "Resume": "Riprendi", + "Record": "Registra", + "CancelRecording": "Annulla registrazione", + "CancelRecordingConfirm": "Sei sicuro di voler annullare la registrazione?", + "RestartRecording": "Riavvia registrazione", + "RestartRecordingConfirm": "Sei sicuro di voler riavviare la registrazione?", + "RecordingTitle": "Registrazione dello schermo {date}", + "ClickToSkip": "Fai clic per saltare" } } diff --git a/plugins/recorder-assets/lang/ja.json b/plugins/recorder-assets/lang/ja.json index 96fd615065..daf5c57577 100644 --- a/plugins/recorder-assets/lang/ja.json +++ b/plugins/recorder-assets/lang/ja.json @@ -5,6 +5,11 @@ "Stop": "停止", "Resume": "再開", "Record": "記録", + "CancelRecording": "録音をキャンセル", + "CancelRecordingConfirm": "録音をキャンセルしてもよろしいですか?", + "RestartRecording": "録音を再開", + "RestartRecordingConfirm": "録音を再開してもよろしいですか?", + "RecordingTitle": "画面録画 {date}", "ClickToSkip": "クリックしてスキップ" } } diff --git a/plugins/recorder-assets/lang/pt.json b/plugins/recorder-assets/lang/pt.json index 0dd103af45..a2ac6ce482 100644 --- a/plugins/recorder-assets/lang/pt.json +++ b/plugins/recorder-assets/lang/pt.json @@ -5,6 +5,11 @@ "Stop": "Parar", "Resume": "Retomar", "Record": "Gravar", + "CancelRecording": "Cancelar gravação", + "CancelRecordingConfirm": "Tem certeza de que deseja cancelar a gravação?", + "RestartRecording": "Reiniciar gravação", + "RestartRecordingConfirm": "Tem certeza de que deseja reiniciar a gravação?", + "RecordingTitle": "Gravação de tela {date}", "ClickToSkip": "Clique para pular" } } \ No newline at end of file diff --git a/plugins/recorder-assets/lang/ru.json b/plugins/recorder-assets/lang/ru.json index ba95797220..40fdeffad0 100644 --- a/plugins/recorder-assets/lang/ru.json +++ b/plugins/recorder-assets/lang/ru.json @@ -5,6 +5,11 @@ "Stop": "Стоп", "Resume": "Продолжить", "Record": "Запись", + "CancelRecording": "Отменить запись", + "CancelRecordingConfirm": "Вы уверены, что хотите отменить запись?", + "RestartRecording": "Перезапустить запись", + "RestartRecordingConfirm": "Вы уверены, что хотите перезапустить запись?", + "RecordingTitle": "Запись экрана {date}", "ClickToSkip": "Нажмите для пропуска" } } \ No newline at end of file diff --git a/plugins/recorder-assets/lang/zh.json b/plugins/recorder-assets/lang/zh.json index 153806c7d3..3907edb8b3 100644 --- a/plugins/recorder-assets/lang/zh.json +++ b/plugins/recorder-assets/lang/zh.json @@ -5,6 +5,11 @@ "Stop": "停止", "Resume": "继续", "Record": "录制", + "CancelRecording": "取消录制", + "CancelRecordingConfirm": "您确定要取消录制吗?", + "RestartRecording": "重新开始录制", + "RestartRecordingConfirm": "您确定要重新开始录制吗?", + "RecordingTitle": "屏幕录制 {date}", "ClickToSkip": "点击跳过" } } \ No newline at end of file diff --git a/plugins/recorder-resources/package.json b/plugins/recorder-resources/package.json index 61d94686fb..c364d49416 100644 --- a/plugins/recorder-resources/package.json +++ b/plugins/recorder-resources/package.json @@ -40,16 +40,21 @@ }, "dependencies": { "@hcengineering/core": "^0.6.32", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/contact-resources": "^0.6.0", "@hcengineering/platform": "^0.6.11", "@hcengineering/presentation": "^0.6.3", "@hcengineering/recorder": "^0.6.0", "@hcengineering/theme": "^0.6.5", + "@hcengineering/media": "^0.6.0", + "@hcengineering/media-resources": "^0.6.0", "@hcengineering/ui": "^0.6.15", "@hcengineering/uploader": "^0.6.0", + "@hcengineering/drive": "^0.6.0", "@hcengineering/view": "^0.6.13", "@hcengineering/view-resources": "^0.6.0", + "@hcengineering/analytics": "^0.6.0", "install": "^0.13.0", - "stream-browserify": "^3.0.0", "svelte": "^4.2.19", "tus-js-client": "^4.3.1" } diff --git a/plugins/recorder-resources/src/components/Camera.svelte b/plugins/recorder-resources/src/components/Camera.svelte index 035c411db2..cc72ce39d8 100644 --- a/plugins/recorder-resources/src/components/Camera.svelte +++ b/plugins/recorder-resources/src/components/Camera.svelte @@ -14,125 +14,159 @@ --> - - -
{ - showButtons = true - }} - on:mouseleave={() => { - showButtons = false - }} -> - {#if showCamera} - - {#if showButtons} -
{ - stopCamera() - }} - > - -
- {/if} +
+ {#if stream != null && isCamEnabled} + +
diff --git a/plugins/recorder-resources/src/components/Draggable.svelte b/plugins/recorder-resources/src/components/Draggable.svelte new file mode 100644 index 0000000000..00a0b556ab --- /dev/null +++ b/plugins/recorder-resources/src/components/Draggable.svelte @@ -0,0 +1,153 @@ + + + + + + +
{ + clientWidth = element.clientWidth + clientHeight = element.clientHeight + }} +> + +
+ + diff --git a/plugins/recorder-resources/src/components/Panel.svelte b/plugins/recorder-resources/src/components/Panel.svelte index 7ce58c1b80..e43871ca79 100644 --- a/plugins/recorder-resources/src/components/Panel.svelte +++ b/plugins/recorder-resources/src/components/Panel.svelte @@ -14,55 +14,43 @@ --> -{#if state === RecordingState.Inactive} - - -
- - - - - -
-{:else} - - -
- {#if expanded} - - - +
+ + {#if state == null} +
-{/if} +
diff --git a/plugins/recorder-resources/src/components/Recorder.svelte b/plugins/recorder-resources/src/components/Recorder.svelte index 4ce676daed..5ea6397326 100644 --- a/plugins/recorder-resources/src/components/Recorder.svelte +++ b/plugins/recorder-resources/src/components/Recorder.svelte @@ -14,61 +14,177 @@ --> - -
- - -
+ + +
+ {#if useCamera} + + {:else} +
+
+ {/if} + + +
+
+
diff --git a/plugins/recorder-resources/src/components/RecorderExt.svelte b/plugins/recorder-resources/src/components/RecorderExt.svelte new file mode 100644 index 0000000000..88a059a742 --- /dev/null +++ b/plugins/recorder-resources/src/components/RecorderExt.svelte @@ -0,0 +1,61 @@ + + + +{#if endpoint !== ''} + {#if $recording !== null} + + {:else} + + {/if} +{/if} diff --git a/plugins/recorder-resources/src/components/RecordingPopup.svelte b/plugins/recorder-resources/src/components/RecordingPopup.svelte new file mode 100644 index 0000000000..93a7dc05cd --- /dev/null +++ b/plugins/recorder-resources/src/components/RecordingPopup.svelte @@ -0,0 +1,83 @@ + + + +
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+
+ +
+
+
+
+ + diff --git a/plugins/recorder-resources/src/components/WorkbenchExtension.svelte b/plugins/recorder-resources/src/components/WorkbenchExtension.svelte new file mode 100644 index 0000000000..701e61f749 --- /dev/null +++ b/plugins/recorder-resources/src/components/WorkbenchExtension.svelte @@ -0,0 +1,14 @@ + + + diff --git a/plugins/recorder-resources/src/components/icons/Camera.svelte b/plugins/recorder-resources/src/components/icons/CircleLarge.svelte similarity index 54% rename from plugins/recorder-resources/src/components/icons/Camera.svelte rename to plugins/recorder-resources/src/components/icons/CircleLarge.svelte index 9a2e7f134e..0cc7c3e36a 100644 --- a/plugins/recorder-resources/src/components/icons/Camera.svelte +++ b/plugins/recorder-resources/src/components/icons/CircleLarge.svelte @@ -18,8 +18,10 @@ export let fill: string = 'currentColor' - + diff --git a/plugins/recorder-resources/src/components/icons/CircleMedium.svelte b/plugins/recorder-resources/src/components/icons/CircleMedium.svelte new file mode 100644 index 0000000000..d99a6937ae --- /dev/null +++ b/plugins/recorder-resources/src/components/icons/CircleMedium.svelte @@ -0,0 +1,27 @@ + + + + + + + diff --git a/plugins/recorder-resources/src/components/icons/CircleSmall.svelte b/plugins/recorder-resources/src/components/icons/CircleSmall.svelte new file mode 100644 index 0000000000..222d718755 --- /dev/null +++ b/plugins/recorder-resources/src/components/icons/CircleSmall.svelte @@ -0,0 +1,27 @@ + + + + + + + diff --git a/plugins/recorder-resources/src/components/icons/Flip.svelte b/plugins/recorder-resources/src/components/icons/Flip.svelte deleted file mode 100644 index 44686e86d4..0000000000 --- a/plugins/recorder-resources/src/components/icons/Flip.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/plugins/recorder-resources/src/components/icons/Rec.svelte b/plugins/recorder-resources/src/components/icons/Rec.svelte new file mode 100644 index 0000000000..e3b5b79f16 --- /dev/null +++ b/plugins/recorder-resources/src/components/icons/Rec.svelte @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/plugins/recorder-resources/src/components/icons/RecordOff.svelte b/plugins/recorder-resources/src/components/icons/RecordOff.svelte new file mode 100644 index 0000000000..1ae79b3c49 --- /dev/null +++ b/plugins/recorder-resources/src/components/icons/RecordOff.svelte @@ -0,0 +1,25 @@ + + + + + + + diff --git a/plugins/recorder-resources/src/components/icons/RecordOn.svelte b/plugins/recorder-resources/src/components/icons/RecordOn.svelte new file mode 100644 index 0000000000..fef911336e --- /dev/null +++ b/plugins/recorder-resources/src/components/icons/RecordOn.svelte @@ -0,0 +1,27 @@ + + + + + + + diff --git a/plugins/recorder-resources/src/components/icons/Cancel.svelte b/plugins/recorder-resources/src/components/icons/Restart.svelte similarity index 100% rename from plugins/recorder-resources/src/components/icons/Cancel.svelte rename to plugins/recorder-resources/src/components/icons/Restart.svelte diff --git a/plugins/recorder-resources/src/index.ts b/plugins/recorder-resources/src/index.ts index 37c3ec93a3..f48539a16a 100644 --- a/plugins/recorder-resources/src/index.ts +++ b/plugins/recorder-resources/src/index.ts @@ -16,9 +16,16 @@ import { type Resources } from '@hcengineering/platform' import { record } from './recording' +import RecorderExt from './components/RecorderExt.svelte' +import WorkbenchExtension from './components/WorkbenchExtension.svelte' + export { ScreenRecorder } from './screen-recorder' export default async (): Promise => ({ + component: { + RecorderExt, + WorkbenchExtension + }, function: { Record: record } diff --git a/plugins/recorder-resources/src/plugin.ts b/plugins/recorder-resources/src/plugin.ts index c2aebe291a..ed60a9a341 100644 --- a/plugins/recorder-resources/src/plugin.ts +++ b/plugins/recorder-resources/src/plugin.ts @@ -13,7 +13,26 @@ // limitations under the License. // -import { mergeIds } from '@hcengineering/platform' +import { type IntlString, mergeIds } from '@hcengineering/platform' import plugin, { recorderId } from '@hcengineering/recorder' +import { type AnyComponent } from '@hcengineering/ui/src/types' -export default mergeIds(recorderId, plugin, {}) +export default mergeIds(recorderId, plugin, { + component: { + RecorderExt: '' as AnyComponent + }, + string: { + Pause: '' as IntlString, + Stop: '' as IntlString, + Resume: '' as IntlString, + Record: '' as IntlString, + Cancel: '' as IntlString, + Restart: '' as IntlString, + CancelRecording: '' as IntlString, + CancelRecordingConfirm: '' as IntlString, + RestartRecording: '' as IntlString, + RestartRecordingConfirm: '' as IntlString, + RecordingTitle: '' as IntlString, + ClickToSkip: '' as IntlString + } +}) diff --git a/plugins/recorder-resources/src/recorder.ts b/plugins/recorder-resources/src/recorder.ts index 939cb710fc..22e9bfeacb 100644 --- a/plugins/recorder-resources/src/recorder.ts +++ b/plugins/recorder-resources/src/recorder.ts @@ -15,16 +15,29 @@ import { ChunkReader } from './stream' +const chunkIntervalMs = 1000 + +export interface RecorderOptions { + mediaStream: MediaStream +} + export class Recorder { private readonly mediaRecorder: MediaRecorder private readonly chunkStream: ChunkReader private readonly mediaStream: MediaStream + // Total elapsed recording time in milliseconds (excluding pauses) + private elapsedMs: number = 0 + // Timestamp when recording or resuming started; null if paused or not started + private lastStartTime: number | null = null + + constructor (options: RecorderOptions) { + const { mediaStream } = options - constructor (mediaStream: MediaStream) { const supportedTypes = getSupportedMimeTypes() if (supportedTypes.length === 0) { - throw new Error('video recording is not supported') + throw new Error('Recorder: video recording is not supported') } + const mimeType = supportedTypes[0] this.mediaStream = mediaStream this.chunkStream = new ChunkReader() @@ -32,10 +45,15 @@ export class Recorder { this.mediaRecorder.ondataavailable = async (e) => { this.chunkStream.push(e.data) } + this.mediaRecorder.onstart = () => { + console.debug('Recorder: MediaRecorder started', new Date().toISOString()) + } this.mediaRecorder.onstop = () => { + console.debug('Recorder: MediaRecorder stopped', new Date().toISOString()) this.chunkStream.close() } this.mediaRecorder.onerror = (ev) => { + console.error('Recorder: MediaRecorder error:', ev) this.chunkStream.close() } } @@ -45,12 +63,16 @@ export class Recorder { } public start (): void { - const readIntervalMs = 200 - this.mediaRecorder.start(readIntervalMs) + this.elapsedMs = 0 + this.lastStartTime = Date.now() + this.mediaRecorder.start(chunkIntervalMs) + console.debug('Recorder: recording started', new Date().toISOString()) } public resume (): void { + this.lastStartTime = Date.now() this.mediaRecorder.resume() + console.debug('Recorder: recording resumed', new Date().toISOString()) } public asStream (): ChunkReader { @@ -58,20 +80,40 @@ export class Recorder { } public stop (): void { - for (const track of this.mediaStream.getTracks()) { - track.stop() + if (this.lastStartTime !== null) { + this.elapsedMs += Date.now() - this.lastStartTime + this.lastStartTime = null } - this.mediaRecorder.stop() + + try { + this.mediaRecorder.stop() + } catch (err) { + console.error('Recorder: error stopping MediaRecorder:', err) + } finally { + for (const track of this.mediaStream.getTracks()) { + track.stop() + } + } + console.debug('Recorder: recording stopped, duration:', this.getRecordedTimeMs(), 'ms') } public pause (): void { + if (this.lastStartTime !== null) { + this.elapsedMs += Date.now() - this.lastStartTime + this.lastStartTime = null + } this.mediaRecorder.pause() + console.debug('Recorder: recording paused, duration:', this.getRecordedTimeMs(), 'ms') + } + + public getRecordedTimeMs (): number { + return this.lastStartTime !== null ? this.elapsedMs + Date.now() - this.lastStartTime : this.elapsedMs } } function getSupportedMimeTypes (): string[] { - const videoTypes = ['mp4', 'webm', 'ogg', 'x-matroska'] - const videoCodecs = [ + const containers = ['mp4', 'webm', 'ogg', 'x-matroska'] + const codecs = [ 'vp9,opus', 'vp8,opus', 'vp9', @@ -86,24 +128,19 @@ function getSupportedMimeTypes (): string[] { 'h.265' ] - const supportedTypes: string[] = [] - videoTypes.forEach((videoType) => { - const type = `video/${videoType}` - videoCodecs.forEach((codec) => { - const variations = [ - `${type}codecs=${codec}`, - `${type}codecs:${codec}`, - `${type}codecs=${codec.toUpperCase()}`, - `${type}codecs:${codec.toUpperCase()}`, - `${type}` - ] - for (const variation of variations) { - if (MediaRecorder.isTypeSupported(variation)) { - supportedTypes.push(variation) - break - } + const supported: string[] = [] + for (const container of containers) { + const base = `video/${container}` + for (const codec of codecs) { + const mime = `${base};codecs="${codec}"` + if (MediaRecorder.isTypeSupported(mime)) { + supported.push(mime) + break } - }) - }) - return supportedTypes + } + if (MediaRecorder.isTypeSupported(base)) { + supported.push(base) + } + } + return supported } diff --git a/plugins/recorder-resources/src/recording.ts b/plugins/recorder-resources/src/recording.ts index c2db64e894..9ac6935fcd 100644 --- a/plugins/recorder-resources/src/recording.ts +++ b/plugins/recorder-resources/src/recording.ts @@ -13,32 +13,31 @@ // limitations under the License. // -import { type FileUploadOptions, type FileUploadCallback } from '@hcengineering/uploader' -import Recorder from './components/Recorder.svelte' +import { getMetadata } from '@hcengineering/platform' +import presentation from '@hcengineering/presentation' import { showPopup } from '@hcengineering/ui' -import { type Blob, type Ref } from '@hcengineering/core' -import { getBlobUrl } from '@hcengineering/presentation' +import { type FileUploadOptions } from '@hcengineering/uploader' +import { get } from 'svelte/store' -let recorderOppened = false +import Recorder from './components/Recorder.svelte' +import plugin from './plugin' +import { recorder, recording } from './stores' +import { createScreenRecorder } from './screen-recorder' -export async function record (options: FileUploadOptions): Promise { - if (recorderOppened) { - return - } - if (options.onFileUploaded === undefined) { - return - } - recorderOppened = true - const onUploaded = options.onFileUploaded - showPopup( +export function record (options: FileUploadOptions): void { + const current = get(recorder) + if (current !== null) return + + const { onFileUploaded } = options + + const popup = showPopup( Recorder, - {}, + { + onFileUploaded + }, undefined, - async (status: any) => { - if (typeof status === 'string') { - await uploadRecording(status, onUploaded) - } - recorderOppened = false + () => { + recorder.set(null) }, undefined, { @@ -47,21 +46,152 @@ export async function record (options: FileUploadOptions): Promise { fixed: true } ) + + recorder.set(popup) } -async function uploadRecording (recordingName: string, onUploaded: FileUploadCallback): Promise { - await onUploaded({ - uuid: getBlobUrl(recordingName) as Ref, - name: 'Recording-' + now(), - file: { ...new Blob(), type: 'video/x-mpegURL' }, - type: 'video/x-mpegURL', - path: undefined, - metadata: undefined, - navigateOnUpload: true +export interface RecordingOptions { + cameraStream: MediaStream | null + microphoneStream: MediaStream | null + fps?: number + onSuccess?: (uploadId: string) => Promise + onError?: (uploadId: string) => void +} + +export async function startRecording (options: RecordingOptions): Promise { + const current = get(recording) + if (current !== null) { + console.warn('Recording already started', current) + return + } + + const { cameraStream, microphoneStream, fps } = options + + let displayStream: MediaStream + try { + displayStream = await navigator.mediaDevices.getDisplayMedia({ + video: { + frameRate: { ideal: fps ?? 30 } + } + }) + for (const track of displayStream.getVideoTracks()) { + track.onended = () => { + void stopRecording() + } + } + } catch (err: any) { + if (err.name === 'NotAllowedError') { + console.debug('User denied screen capture permission', err) + } else { + console.error('Failed to get display media', err) + } + return + } + + const mediaStream = makeCombinedStream(displayStream, cameraStream, microphoneStream) + + const token = getMetadata(presentation.metadata.Token) ?? '' + const workspace = getMetadata(presentation.metadata.WorkspaceUuid) ?? '' + const endpoint = getMetadata(plugin.metadata.StreamUrl) ?? '' + + let width = 0 + let height = 0 + + mediaStream.getVideoTracks().forEach((track) => { + width = Math.max(track.getSettings().width ?? width, width) + height = Math.max(track.getSettings().height ?? height, height) }) + + // Callback invoked when upload succeeds, passing blobId + const onSuccess = (blobId: string): void => { + console.debug('Recording upload success', { blobId }) + void options.onSuccess?.(blobId) + } + + // Callback invoked when upload fails, passing error + const onError = (error: any): void => { + console.error('Recording upload failed', error) + options.onError?.(error) + } + + const recorder = createScreenRecorder(mediaStream, { token, endpoint, workspace, width, height, onSuccess, onError }) + await recorder.start() + recording.set({ recorder, stream: recorder.stream, state: 'recording' }) } -function now (): string { - const date = new Date() - return date.toLocaleDateString() + ' ' + date.toLocaleTimeString() +export async function stopRecording (): Promise { + const current = get(recording) + if (current !== null && current.state === 'recording') { + recording.set({ ...current, state: 'stopped' }) + await current.recorder.stop() + recording.set(null) + get(recorder)?.close() + } else { + console.warn('Recording is not in `recording` state', current) + } +} + +export async function pauseRecording (): Promise { + const current = get(recording) + if (current !== null && current.state === 'recording') { + await current.recorder.pause() + recording.set({ ...current, state: 'paused' }) + } else { + console.warn('Recording is not in `recording` state', current) + } +} + +export async function restartRecording (): Promise { + const current = get(recording) + if (current !== null) { + await current.recorder.cancel() + await current.recorder.start() + recording.set({ ...current, state: 'recording' }) + } else { + console.warn('Recording is not in `paused` state', current) + } +} + +export async function resumeRecording (): Promise { + const current = get(recording) + if (current !== null && current.state === 'paused') { + await current.recorder.resume() + recording.set({ ...current, state: 'recording' }) + } else { + console.warn('Recording is not in `paused` state', current) + } +} + +export async function cancelRecording (): Promise { + const current = get(recording) + if (current !== null) { + await current.recorder.cancel() + recording.set(null) + } +} + +function makeCombinedStream ( + screenStream: MediaStream, + cameraStream: MediaStream | null, + microphoneStream: MediaStream | null +): MediaStream { + const tracks: MediaStreamTrack[] = [] + + // Add screen tracks + tracks.push(...screenStream.getTracks()) + + if (cameraStream != null && cameraStream.getVideoTracks().length > 0) { + // Don't add camera video to the recording, it will just be displayed + // in picture-in-picture style via UI + } + + if (cameraStream != null && cameraStream.getAudioTracks().length > 0) { + tracks.push(...cameraStream.getAudioTracks()) + } + + if (microphoneStream != null && microphoneStream.getAudioTracks().length > 0) { + tracks.push(...microphoneStream.getAudioTracks()) + } + + return new MediaStream(tracks) } diff --git a/plugins/recorder-resources/src/screen-recorder.ts b/plugins/recorder-resources/src/screen-recorder.ts index 8b699310ec..e2ff44f2fd 100644 --- a/plugins/recorder-resources/src/screen-recorder.ts +++ b/plugins/recorder-resources/src/screen-recorder.ts @@ -13,63 +13,37 @@ // limitations under the License. // -import plugin from '@hcengineering/recorder' import { Recorder } from './recorder' -import { TusUploader, type Uploader, type Options } from './uploader' -import { getMetadata } from '@hcengineering/platform' +import { type Uploader, type TusUploaderOptions, TusUploader } from './uploader' + +export function createScreenRecorder (mediaStream: MediaStream, options: TusUploaderOptions): ScreenRecorder { + const recorder = new Recorder({ mediaStream }) + const uploader = new TusUploader(recorder.asStream(), options) + + return new ScreenRecorder(mediaStream, recorder, uploader) +} export class ScreenRecorder { - private readonly recorder: Recorder - private readonly uploader: Uploader + constructor ( + readonly stream: MediaStream, + private readonly recorder: Recorder, + private readonly uploader: Uploader + ) {} - constructor (recorder: Recorder, uploader: Uploader) { - this.recorder = recorder - this.uploader = uploader + get elapsedTime (): number { + return this.recorder.getRecordedTimeMs() } - static async fromNavigatorMediaDevices (opts: Options): Promise { - let width = 0 - let height = 0 - const combinedStream = new MediaStream() - const getMediaStream = - getMetadata(plugin.metadata.GetCustomMediaStream) ?? - (async (op) => await navigator.mediaDevices.getDisplayMedia(op)) - const displayStream = await getMediaStream({ - video: { frameRate: opts.fps ?? 30 } - }) - try { - const microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: true }) - microphoneStream.getAudioTracks().forEach((track) => { - combinedStream.addTrack(track) - }) - } catch (err) { - console.warn('microphone is disabled', err) - } - displayStream.getVideoTracks().forEach((track) => { - combinedStream.addTrack(track) - width = Math.max(track.getSettings().width ?? width, width) - height = Math.max(track.getSettings().height ?? height, height) - }) - displayStream.getAudioTracks().forEach((track) => { - combinedStream.addTrack(track) - }) - - const recorder = new Recorder(combinedStream) - const uploader = new TusUploader(recorder.asStream(), { ...opts, metadata: { resolution: width + ':' + height } }) - - return new ScreenRecorder(recorder, uploader) - } - - public start (): void { + public async start (): Promise { this.uploader.start() this.recorder.start() } - public pause (): void { + public async pause (): Promise { this.recorder.pause() } - public resume (): void { + public async resume (): Promise { this.recorder.resume() } diff --git a/plugins/recorder-resources/src/stores.ts b/plugins/recorder-resources/src/stores.ts new file mode 100644 index 0000000000..aaac60382d --- /dev/null +++ b/plugins/recorder-resources/src/stores.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { type PopupResult } from '@hcengineering/ui' +import { writable } from 'svelte/store' +import { type RecordingState } from './types' + +export const recorder = writable(null) +export const recording = writable(null) diff --git a/plugins/recorder-resources/src/stream.ts b/plugins/recorder-resources/src/stream.ts index e01e05faf9..179a9b693f 100644 --- a/plugins/recorder-resources/src/stream.ts +++ b/plugins/recorder-resources/src/stream.ts @@ -21,7 +21,7 @@ export interface ChunkReadResult { export class ChunkReader { private readonly chunks: Blob[] = [] private done: boolean = false - private awaitingResolve: ((val: ChunkReadResult | PromiseLike) => void) | null = null + private awaitingResolve: ((val: ChunkReadResult | Promise) => void) | null = null public async read (): Promise { if (this.done && this.chunks.length === 0) { @@ -36,8 +36,16 @@ export class ChunkReader { } public push (blob: Blob): void { + if (this.done) { + console.warn('ChunkReader: push into closed stream') + // close just in case someone is waiting for a chunk + if (this.awaitingResolve !== null) { + this.awaitingResolve({ done: true, value: undefined }) + } + return + } + this.chunks.push(blob) - console.log(this.awaitingResolve) if (this.awaitingResolve !== null) { this.awaitingResolve(this.read()) this.awaitingResolve = null diff --git a/plugins/recorder-resources/src/types.ts b/plugins/recorder-resources/src/types.ts new file mode 100644 index 0000000000..9fbda13a1f --- /dev/null +++ b/plugins/recorder-resources/src/types.ts @@ -0,0 +1,22 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import { type ScreenRecorder } from './screen-recorder' + +export type CameraSize = 'small' | 'medium' | 'large' + +export interface RecordingState { + recorder: ScreenRecorder + stream: MediaStream + state: 'recording' | 'paused' | 'stopped' +} diff --git a/plugins/recorder-resources/src/uploader.ts b/plugins/recorder-resources/src/uploader.ts index 71d4c5281a..66420dd4da 100644 --- a/plugins/recorder-resources/src/uploader.ts +++ b/plugins/recorder-resources/src/uploader.ts @@ -16,45 +16,70 @@ import * as tus from 'tus-js-client' import type { ChunkReader } from './stream' -interface IMap { - [index: string]: T - [index: number]: T +export interface UploadResult { + blobId?: string + error?: any } -export interface Options { - fps?: number +export interface Uploader { + start: () => void + cancel: () => Promise + wait: () => Promise +} + +export interface TusUploaderOptions { endpoint: string - token: string workspace: string - metadata?: IMap - onFinish?: (x: string) => Promise + token: string + width: number + height: number + /** Optional callback invoked on successful upload with generated blobId */ + onSuccess?: (blobId: string) => void + /** Optional callback invoked on upload error */ + onError?: (error: any) => void } -export abstract class Uploader { - public abstract start (): void - public abstract cancel (): Promise - public abstract wait (): Promise -} - -export class TusUploader extends Uploader { +export class TusUploader implements Uploader { private readonly upload: tus.Upload - private waiter: (() => void) | null = null + private readonly waiterPromise: Promise + private waiterResolve: (value: UploadResult) => void = () => {} - constructor (target: ChunkReader, opts: Options) { - super() - this.upload = new tus.Upload(target, { + constructor (reader: ChunkReader, options: TusUploaderOptions) { + const { endpoint, workspace, token, width, height, onSuccess, onError } = options + const resolution = width + ':' + height + + this.waiterPromise = new Promise((resolve) => { + this.waiterResolve = resolve + }) + + console.debug('TusUploader: uploading', workspace, endpoint) + + this.upload = new tus.Upload(reader, { retryDelays: [0, 1000, 1500, 2000, 2500, 3000], chunkSize: 2 * 1024 * 1024, uploadLengthDeferred: true, - endpoint: opts.endpoint, - metadata: { ...opts.metadata, token: opts.token, workspace: opts.workspace }, + endpoint, + metadata: { resolution, token, workspace }, onSuccess: () => { - if (this.waiter !== null) { - this.waiter() - } - if (this.upload.url !== null && opts.onFinish !== undefined) { - void opts.onFinish(this.upload.url.substring(this.upload.url.lastIndexOf('/') + 1)) + const uploadId = this.upload.url?.split('/').pop() + console.debug('TusUploader: upload success:', uploadId) + if (uploadId === undefined) { + console.error('TusUploader: upload URL does not contain upload ID') + return } + const blobId = uploadId + '_master.m3u8' + // invoke callback if provided + onSuccess?.(blobId) + this.waiterResolve({ blobId }) + }, + onError: (error) => { + console.error('TusUploader: upload failed:', error) + // invoke error callback if provided + onError?.(error) + this.waiterResolve({ error }) + }, + onProgress: (progress) => { + console.debug('TusUploader: upload progress:', progress) } }) } @@ -63,10 +88,8 @@ export class TusUploader extends Uploader { this.upload.start() } - public async wait (): Promise { - await new Promise((resolve) => { - this.waiter = resolve - }) + public async wait (): Promise { + return await this.waiterPromise } public async cancel (): Promise { diff --git a/plugins/recorder-resources/src/utils.ts b/plugins/recorder-resources/src/utils.ts new file mode 100644 index 0000000000..7bef69cb73 --- /dev/null +++ b/plugins/recorder-resources/src/utils.ts @@ -0,0 +1,56 @@ +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. + +import drive, { createFile } from '@hcengineering/drive' +import { translate } from '@hcengineering/platform' +import { getClient } from '@hcengineering/presentation' +import { type FileUploadCallback, type FileUploadCallbackParams } from '@hcengineering/uploader' +import recorder from './plugin' + +export function formatElapsedTime (elapsed: number): string { + const seconds = Math.floor(elapsed / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + + const displaySeconds = (seconds % 60).toString().padStart(2, '0') + const displayMinutes = (minutes % 60).toString().padStart(2, '0') + + return hours > 0 ? `${hours}:${displayMinutes}:${displaySeconds}` : `${displayMinutes}:${displaySeconds}` +} + +export async function formatRecordingName (date: Date): Promise { + const timeStr = date.toLocaleTimeString() + const dateStr = date.toLocaleDateString(undefined, { + day: 'numeric', + month: 'long', + year: 'numeric' + }) + + return await translate(recorder.string.RecordingTitle, { date: dateStr + ' ' + timeStr }) +} + +export const createRecordingInDrive: FileUploadCallback = async (params: FileUploadCallbackParams): Promise => { + const { uuid, name, file, metadata } = params + + const client = getClient() + const data = { + file: uuid, + size: file.size, + type: file.type, + lastModified: file instanceof File ? file.lastModified : Date.now(), + title: name, + metadata + } + + await createFile(client, recorder.space.Drive, drive.ids.Root, data) +} diff --git a/plugins/recorder/package.json b/plugins/recorder/package.json index 9c75b9e43d..0dbff283a1 100644 --- a/plugins/recorder/package.json +++ b/plugins/recorder/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@hcengineering/core": "^0.6.32", + "@hcengineering/drive": "^0.6.0", "@hcengineering/platform": "^0.6.11", "@hcengineering/uploader": "^0.6.0", "tus-js-client": "^4.3.1" diff --git a/plugins/recorder/src/index.ts b/plugins/recorder/src/index.ts index 21793e5258..f300d7b733 100644 --- a/plugins/recorder/src/index.ts +++ b/plugins/recorder/src/index.ts @@ -11,7 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Asset, IntlString, plugin, Plugin, Resource, Metadata } from '@hcengineering/platform' +import { type Ref } from '@hcengineering/core' +import { type Drive } from '@hcengineering/drive' +import { Asset, plugin, Plugin, Resource, Metadata } from '@hcengineering/platform' import { type UploadHandler } from '@hcengineering/uploader' /** @@ -35,13 +37,8 @@ const recordPlugin = plugin(recorderId, { StreamUrl: '' as Metadata, GetCustomMediaStream: '' as Metadata }, - string: { - Pause: '' as IntlString, - Stop: '' as IntlString, - Resume: '' as IntlString, - Record: '' as IntlString, - Cancel: '' as IntlString, - ClickToSkip: '' as IntlString + space: { + Drive: '' as Ref }, function: { Record: '' as Resource diff --git a/plugins/uploader-resources/package.json b/plugins/uploader-resources/package.json index 2886db2301..10a4f66e52 100644 --- a/plugins/uploader-resources/package.json +++ b/plugins/uploader-resources/package.json @@ -49,7 +49,6 @@ "@hcengineering/view": "^0.6.13", "@hcengineering/view-resources": "^0.6.0", "install": "^0.13.0", - "stream-browserify": "^3.0.0", "svelte": "^4.2.19" } }