diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml deleted file mode 100644 index 9a12632274..0000000000 --- a/.github/workflows/pre-release.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Pre-release CI - -on: - push: - branches: - - 'pre-release/[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+' - - 'pre-release/[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' - - 'pre-release/[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+' - -jobs: - build: - runs-on: macOS-latest # 如果用了electron,记得改成 macOS-latest - - permissions: - contents: write - - strategy: - matrix: - node-version: [18.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - cache-dependency-path: './common/config/rush/pnpm-lock.yaml' - - # Install rush - - name: Install rush - run: node common/scripts/install-run-rush.js install --bypass-policy - - - name: Parse semver version from branch name - id: semver_parser - uses: xile611/read-package-version-action@v2.1 - with: - path: packages/vtable - semver_string: ${{ github.ref_name }} - semver_pattern: '^pre-release/(.*)$' # ^v?(.*)$ by default - - - name: Apply prereleaseName - run: node common/scripts/apply-prerelease-version.js ${{ steps.semver_parser.outputs.pre_release_name }} ${{ steps.semver_parser.outputs.main }} - - - name: Build packages - env: - NODE_OPTIONS: "--max_old_space_size=4096" - run: node common/scripts/install-run-rush.js build --only tag:package - - - name: Publish to npm - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - NPM_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - run: node common/scripts/install-run-rush.js publish --publish --include-all --tag ${{ steps.semver_parser.outputs.pre_release_type }} - - - name: Update shrinkwrap - run: node common/scripts/install-run-rush.js update - - - name: Get npm version - id: package-version - uses: xile611/read-package-version-action@main - with: - path: packages/vtable - - - name: Commit & Push changes - uses: actions-js/push@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - message: 'build: prelease version ${{ steps.package-version.outputs.current_version }}' - branch: ${{ github.ref_name }} - author_name: ${{ github.actor }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78b806c2a9..34428165fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -303,13 +303,27 @@ jobs: fi done - - name: Generate rush version (release) + - name: Reset version-policy base version (release) if: startsWith(github.ref_name, 'release/') - run: node common/scripts/install-run-rush.js version --bump + run: | + node <<'NODE' + const fs = require('fs'); + + const packageJson = JSON.parse(fs.readFileSync('packages/vtable/package.json', 'utf8')); + const policiesPath = 'common/config/rush/version-policies.json'; + const policies = JSON.parse(fs.readFileSync(policiesPath, 'utf8')); - - name: Update version (release) + if (!policies[0]) { + throw new Error('Missing version policy.'); + } + + policies[0].version = packageJson.version; + fs.writeFileSync(policiesPath, JSON.stringify(policies)); + NODE + + - name: Generate rush version (release) if: startsWith(github.ref_name, 'release/') - run: node common/scripts/apply-release-version.js 'none' ${{ steps.semver_release.outputs.main }} + run: node common/scripts/install-run-rush.js version --bump - name: Build packages env: diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 1e45fae1a3..1d807dbfd5 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -19,34 +19,34 @@ importers: specifier: ^2.11.0 version: 2.57.0(vue@3.5.31(typescript@4.9.5)) '@visactor/openinula-vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/openinula-vtable '@visactor/react-vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/react-vtable '@visactor/vchart': specifier: 2.0.13-alpha.10 version: 2.0.13-alpha.10 '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/vtable '@visactor/vtable-editors': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/vtable-editors '@visactor/vtable-export': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/vtable-export '@visactor/vtable-gantt': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/vtable-gantt '@visactor/vtable-search': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/vtable-search '@visactor/vtable-sheet': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/vtable-sheet '@visactor/vue-vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../packages/vue-vtable '@visactor/vutils': specifier: ~1.0.17 @@ -140,7 +140,7 @@ importers: ../../packages/openinula-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -270,7 +270,7 @@ importers: ../../packages/react-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -445,7 +445,7 @@ importers: specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable-editors': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable-editors '@visactor/vutils': specifier: ~1.0.17 @@ -626,7 +626,7 @@ importers: ../../packages/vtable-calendar: dependencies: '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -849,7 +849,7 @@ importers: specifier: 2.0.7 version: 2.0.7 '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -1009,10 +1009,10 @@ importers: specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable-editors '@visactor/vutils': specifier: ~1.0.17 @@ -1245,13 +1245,13 @@ importers: specifier: 2.0.13-alpha.10 version: 2.0.13-alpha.10 '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable-editors '@visactor/vtable-gantt': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable-gantt '@vitejs/plugin-react': specifier: 3.1.0 @@ -1350,7 +1350,7 @@ importers: ../../packages/vtable-search: dependencies: '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -1504,13 +1504,13 @@ importers: specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable-editors '@visactor/vtable-plugins': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable-plugins '@visactor/vutils': specifier: ~1.0.17 @@ -1649,7 +1649,7 @@ importers: ../../packages/vue-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -1830,19 +1830,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vtable': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../../packages/vtable '@visactor/vtable-editors': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../../packages/vtable-editors '@visactor/vtable-gantt': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../../packages/vtable-gantt '@visactor/vtable-plugins': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../../packages/vtable-plugins '@visactor/vtable-sheet': - specifier: workspace:1.26.3 + specifier: workspace:1.26.4 version: link:../../packages/vtable-sheet devDependencies: '@internal/bundler': diff --git a/docs/assets/changelog/en/release.md b/docs/assets/changelog/en/release.md index 818ee386c0..1c7dbc8273 100644 --- a/docs/assets/changelog/en/release.md +++ b/docs/assets/changelog/en/release.md @@ -1,3 +1,15 @@ +# v1.26.4 + +2026-07-06 + +**🆕 New Features** + +- **@visactor/vtable**: Support rendering video cells from a first-frame canvas snapshot to release video resources after loading. + +**Full Changelog**: https://github.com/VisActor/VTable/compare/v1.26.3...v1.26.4 + +[more detail about v1.26.4](https://github.com/VisActor/VTable/releases/tag/v1.26.4) + # v1.26.3 2026-06-25 diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md index e0d7952546..82bb4fbecf 100644 --- a/docs/assets/changelog/zh/release.md +++ b/docs/assets/changelog/zh/release.md @@ -1,3 +1,15 @@ +# v1.26.4 + +2026-07-06 + +**🆕 新增功能** + +- **@visactor/vtable**: 支持通过首帧 canvas 快照渲染视频单元格,并在加载后释放视频资源。 + +**Full Changelog**: https://github.com/VisActor/VTable/compare/v1.26.3...v1.26.4 + +[更多详情请查看 v1.26.4](https://github.com/VisActor/VTable/releases/tag/v1.26.4) + # v1.26.3 2026-06-25 diff --git a/docs/package.json b/docs/package.json index f277369458..752e0c7ddf 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,15 +12,15 @@ "dependencies": { "@arco-design/web-react": "2.66.12", "@arco-design/web-vue": "^2.11.0", - "@visactor/vtable": "workspace:1.26.3", - "@visactor/vtable-gantt": "workspace:1.26.3", - "@visactor/react-vtable": "workspace:1.26.3", - "@visactor/vue-vtable": "workspace:1.26.3", - "@visactor/openinula-vtable": "workspace:1.26.3", - "@visactor/vtable-editors": "workspace:1.26.3", - "@visactor/vtable-export": "workspace:1.26.3", - "@visactor/vtable-search": "workspace:1.26.3", - "@visactor/vtable-sheet": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable-gantt": "workspace:1.26.4", + "@visactor/react-vtable": "workspace:1.26.4", + "@visactor/vue-vtable": "workspace:1.26.4", + "@visactor/openinula-vtable": "workspace:1.26.4", + "@visactor/vtable-editors": "workspace:1.26.4", + "@visactor/vtable-export": "workspace:1.26.4", + "@visactor/vtable-search": "workspace:1.26.4", + "@visactor/vtable-sheet": "workspace:1.26.4", "buble": "^0.20.0", "@visactor/vchart": "2.0.13-alpha.10", "markdown-it": "^13.0.0", diff --git a/packages/openinula-vtable/package.json b/packages/openinula-vtable/package.json index 919ad191f4..06b70d8a3f 100644 --- a/packages/openinula-vtable/package.json +++ b/packages/openinula-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/openinula-vtable", - "version": "1.26.3", + "version": "1.26.4", "description": "The openinula version of VTable", "keywords": [ "openinula", @@ -44,7 +44,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", "@visactor/vutils": "~1.0.17" }, "devDependencies": { diff --git a/packages/react-vtable/package.json b/packages/react-vtable/package.json index daa67cbc29..ba75a1a5e3 100644 --- a/packages/react-vtable/package.json +++ b/packages/react-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vtable", - "version": "1.26.3", + "version": "1.26.4", "description": "The react version of VTable", "keywords": [ "react", @@ -57,7 +57,7 @@ "react-dom": "^18.2.0 || ^19.0.0" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", "@visactor/vutils": "~1.0.17", "react-is": "^18.2.0", "react-reconciler": "0.29.0" diff --git a/packages/vtable-calendar/package.json b/packages/vtable-calendar/package.json index 8d26224b0d..1d2fd909ab 100644 --- a/packages/vtable-calendar/package.json +++ b/packages/vtable-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-calendar", - "version": "1.26.3", + "version": "1.26.4", "description": "The calendar component of VTable", "author": { "name": "VisActor", @@ -37,7 +37,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", "@visactor/vutils": "~1.0.17", "date-fns": "3.6.0" }, diff --git a/packages/vtable-editors/package.json b/packages/vtable-editors/package.json index 81e46f1a11..d915b63f85 100644 --- a/packages/vtable-editors/package.json +++ b/packages/vtable-editors/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-editors", - "version": "1.26.3", + "version": "1.26.4", "description": "", "sideEffects": false, "main": "cjs/index.js", diff --git a/packages/vtable-export/package.json b/packages/vtable-export/package.json index 3a99af6394..d1e692f8c1 100644 --- a/packages/vtable-export/package.json +++ b/packages/vtable-export/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-export", - "version": "1.26.3", + "version": "1.26.4", "description": "The export util of VTable", "author": { "name": "VisActor", @@ -37,7 +37,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", "@visactor/vutils": "~1.0.17", "file-saver": "2.0.5", "@types/file-saver": "2.0.7", diff --git a/packages/vtable-gantt/package.json b/packages/vtable-gantt/package.json index 13838cd9bd..5da89165a5 100644 --- a/packages/vtable-gantt/package.json +++ b/packages/vtable-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-gantt", - "version": "1.26.3", + "version": "1.26.4", "description": "canvas table width high performance", "keywords": [ "vtable-gantt", @@ -50,8 +50,8 @@ "fix-memory-limit": "cross-env LIMIT=10240 increase-memory-limit" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", - "@visactor/vtable-editors": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable-editors": "workspace:1.26.4", "@visactor/vutils": "~1.0.17", "@visactor/vscale": "~1.0.17", "@visactor/vdataset": "~1.0.17", diff --git a/packages/vtable-plugins/package.json b/packages/vtable-plugins/package.json index ce9184028d..4e49a9db8d 100644 --- a/packages/vtable-plugins/package.json +++ b/packages/vtable-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-plugins", - "version": "1.26.3", + "version": "1.26.4", "description": "The search util of VTable", "author": { "name": "VisActor", @@ -61,9 +61,9 @@ "devDependencies": { "cross-env": "^7.0.3", "increase-memory-limit": "^1.0.7", - "@visactor/vtable": "workspace:1.26.3", - "@visactor/vtable-editors": "workspace:1.26.3", - "@visactor/vtable-gantt": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable-editors": "workspace:1.26.4", + "@visactor/vtable-gantt": "workspace:1.26.4", "@visactor/vchart": "2.0.13-alpha.10", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", diff --git a/packages/vtable-search/package.json b/packages/vtable-search/package.json index fb4908e665..0002f0190d 100644 --- a/packages/vtable-search/package.json +++ b/packages/vtable-search/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-search", - "version": "1.26.3", + "version": "1.26.4", "description": "The search util of VTable", "author": { "name": "VisActor", @@ -36,7 +36,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", "@visactor/vutils": "~1.0.17" }, "devDependencies": { diff --git a/packages/vtable-sheet/package.json b/packages/vtable-sheet/package.json index 69f3f258cd..29e6fc9b97 100644 --- a/packages/vtable-sheet/package.json +++ b/packages/vtable-sheet/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-sheet", - "version": "1.26.3", + "version": "1.26.4", "description": "Lightweight editable spreadsheet component based on VTable", "keywords": [ "vtable-sheet", @@ -50,9 +50,9 @@ "build:es": "bundle --clean -f es --ignorePostTasks" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", - "@visactor/vtable-editors": "workspace:1.26.3", - "@visactor/vtable-plugins": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable-editors": "workspace:1.26.4", + "@visactor/vtable-plugins": "workspace:1.26.4", "@visactor/vutils": "~1.0.17", "@visactor/vscale": "~1.0.17", "@visactor/vdataset": "~1.0.17", diff --git a/packages/vtable/CHANGELOG.json b/packages/vtable/CHANGELOG.json index ca03d5b377..9ab4e7dde2 100644 --- a/packages/vtable/CHANGELOG.json +++ b/packages/vtable/CHANGELOG.json @@ -4,6 +4,18 @@ { "version": "1.26.4", "tag": "@visactor/vtable_v1.26.4", + "date": "Mon, 06 Jul 2026 09:35:12 GMT", + "comments": { + "minor": [ + { + "comment": "Support rendering video cells from a first-frame canvas snapshot to release video resources after loading." + } + ] + } + }, + { + "version": "1.26.3", + "tag": "@visactor/vtable_v1.26.3", "date": "Thu, 25 Jun 2026 03:15:59 GMT", "comments": { "patch": [ @@ -36,39 +48,10 @@ ] } }, - { - "version": "1.26.3", - "tag": "@visactor/vtable_v1.26.3", - "date": "Tue, 16 Jun 2026 06:22:51 GMT", - "comments": { - "none": [ - { - "comment": "Merge pull request #5144 from VisActor/fix/issue-5027\n\nfix: avoid selection gap with frozen rows\n" - }, - { - "comment": "fix: preserve sticky group position after toggle\n\n" - }, - { - "comment": "fix: avoid selection gap with frozen rows\n\n" - }, - { - "comment": "fix: preserve hidden column order after filter update\n\n" - }, - { - "comment": "fix: resolve regression from #5137 fix which broke nested header drag and exposed internal state\n\n" - } - ], - "patch": [ - { - "comment": "Fix an issue where keyboard multi-selection with Shift plus arrow keys stopped expanding after the second cell (GitHub #5146)" - } - ] - } - }, { "version": "1.26.2", "tag": "@visactor/vtable_v1.26.2", - "date": "Tue, 16 Jun 2026 01:39:41 GMT", + "date": "Tue, 16 Jun 2026 06:22:51 GMT", "comments": { "none": [ { diff --git a/packages/vtable/CHANGELOG.md b/packages/vtable/CHANGELOG.md index 3c38a148e1..6efebe57c4 100644 --- a/packages/vtable/CHANGELOG.md +++ b/packages/vtable/CHANGELOG.md @@ -1,8 +1,15 @@ # Change Log - @visactor/vtable -This log was last generated on Thu, 25 Jun 2026 03:15:59 GMT and should not be manually modified. +This log was last generated on Mon, 06 Jul 2026 09:35:12 GMT and should not be manually modified. ## 1.26.4 +Mon, 06 Jul 2026 09:35:12 GMT + +### Minor changes + +- Support rendering video cells from a first-frame canvas snapshot to release video resources after loading. + +## 1.26.3 Thu, 25 Jun 2026 03:15:59 GMT ### Patches @@ -21,34 +28,8 @@ Thu, 25 Jun 2026 03:15:59 GMT - fix: preserve quad style values -## 1.26.3 -Tue, 16 Jun 2026 06:22:51 GMT - -### Patches - -- Fix an issue where keyboard multi-selection with Shift plus arrow keys stopped expanding after the second cell (GitHub #5146) - -### Updates - -- Merge pull request #5144 from VisActor/fix/issue-5027 - -fix: avoid selection gap with frozen rows - -- fix: preserve sticky group position after toggle - - -- fix: avoid selection gap with frozen rows - - -- fix: preserve hidden column order after filter update - - -- fix: resolve regression from #5137 fix which broke nested header drag and exposed internal state - - - ## 1.26.2 -Tue, 16 Jun 2026 01:39:41 GMT +Tue, 16 Jun 2026 06:22:51 GMT ### Patches diff --git a/packages/vtable/__tests__/cell-type/video-cell-first-frame.test.ts b/packages/vtable/__tests__/cell-type/video-cell-first-frame.test.ts new file mode 100644 index 0000000000..2080a366fb --- /dev/null +++ b/packages/vtable/__tests__/cell-type/video-cell-first-frame.test.ts @@ -0,0 +1,240 @@ +// @ts-nocheck +import { createVideoCellGroup } from '../../src/scenegraph/group-creater/cell-type/video-cell'; +import { updateImageCellContentWhileResize } from '../../src/scenegraph/group-creater/cell-type/image-cell'; +import { registerForVrender } from '../../src/vrender'; +import * as icons from '../../src/icons'; + +global.__VERSION__ = 'none'; + +registerForVrender(); + +describe('video cell first frame snapshot', () => { + const originalCreateElement = document.createElement.bind(document); + let createdVideo: HTMLVideoElement; + let drawImage: jest.Mock; + + function createTable(customConfig?: Record) { + return { + options: { + customConfig + }, + _getCellStyle: jest.fn(() => ({})), + getCellValue: jest.fn(() => 'https://example.com/video.mp4'), + colCount: 1, + rowCount: 1, + theme: { + cellInnerBorder: true, + frameStyle: {} + }, + getCellIcons: jest.fn(), + scenegraph: { + updateNextFrame: jest.fn(), + highPerformanceGetCell: jest.fn(), + getCell: jest.fn() + } + }; + } + + function createCell(customConfig?: Record, size = { width: 200, height: 120 }) { + const table = createTable(customConfig); + const cellGroup = createVideoCellGroup( + undefined, + 0, + 0, + 0, + 0, + size.width, + size.height, + false, + false, + [0, 0, 0, 0], + 'left', + 'middle', + false, + table, + { + group: {}, + text: {} + }, + undefined, + false + ); + table.scenegraph.getCell.mockReturnValue(cellGroup); + + return { + table, + cellGroup, + image: cellGroup.getChildByName('image', true), + video: createdVideo + }; + } + + beforeEach(() => { + drawImage = jest.fn(); + jest.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({ + drawImage + } as any); + jest.spyOn(document, 'createElement').mockImplementation((tagName: string, options?: ElementCreationOptions) => { + const element = originalCreateElement(tagName, options); + if (tagName.toLowerCase() === 'video') { + createdVideo = element as HTMLVideoElement; + Object.defineProperties(createdVideo, { + videoWidth: { + value: 640, + configurable: true + }, + videoHeight: { + value: 360, + configurable: true + }, + pause: { + value: jest.fn(), + configurable: true + }, + load: { + value: jest.fn(), + configurable: true + } + }); + } + return element; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('keeps existing video rendering when first frame snapshot is not enabled', () => { + const { image, video } = createCell(); + + video.dispatchEvent(new Event('loadeddata')); + + expect(image.attribute.image).toBe(video); + expect(drawImage).not.toHaveBeenCalled(); + expect(video.pause).not.toHaveBeenCalled(); + expect(video.load).not.toHaveBeenCalled(); + }); + + it('uses a canvas snapshot and releases the video when enabled', () => { + const { image, table, video } = createCell({ + videoFirstFrameSnapshot: true, + videoFirstFrameMaxCanvasSize: 128 + }); + + expect(video.getAttribute('preload')).toBe('auto'); + expect(video.getAttribute('src')).toBe('https://example.com/video.mp4'); + + video.dispatchEvent(new Event('loadeddata')); + + expect(drawImage).toHaveBeenCalledWith(video, 0, 0, 128, 77); + expect(image.attribute.image).toBeInstanceOf(HTMLCanvasElement); + expect((image.attribute.image as HTMLCanvasElement).width).toBe(128); + expect((image.attribute.image as HTMLCanvasElement).height).toBe(77); + expect(video.pause).toHaveBeenCalledTimes(1); + expect(video.hasAttribute('src')).toBe(false); + expect(video.load).toHaveBeenCalledTimes(1); + expect(table.scenegraph.updateNextFrame).toHaveBeenCalled(); + }); + + it('releases the video and redraws on load error', () => { + const { table, video } = createCell({ + videoFirstFrameSnapshot: true + }); + + video.dispatchEvent(new Event('error')); + + expect(video.pause).toHaveBeenCalledTimes(1); + expect(video.hasAttribute('src')).toBe(false); + expect(video.load).toHaveBeenCalledTimes(1); + expect(table.scenegraph.updateNextFrame).toHaveBeenCalled(); + }); + + it('keeps the video damage image aspect ratio immediately after load error', () => { + const { image, video } = createCell( + { + videoFirstFrameSnapshot: true + }, + { width: 360, height: 40 } + ); + const regedIcons = icons.get(); + const damageImage = regedIcons.video_damage_pic + ? (regedIcons.video_damage_pic as any).svg + : (regedIcons.damage_pic as any).svg; + + image.resources.set(damageImage, { + state: 'success', + data: { + width: 24, + height: 24 + } + }); + video.dispatchEvent(new Event('error')); + + expect(image.attribute.image).toBe(damageImage); + expect(image.attribute.x).toBe(0); + expect(image.attribute.y).toBe(0); + expect(image.attribute.width).toBe(40); + expect(image.attribute.height).toBe(40); + }); + + it('keeps custom video damage svg aspect ratio before the icon resource is cached', () => { + const originalVideoDamageIcon = (icons.icons as any).video_damage_pic; + (icons.icons as any).video_damage_pic = { + type: 'svg', + svg: '', + name: 'video_damage_pic', + positionType: 'right' + }; + + try { + const { image, video } = createCell( + { + videoFirstFrameSnapshot: true + }, + { width: 360, height: 40 } + ); + + video.dispatchEvent(new Event('error')); + + expect(image.attribute.image).toBe((icons.icons as any).video_damage_pic.svg); + expect(image.attribute.x).toBe(0); + expect(image.attribute.y).toBe(0); + expect(image.attribute.width).toBe(40); + expect(image.attribute.height).toBe(40); + } finally { + if (originalVideoDamageIcon) { + (icons.icons as any).video_damage_pic = originalVideoDamageIcon; + } else { + delete (icons.icons as any).video_damage_pic; + } + } + }); + + it('keeps the video damage image aspect ratio when the column resizes after load error', () => { + const { cellGroup, image, table, video } = createCell({ + videoFirstFrameSnapshot: true + }); + const regedIcons = icons.get(); + const damageImage = regedIcons.video_damage_pic + ? (regedIcons.video_damage_pic as any).svg + : (regedIcons.damage_pic as any).svg; + + video.dispatchEvent(new Event('error')); + image.resources.set(damageImage, { + state: 'success', + data: { + width: 24, + height: 24 + } + }); + cellGroup.setAttribute('width', 360); + updateImageCellContentWhileResize(cellGroup, 0, 0, 160, 0, table); + + expect(image.attribute.image).toBe(damageImage); + expect(image.attribute.x).toBe(0); + expect(image.attribute.y).toBe(0); + expect(image.attribute.width).toBe(120); + expect(image.attribute.height).toBe(120); + }); +}); diff --git a/packages/vtable/package.json b/packages/vtable/package.json index d722b5d298..86dcb61662 100644 --- a/packages/vtable/package.json +++ b/packages/vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable", - "version": "1.26.3", + "version": "1.26.4", "description": "canvas table width high performance", "keywords": [ "grid", @@ -52,7 +52,7 @@ "fix-memory-limit": "cross-env LIMIT=10240 increase-memory-limit" }, "dependencies": { - "@visactor/vtable-editors": "workspace:1.26.3", + "@visactor/vtable-editors": "workspace:1.26.4", "@visactor/vrender-core": "~1.0.41", "@visactor/vrender-kits": "~1.0.41", "@visactor/vrender-components": "~1.0.41", diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/image-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/image-cell.ts index 8cffa12ad0..0402bf657b 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/image-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/image-cell.ts @@ -344,7 +344,7 @@ export function updateImageCellContentWhileResize( const leftIconWidth = (cellGroup as any)._cellLeftIconWidth ?? 0; const rightIconWidth = (cellGroup as any)._cellRightIconWidth ?? 0; - if ((image as any).keepAspectRatio) { + if ((image as any).keepAspectRatio || isDamagePic(image)) { const { width: imageWidth, height: imageHeight } = calcKeepAspectRatioSize( originImage.width || (originImage as any).videoWidth, originImage.height || (originImage as any).videoHeight, diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/video-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/video-cell.ts index dea5246088..6fb63e5347 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/video-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/video-cell.ts @@ -23,6 +23,89 @@ import { dealWithIconLayout } from '../../utils/text-icon-layout'; const regedIcons = icons.get(); +function releaseVideoResource(video: HTMLVideoElement): void { + try { + video.pause(); + } catch (err) { + // ignore media cleanup errors + } + video.removeAttribute('src'); + try { + video.load(); + } catch (err) { + // ignore media cleanup errors + } +} + +function getVideoFirstFrameTimeout(table: BaseTableAPI): number { + const timeout = table.options.customConfig?.videoFirstFrameTimeout; + return typeof timeout === 'number' && timeout >= 0 ? timeout : 8000; +} + +function getVideoFirstFrameMaxCanvasSize(table: BaseTableAPI): number { + const maxCanvasSize = table.options.customConfig?.videoFirstFrameMaxCanvasSize; + return typeof maxCanvasSize === 'number' && maxCanvasSize > 0 ? maxCanvasSize : 512; +} + +function snapshotVideoFirstFrame(video: HTMLVideoElement, image: IImage, table: BaseTableAPI): boolean { + const displayWidth = image.attribute.width; + const displayHeight = image.attribute.height; + if ( + typeof displayWidth !== 'number' || + typeof displayHeight !== 'number' || + displayWidth <= 0 || + displayHeight <= 0 + ) { + return false; + } + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) { + return false; + } + + const dpr = Math.min((typeof window === 'undefined' ? 1 : window.devicePixelRatio) || 1, 2); + const maxSize = getVideoFirstFrameMaxCanvasSize(table); + const scale = Math.min(dpr, maxSize / Math.max(displayWidth, displayHeight)); + canvas.width = Math.max(1, Math.ceil(displayWidth * scale)); + canvas.height = Math.max(1, Math.ceil(displayHeight * scale)); + canvas.style.width = `${displayWidth}px`; + canvas.style.height = `${displayHeight}px`; + + try { + context.drawImage(video, 0, 0, canvas.width, canvas.height); + image.setAttributes({ image: canvas as any }); + return true; + } catch (err) { + return false; + } +} + +function getSvgSize(svg: string): { width: number; height: number } | undefined { + const svgTag = svg.match(/]*>/i)?.[0]; + if (!svgTag) { + return undefined; + } + + const widthMatch = svgTag.match(/\bwidth=["']?([\d.]+)/i); + const heightMatch = svgTag.match(/\bheight=["']?([\d.]+)/i); + const width = widthMatch ? Number(widthMatch[1]) : undefined; + const height = heightMatch ? Number(heightMatch[1]) : undefined; + if (width > 0 && height > 0) { + return { width, height }; + } + + const viewBoxMatch = svgTag.match(/\bviewBox=["']\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+([\d.]+)\s*["']/i); + const viewBoxWidth = viewBoxMatch ? Number(viewBoxMatch[1]) : undefined; + const viewBoxHeight = viewBoxMatch ? Number(viewBoxMatch[2]) : undefined; + if (viewBoxWidth > 0 && viewBoxHeight > 0) { + return { width: viewBoxWidth, height: viewBoxHeight }; + } + + return undefined; +} + export function createVideoCellGroup( columnGroup: Group, xOrigin: number, @@ -162,7 +245,100 @@ export function createVideoCellGroup( // video const value = table.getCellValue(col, row); const video = document.createElement('video'); + video.muted = true; + video.playsInline = true; + const shouldSnapshot = table.options.customConfig?.videoFirstFrameSnapshot === true; + let loadTimer: ReturnType | undefined; + let videoReleased = false; + + const clearVideoLoadTimer = (): void => { + if (loadTimer !== undefined) { + clearTimeout(loadTimer); + loadTimer = undefined; + } + }; + const releaseCurrentVideo = (): void => { + if (videoReleased) { + return; + } + videoReleased = true; + clearVideoLoadTimer(); + releaseVideoResource(video); + }; + const isCurrentImage = (): boolean => cellGroup.getChildByName('image', true) === image; + const setVideoDamageImage = (): void => { + const regedIcons = icons.get(); + const damageIcon = regedIcons.video_damage_pic || regedIcons.damage_pic; + const damageImage = (damageIcon as any).svg; + image.setAttributes({ + image: damageImage + } as any); + const originImage = image.resources?.get(damageImage)?.data; + const svgSize = typeof damageImage === 'string' ? getSvgSize(damageImage) : undefined; + const originWidth = originImage?.width || (damageIcon as any).width || svgSize?.width || 24; + const originHeight = originImage?.height || (damageIcon as any).height || svgSize?.height || 24; + const { width: cellWidth, height: cellHeight, isMerge } = getCellRange(cellGroup, table); + const availableWidth = cellWidth - padding[1] - padding[3]; + const availableHeight = cellHeight - padding[0] - padding[2]; + + if (originWidth > 0 && originHeight > 0 && availableWidth > 0 && availableHeight > 0) { + const { width: imageWidth, height: imageHeight } = calcKeepAspectRatioSize( + originWidth, + originHeight, + availableWidth, + availableHeight + ); + const pos = calcStartPosition( + 0, + 0, + cellWidth, + cellHeight, + imageWidth, + imageHeight, + textAlign, + textBaseline, + padding + ); + + image.setAttributes({ + x: pos.x, + y: pos.y, + width: imageWidth, + height: imageHeight + }); + + if (isMerge) { + updateImageDxDy( + cellGroup.mergeStartCol, + cellGroup.mergeEndCol, + cellGroup.mergeStartRow, + cellGroup.mergeEndRow, + table + ); + } + } + }; + const handleVideoLoadFail = (): void => { + if (videoReleased) { + return; + } + if (isCurrentImage()) { + setVideoDamageImage(); + table.scenegraph.updateNextFrame(); + } + if (shouldSnapshot) { + releaseCurrentVideo(); + } + }; video.addEventListener('loadeddata', (): void => { + clearVideoLoadTimer(); + if (videoReleased) { + return; + } + if (!isCurrentImage()) { + releaseCurrentVideo(); + return; + } if (imageAutoSizing) { _adjustWidthHeight(col, row, video.videoWidth, video.videoHeight, table.scenegraph, padding, cellGroup); } @@ -248,17 +424,14 @@ export function createVideoCellGroup( }); playIcon.name = 'play-icon'; cellGroup.appendChild(playIcon); + if (shouldSnapshot && snapshotVideoFirstFrame(video, image, table)) { + releaseCurrentVideo(); + } // 触发重绘 table.scenegraph.updateNextFrame(); }); - video.onerror = (): void => { - const regedIcons = icons.get(); - (image as any).image = regedIcons.video_damage_pic - ? (regedIcons.video_damage_pic as any).svg - : (regedIcons.damage_pic as any).svg; - }; - video.src = value; - video.setAttribute('preload', 'auto'); + video.addEventListener('error', handleVideoLoadFail); + video.addEventListener('abort', handleVideoLoadFail); const image: IImage = createImage({ x: padding[3], @@ -291,6 +464,13 @@ export function createVideoCellGroup( table.scenegraph.updateNextFrame(); } }; + + video.setAttribute('preload', 'auto'); + video.src = value; + const timeout = getVideoFirstFrameTimeout(table); + if (shouldSnapshot && timeout > 0) { + loadTimer = setTimeout(handleVideoLoadFail, timeout); + } return cellGroup; } diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 4c9811d85c..2a4e8740d1 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -639,6 +639,12 @@ export interface BaseTableConstructorOptions { // 图片资源请求时是否使用anonymous模式 imageAnonymous?: boolean; + // 视频单元格首帧绘制后是否替换为canvas快照并释放video资源 + videoFirstFrameSnapshot?: boolean; + // 视频单元格等待首帧的超时时间,单位ms,默认8000 + videoFirstFrameTimeout?: number; + // 视频单元格首帧快照canvas的最大边长,默认512 + videoFirstFrameMaxCanvasSize?: number; // 滚动到边界是否继续触发滚动事件 scrollEventAlwaysTrigger?: boolean; diff --git a/packages/vue-vtable/package.json b/packages/vue-vtable/package.json index 078f9ceb2a..1d4140aea0 100644 --- a/packages/vue-vtable/package.json +++ b/packages/vue-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vue-vtable", - "version": "1.26.3", + "version": "1.26.4", "description": "The vue version of VTable", "keywords": [ "vue", @@ -59,7 +59,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", + "@visactor/vtable": "workspace:1.26.4", "@visactor/vutils": "~1.0.17" }, "devDependencies": { diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index ce0b0b207a..5212375b0b 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.3", - "@visactor/vtable-gantt": "workspace:1.26.3", - "@visactor/vtable-editors": "workspace:1.26.3", - "@visactor/vtable-plugins": "workspace:1.26.3", - "@visactor/vtable-sheet": "workspace:1.26.3" + "@visactor/vtable": "workspace:1.26.4", + "@visactor/vtable-gantt": "workspace:1.26.4", + "@visactor/vtable-editors": "workspace:1.26.4", + "@visactor/vtable-plugins": "workspace:1.26.4", + "@visactor/vtable-sheet": "workspace:1.26.4" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4",